From 1c452fd97640233fb96f72088a556237bf709bd3 Mon Sep 17 00:00:00 2001 From: svonava Date: Sun, 18 Oct 2026 05:56:54 -0700 Subject: [PATCH 1/3] fix(server): guard Transformers 4 model loading --- packages/sie_server/bundles/default.yaml | 4 +- packages/sie_server/pyproject.toml | 18 +- .../adapters/_sentence_transformer_trust.py | 3 + .../adapters/_transformers_compat.py | 98 ++++ .../adapters/bert_flash/__init__.py | 2 + .../bert_flash_cross_encoder/__init__.py | 2 + .../sie_server/adapters/bge_m3/__init__.py | 2 + .../adapters/bge_m3_flag/__init__.py | 2 + .../adapters/bge_m3_flash/__init__.py | 2 + .../src/sie_server/adapters/clip/__init__.py | 2 + .../sie_server/adapters/colbert/__init__.py | 2 + .../colbert_modernbert_flash/adapter.py | 2 + .../adapters/colbert_rotary_flash/__init__.py | 2 + .../sie_server/adapters/colpali/__init__.py | 2 + .../sie_server/adapters/colqwen2/__init__.py | 2 + .../sie_server/adapters/colqwen3/__init__.py | 2 + .../sie_server/adapters/colsmol/__init__.py | 2 + .../sie_server/adapters/docling/adapter.py | 2 + .../src/sie_server/adapters/donut/__init__.py | 2 + .../sie_server/adapters/florence2/__init__.py | 2 + .../sie_server/adapters/gliclass/__init__.py | 2 + .../sie_server/adapters/gliformer/adapter.py | 2 + .../sie_server/adapters/gliner/__init__.py | 2 + .../sie_server/adapters/gliner2/adapter.py | 2 + .../src/sie_server/adapters/gliner2/decide.py | 2 + .../sie_server/adapters/gliner2/entities.py | 2 + .../sie_server/adapters/gliner_bi/__init__.py | 2 + .../sie_server/adapters/glirel/__init__.py | 2 + .../sie_server/adapters/glm_ocr/__init__.py | 2 + .../adapters/grounding_dino/adapter.py | 2 + .../sie_server/adapters/gte_sparse_flash.py | 2 + .../jina_flash_cross_encoder/__init__.py | 2 + .../src/sie_server/adapters/laya/adapter.py | 2 + .../adapters/lighton_ocr/adapter.py | 3 + .../sie_server/adapters/mineru_vl/__init__.py | 2 + .../adapters/modernbert_flash/__init__.py | 2 + .../__init__.py | 2 + .../adapters/nemo_colembed/__init__.py | 2 + .../adapters/nli_classification/__init__.py | 2 + .../nli_classification_flash/__init__.py | 2 + .../src/sie_server/adapters/owlv2/adapter.py | 2 + .../adapters/paddleocr_vl/__init__.py | 2 + .../sie_server/adapters/parakeet/adapter.py | 2 + .../adapters/pplx_decider/adapter.py | 2 + .../adapters/pytorch_embedding/__init__.py | 2 + .../adapters/qwen2_flash/__init__.py | 2 + .../qwen2_flash_cross_encoder/adapter.py | 2 + .../adapters/qwen3_vl_embedding/__init__.py | 2 + .../adapters/qwen3_vl_reranker/adapter.py | 2 + .../adapters/rope_flash/__init__.py | 2 + .../src/sie_server/adapters/sam3/adapter.py | 2 + .../src/sie_server/adapters/siglip/adapter.py | 9 + .../adapters/splade_flash/adapter.py | 2 + .../adapters/stablebridge_pruner/__init__.py | 2 + .../sie_server/adapters/topk_embed/adapter.py | 2 + .../sie_server/adapters/whisper/adapter.py | 2 + .../adapters/xlm_roberta_flash/__init__.py | 2 + .../tests/adapters/test_lighton_ocr3.py | 2 + .../sie_server/tests/adapters/test_sam3.py | 1 + .../adapters/test_transformers_compat.py | 250 ++++++++++ .../test_transformers_security_integration.py | 467 ++++++++++++++++++ .../config/test_native_zerank_catalog.py | 2 +- .../test_transformers_dependency_policy.py | 63 +++ uv.lock | 4 +- 64 files changed, 1012 insertions(+), 14 deletions(-) create mode 100644 packages/sie_server/src/sie_server/adapters/_transformers_compat.py create mode 100644 packages/sie_server/tests/adapters/test_transformers_compat.py create mode 100644 packages/sie_server/tests/adapters/test_transformers_security_integration.py create mode 100644 packages/sie_server/tests/config/test_transformers_dependency_policy.py diff --git a/packages/sie_server/bundles/default.yaml b/packages/sie_server/bundles/default.yaml index e495c0339..6a2b8ab5c 100644 --- a/packages/sie_server/bundles/default.yaml +++ b/packages/sie_server/bundles/default.yaml @@ -53,8 +53,8 @@ adapters: - sie_server.adapters.paddleocr_vl - sie_server.adapters.mineru_vl deps: - # Most flash adapters; sentence_transformer needs >=4.57 - transformers: '>=4.57,<5' + # Exact TF4 runtime reviewed with the load-time kernel/config security guard. + transformers: '==4.57.6' # Pin torch — flash-attn's unpinned torch dep can pull in 2.10+ which breaks torchvision torch: '>=2.9,<2.10' # bge_m3, bge_m3_flash, nemo_colembed, nomic_flash, xlm_roberta_flash diff --git a/packages/sie_server/pyproject.toml b/packages/sie_server/pyproject.toml index b8d9c8d7d..a85642fa3 100644 --- a/packages/sie_server/pyproject.toml +++ b/packages/sie_server/pyproject.toml @@ -17,11 +17,12 @@ dependencies = [ # ML / Inference # torch 2.9.x required - flash-attn prebuilt wheels are version-specific "torch>=2.9,<2.10", - # Bundle-controlled: each bundle overlays its exact transformers into - # bundle-libs (bundles/default.yaml >=4.57,<5; transformers5.yaml / gemma via - # sglang >=5). The base range is bounded <6 so the >=5 overlays resolve while - # the base venv can't silently jump to a transformers 6 major. - "transformers>=4.57,<6", + # Bundle-controlled: default/local and the root lock use exactly 4.57.6, + # whose loader receives the reviewed security backport. TF5 bundles use + # upstream-fixed >=5.3 releases. PEP 440 cannot express that disjoint set: + # exclude known unsupported releases here; runtime loading also refuses + # any future unreviewed 4.x version. Keep <6 for bundle overlay compatibility. + "transformers>=4.57.6,<6,!=5.0.*,!=5.1.*,!=5.2.*", "tokenizers>=0.20,<1", "sentence-transformers==5.7.0", "safetensors>=0.4,<1", @@ -130,9 +131,8 @@ gpu-metrics = ["nvidia-ml-py>=13.5,<14"] top = ["textual>=3.0,<4"] # Local embeddings + reranking on Apple Silicon / CPU (no Docker/mise). The torch / # sentence-transformers / FlagEmbedding stack is already in the base deps; the base -# leaves transformers at >=4.57,<6 so the transformers-5 bundle overlays resolve. -# This extra tightens it to <5 — the version the embed/rerank adapters need — for a -# bare install with no bundle overlay: +# permits upstream-fixed transformers-5 bundle overlays. This extra pins the +# exact reviewed TF4 loader for a bare install with no bundle overlay: # uv tool install "sie-server[local]" && sie-server serve # # Generation is deliberately NOT a locked extra. mlx-lm requires transformers>=5; as @@ -142,7 +142,7 @@ top = ["textual>=3.0,<4"] # uvx --with "mlx-lm>=0.30.7" --from sie-server sie-server serve -b sglang -p 8081 # (Linux/CUDA generation uses the Docker image: SGLang pins timm==1.0.16, which # conflicts with the embed/rerank stack's timm<1.0, so it can't share a pip env.) -local = ["transformers>=4.57,<5"] +local = ["transformers==4.57.6"] [project.scripts] sie-server = "sie_server.cli:app" diff --git a/packages/sie_server/src/sie_server/adapters/_sentence_transformer_trust.py b/packages/sie_server/src/sie_server/adapters/_sentence_transformer_trust.py index e5d12b423..aa98e14f3 100644 --- a/packages/sie_server/src/sie_server/adapters/_sentence_transformer_trust.py +++ b/packages/sie_server/src/sie_server/adapters/_sentence_transformer_trust.py @@ -15,6 +15,8 @@ import sentence_transformers from sentence_transformers import CrossEncoder, SentenceTransformer, SparseEncoder +from sie_server.adapters._transformers_compat import install_transformers_security_fixes + _SUPPORTED_VERSIONS = frozenset({"5.4.1", "5.6.0", "5.7.0"}) _ROOT_CLASSES = (SentenceTransformer, SparseEncoder, CrossEncoder) _RESOLVER = "_load_module_class_from_ref" @@ -139,6 +141,7 @@ def load_sentence_transformer[T: (SentenceTransformer, SparseEncoder, CrossEncod releases. An unknown version fails before an untrusted model is loaded. Explicit True preserves the original constructor path exactly. """ + install_transformers_security_fixes() if trust_remote_code is True: return model_class(model_name_or_path, trust_remote_code=True, **kwargs) if trust_remote_code is not False and trust_remote_code is not None: diff --git a/packages/sie_server/src/sie_server/adapters/_transformers_compat.py b/packages/sie_server/src/sie_server/adapters/_transformers_compat.py new file mode 100644 index 000000000..e1650cd81 --- /dev/null +++ b/packages/sie_server/src/sie_server/adapters/_transformers_compat.py @@ -0,0 +1,98 @@ +"""Load-time security backport for the supported Transformers 4 runtime. + +Transformers 5.3 restricts Hub attention kernels to ``kernels-community`` and +ignores serialized private attention settings (GHSA-29pf-2h5f-8g72). The default +SIE bundle still needs Transformers 4.57.6. Install this compatibility guard once, +before loading models, to retain that runtime without retaining the vulnerable +loader. It never wraps model inference or temporarily changes a trust setting. + +Upstream fix: huggingface/transformers@a7f8e7ff37d87d1a1a0c8cf607971c607741452f. +""" + +from __future__ import annotations + +import functools +import re +import threading +from typing import Any + +from packaging.version import Version + +_INSTALL_LOCK = threading.Lock() +_PATCHED_VERSION = Version("4.57.6") +_UPSTREAM_FIXED_VERSION = Version("5.3.0") +# The native loader splits on ANY pipe, before parsing the repository/revision. +# A pipe in a later component must not redirect an allowlisted-looking string. +_KERNEL_PATTERN = re.compile(r"(?:paged\|)?kernels-community/[^/:|@\s]+(?:@[^/:|@\s]+)?(?::[^/:|@\s]+)?") +_PRIVATE_IMPLEMENTATION_KEYS = frozenset( + { + "_attn_implementation_internal", + "_attn_implementation", + "_experts_implementation_internal", + "_experts_implementation", + } +) +_CONFIG_STRUCTURE_KEYS = frozenset({"__dict__", "__class__", "attribute_map", "sub_configs"}) + + +def _is_community_kernel(attn_implementation: str | None) -> bool: + """Recognize the upstream allowlisted namespace before any Hub access.""" + return isinstance(attn_implementation, str) and _KERNEL_PATTERN.fullmatch(attn_implementation) is not None + + +def install_transformers_security_fixes() -> bool: + """Install permanent loader guards; return whether this call installed them. + + Call during process initialization, before model loading begins. Installation + is idempotent across threads. All subsequent loads use the same policy, + including loads with ``trust_remote_code=True``, just as in upstream 5.3. + Unsupported vulnerable versions fail closed instead of receiving an untested + private-API patch. Versions with the upstream fix require no patch. + """ + # Importing this helper must not require Transformers in remote-only runtimes. + import transformers + + version = Version(transformers.__version__) + if version >= _UPSTREAM_FIXED_VERSION: + return False + if version != _PATCHED_VERSION: + raise RuntimeError( + "SIE's Transformers security backport requires transformers==4.57.6; " + "use that supported runtime or transformers>=5.3.0." + ) + + from transformers import PretrainedConfig, modeling_utils + from transformers.integrations import hub_kernels + + with _INSTALL_LOCK: + original_init = PretrainedConfig.__init__ + if getattr(original_init, "_sie_kernel_security_backport", False): + if ( + hub_kernels.is_kernel is not _is_community_kernel + or modeling_utils.is_kernel is not _is_community_kernel + ): + raise RuntimeError("The installed Transformers kernel security guard was replaced.") + return False + + @functools.wraps(original_init) + def guarded_init(self: Any, *args: Any, **kwargs: Any) -> None: + # The property names are aliases for the private storage fields. + # Keeping either alias would let model-owned JSON restore the same + # vulnerable value after the native constructor chooses attention. + # Native attention propagation follows class-defined sub_configs. + # Model-owned maps could redirect it to __class__ and mutate the + # shared configuration class during an otherwise ordinary load. + forbidden_structure = _CONFIG_STRUCTURE_KEYS.intersection(kwargs) + if forbidden_structure: + raise ValueError("Model configuration cannot override Python object or config attribute structure.") + clean_kwargs = {key: value for key, value in kwargs.items() if key not in _PRIVATE_IMPLEMENTATION_KEYS} + original_init(self, *args, **clean_kwargs) + + guarded_init.__dict__["_sie_kernel_security_backport"] = True + # Both functions resolve their globals in their original modules. The + # native kernel loader therefore also sees the restricted predicate. + # Apply this policy before exposing the constructor guard to callers. + hub_kernels.is_kernel = _is_community_kernel # ty: ignore[invalid-assignment] + modeling_utils.is_kernel = _is_community_kernel # ty: ignore[invalid-assignment] + PretrainedConfig.__init__ = guarded_init # ty: ignore[invalid-assignment] + return True diff --git a/packages/sie_server/src/sie_server/adapters/bert_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/bert_flash/__init__.py index 7008d5e7a..4cb383b76 100644 --- a/packages/sie_server/src/sie_server/adapters/bert_flash/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/bert_flash/__init__.py @@ -26,6 +26,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy from sie_server.adapters._utils import ( extract_texts, @@ -154,6 +155,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs) # Load model with eager attention - we'll run our own flash attention diff --git a/packages/sie_server/src/sie_server/adapters/bert_flash_cross_encoder/__init__.py b/packages/sie_server/src/sie_server/adapters/bert_flash_cross_encoder/__init__.py index 9faa8f1ec..bc0430231 100644 --- a/packages/sie_server/src/sie_server/adapters/bert_flash_cross_encoder/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/bert_flash_cross_encoder/__init__.py @@ -8,6 +8,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._utils import extract_text from sie_server.core.inference_output import ScoreOutput @@ -111,6 +112,7 @@ def load(self, device: str) -> None: shared_kwargs["revision"] = self._revision # Load tokenizer + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/bge_m3/__init__.py b/packages/sie_server/src/sie_server/adapters/bge_m3/__init__.py index 9e06a35e3..d54c6154b 100644 --- a/packages/sie_server/src/sie_server/adapters/bge_m3/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/bge_m3/__init__.py @@ -25,6 +25,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters.bge_m3_score_mixin import BGEM3ScoreMixin from sie_server.core.inference_output import EncodeOutput, SparseVector @@ -122,6 +123,7 @@ def load(self, device: str) -> None: shared_kwargs["revision"] = self._revision # Load tokenizer (fast Rust tokenizer) + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs) # Load base model with Flash Attention 2 if available diff --git a/packages/sie_server/src/sie_server/adapters/bge_m3_flag/__init__.py b/packages/sie_server/src/sie_server/adapters/bge_m3_flag/__init__.py index b0a5879ee..462ae5172 100644 --- a/packages/sie_server/src/sie_server/adapters/bge_m3_flag/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/bge_m3_flag/__init__.py @@ -22,6 +22,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters.bge_m3_score_mixin import BGEM3ScoreMixin from sie_server.core.inference_output import EncodeOutput, SparseVector @@ -122,6 +123,7 @@ def load(self, device: str) -> None: # kwarg — its constructor forwards nothing to the internal # ``from_pretrained`` load — so ``self._revision`` cannot be honored on this # path. Use the ``bge_m3`` / ``bge_m3_flash`` adapters to pin a revision. + install_transformers_security_fixes() self._model = BGEM3FlagModel( self._model_name_or_path, use_fp16=use_fp16, diff --git a/packages/sie_server/src/sie_server/adapters/bge_m3_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/bge_m3_flash/__init__.py index b3f603c2b..60cadf22f 100644 --- a/packages/sie_server/src/sie_server/adapters/bge_m3_flash/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/bge_m3_flash/__init__.py @@ -14,6 +14,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._flash_pack import build_position_ids from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters._utils import validate_output_types from sie_server.adapters.bge_m3_score_mixin import BGEM3ScoreMixin @@ -114,6 +115,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs) # Load model with eager attention - we'll run our own flash attention diff --git a/packages/sie_server/src/sie_server/adapters/clip/__init__.py b/packages/sie_server/src/sie_server/adapters/clip/__init__.py index 2270c22ba..135228fc8 100644 --- a/packages/sie_server/src/sie_server/adapters/clip/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/clip/__init__.py @@ -31,6 +31,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.core.inference_output import EncodeOutput from sie_server.core.preprocessor import ImagePreprocessor @@ -161,6 +162,7 @@ def load(self, device: str) -> None: shared_kwargs["revision"] = self._revision # Load processor (handles both text tokenization and image preprocessing) + install_transformers_security_fixes() self._processor = CLIPProcessor.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/colbert/__init__.py b/packages/sie_server/src/sie_server/adapters/colbert/__init__.py index 575ad4ed5..09a3d1f06 100644 --- a/packages/sie_server/src/sie_server/adapters/colbert/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/colbert/__init__.py @@ -39,6 +39,7 @@ from sie_server.adapters._multivector import maxsim_scores_batched from sie_server.adapters._pylate_dense import apply_dense_chain, load_pylate_dense_chain from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._utils import grouped_score_pairs from sie_server.core.inference_output import EncodeOutput, ScoreOutput @@ -221,6 +222,7 @@ def load(self, device: str) -> None: if self._code_revision is not None: shared_kwargs["code_revision"] = self._code_revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/colbert_modernbert_flash/adapter.py b/packages/sie_server/src/sie_server/adapters/colbert_modernbert_flash/adapter.py index 38fdcb101..be19e2065 100644 --- a/packages/sie_server/src/sie_server/adapters/colbert_modernbert_flash/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/colbert_modernbert_flash/adapter.py @@ -28,6 +28,7 @@ from sie_server.adapters._multivector import maxsim_scores_batched from sie_server.adapters._pylate_dense import apply_dense_chain, load_pylate_dense_chain from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._utils import ( grouped_score_pairs, @@ -182,6 +183,7 @@ def load(self, device: str) -> None: dtype, ) + install_transformers_security_fixes() self._tokenizer = self._load_tokenizer() if self._doc_punctuation_skiplist: diff --git a/packages/sie_server/src/sie_server/adapters/colbert_rotary_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/colbert_rotary_flash/__init__.py index 9fc9d6d37..3a7734c72 100644 --- a/packages/sie_server/src/sie_server/adapters/colbert_rotary_flash/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/colbert_rotary_flash/__init__.py @@ -11,6 +11,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._multivector import maxsim_scores_batched from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._utils import ( apply_rotary_pos_emb, @@ -152,6 +153,7 @@ def load(self, device: str) -> None: if self._code_revision is not None: shared_kwargs["code_revision"] = self._code_revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, trust_remote_code=True, diff --git a/packages/sie_server/src/sie_server/adapters/colpali/__init__.py b/packages/sie_server/src/sie_server/adapters/colpali/__init__.py index 73e32f72f..1f18a65da 100644 --- a/packages/sie_server/src/sie_server/adapters/colpali/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/colpali/__init__.py @@ -29,6 +29,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._multivector import maxsim_scores from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.core.inference_output import EncodeOutput from sie_server.core.postprocessor import MuveraConfig, MuveraPostprocessor @@ -153,6 +154,7 @@ def load(self, device: str) -> None: self._processor_load_kwargs = dict(shared_kwargs) # Load processor + install_transformers_security_fixes() self._processor = ColPaliProcessor.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/colqwen2/__init__.py b/packages/sie_server/src/sie_server/adapters/colqwen2/__init__.py index 68291a562..12eb1f732 100644 --- a/packages/sie_server/src/sie_server/adapters/colqwen2/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/colqwen2/__init__.py @@ -33,6 +33,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._multivector import maxsim_scores from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed from sie_server.core.inference_output import EncodeOutput @@ -222,6 +223,7 @@ def load(self, device: str) -> None: } if self._revision is not None: proc_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._processor = Qwen2VLProcessor.from_pretrained(self._model_name_or_path, **proc_kwargs) tokenizer = self._processor.tokenizer # ty: ignore[unresolved-attribute] tokenizer.padding_side = "left" diff --git a/packages/sie_server/src/sie_server/adapters/colqwen3/__init__.py b/packages/sie_server/src/sie_server/adapters/colqwen3/__init__.py index 7bfdb2508..1910c412d 100644 --- a/packages/sie_server/src/sie_server/adapters/colqwen3/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/colqwen3/__init__.py @@ -12,6 +12,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._multivector import maxsim_scores from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed from sie_server.core.inference_output import EncodeOutput @@ -125,6 +126,7 @@ def load(self, device: str) -> None: attn_impl, ) + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained( self._model_name_or_path, trust_remote_code=self._trust_remote_code, diff --git a/packages/sie_server/src/sie_server/adapters/colsmol/__init__.py b/packages/sie_server/src/sie_server/adapters/colsmol/__init__.py index 3ca7e6e21..6dfe55908 100644 --- a/packages/sie_server/src/sie_server/adapters/colsmol/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/colsmol/__init__.py @@ -41,6 +41,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._multivector import maxsim_scores from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.core.inference_output import EncodeOutput from sie_server.core.postprocessor import MuveraConfig, MuveraPostprocessor @@ -186,6 +187,7 @@ def load(self, device: str) -> None: # The colSmol-* repos are LoRA adapters; discover the full base checkpoint # (which carries the ColIdefics3 projection ``linear``) from adapter_config. + install_transformers_security_fixes() peft_cfg = PeftConfig.from_pretrained(self._model_name_or_path, revision=self._revision) base_id = peft_cfg.base_model_name_or_path if not base_id: diff --git a/packages/sie_server/src/sie_server/adapters/docling/adapter.py b/packages/sie_server/src/sie_server/adapters/docling/adapter.py index ea7fe2dbd..d3b4fcf4a 100644 --- a/packages/sie_server/src/sie_server/adapters/docling/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/docling/adapter.py @@ -8,6 +8,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.core.inference_output import ExtractItemError, ExtractOutput from sie_server.types.inputs import InvalidMediaError, is_image_input, media_bytes from sie_server.types.responses import ErrorCode @@ -164,6 +165,7 @@ def load(self, device: str) -> None: # Models cache globally, so subsequent per-task converters are cheap. # This is the born-digital parse path that BOTH profiles depend on, so # a staged artifact root keeps it fail-closed. + install_transformers_security_fixes() try: warm_converter = self._get_converter(ocr_enabled=False) _payload, _pages, warm_error = self._convert_bytes( diff --git a/packages/sie_server/src/sie_server/adapters/donut/__init__.py b/packages/sie_server/src/sie_server/adapters/donut/__init__.py index 6ef6ce89b..87913aa1e 100644 --- a/packages/sie_server/src/sie_server/adapters/donut/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/donut/__init__.py @@ -30,6 +30,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.core.inference_output import EncodeOutput, ExtractOutput from sie_server.types.inputs import decode_image @@ -139,6 +140,7 @@ def load(self, device: str) -> None: ) # Load processor + install_transformers_security_fixes() self._processor = DonutProcessor.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/florence2/__init__.py b/packages/sie_server/src/sie_server/adapters/florence2/__init__.py index e62070112..b66de5345 100644 --- a/packages/sie_server/src/sie_server/adapters/florence2/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/florence2/__init__.py @@ -26,6 +26,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.core.inference_output import EncodeOutput, ExtractOutput from sie_server.core.preprocessor.vision import resolve_florence2_prompt @@ -146,6 +147,7 @@ def load(self, device: str) -> None: # Load the checkpoint's own (remote) processor. AutoProcessor alone is not # safe on transformers >= 4.49 — see _load_processor for the incompatibility. + install_transformers_security_fixes() self._processor = self._load_processor(shared_kwargs) # Load model with trust_remote_code to use model's custom code diff --git a/packages/sie_server/src/sie_server/adapters/gliclass/__init__.py b/packages/sie_server/src/sie_server/adapters/gliclass/__init__.py index 1f0466359..3871fa514 100644 --- a/packages/sie_server/src/sie_server/adapters/gliclass/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/gliclass/__init__.py @@ -74,6 +74,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters.base import released_bytes from sie_server.adapters.errors import InputTooLongError @@ -596,6 +597,7 @@ def load(self, device: str) -> None: shared_kwargs: dict[str, Any] = {} if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() config = GLiClassModelConfig.from_pretrained(self._model_name_or_path, **shared_kwargs) if _restore_modernbert_rope_fields(config.encoder_config): model = GLiClassModel.from_pretrained(self._model_name_or_path, config=config, **shared_kwargs) diff --git a/packages/sie_server/src/sie_server/adapters/gliformer/adapter.py b/packages/sie_server/src/sie_server/adapters/gliformer/adapter.py index 39bfa4032..8da528f27 100644 --- a/packages/sie_server/src/sie_server/adapters/gliformer/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/gliformer/adapter.py @@ -29,6 +29,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters.base import released_bytes from sie_server.adapters.gliformer.cuda_graphs import GRAPH_MODES, EncoderGraphRunner, unsupported_reason @@ -724,6 +725,7 @@ def load(self, device: str) -> None: ignore_patterns=_SNAPSHOT_IGNORE_PATTERNS, ) + install_transformers_security_fixes() with warnings.catch_warnings(): # The checkpoints request the optional flashdeberta kernels. Without # them GLiFormer uses its eager attention, which is what SIE serves. diff --git a/packages/sie_server/src/sie_server/adapters/gliner/__init__.py b/packages/sie_server/src/sie_server/adapters/gliner/__init__.py index 0c4b4488c..08f9f69b6 100644 --- a/packages/sie_server/src/sie_server/adapters/gliner/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/gliner/__init__.py @@ -46,6 +46,7 @@ gliner_prompt_counter, ) from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._word_window import ( MAX_DOCUMENT_WINDOWS, @@ -202,6 +203,7 @@ def load(self, device: str) -> None: load_kwargs: dict[str, Any] = {} if self._revision is not None: load_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._model = GLiNER.from_pretrained( self._model_name_or_path, **load_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/gliner2/adapter.py b/packages/sie_server/src/sie_server/adapters/gliner2/adapter.py index d38b9d684..309447be7 100644 --- a/packages/sie_server/src/sie_server/adapters/gliner2/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/gliner2/adapter.py @@ -15,6 +15,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._prompt_limit import DEFAULT_MAX_SCHEMA_PROMPT_TOKENS, PromptLimit, check_label_chars from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._word_window import ( MAX_WORD_CHARS, @@ -196,6 +197,7 @@ def load(self, device: str) -> None: model_path = snapshot_download(repo_id=model_path, revision=self._revision) use_quantize = device != "cpu" and self._compute_precision == "float16" + install_transformers_security_fixes() model = GLiNER2.from_pretrained( model_path, map_location=device, diff --git a/packages/sie_server/src/sie_server/adapters/gliner2/decide.py b/packages/sie_server/src/sie_server/adapters/gliner2/decide.py index aa2b442bf..a4170eddf 100644 --- a/packages/sie_server/src/sie_server/adapters/gliner2/decide.py +++ b/packages/sie_server/src/sie_server/adapters/gliner2/decide.py @@ -71,6 +71,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters.errors import InputTooLongError from sie_server.adapters.gliner2.decide_graphs import ( @@ -473,6 +474,7 @@ def load(self, device: str) -> None: checkpoint = self._checkpoint_dir() encoder_config = _read_json(checkpoint / "encoder_config" / "config.json") + install_transformers_security_fixes() with loadable_checkpoint(checkpoint) as path: model = AutoExtractor.from_pretrained(str(path), map_location=device) if not hasattr(model, "classifier") or not hasattr(model, "encoder"): diff --git a/packages/sie_server/src/sie_server/adapters/gliner2/entities.py b/packages/sie_server/src/sie_server/adapters/gliner2/entities.py index f7c6a50a0..4cc030416 100644 --- a/packages/sie_server/src/sie_server/adapters/gliner2/entities.py +++ b/packages/sie_server/src/sie_server/adapters/gliner2/entities.py @@ -20,6 +20,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._prompt_limit import MAX_PROMPT_CHARS_PER_TOKEN, check_label_chars from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.adapters._word_window import plan_forwards from sie_server.adapters.gliner2.decisions import MARKERS @@ -119,6 +120,7 @@ def load(self, device: str) -> None: path = self._model_name_or_path if not Path(path).is_dir(): path = snapshot_download(repo_id=path, revision=self._revision, allow_patterns=list(_CHECKPOINT_FILES)) + install_transformers_security_fixes() model = AutoExtractor.from_pretrained(path, map_location=device, quantize=False) architecture = getattr(model, "architecture", None) if architecture not in {"span", "boundary"}: diff --git a/packages/sie_server/src/sie_server/adapters/gliner_bi/__init__.py b/packages/sie_server/src/sie_server/adapters/gliner_bi/__init__.py index a64a9d260..22ea1079d 100644 --- a/packages/sie_server/src/sie_server/adapters/gliner_bi/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/gliner_bi/__init__.py @@ -11,6 +11,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._prompt_limit import check_label_chars from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._word_window import ( MAX_DOCUMENT_WINDOWS, @@ -169,6 +170,7 @@ def load(self, device: str) -> None: if self._revision is not None: load_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._model = GLiNER.from_pretrained( self._model_name_or_path, **load_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/glirel/__init__.py b/packages/sie_server/src/sie_server/adapters/glirel/__init__.py index 3d520e353..4d48ac601 100644 --- a/packages/sie_server/src/sie_server/adapters/glirel/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/glirel/__init__.py @@ -24,6 +24,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._prompt_limit import DEFAULT_MAX_PROMPT_TOKENS, PromptLimit, check_label_chars from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._word_window import SubwordCounter, WindowedSplitter, split_word_counter, subword_budget from sie_server.core.inference_output import ExtractOutput @@ -127,6 +128,7 @@ def load(self, device: str) -> None: load_kwargs: dict[str, Any] = {} if self._revision is not None: load_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._model = GLiREL.from_pretrained(self._model_name_or_path, **load_kwargs) # Move to device diff --git a/packages/sie_server/src/sie_server/adapters/glm_ocr/__init__.py b/packages/sie_server/src/sie_server/adapters/glm_ocr/__init__.py index 8e9fc2435..5cb50c4bb 100644 --- a/packages/sie_server/src/sie_server/adapters/glm_ocr/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/glm_ocr/__init__.py @@ -7,6 +7,7 @@ import torch +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed from sie_server.adapters.base import ModelAdapter, ModelCapabilities, ModelDims from sie_server.core.inference_output import EncodeOutput, ExtractOutput @@ -115,6 +116,7 @@ def load(self, device: str) -> None: self._attn_implementation, ) + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained(self._model_name_or_path, **shared_kwargs) self._model = AutoModelForImageTextToText.from_pretrained( diff --git a/packages/sie_server/src/sie_server/adapters/grounding_dino/adapter.py b/packages/sie_server/src/sie_server/adapters/grounding_dino/adapter.py index 52d6625e2..9d4c6474b 100644 --- a/packages/sie_server/src/sie_server/adapters/grounding_dino/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/grounding_dino/adapter.py @@ -36,6 +36,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.core.inference_output import EncodeOutput, ExtractOutput from sie_server.core.preprocessor import DetectionPreprocessor @@ -156,6 +157,7 @@ def load(self, device: str) -> None: dtype, ) + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/gte_sparse_flash.py b/packages/sie_server/src/sie_server/adapters/gte_sparse_flash.py index 780bd0987..467a25896 100644 --- a/packages/sie_server/src/sie_server/adapters/gte_sparse_flash.py +++ b/packages/sie_server/src/sie_server/adapters/gte_sparse_flash.py @@ -11,6 +11,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._sparse_rows import sparse_rows from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._utils import ( apply_rotary_pos_emb, @@ -150,6 +151,7 @@ def load(self, device: str) -> None: if self._code_revision is not None: shared_kwargs["code_revision"] = self._code_revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/jina_flash_cross_encoder/__init__.py b/packages/sie_server/src/sie_server/adapters/jina_flash_cross_encoder/__init__.py index 12c93e698..a4ffe7e8e 100644 --- a/packages/sie_server/src/sie_server/adapters/jina_flash_cross_encoder/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/jina_flash_cross_encoder/__init__.py @@ -7,6 +7,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._utils import extract_text from sie_server.core.inference_output import ScoreOutput @@ -100,6 +101,7 @@ def load(self, device: str) -> None: shared_kwargs["revision"] = self._revision # Load tokenizer + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/laya/adapter.py b/packages/sie_server/src/sie_server/adapters/laya/adapter.py index c240aa3a1..5c8f25a8a 100644 --- a/packages/sie_server/src/sie_server/adapters/laya/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/laya/adapter.py @@ -85,6 +85,7 @@ unsupported_reason, ) from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters.laya.model import LayaDecisionHead, LayaModel, apply_rope_parameters, split_checkpoint from sie_server.adapters.laya.questions import ( @@ -309,6 +310,7 @@ def load(self, device: str) -> None: trained_max_len = int(cfg.get("max_len", 512)) self._max_len = min(trained_max_len, self._max_seq_length) if self._max_seq_length else trained_max_len self._head_max_len = int(cfg.get("head_max_len", 192)) + install_transformers_security_fixes() self._calibration = Calibration.from_config(cfg) tokenizer = self._load_tokenizer(checkpoint_dir / "tokenizer") diff --git a/packages/sie_server/src/sie_server/adapters/lighton_ocr/adapter.py b/packages/sie_server/src/sie_server/adapters/lighton_ocr/adapter.py index b1fb95a13..3231c1095 100644 --- a/packages/sie_server/src/sie_server/adapters/lighton_ocr/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/lighton_ocr/adapter.py @@ -9,6 +9,7 @@ import torch import torch.nn.functional as F +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed from sie_server.adapters.base import ModelAdapter, ModelCapabilities, ModelDims from sie_server.adapters.lighton_ocr._markdown import lightonocr3_markdown @@ -131,6 +132,7 @@ def load(self, device: str) -> None: self._attn_implementation, ) + install_transformers_security_fixes() self._processor = LightOnOcrProcessor.from_pretrained( self._model_name_or_path, **shared_kwargs, @@ -577,6 +579,7 @@ def load(self, device: str) -> None: shared_kwargs: dict[str, Any] = {"trust_remote_code": False} if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained(self._model_name_or_path, **shared_kwargs) self._model = Qwen3_5ForConditionalGeneration.from_pretrained( self._model_name_or_path, diff --git a/packages/sie_server/src/sie_server/adapters/mineru_vl/__init__.py b/packages/sie_server/src/sie_server/adapters/mineru_vl/__init__.py index e95f121c9..a42d941b5 100644 --- a/packages/sie_server/src/sie_server/adapters/mineru_vl/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/mineru_vl/__init__.py @@ -8,6 +8,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.core.inference_output import EncodeOutput, ExtractOutput from sie_server.types.inputs import decode_image @@ -202,6 +203,7 @@ def load(self, device: str) -> None: self._attn_implementation, ) + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained( self._model_name_or_path, use_fast=True, diff --git a/packages/sie_server/src/sie_server/adapters/modernbert_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/modernbert_flash/__init__.py index c311d6366..38869fe01 100644 --- a/packages/sie_server/src/sie_server/adapters/modernbert_flash/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/modernbert_flash/__init__.py @@ -21,6 +21,7 @@ parse_graph_mode, ) from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy from sie_server.adapters._utils import ( extract_texts, @@ -154,6 +155,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/modernbert_flash_cross_encoder/__init__.py b/packages/sie_server/src/sie_server/adapters/modernbert_flash_cross_encoder/__init__.py index be8b54753..8fb2cfa7b 100644 --- a/packages/sie_server/src/sie_server/adapters/modernbert_flash_cross_encoder/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/modernbert_flash_cross_encoder/__init__.py @@ -16,6 +16,7 @@ ) from sie_server.adapters._packed_rope import packed_rope_available, rotate_packed_qkv_ from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._utils import extract_text from sie_server.core.inference_output import ScoreOutput @@ -120,6 +121,7 @@ def load(self, device: str) -> None: shared_kwargs["revision"] = self._revision # Load tokenizer + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/nemo_colembed/__init__.py b/packages/sie_server/src/sie_server/adapters/nemo_colembed/__init__.py index c9e1f4a40..3833aaee3 100644 --- a/packages/sie_server/src/sie_server/adapters/nemo_colembed/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/nemo_colembed/__init__.py @@ -35,6 +35,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._multivector import maxsim_scores from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed from sie_server.core.inference_output import EncodeOutput @@ -167,6 +168,7 @@ def load(self, device: str) -> None: if self._revision is not None: automodel_kwargs["revision"] = self._revision loaded_via_v1_path = False + install_transformers_security_fixes() try: self._model = AutoModel.from_pretrained( self._model_name_or_path, diff --git a/packages/sie_server/src/sie_server/adapters/nli_classification/__init__.py b/packages/sie_server/src/sie_server/adapters/nli_classification/__init__.py index d0f50f498..e8674ea6c 100644 --- a/packages/sie_server/src/sie_server/adapters/nli_classification/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/nli_classification/__init__.py @@ -13,6 +13,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.core.inference_output import ExtractOutput from sie_server.types.responses import Classification @@ -113,6 +114,7 @@ def load(self, device: str) -> None: } if self._revision is not None: pipeline_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._pipeline = pipeline("zero-shot-classification", **pipeline_kwargs) def _extract_text(self, item: Item) -> str: diff --git a/packages/sie_server/src/sie_server/adapters/nli_classification_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/nli_classification_flash/__init__.py index 302253a14..b85b227ff 100644 --- a/packages/sie_server/src/sie_server/adapters/nli_classification_flash/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/nli_classification_flash/__init__.py @@ -8,6 +8,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.core.extract_cost import MAX_EXTRACT_LABELS from sie_server.core.inference_output import ExtractOutput @@ -113,6 +114,7 @@ def load(self, device: str) -> None: shared_kwargs["revision"] = self._revision # Load tokenizer + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs) # Load model (use 'dtype' not deprecated 'torch_dtype') diff --git a/packages/sie_server/src/sie_server/adapters/owlv2/adapter.py b/packages/sie_server/src/sie_server/adapters/owlv2/adapter.py index 8390a6916..5c7cc2a44 100644 --- a/packages/sie_server/src/sie_server/adapters/owlv2/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/owlv2/adapter.py @@ -36,6 +36,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.core.inference_output import EncodeOutput, ExtractOutput from sie_server.core.preprocessor import DetectionPreprocessor @@ -136,6 +137,7 @@ def load(self, device: str) -> None: # 0.24 s on a 640 x 480 photo and 15.6 s on an 8192 x 1728 one on an # L4-class host, against a 0.1 s forward pass, and produced the same # 960 x 960 input to within resampling noise. + install_transformers_security_fixes() self._processor = Owlv2Processor.from_pretrained( self._model_name_or_path, use_fast=True, diff --git a/packages/sie_server/src/sie_server/adapters/paddleocr_vl/__init__.py b/packages/sie_server/src/sie_server/adapters/paddleocr_vl/__init__.py index 023cd00e6..cb10452d9 100644 --- a/packages/sie_server/src/sie_server/adapters/paddleocr_vl/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/paddleocr_vl/__init__.py @@ -9,6 +9,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed from sie_server.core.inference_output import EncodeOutput, ExtractOutput @@ -152,6 +153,7 @@ def load(self, device: str) -> None: self._attn_implementation, ) + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained( model_dir, trust_remote_code=self._trust_remote_code, diff --git a/packages/sie_server/src/sie_server/adapters/parakeet/adapter.py b/packages/sie_server/src/sie_server/adapters/parakeet/adapter.py index 33ff3796a..b8b65c66c 100644 --- a/packages/sie_server/src/sie_server/adapters/parakeet/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/parakeet/adapter.py @@ -47,6 +47,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.adapters.parakeet.decoding import TdtHypothesis, greedy_tdt_decode from sie_server.core.inference_output import EncodeOutput, ExtractOutput @@ -191,6 +192,7 @@ def load(self, device: str) -> None: device, self._dtype, ) + install_transformers_security_fixes() processor = AutoProcessor.from_pretrained(self._model_name_or_path, **revision_kwargs) model = AutoModelForTDT.from_pretrained(self._model_name_or_path, dtype=self._dtype, **revision_kwargs) model.to(device) diff --git a/packages/sie_server/src/sie_server/adapters/pplx_decider/adapter.py b/packages/sie_server/src/sie_server/adapters/pplx_decider/adapter.py index f57fd7328..bab4fc409 100644 --- a/packages/sie_server/src/sie_server/adapters/pplx_decider/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/pplx_decider/adapter.py @@ -17,6 +17,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters.errors import InputTooLongError from sie_server.adapters.pplx_decider.model import DecisionModel, checkpoint_config, load_model, validate_tokenizer @@ -119,6 +120,7 @@ def load(self, device: str) -> None: ) ) config = checkpoint_config(directory) + install_transformers_security_fixes() processor = AutoProcessor.from_pretrained(str(directory), local_files_only=True, trust_remote_code=False) processor.tokenizer.padding_side = "left" validate_tokenizer(processor.tokenizer, config) diff --git a/packages/sie_server/src/sie_server/adapters/pytorch_embedding/__init__.py b/packages/sie_server/src/sie_server/adapters/pytorch_embedding/__init__.py index 557539131..6fcc8d7f8 100644 --- a/packages/sie_server/src/sie_server/adapters/pytorch_embedding/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/pytorch_embedding/__init__.py @@ -48,6 +48,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters.peft_lora_mixin import PEFTLoRAMixin from sie_server.core.inference_output import EncodeOutput @@ -201,6 +202,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, padding_side=padding_side, diff --git a/packages/sie_server/src/sie_server/adapters/qwen2_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/qwen2_flash/__init__.py index c4c7bbf35..b66e3b255 100644 --- a/packages/sie_server/src/sie_server/adapters/qwen2_flash/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/qwen2_flash/__init__.py @@ -13,6 +13,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._flash_pack import build_position_ids, mean_pool_packed from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy from sie_server.adapters._utils import ( apply_rotary_pos_emb, @@ -152,6 +153,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs) # Load config first to disable optional xformers features diff --git a/packages/sie_server/src/sie_server/adapters/qwen2_flash_cross_encoder/adapter.py b/packages/sie_server/src/sie_server/adapters/qwen2_flash_cross_encoder/adapter.py index d217fc800..ca6889005 100644 --- a/packages/sie_server/src/sie_server/adapters/qwen2_flash_cross_encoder/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/qwen2_flash_cross_encoder/adapter.py @@ -11,6 +11,7 @@ from sie_server.adapters._flash_pack import build_position_ids from sie_server.adapters._modernbert_flash_graphs import PackedForward, VarlenGraphRunner, unsupported_reason from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ERR_REQUIRES_TEXT, ComputePrecision from sie_server.adapters._utils import apply_rotary_pos_emb from sie_server.adapters.qwen2_flash import _fused_layers @@ -311,6 +312,7 @@ def load(self, device: str) -> None: shared_kwargs["revision"] = self._revision # Load tokenizer + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/qwen3_vl_embedding/__init__.py b/packages/sie_server/src/sie_server/adapters/qwen3_vl_embedding/__init__.py index 352680bfa..7349013fb 100644 --- a/packages/sie_server/src/sie_server/adapters/qwen3_vl_embedding/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/qwen3_vl_embedding/__init__.py @@ -11,6 +11,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed from sie_server.core.inference_output import EncodeOutput @@ -167,6 +168,7 @@ def load(self, device: str) -> None: } if self._revision is not None: proc_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained( self._model_name_or_path, **proc_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/qwen3_vl_reranker/adapter.py b/packages/sie_server/src/sie_server/adapters/qwen3_vl_reranker/adapter.py index cbecf1272..ebd4f28eb 100644 --- a/packages/sie_server/src/sie_server/adapters/qwen3_vl_reranker/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/qwen3_vl_reranker/adapter.py @@ -10,6 +10,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.adapters._vision_patch_embed import rebind_vision_patch_embed from sie_server.core.inference_output import ScoreOutput @@ -151,6 +152,7 @@ def load(self, device: str) -> None: # file reference is None in some transformers versions). Try # AutoProcessor first; if that fails, load processor from the base # Qwen3-VL-2B-Instruct model and override its tokenizer. + install_transformers_security_fixes() try: self._processor = AutoProcessor.from_pretrained( self._model_name_or_path, diff --git a/packages/sie_server/src/sie_server/adapters/rope_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/rope_flash/__init__.py index ecc7a95e8..0494048bc 100644 --- a/packages/sie_server/src/sie_server/adapters/rope_flash/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/rope_flash/__init__.py @@ -10,6 +10,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._flash_pack import mean_pool_packed from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy from sie_server.adapters._utils import ( apply_rotary_pos_emb, @@ -123,6 +124,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs) # Load config first to disable optional xformers features diff --git a/packages/sie_server/src/sie_server/adapters/sam3/adapter.py b/packages/sie_server/src/sie_server/adapters/sam3/adapter.py index 618d03d26..892947767 100644 --- a/packages/sie_server/src/sie_server/adapters/sam3/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/sam3/adapter.py @@ -43,6 +43,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.core.inference_output import EncodeOutput, ExtractOutput from sie_server.core.preprocessor import DetectionPreprocessor @@ -232,6 +233,7 @@ def load(self, device: str) -> None: logger.info("Loading SAM 3 model %s on device=%s with dtype=%s", self._model_name_or_path, device, dtype) + install_transformers_security_fixes() self._processor = processor_class.from_pretrained(self._model_name_or_path, **shared_kwargs) self._model = model_class.from_pretrained(self._model_name_or_path, dtype=dtype, **shared_kwargs) self._model.to(device) diff --git a/packages/sie_server/src/sie_server/adapters/siglip/adapter.py b/packages/sie_server/src/sie_server/adapters/siglip/adapter.py index fea4249c3..ab5b045cd 100644 --- a/packages/sie_server/src/sie_server/adapters/siglip/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/siglip/adapter.py @@ -55,6 +55,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.core.inference_output import EncodeOutput from sie_server.core.preprocessor.image import ImagePreprocessor, OpenCLIPImagePreprocessor @@ -273,6 +274,7 @@ def _load_transformers(self, device: str, dtype: torch.dtype) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._processor = SiglipProcessor.from_pretrained( self._model_name_or_path, use_fast=False, @@ -334,6 +336,10 @@ def _load_open_clip(self, device: str, dtype: torch.dtype) -> None: assert self._open_clip_model_id is not None # guarded in __init__ if not self._open_clip_model_id.startswith("hf-hub:"): + # Match the native factory's legacy ViT name normalization. + model_cfg = open_clip.get_model_config(self._open_clip_model_id.replace("/", "-")) + if model_cfg is not None and model_cfg.get("text_cfg", {}).get("hf_model_name"): + install_transformers_security_fixes() model, _train_preproc, val_preproc = open_clip.create_model_and_transforms(self._open_clip_model_id) tokenizer = open_clip.get_tokenizer(self._open_clip_model_id) else: @@ -396,6 +402,9 @@ def _load_open_clip(self, device: str, dtype: torch.dtype) -> None: except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError) as exc: raise RuntimeError(f"invalid pinned OpenCLIP config for {repo_id}@{self._revision}") from exc + if text_cfg.get("hf_model_name"): + install_transformers_security_fixes() + # Keep tokenization in the same immutable model snapshot. The upstream # OpenCLIP config names a separate tokenizer repo, which would otherwise # reintroduce default-branch drift after the checkpoint itself is pinned. diff --git a/packages/sie_server/src/sie_server/adapters/splade_flash/adapter.py b/packages/sie_server/src/sie_server/adapters/splade_flash/adapter.py index 0e56b5c8f..ba225b972 100644 --- a/packages/sie_server/src/sie_server/adapters/splade_flash/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/splade_flash/adapter.py @@ -15,6 +15,7 @@ from sie_server.adapters._flash_pack import build_position_ids from sie_server.adapters._sparse_rows import sparse_rows from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters._utils import extract_texts, resolve_query_instruction, validate_output_types from sie_server.adapters.base import ModelAdapter @@ -128,6 +129,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/stablebridge_pruner/__init__.py b/packages/sie_server/src/sie_server/adapters/stablebridge_pruner/__init__.py index 16c27d51e..c17eeda91 100644 --- a/packages/sie_server/src/sie_server/adapters/stablebridge_pruner/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/stablebridge_pruner/__init__.py @@ -13,6 +13,7 @@ from torch import nn from transformers import AutoModelForSequenceClassification, AutoTokenizer +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters.base import ModelAdapter, ModelCapabilities, ModelDims from sie_server.core.inference_output import ExtractOutput, ScoreOutput from sie_server.core.preprocessor import CharCountPreprocessor @@ -125,6 +126,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/topk_embed/adapter.py b/packages/sie_server/src/sie_server/adapters/topk_embed/adapter.py index b355e498f..36d3b5baa 100644 --- a/packages/sie_server/src/sie_server/adapters/topk_embed/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/topk_embed/adapter.py @@ -53,6 +53,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._multivector import maxsim_scores_batched from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.adapters._utils import grouped_score_pairs, validate_output_types from sie_server.adapters.topk_embed import vision_rotary @@ -276,6 +277,7 @@ def load(self, device: str) -> None: topk = {key: raw.pop(key) for key in _TOPK_CONFIG_KEYS if key in raw} for key in _REMOTE_CODE_CONFIG_KEYS: raw.pop(key, None) + install_transformers_security_fixes() config = Qwen3_5Config(**raw) # Bidirectional full-attention layers; the linear-attention layers stay causal recurrences. config.text_config.is_causal = False diff --git a/packages/sie_server/src/sie_server/adapters/whisper/adapter.py b/packages/sie_server/src/sie_server/adapters/whisper/adapter.py index 04a53c6c8..9725d4e27 100644 --- a/packages/sie_server/src/sie_server/adapters/whisper/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/whisper/adapter.py @@ -12,6 +12,7 @@ from sie_server.adapters._base_adapter import BaseAdapter from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.core.inference_output import EncodeOutput, ExtractOutput from sie_server.core.prepared import AudioPayload @@ -79,6 +80,7 @@ def load(self, device: str) -> None: dtype, self._attn_implementation, ) + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained( self._model_name_or_path, **shared_kwargs, diff --git a/packages/sie_server/src/sie_server/adapters/xlm_roberta_flash/__init__.py b/packages/sie_server/src/sie_server/adapters/xlm_roberta_flash/__init__.py index 397a17eaf..a9eeec817 100644 --- a/packages/sie_server/src/sie_server/adapters/xlm_roberta_flash/__init__.py +++ b/packages/sie_server/src/sie_server/adapters/xlm_roberta_flash/__init__.py @@ -12,6 +12,7 @@ from sie_server.adapters._flash_base import FlashBaseAdapter from sie_server.adapters._flash_pack import build_position_ids, mean_pool_packed from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy from sie_server.adapters._utils import extract_texts, validate_output_types from sie_server.adapters.peft_lora_mixin import PEFTLoRAMixin @@ -117,6 +118,7 @@ def load(self, device: str) -> None: if self._revision is not None: shared_kwargs["revision"] = self._revision + install_transformers_security_fixes() self._tokenizer = AutoTokenizer.from_pretrained(self._model_name_or_path, **shared_kwargs) # Load model with eager attention - we'll run our own flash attention diff --git a/packages/sie_server/tests/adapters/test_lighton_ocr3.py b/packages/sie_server/tests/adapters/test_lighton_ocr3.py index 719087b75..e4c91df4a 100644 --- a/packages/sie_server/tests/adapters/test_lighton_ocr3.py +++ b/packages/sie_server/tests/adapters/test_lighton_ocr3.py @@ -286,6 +286,7 @@ def load_model(name: str, **kwargs: Any) -> _Model: transformers = ModuleType("transformers") transformers.__dict__.update( + __version__="5.17.0", AutoProcessor=SimpleNamespace(from_pretrained=load_processor), Qwen3_5ForConditionalGeneration=SimpleNamespace(from_pretrained=load_model), ) @@ -353,6 +354,7 @@ def test_load_rebinds_qwen35_patch_embed_to_equivalent_linear(monkeypatch: pytes expected = conv(patches).clone() transformers = ModuleType("transformers") transformers.__dict__.update( + __version__="5.17.0", AutoProcessor=SimpleNamespace(from_pretrained=lambda *_, **__: _Processor()), Qwen3_5ForConditionalGeneration=SimpleNamespace(from_pretrained=lambda *_, **__: loaded_model), ) diff --git a/packages/sie_server/tests/adapters/test_sam3.py b/packages/sie_server/tests/adapters/test_sam3.py index 408e41e5d..8bd74d5cb 100644 --- a/packages/sie_server/tests/adapters/test_sam3.py +++ b/packages/sie_server/tests/adapters/test_sam3.py @@ -194,6 +194,7 @@ def test_load_uses_sam3_classes_revision_and_dtype(monkeypatch, device, precisio sys.modules, "transformers", SimpleNamespace( + __version__="5.17.0", Sam3Processor=SimpleNamespace(from_pretrained=processor_loader), Sam3Model=SimpleNamespace(from_pretrained=model_loader), ), diff --git a/packages/sie_server/tests/adapters/test_transformers_compat.py b/packages/sie_server/tests/adapters/test_transformers_compat.py new file mode 100644 index 000000000..834af5408 --- /dev/null +++ b/packages/sie_server/tests/adapters/test_transformers_compat.py @@ -0,0 +1,250 @@ +"""Offline adversarial controls for the actual Transformers 4 model loader.""" + +from __future__ import annotations + +import copy +import json +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import Any + +import pytest +import torch +import transformers +from sie_server.adapters._transformers_compat import install_transformers_security_fixes +from transformers import ( + AutoModelForCausalLM, + CLIPConfig, + LlamaConfig, + LlamaForCausalLM, + PretrainedConfig, + modeling_utils, +) +from transformers.integrations import hub_kernels + +pytestmark = pytest.mark.skipif(transformers.__version__ != "4.57.6", reason="Tests the exact supported 4.x runtime") + +_ATTACKER_KERNEL = "sie-security-probe.invalid/owned-marker:attention" +# Capture native bindings at collection, before adapter tests install the +# permanent production policy. Restore the preceding test's policy on teardown. +_NATIVE_CONFIG_INIT = PretrainedConfig.__init__ +_NATIVE_HUB_IS_KERNEL = hub_kernels.is_kernel +_NATIVE_MODEL_IS_KERNEL = modeling_utils.is_kernel + + +@pytest.fixture(autouse=True) +def _restore_loader_and_disable_network(monkeypatch: pytest.MonkeyPatch) -> None: + # Production installs permanently. Tests restore the original bindings so a + # preceding test cannot make an unguarded negative control pass accidentally. + monkeypatch.setenv("HF_HUB_OFFLINE", "1") + monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1") + assert not getattr(_NATIVE_CONFIG_INIT, "_sie_kernel_security_backport", False) + monkeypatch.setattr(PretrainedConfig, "__init__", _NATIVE_CONFIG_INIT) + monkeypatch.setattr(hub_kernels, "is_kernel", _NATIVE_HUB_IS_KERNEL) + monkeypatch.setattr(modeling_utils, "is_kernel", _NATIVE_MODEL_IS_KERNEL) + + +@pytest.fixture +def checkpoint(tmp_path: Path) -> Path: + with torch.random.fork_rng(): + torch.manual_seed(137) + config = LlamaConfig( + vocab_size=16, + hidden_size=8, + intermediate_size=16, + num_hidden_layers=1, + num_attention_heads=2, + num_key_value_heads=2, + attn_implementation="eager", + ) + LlamaForCausalLM(config).save_pretrained(tmp_path) + return tmp_path + + +@pytest.fixture +def kernel_requests(monkeypatch: pytest.MonkeyPatch) -> list[str]: + calls: list[str] = [] + + def intercept(repo_id: str, **kwargs: Any) -> None: + calls.append(repo_id) + raise RuntimeError("Owned offline kernel boundary reached; no fetch or external code executed") + + # Exercise the real native loader even if the optional kernels package is + # absent. Intercept only its network/code-loading boundary. + monkeypatch.setattr(hub_kernels, "_kernels_available", True) + monkeypatch.setattr(hub_kernels, "get_kernel", intercept, raising=False) + return calls + + +def _write_config(checkpoint: Path, updates: dict[str, Any]) -> None: + path = checkpoint / "config.json" + config = json.loads(path.read_text()) + config.update(updates) + path.write_text(json.dumps(config)) + + +@pytest.mark.parametrize("field", ["_attn_implementation_internal", "_attn_implementation"]) +@pytest.mark.parametrize("trust", [False, None, True]) +def test_unguarded_checkpoint_reaches_kernel_boundary( + checkpoint: Path, kernel_requests: list[str], field: str, trust: bool | None +) -> None: + _write_config(checkpoint, {field: _ATTACKER_KERNEL}) + with pytest.raises(ValueError, match="Owned offline kernel boundary reached"): + AutoModelForCausalLM.from_pretrained( + checkpoint, local_files_only=True, trust_remote_code=trust, attn_implementation="eager" + ) + assert kernel_requests == ["sie-security-probe.invalid/owned-marker"] + + +@pytest.mark.parametrize("field", ["_attn_implementation_internal", "_attn_implementation"]) +@pytest.mark.parametrize("trust", [False, None, True]) +def test_checkpoint_cannot_replace_explicit_attention( + checkpoint: Path, kernel_requests: list[str], field: str, trust: bool | None +) -> None: + _write_config(checkpoint, {field: _ATTACKER_KERNEL}) + assert install_transformers_security_fixes() + model = AutoModelForCausalLM.from_pretrained( + checkpoint, local_files_only=True, trust_remote_code=trust, attn_implementation="eager" + ) + assert type(model) is LlamaForCausalLM + assert model.config._attn_implementation == "eager" + assert not kernel_requests + + +@pytest.mark.parametrize( + "kernel", + [ + _ATTACKER_KERNEL, + "paged|sie-security-probe.invalid/owned-marker@main:attention", + "kernels-community.evil/owned-marker", + "Kernels-community/owned-marker", + " kernels-community/owned-marker", + "kernels-community/../owned-marker", + "other|kernels-community/owned-marker", + "kernels-community/name|owned-marker:attention", + "kernels-community/name@main|owned-marker:attention", + "kernels-community/name:attention|owned-marker", + "kernels-community/name@main@other:attention", + "paged|paged|kernels-community/owned-marker:attention", + "kernels-community/owned-marker:attention\n", + ], +) +def test_explicit_foreign_kernel_never_reaches_loader( + checkpoint: Path, kernel_requests: list[str], kernel: str +) -> None: + install_transformers_security_fixes() + with pytest.raises(ValueError, match="not supported"): + AutoModelForCausalLM.from_pretrained( + checkpoint, local_files_only=True, trust_remote_code=False, attn_implementation=kernel + ) + # The direct native entrypoint and its modeling_utils alias must also be + # guarded; checking only the caller's predicate would leave a second route. + hub_kernels.load_and_register_kernel(kernel) + modeling_utils.load_and_register_kernel(kernel) + assert not kernel_requests + + +@pytest.mark.parametrize( + "kernel", + [ + "kernels-community/owned-marker:attention", + "kernels-community/owned-marker@main:attention", + "paged|kernels-community/owned-marker@main:attention", + ], +) +def test_upstream_community_namespace_remains_available(kernel_requests: list[str], kernel: str) -> None: + install_transformers_security_fixes() + with pytest.raises(ValueError, match="Owned offline kernel boundary reached"): + hub_kernels.load_and_register_kernel(kernel) + assert kernel_requests == ["kernels-community/owned-marker"] + + +@pytest.mark.parametrize("field", ["_attn_implementation_internal", "_attn_implementation"]) +def test_nested_native_configs_discard_serialized_private_attention(field: str) -> None: + data = { + "text_config": {"hidden_size": 8, "num_attention_heads": 2, field: _ATTACKER_KERNEL}, + "vision_config": {"hidden_size": 8, "num_attention_heads": 2, field: _ATTACKER_KERNEL}, + } + untouched = copy.deepcopy(data) + native_subconfigs = dict(CLIPConfig.sub_configs) + install_transformers_security_fixes() + config = CLIPConfig(**data) + assert config.text_config._attn_implementation is None + assert config.vision_config._attn_implementation is None + assert data == untouched + assert CLIPConfig.sub_configs == native_subconfigs + + +@pytest.mark.parametrize("field", ["__dict__", "__class__", "attribute_map", "sub_configs"]) +def test_checkpoint_cannot_replace_config_structure(checkpoint: Path, kernel_requests: list[str], field: str) -> None: + attack = ( + {"__class__": "owned-json-marker"} + if field == "sub_configs" + else {"_attn_implementation_internal": _ATTACKER_KERNEL} + ) + _write_config(checkpoint, {field: attack}) + native_attention_property = LlamaConfig._attn_implementation + install_transformers_security_fixes() + with pytest.raises(ValueError, match="cannot override"): + AutoModelForCausalLM.from_pretrained(checkpoint, local_files_only=True, trust_remote_code=False) + assert not kernel_requests + assert LlamaConfig._attn_implementation is native_attention_property + + +@pytest.mark.parametrize("attention", ["eager", "sdpa"]) +def test_native_forward_weights_and_outputs_unchanged(checkpoint: Path, attention: str) -> None: + before = AutoModelForCausalLM.from_pretrained(checkpoint, local_files_only=True, attn_implementation=attention) + original_forward = LlamaForCausalLM.forward + tokens = torch.tensor([[1, 2, 3]]) + with torch.inference_mode(): + expected = before(tokens).logits + install_transformers_security_fixes() + after = AutoModelForCausalLM.from_pretrained(checkpoint, local_files_only=True, attn_implementation=attention) + assert type(before) is type(after) is LlamaForCausalLM + assert LlamaForCausalLM.forward is original_forward + assert before.config.to_dict() == after.config.to_dict() + for key, value in before.state_dict().items(): + assert torch.equal(value, after.state_dict()[key]) + with torch.inference_mode(): + assert torch.equal(expected, after(tokens).logits) + + +def test_installation_is_idempotent_across_concurrent_loaders(kernel_requests: list[str]) -> None: + original_forward = LlamaForCausalLM.forward + + def load(_: int) -> bool: + installed = install_transformers_security_fixes() + config = LlamaConfig(_attn_implementation_internal=_ATTACKER_KERNEL) + assert config._attn_implementation is None + return installed + + with ThreadPoolExecutor(max_workers=8) as executor: + assert sum(executor.map(load, range(32))) == 1 + assert LlamaForCausalLM.forward is original_forward + assert not kernel_requests + + +@pytest.mark.parametrize("version", ["4.57.5", "5.0.0", "5.2.0", "5.3.0rc1"]) +def test_unsupported_vulnerable_runtime_fails_before_mutation(monkeypatch: pytest.MonkeyPatch, version: str) -> None: + original = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel) + # Importing model classes can replace the root lazy module; resolve the + # current sys.modules binding, as the optional installer import does. + monkeypatch.setattr("transformers.__version__", version) + with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"): + install_transformers_security_fixes() + assert original == (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel) + + +@pytest.mark.parametrize("version", ["5.3.0", "5.12.1", "5.17.0"]) +def test_fixed_runtime_is_untouched(monkeypatch: pytest.MonkeyPatch, version: str) -> None: + original = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel) + monkeypatch.setattr("transformers.__version__", version) + assert not install_transformers_security_fixes() + assert original == (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel) + + +def test_replaced_guard_is_detected(monkeypatch: pytest.MonkeyPatch) -> None: + install_transformers_security_fixes() + monkeypatch.setattr(hub_kernels, "is_kernel", lambda _: True) + with pytest.raises(RuntimeError, match="guard was replaced"): + install_transformers_security_fixes() diff --git a/packages/sie_server/tests/adapters/test_transformers_security_integration.py b/packages/sie_server/tests/adapters/test_transformers_security_integration.py new file mode 100644 index 000000000..0395fdcbc --- /dev/null +++ b/packages/sie_server/tests/adapters/test_transformers_security_integration.py @@ -0,0 +1,467 @@ +"""Offline controls at SIE's actual native/dependent model-load boundaries.""" + +from __future__ import annotations + +import importlib +import json +import os +import subprocess +import sys +from collections.abc import Iterator +from pathlib import Path +from types import SimpleNamespace +from typing import Any +from unittest.mock import Mock + +import pytest +import torch +import transformers +from sie_server.adapters import _transformers_compat as compat +from sie_server.adapters.cross_encoder import CrossEncoderAdapter +from sie_server.adapters.cross_encoder.runtime_lora import CrossEncoderLoraAdapter +from sie_server.adapters.docling.adapter import DoclingAdapter +from sie_server.adapters.pytorch_embedding import PyTorchEmbeddingAdapter +from sie_server.adapters.siglip.adapter import SiglipAdapter +from sie_server.types.inputs import Item +from transformers import ( + AutoConfig, + BertConfig, + BertForSequenceClassification, + BertModel, + BertTokenizerFast, + PretrainedConfig, + modeling_utils, +) +from transformers.integrations import hub_kernels + +_EXACT_TF4 = pytest.mark.skipif(transformers.__version__ != "4.57.6", reason="Exact TF4 backport controls") +_ATTACK = "sie-security-probe.invalid/owned-marker:attention" +_PRIVATE_FIELDS = ( + "_attn_implementation_internal", + "_attn_implementation", + "_experts_implementation_internal", + "_experts_implementation", +) +_STRUCTURE_FIELDS = ("__dict__", "__class__", "attribute_map", "sub_configs") +_NATIVE_CONFIG_INIT = PretrainedConfig.__init__ +_NATIVE_HUB_IS_KERNEL = hub_kernels.is_kernel +_NATIVE_MODEL_IS_KERNEL = modeling_utils.is_kernel + + +@pytest.fixture(autouse=True) +def offline_native_loader(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: + monkeypatch.setenv("HF_HUB_OFFLINE", "1") + monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1") + assert not getattr(_NATIVE_CONFIG_INIT, "_sie_kernel_security_backport", False) + monkeypatch.setattr(PretrainedConfig, "__init__", _NATIVE_CONFIG_INIT) + monkeypatch.setattr(hub_kernels, "is_kernel", _NATIVE_HUB_IS_KERNEL) + monkeypatch.setattr(modeling_utils, "is_kernel", _NATIVE_MODEL_IS_KERNEL) + threads = torch.get_num_threads() + torch.set_num_threads(1) + yield + torch.set_num_threads(threads) + + +@pytest.fixture +def kernel_boundary(monkeypatch: pytest.MonkeyPatch) -> Mock: + boundary = Mock(side_effect=AssertionError("Forbidden kernel loader reached")) + monkeypatch.setattr(hub_kernels, "_kernels_available", True) + monkeypatch.setattr(hub_kernels, "get_kernel", boundary, raising=False) + return boundary + + +def _update_config(path: Path, **values: Any) -> None: + config = json.loads(path.read_text()) + config.update(values) + path.write_text(json.dumps(config)) + + +def _tokenizer(directory: Path) -> BertTokenizerFast: + directory.mkdir(exist_ok=True) + vocab = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]", "alice", "bob", "met", "person", "."] + vocabulary = directory / "vocab.txt" + vocabulary.write_text("\n".join(vocab) + "\n") + tokenizer = BertTokenizerFast(vocab_file=str(vocabulary), model_max_length=64) + tokenizer.save_pretrained(directory) + return tokenizer + + +def _bert_config(tokenizer: BertTokenizerFast) -> BertConfig: + return BertConfig( + vocab_size=len(tokenizer), + hidden_size=16, + intermediate_size=32, + num_hidden_layers=1, + num_attention_heads=2, + max_position_embeddings=128, + hidden_dropout_prob=0, + attention_probs_dropout_prob=0, + num_labels=1, + ) + + +@pytest.fixture +def bert_checkpoints(tmp_path: Path) -> tuple[Path, Path]: + embedding, cross_encoder = tmp_path / "embedding", tmp_path / "cross-encoder" + with torch.random.fork_rng(): + torch.manual_seed(81) + BertModel(_bert_config(_tokenizer(embedding))).save_pretrained(embedding) + BertForSequenceClassification(_bert_config(_tokenizer(cross_encoder))).save_pretrained(cross_encoder) + return embedding, cross_encoder + + +@_EXACT_TF4 +@pytest.mark.parametrize("field", _PRIVATE_FIELDS) +def test_direct_embedding_load_drops_serialized_private_settings( + bert_checkpoints: tuple[Path, Path], kernel_boundary: Mock, field: str +) -> None: + path, _ = bert_checkpoints + tokens = torch.tensor([[2, 5, 6, 3]]) + native = BertModel.from_pretrained(path, attn_implementation="eager").eval() + with torch.inference_mode(): + expected = native(tokens).last_hidden_state + _update_config(path / "config.json", **{field: _ATTACK}) + adapter = PyTorchEmbeddingAdapter(path, attn_implementation="eager", compute_precision="float32") + try: + adapter.load("cpu") + assert type(adapter._model) is BertModel + assert "forward" not in adapter._model.__dict__ + assert adapter._model.config._attn_implementation == "eager" + for key, value in native.state_dict().items(): + assert torch.equal(value, adapter._model.state_dict()[key]) + with torch.inference_mode(): + assert torch.equal(expected, adapter._model(tokens).last_hidden_state) + kernel_boundary.assert_not_called() + finally: + adapter.unload() + + +@_EXACT_TF4 +@pytest.mark.parametrize("field", _STRUCTURE_FIELDS) +def test_direct_load_rejects_config_structure_before_model_construction( + bert_checkpoints: tuple[Path, Path], kernel_boundary: Mock, field: str +) -> None: + path, _ = bert_checkpoints + _update_config(path / "config.json", **{field: {"__class__": "marker"}}) + adapter = PyTorchEmbeddingAdapter(path) + with pytest.raises(ValueError, match="config attribute structure"): + adapter.load("cpu") + assert adapter._model is None + kernel_boundary.assert_not_called() + + +@_EXACT_TF4 +@pytest.mark.parametrize("adapter_class", [CrossEncoderAdapter, CrossEncoderLoraAdapter]) +@pytest.mark.parametrize("trust", [False, True]) +def test_st_and_inherited_lora_load_install_before_native_constructor( + bert_checkpoints: tuple[Path, Path], kernel_boundary: Mock, adapter_class: type, trust: bool +) -> None: + _, path = bert_checkpoints + _update_config(path / "config.json", _attn_implementation_internal=_ATTACK) + kwargs = {"lora_paths": {"deferred": str(path / "never-read")}} if adapter_class is CrossEncoderLoraAdapter else {} + adapter = adapter_class(path, trust_remote_code=trust, compute_precision="float32", **kwargs) + try: + adapter.load("cpu") + assert type(adapter._model.model) is BertForSequenceClassification + assert "forward" not in adapter._model.model.__dict__ + scores = adapter.score(Item(text="alice"), [Item(text="alice met bob"), Item(text="bob")]) + assert len(scores) == 2 + assert torch.isfinite(torch.tensor(scores)).all() + kernel_boundary.assert_not_called() + finally: + adapter.unload() + + +@_EXACT_TF4 +@pytest.mark.parametrize("variant", ["entities", "classification", "entity_lora"]) +def test_actual_dependent_gliner2_load_and_inherited_variants( + tmp_path: Path, kernel_boundary: Mock, variant: str +) -> None: + gliner2 = pytest.importorskip("gliner2") + from gliner2.model import ExtractorConfig + from sie_server.adapters.gliner2.adapter import GLiNER2Adapter + from sie_server.adapters.gliner2.classification import GLiNER2ClassificationAdapter + from sie_server.adapters.gliner2.entity_lora import GLiNER2EntityLoraAdapter + + checkpoint = tmp_path / "extractor" + tokenizer = _tokenizer(checkpoint) + with torch.random.fork_rng(): + torch.manual_seed(83) + native = gliner2.GLiNER2( + ExtractorConfig(model_name="generated-local", max_width=3, counting_layer="count_lstm"), + encoder_config=_bert_config(tokenizer), + tokenizer=tokenizer, + ) + native.save_pretrained(str(checkpoint)) + _update_config(checkpoint / "encoder_config/config.json", _attn_implementation_internal=_ATTACK) + adapter_class = {"entities": GLiNER2Adapter, "classification": GLiNER2ClassificationAdapter}.get(variant) + if adapter_class is None: + adapter = GLiNER2EntityLoraAdapter(checkpoint, lora_paths={"deferred": str(tmp_path / "never-read")}) + else: + adapter = adapter_class(checkpoint) + try: + adapter.load("cpu") + assert type(adapter._model) is type(native) + assert type(adapter._model.encoder) is BertModel + assert "forward" not in adapter._model.encoder.__dict__ + for name, value in native.state_dict().items(): + assert torch.equal(value, adapter._model.state_dict()[name]) + tokens = torch.tensor([[2, 5, 6, 3]]) + native.eval() + adapter._model.eval() + with torch.inference_mode(): + assert torch.equal( + native.encoder(tokens).last_hidden_state, adapter._model.encoder(tokens).last_hidden_state + ) + kernel_boundary.assert_not_called() + finally: + adapter.unload() + + +@_EXACT_TF4 +def test_docling_guard_failure_is_not_swallowed_by_prewarm(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("transformers.__version__", "4.57.5") + adapter = DoclingAdapter("owned-fixture") + converter = Mock(side_effect=AssertionError("Converter must not run")) + monkeypatch.setattr(adapter, "_get_converter", converter) + monkeypatch.setattr(adapter, "_resolve_artifacts_path", lambda: None) + with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"): + adapter.load("cpu") + assert not adapter._loaded + converter.assert_not_called() + + +@_EXACT_TF4 +def test_docling_lazy_models_inherit_permanent_load_policy( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, kernel_boundary: Mock +) -> None: + BertConfig().save_pretrained(tmp_path) + _update_config(tmp_path / "config.json", _attn_implementation_internal=_ATTACK) + adapter = DoclingAdapter("owned-fixture") + monkeypatch.setattr(adapter, "_resolve_artifacts_path", lambda: None) + calls: list[bool] = [] + + def converter(ocr_enabled: bool) -> object: + config = AutoConfig.from_pretrained(tmp_path, local_files_only=True) + assert config._attn_implementation is None + calls.append(ocr_enabled) + return object() + + monkeypatch.setattr(adapter, "_make_converter", converter) + monkeypatch.setattr(adapter, "_convert_bytes", lambda *args, **kwargs: ([], 0, None)) + adapter.load("cpu") + assert adapter._loaded + assert calls == [False, True] + # The native lazy cache can reconstruct a converter after prewarming. It + # still sees the permanent policy, without a request-time installer call. + adapter._converters.clear() + adapter._get_converter(ocr_enabled=True) + assert calls == [False, True, True] + kernel_boundary.assert_not_called() + + +@_EXACT_TF4 +@pytest.mark.parametrize("variant", ["pplx", "nemo"]) +def test_guard_precedes_dependent_and_fallback_native_loaders( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, variant: str +) -> None: + monkeypatch.setattr("transformers.__version__", "4.57.5") + model = Mock(side_effect=AssertionError("Model constructor must not run")) + if variant == "pplx": + module = importlib.import_module("sie_server.adapters.pplx_decider.adapter") + monkeypatch.setattr(module, "checkpoint_config", lambda path: {}) + monkeypatch.setattr(module, "load_model", model) + adapter = module.PerplexityDecisionAdapter(str(tmp_path)) + else: + module = importlib.import_module("sie_server.adapters.nemo_colembed") + adapter = module.NemoColEmbedAdapter(str(tmp_path)) + monkeypatch.setattr(adapter, "_load_v1_dynamic", model) + monkeypatch.setattr(transformers.AutoModel, "from_pretrained", model) + with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"): + adapter.load("cpu") + model.assert_not_called() + + +@pytest.mark.parametrize("hub", [False, True]) +@pytest.mark.parametrize("hf_text", [False, True]) +def test_open_clip_checks_the_selected_text_tower( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, hub: bool, hf_text: bool +) -> None: + open_clip = pytest.importorskip("open_clip") + module = importlib.import_module("sie_server.adapters.siglip.adapter") + text_config = {"context_length": 16, **({"hf_model_name": "owned-tiny"} if hf_text else {})} + model_config = {"embed_dim": 8, "vision_cfg": {}, "text_cfg": text_config} + selected: list[str] = [] + events: list[str] = [] + monkeypatch.setattr(module, "install_transformers_security_fixes", lambda: events.append("guard")) + + def get_config(name: str) -> dict[str, Any]: + selected.append(name) + return model_config + + def create(*args: Any, **kwargs: Any) -> tuple[Any, object, object]: + assert events == (["guard"] if hf_text else []) + events.append("construct") + return SimpleNamespace(to=lambda **kw: None, eval=lambda: None), object(), object() + + monkeypatch.setattr(open_clip, "get_model_config", get_config) + monkeypatch.setattr(open_clip, "create_model_and_transforms", create) + monkeypatch.setattr(open_clip, "get_tokenizer", lambda name: object()) + model_id = "ViT/owned" # The real factory normalizes legacy slash names. + if hub: + model_id = "hf-hub:owned/tiny" + (tmp_path / "open_clip_model.safetensors").write_bytes(b"owned-placeholder-not-loaded") + (tmp_path / "open_clip_config.json").write_text( + json.dumps( + { + "model_cfg": model_config, + "preprocess_cfg": { + "mean": [0, 0, 0], + "std": [1, 1, 1], + "interpolation": "bicubic", + "resize_mode": "shortest", + }, + } + ) + ) + monkeypatch.setattr("huggingface_hub.snapshot_download", lambda **kwargs: str(tmp_path)) + monkeypatch.setattr(open_clip, "add_model_config", lambda path: None) + adapter = SiglipAdapter( + "owned", backend="open_clip", open_clip_model_id=model_id, dense_dim=8, revision="a" * 40 if hub else None + ) + adapter.load("cpu") + assert events == (["guard", "construct"] if hf_text else ["construct"]) + assert selected == ([] if hub else ["ViT-owned"]) + + +@_EXACT_TF4 +def test_actual_open_clip_hf_text_constructor_sees_guard( + bert_checkpoints: tuple[Path, Path], monkeypatch: pytest.MonkeyPatch, kernel_boundary: Mock +) -> None: + open_clip = pytest.importorskip("open_clip") + from open_clip.model import CustomTextCLIP + + path, _ = bert_checkpoints + _update_config(path / "config.json", _attn_implementation_internal=_ATTACK) + config = { + "embed_dim": 8, + "vision_cfg": {"layers": 1, "width": 32, "head_width": 16, "patch_size": 4, "image_size": 8}, + "text_cfg": { + "hf_model_name": str(path), + "hf_model_pretrained": True, + "hf_proj_type": "linear", + "hf_pooler_type": "mean_pooler", + }, + } + monkeypatch.setattr(open_clip.factory, "_MODEL_CONFIGS", {**open_clip.factory._MODEL_CONFIGS, "owned-tiny": config}) + # Tokenizer choice is independent of the HF model loader under test. + monkeypatch.setattr(open_clip, "get_tokenizer", lambda name: object()) + adapter = SiglipAdapter( + "owned", backend="open_clip", open_clip_model_id="owned-tiny", dense_dim=8, compute_precision="float32" + ) + try: + adapter.load("cpu") + assert type(adapter._model) is CustomTextCLIP + assert type(adapter._model.text.transformer) is BertModel + assert "forward" not in adapter._model.text.transformer.__dict__ + with torch.inference_mode(): + output = adapter._model.encode_text(torch.tensor([[2, 5, 6, 3]])) + assert output.shape == (1, 8) + assert torch.isfinite(output).all() + kernel_boundary.assert_not_called() + finally: + adapter.unload() + + +@pytest.mark.parametrize( + "module", + ["sie_server.adapters.remote.sie", "sie_server.adapters._transformers_compat"], +) +def test_optional_paths_do_not_import_transformers(module: str, tmp_path: Path) -> None: + script = """ +import importlib, importlib.abc, sys +class DenyTransformers(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == 'transformers' or fullname.startswith('transformers.'): + raise AssertionError('Optional path imported Transformers') +sys.meta_path.insert(0, DenyTransformers()) +importlib.import_module(sys.argv[1]) +assert not any(name == 'transformers' or name.startswith('transformers.') for name in sys.modules) +""" + result = subprocess.run( # noqa: S603 — fixed interpreter, inline script and parametrized internal module + [sys.executable, "-c", script, module], + cwd=tmp_path, + env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_actual_ctranslate2_import_adds_no_model_loading_guard(tmp_path: Path) -> None: + # CT2 already imports AutoTokenizer. Its actual adapter must not acquire a + # native model-loading policy merely by importing the shared base classes. + script = """ +import importlib.abc, sys +class DenyCompat(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == 'sie_server.adapters._transformers_compat': + raise AssertionError('CT2 imported a native model-loading guard') +sys.meta_path.insert(0, DenyCompat()) +from sie_server.adapters.ctranslate2.generation import CTranslate2GenerationAdapter, AutoTokenizer +assert CTranslate2GenerationAdapter.__module__ == 'sie_server.adapters.ctranslate2.generation' +assert AutoTokenizer.__module__ == 'transformers.models.auto.tokenization_auto' +assert 'sie_server.adapters._transformers_compat' not in sys.modules +""" + result = subprocess.run( # noqa: S603 — fixed interpreter and inline script + [sys.executable, "-c", script], + cwd=tmp_path, + env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_actual_native_open_clip_stays_available_without_transformers(tmp_path: Path) -> None: + pytest.importorskip("open_clip") + script = """ +import importlib.abc, sys +class NoTransformers(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == 'transformers' or fullname.startswith('transformers.'): + raise ImportError('Transformers intentionally unavailable') +sys.meta_path.insert(0, NoTransformers()) +import open_clip, torch +from sie_server.adapters.siglip.adapter import SiglipAdapter +torch.set_num_threads(1) +open_clip.factory._MODEL_CONFIGS['owned-native'] = { + 'embed_dim': 8, + 'vision_cfg': {'layers': 1, 'width': 32, 'head_width': 16, 'patch_size': 4, 'image_size': 8}, + 'text_cfg': {'context_length': 8, 'vocab_size': 49408, 'width': 16, 'heads': 2, 'layers': 1}, +} +adapter = SiglipAdapter('owned', backend='open_clip', open_clip_model_id='owned-native', dense_dim=8, compute_precision='float32') +try: + adapter.load('cpu') + assert type(adapter._model) is open_clip.model.CLIP + with torch.inference_mode(): + result = adapter._model.encode_text(open_clip.tokenize(['hello'], context_length=8)) + assert result.shape == (1, 8) and torch.isfinite(result).all() + assert not any(n == 'transformers' or n.startswith('transformers.') for n in sys.modules) +finally: + adapter.unload() +""" + result = subprocess.run( # noqa: S603 — fixed interpreter and inline offline native fixture + [sys.executable, "-c", script], + cwd=tmp_path, + env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/packages/sie_server/tests/config/test_native_zerank_catalog.py b/packages/sie_server/tests/config/test_native_zerank_catalog.py index 600c6cfef..f176886fb 100644 --- a/packages/sie_server/tests/config/test_native_zerank_catalog.py +++ b/packages/sie_server/tests/config/test_native_zerank_catalog.py @@ -29,7 +29,7 @@ def test_native_catalog_pin_profile_and_bundle() -> None: bundle = yaml.safe_load((_SERVER / "bundles/default.yaml").read_text()) assert _ADAPTER in bundle["adapters"] assert bundle["deps"]["sentence-transformers"] == "==5.7.0" - assert bundle["deps"]["transformers"] == ">=4.57,<5" + assert bundle["deps"]["transformers"] == "==4.57.6" assert bundle["deps"]["gliner2"] == ">=1.3.2,<=2.0.0" diff --git a/packages/sie_server/tests/config/test_transformers_dependency_policy.py b/packages/sie_server/tests/config/test_transformers_dependency_policy.py new file mode 100644 index 000000000..bfcba9a98 --- /dev/null +++ b/packages/sie_server/tests/config/test_transformers_dependency_policy.py @@ -0,0 +1,63 @@ +"""Declared deployments match the reviewed Transformers loading boundary.""" + +import tomllib +from pathlib import Path + +import pytest +import yaml +from packaging.requirements import Requirement +from packaging.specifiers import SpecifierSet + +_SERVER = Path(__file__).resolve().parents[2] +_ROOT = _SERVER.parents[1] + + +def _requirement(values: list[str]) -> Requirement: + matches = [Requirement(value) for value in values if Requirement(value).name == "transformers"] + assert len(matches) == 1 + return matches[0] + + +def test_default_local_and_resolved_tf4_are_the_exact_reviewed_version() -> None: + project = tomllib.loads((_SERVER / "pyproject.toml").read_text())["project"] + default = yaml.safe_load((_SERVER / "bundles/default.yaml").read_text()) + locked = tomllib.loads((_ROOT / "uv.lock").read_text()) + runtime = [package for package in locked["package"] if package["name"] == "transformers"] + assert len(runtime) == 1 + assert runtime[0]["version"] == "4.57.6" + assert str(_requirement(project["optional-dependencies"]["local"]).specifier) == "==4.57.6" + assert default["deps"]["transformers"] == "==4.57.6" + assert "4.57.6" in _requirement(project["dependencies"]).specifier + + +@pytest.mark.parametrize("version", ["4.57.0", "4.57.5", "5.0.0", "5.0.9", "5.1.9", "5.2.9", "6.0.0"]) +def test_server_excludes_known_unreviewed_runtime_ranges(version: str) -> None: + project = tomllib.loads((_SERVER / "pyproject.toml").read_text())["project"] + assert version not in _requirement(project["dependencies"]).specifier + + +def test_server_keeps_fixed_tf5_overlays_and_documents_future_tf4_limit() -> None: + text = (_SERVER / "pyproject.toml").read_text() + project = tomllib.loads(text)["project"] + requirement = _requirement(project["dependencies"]) + assert requirement.specifier == SpecifierSet(">=4.57.6,<6,!=5.0.*,!=5.1.*,!=5.2.*") + for version in ("4.57.6", "5.3.0", "5.17.0"): + assert version in requirement.specifier + # Metadata cannot represent the disjoint support set. The permanent loader + # guard, not this declaration, refuses future unreviewed 4.x versions. + assert "4.58.0" in requirement.specifier + assert "any future unreviewed 4.x version" in text + tf5 = yaml.safe_load((_SERVER / "bundles/transformers5.yaml").read_text()) + assert tf5["deps"]["transformers"] == ">=5.17,<6" + + +def test_lock_metadata_preserves_the_declared_base_and_local_requirements() -> None: + project = tomllib.loads((_SERVER / "pyproject.toml").read_text())["project"] + locked = tomllib.loads((_ROOT / "uv.lock").read_text()) + server = next(package for package in locked["package"] if package["name"] == "sie-server") + requirements = [item for item in server["metadata"]["requires-dist"] if item["name"] == "transformers"] + base = [item for item in requirements if "marker" not in item] + local = [item for item in requirements if item.get("marker") == "extra == 'local'"] + assert len(base) == len(local) == 1 + assert SpecifierSet(base[0]["specifier"]) == _requirement(project["dependencies"]).specifier + assert SpecifierSet(local[0]["specifier"]) == _requirement(project["optional-dependencies"]["local"]).specifier diff --git a/uv.lock b/uv.lock index 6ca7db56a..1cc73efa8 100644 --- a/uv.lock +++ b/uv.lock @@ -4968,8 +4968,8 @@ requires-dist = [ { name = "torch", marker = "sys_platform == 'linux'", specifier = ">=2.9,<2.10", index = "https://download.pytorch.org/whl/cu129" }, { name = "torchvision", marker = "sys_platform != 'linux'", specifier = ">=0.18,<1" }, { name = "torchvision", marker = "sys_platform == 'linux'", specifier = ">=0.18,<1", index = "https://download.pytorch.org/whl/cu129" }, - { name = "transformers", specifier = ">=4.57,<6" }, - { name = "transformers", marker = "extra == 'local'", specifier = ">=4.57,<5" }, + { name = "transformers", specifier = ">=4.57.6,!=5.0.*,!=5.1.*,!=5.2.*,<6" }, + { name = "transformers", marker = "extra == 'local'", specifier = "==4.57.6" }, { name = "typer", specifier = ">=0.14,<1" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.32,<1" }, { name = "uvloop", specifier = ">=0.22" }, From 0062790bd831e428b84d12195cefd117028f49b2 Mon Sep 17 00:00:00 2001 From: svonava Date: Sun, 18 Oct 2026 06:49:51 -0700 Subject: [PATCH 2/3] fix(server): guard OpenCLIP before model construction --- .../src/sie_server/adapters/siglip/adapter.py | 16 +- .../test_transformers_security_integration.py | 217 ++++++++++++++++-- 2 files changed, 208 insertions(+), 25 deletions(-) diff --git a/packages/sie_server/src/sie_server/adapters/siglip/adapter.py b/packages/sie_server/src/sie_server/adapters/siglip/adapter.py index ab5b045cd..b1400b366 100644 --- a/packages/sie_server/src/sie_server/adapters/siglip/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/siglip/adapter.py @@ -46,6 +46,7 @@ import tempfile import threading from concurrent.futures import ThreadPoolExecutor +from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Literal @@ -334,12 +335,16 @@ def _load_open_clip(self, device: str, dtype: torch.dtype) -> None: import open_clip from huggingface_hub import snapshot_download + # The factory can reread mutable configs and registries. Guard any + # available Transformers runtime before construction, without using a + # separate config preview to decide whether its HF text tower is safe. + # Native-only OpenCLIP remains usable when Transformers is absent; + # an installed unsupported version deliberately fails closed. + if find_spec("transformers") is not None: + install_transformers_security_fixes() + assert self._open_clip_model_id is not None # guarded in __init__ if not self._open_clip_model_id.startswith("hf-hub:"): - # Match the native factory's legacy ViT name normalization. - model_cfg = open_clip.get_model_config(self._open_clip_model_id.replace("/", "-")) - if model_cfg is not None and model_cfg.get("text_cfg", {}).get("hf_model_name"): - install_transformers_security_fixes() model, _train_preproc, val_preproc = open_clip.create_model_and_transforms(self._open_clip_model_id) tokenizer = open_clip.get_tokenizer(self._open_clip_model_id) else: @@ -402,9 +407,6 @@ def _load_open_clip(self, device: str, dtype: torch.dtype) -> None: except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError) as exc: raise RuntimeError(f"invalid pinned OpenCLIP config for {repo_id}@{self._revision}") from exc - if text_cfg.get("hf_model_name"): - install_transformers_security_fixes() - # Keep tokenization in the same immutable model snapshot. The upstream # OpenCLIP config names a separate tokenizer repo, which would otherwise # reintroduce default-branch drift after the checkpoint itself is pinned. diff --git a/packages/sie_server/tests/adapters/test_transformers_security_integration.py b/packages/sie_server/tests/adapters/test_transformers_security_integration.py index 0395fdcbc..5e7c6148c 100644 --- a/packages/sie_server/tests/adapters/test_transformers_security_integration.py +++ b/packages/sie_server/tests/adapters/test_transformers_security_integration.py @@ -284,7 +284,7 @@ def test_guard_precedes_dependent_and_fallback_native_loaders( @pytest.mark.parametrize("hub", [False, True]) @pytest.mark.parametrize("hf_text", [False, True]) -def test_open_clip_checks_the_selected_text_tower( +def test_open_clip_guards_available_transformers_before_factory( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, hub: bool, hf_text: bool ) -> None: open_clip = pytest.importorskip("open_clip") @@ -297,10 +297,10 @@ def test_open_clip_checks_the_selected_text_tower( def get_config(name: str) -> dict[str, Any]: selected.append(name) - return model_config + raise AssertionError("A separate config preview cannot authorize the later factory read") def create(*args: Any, **kwargs: Any) -> tuple[Any, object, object]: - assert events == (["guard"] if hf_text else []) + assert events == ["guard"] events.append("construct") return SimpleNamespace(to=lambda **kw: None, eval=lambda: None), object(), object() @@ -330,8 +330,88 @@ def create(*args: Any, **kwargs: Any) -> tuple[Any, object, object]: "owned", backend="open_clip", open_clip_model_id=model_id, dense_dim=8, revision="a" * 40 if hub else None ) adapter.load("cpu") - assert events == (["guard", "construct"] if hf_text else ["construct"]) - assert selected == ([] if hub else ["ViT-owned"]) + assert events == ["guard", "construct"] + assert selected == [] + + +@_EXACT_TF4 +def test_open_clip_native_model_refuses_installed_unsupported_transformers(monkeypatch: pytest.MonkeyPatch) -> None: + open_clip = pytest.importorskip("open_clip") + monkeypatch.setattr("transformers.__version__", "4.57.5") + constructor = Mock(side_effect=AssertionError("Factory must not run")) + monkeypatch.setattr(open_clip, "create_model_and_transforms", constructor) + adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id="ViT-B-32", dense_dim=8) + with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"): + adapter.load("cpu") + constructor.assert_not_called() + + +@pytest.mark.parametrize("error", [ImportError("broken discovery"), ValueError("missing module spec")]) +def test_open_clip_does_not_hide_transformers_discovery_errors( + monkeypatch: pytest.MonkeyPatch, error: Exception +) -> None: + open_clip = pytest.importorskip("open_clip") + constructor = Mock(side_effect=AssertionError("Factory must not run")) + monkeypatch.setattr(open_clip, "create_model_and_transforms", constructor) + module = importlib.import_module("sie_server.adapters.siglip.adapter") + monkeypatch.setattr(module, "find_spec", Mock(side_effect=error)) + adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id="ViT-B-32", dense_dim=8) + with pytest.raises(type(error), match=str(error)): + adapter.load("cpu") + constructor.assert_not_called() + + +def test_open_clip_installer_failure_prevents_factory(monkeypatch: pytest.MonkeyPatch) -> None: + open_clip = pytest.importorskip("open_clip") + constructor = Mock(side_effect=AssertionError("Factory must not run")) + monkeypatch.setattr(open_clip, "create_model_and_transforms", constructor) + module = importlib.import_module("sie_server.adapters.siglip.adapter") + monkeypatch.setattr( + module, "install_transformers_security_fixes", Mock(side_effect=RuntimeError("Owned installation failure")) + ) + adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id="ViT-B-32", dense_dim=8) + with pytest.raises(RuntimeError, match="Owned installation failure"): + adapter.load("cpu") + constructor.assert_not_called() + + +@pytest.mark.parametrize("version", ["5.3.0", "5.17.0"]) +def test_open_clip_fixed_version_policy_preserves_native_bindings( + monkeypatch: pytest.MonkeyPatch, version: str +) -> None: + open_clip = pytest.importorskip("open_clip") + # This is a version-dispatch control, not a claim of loading those wheels. + monkeypatch.setattr("transformers.__version__", version) + before = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel) + model = SimpleNamespace(to=lambda **kw: None, eval=lambda: None) + factory = Mock(return_value=(model, object(), object())) + monkeypatch.setattr(open_clip, "create_model_and_transforms", factory) + monkeypatch.setattr(open_clip, "get_tokenizer", lambda name: object()) + adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id="owned-native", dense_dim=8) + adapter.load("cpu") + after = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel) + assert all(left is right for left, right in zip(before, after, strict=True)) + factory.assert_called_once_with("owned-native") + adapter.unload() + + +@pytest.mark.parametrize("kind", ["unknown", "absent", "malformed", "invalid-json", "empty"]) +def test_open_clip_preserves_native_identifier_errors(tmp_path: Path, kind: str) -> None: + open_clip = pytest.importorskip("open_clip") + identifier = f"local-dir:{tmp_path / 'missing'}" + if kind == "unknown": + identifier = "owned-unregistered-model" + elif kind == "empty": + identifier = "local-dir:" + elif kind in {"malformed", "invalid-json"}: + identifier = f"local-dir:{tmp_path}" + (tmp_path / "open_clip_config.json").write_text("{}" if kind == "malformed" else "{invalid-json") + with pytest.raises((ValueError, RuntimeError, FileNotFoundError)) as native: + open_clip.create_model_and_transforms(identifier) + adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id=identifier, dense_dim=8) + with pytest.raises(type(native.value)) as guarded: + adapter.load("cpu") + assert str(guarded.value) == str(native.value) @_EXACT_TF4 @@ -359,15 +439,102 @@ def test_actual_open_clip_hf_text_constructor_sees_guard( adapter = SiglipAdapter( "owned", backend="open_clip", open_clip_model_id="owned-tiny", dense_dim=8, compute_precision="float32" ) + # OpenCLIP captured its AutoModel/AutoConfig aliases before installation. + assert open_clip.hf_model.AutoModel is transformers.AutoModel + assert open_clip.hf_model.AutoConfig is AutoConfig + installed = None + first_output = None + for _ in range(2): + try: + with torch.random.fork_rng(): + torch.manual_seed(187) + adapter.load("cpu") + bindings = (PretrainedConfig.__init__, hub_kernels.is_kernel, modeling_utils.is_kernel) + if installed is None: + installed = bindings + else: + assert all(before is after for before, after in zip(installed, bindings, strict=True)) + assert type(adapter._model) is CustomTextCLIP + assert type(adapter._model.text.transformer) is BertModel + assert "forward" not in adapter._model.text.transformer.__dict__ + with torch.inference_mode(): + output = adapter._model.encode_text(torch.tensor([[2, 5, 6, 3]])) + assert output.shape == (1, 8) + assert torch.isfinite(output).all() + if first_output is None: + first_output = output + else: + assert torch.equal(first_output, output) + kernel_boundary.assert_not_called() + finally: + adapter.unload() + + +@_EXACT_TF4 +@pytest.mark.parametrize("case", ["absolute", "collision", "factory-change"]) +def test_actual_open_clip_local_directory_cannot_bypass_guard( + tmp_path: Path, + bert_checkpoints: tuple[Path, Path], + monkeypatch: pytest.MonkeyPatch, + kernel_boundary: Mock, + case: str, +) -> None: + open_clip = pytest.importorskip("open_clip", minversion="3.0.0") + path, _ = bert_checkpoints + tokens = torch.tensor([[2, 5, 6, 3]]) + expected = BertModel.from_pretrained(path, add_pooling_layer=False).eval() + _update_config(path / "config.json", _attn_implementation_internal=_ATTACK) + vision = {"layers": 1, "width": 32, "head_width": 16, "patch_size": 4, "image_size": 8} + native = { + "embed_dim": 8, + "vision_cfg": vision, + "text_cfg": {"context_length": 8, "vocab_size": 49408, "width": 16, "heads": 2, "layers": 1}, + } + hf = { + "embed_dim": 8, + "vision_cfg": vision, + "text_cfg": { + "hf_model_name": str(path), + "hf_tokenizer_name": str(path), + "hf_model_pretrained": True, + "hf_proj_type": "linear", + "hf_pooler_type": "mean_pooler", + }, + } + selected = tmp_path / "nested" / "selected" + selected.mkdir(parents=True) + # OpenCLIP 3 loads a local-dir tokenizer from that same directory, even + # when text_cfg names a separate model path. + _tokenizer(selected) + config_path = selected / "open_clip_config.json" + config_path.write_text(json.dumps({"model_cfg": hf})) + monkeypatch.chdir(tmp_path) + identifier = f"local-dir:{selected}" if case == "absolute" else "local-dir:nested/selected" + if case == "collision": + sibling = tmp_path / "nested-selected" + sibling.mkdir() + (sibling / "open_clip_config.json").write_text(json.dumps({"model_cfg": native})) + elif case == "factory-change": + # A file can change after a security preview but before the native + # factory reads it. The permanent policy must already be installed. + config_path.write_text(json.dumps({"model_cfg": native})) + factory = open_clip.create_model_and_transforms + + def changed_factory(*args: Any, **kwargs: Any) -> Any: + config_path.write_text(json.dumps({"model_cfg": hf})) + return factory(*args, **kwargs) + + monkeypatch.setattr(open_clip, "create_model_and_transforms", changed_factory) + adapter = SiglipAdapter("owned", backend="open_clip", open_clip_model_id=identifier, dense_dim=8) try: adapter.load("cpu") - assert type(adapter._model) is CustomTextCLIP assert type(adapter._model.text.transformer) is BertModel assert "forward" not in adapter._model.text.transformer.__dict__ + assert adapter._model.text.transformer.config._attn_implementation == expected.config._attn_implementation with torch.inference_mode(): - output = adapter._model.encode_text(torch.tensor([[2, 5, 6, 3]])) - assert output.shape == (1, 8) - assert torch.isfinite(output).all() + assert torch.equal( + adapter._model.text.transformer(tokens).last_hidden_state, expected(tokens).last_hidden_state + ) kernel_boundary.assert_not_called() finally: adapter.unload() @@ -427,24 +594,38 @@ def find_spec(self, fullname, path=None, target=None): assert result.returncode == 0, result.stdout + result.stderr -def test_actual_native_open_clip_stays_available_without_transformers(tmp_path: Path) -> None: - pytest.importorskip("open_clip") +@pytest.mark.parametrize("local_directory", [False, True]) +def test_actual_native_open_clip_stays_available_without_transformers(tmp_path: Path, local_directory: bool) -> None: + pytest.importorskip("open_clip", minversion="3.0.0" if local_directory else None) script = """ -import importlib.abc, sys -class NoTransformers(importlib.abc.MetaPathFinder): +import importlib.machinery, importlib.util, json, pathlib, sys +# Model an absent optional package through actual import discovery in this +# subprocess; do not suppress a discoverable-but-broken package's errors. +class WithoutTransformers(importlib.machinery.PathFinder): + @classmethod def find_spec(self, fullname, path=None, target=None): if fullname == 'transformers' or fullname.startswith('transformers.'): - raise ImportError('Transformers intentionally unavailable') -sys.meta_path.insert(0, NoTransformers()) + return None + return super().find_spec(fullname, path, target) +sys.meta_path[:] = [WithoutTransformers if x is importlib.machinery.PathFinder else x for x in sys.meta_path] +assert importlib.util.find_spec('transformers') is None import open_clip, torch from sie_server.adapters.siglip.adapter import SiglipAdapter torch.set_num_threads(1) -open_clip.factory._MODEL_CONFIGS['owned-native'] = { +config = { 'embed_dim': 8, 'vision_cfg': {'layers': 1, 'width': 32, 'head_width': 16, 'patch_size': 4, 'image_size': 8}, 'text_cfg': {'context_length': 8, 'vocab_size': 49408, 'width': 16, 'heads': 2, 'layers': 1}, } -adapter = SiglipAdapter('owned', backend='open_clip', open_clip_model_id='owned-native', dense_dim=8, compute_precision='float32') +identifier = 'owned-native' +if sys.argv[1] == 'local': + directory = pathlib.Path.cwd() / 'nested' / 'native' + directory.mkdir(parents=True) + (directory / 'open_clip_config.json').write_text(json.dumps({'model_cfg': config})) + identifier = f'local-dir:{directory}' +else: + open_clip.factory._MODEL_CONFIGS[identifier] = config +adapter = SiglipAdapter('owned', backend='open_clip', open_clip_model_id=identifier, dense_dim=8, compute_precision='float32') try: adapter.load('cpu') assert type(adapter._model) is open_clip.model.CLIP @@ -456,7 +637,7 @@ def find_spec(self, fullname, path=None, target=None): adapter.unload() """ result = subprocess.run( # noqa: S603 — fixed interpreter and inline offline native fixture - [sys.executable, "-c", script], + [sys.executable, "-c", script, "local" if local_directory else "registered"], cwd=tmp_path, env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, capture_output=True, From 166f50627a4efc051b52f98777e4a1d63b45f08d Mon Sep 17 00:00:00 2001 From: svonava Date: Sun, 18 Oct 2026 07:30:34 -0700 Subject: [PATCH 3/3] fix(server): guard SGLang parent preprocessing --- .../sie_server/adapters/sglang/generation.py | 2 + .../adapters/sglang_vision_extract/adapter.py | 2 + .../test_transformers_security_integration.py | 154 ++++++++++++++++++ 3 files changed, 158 insertions(+) diff --git a/packages/sie_server/src/sie_server/adapters/sglang/generation.py b/packages/sie_server/src/sie_server/adapters/sglang/generation.py index 92b09deed..8a79aa645 100644 --- a/packages/sie_server/src/sie_server/adapters/sglang/generation.py +++ b/packages/sie_server/src/sie_server/adapters/sglang/generation.py @@ -53,6 +53,7 @@ resolve_reasoning_format, ) from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision from sie_server.adapters.sglang import _server from sie_server.adapters.sglang._compat.sie_thinking_budget import PARAM as THINKING_BUDGET_PARAM @@ -221,6 +222,7 @@ def _reasoning_control_token_ids( """ from transformers import AutoTokenizer + install_transformers_security_fixes() tokenizer = AutoTokenizer.from_pretrained( model_name_or_path, revision=revision, trust_remote_code=trust_remote_code ) diff --git a/packages/sie_server/src/sie_server/adapters/sglang_vision_extract/adapter.py b/packages/sie_server/src/sie_server/adapters/sglang_vision_extract/adapter.py index e58534b47..40c87b025 100644 --- a/packages/sie_server/src/sie_server/adapters/sglang_vision_extract/adapter.py +++ b/packages/sie_server/src/sie_server/adapters/sglang_vision_extract/adapter.py @@ -19,6 +19,7 @@ from sie_server.adapters._generation_base import GenerationResult, collect_generation from sie_server.adapters._spec import AdapterSpec +from sie_server.adapters._transformers_compat import install_transformers_security_fixes from sie_server.adapters._types import ComputePrecision from sie_server.adapters.base import ModelCapabilities, ModelDims from sie_server.adapters.lighton_ocr._markdown import lightonocr3_markdown @@ -175,6 +176,7 @@ def load(self, device: str) -> None: processor_kwargs: dict[str, Any] = {"trust_remote_code": self._trust_remote_code} if self._processor_use_fast is not None: processor_kwargs["use_fast"] = self._processor_use_fast + install_transformers_security_fixes() self._processor = AutoProcessor.from_pretrained(processor_dir, **processor_kwargs) try: diff --git a/packages/sie_server/tests/adapters/test_transformers_security_integration.py b/packages/sie_server/tests/adapters/test_transformers_security_integration.py index 5e7c6148c..c5d0193cc 100644 --- a/packages/sie_server/tests/adapters/test_transformers_security_integration.py +++ b/packages/sie_server/tests/adapters/test_transformers_security_integration.py @@ -21,10 +21,14 @@ from sie_server.adapters.cross_encoder.runtime_lora import CrossEncoderLoraAdapter from sie_server.adapters.docling.adapter import DoclingAdapter from sie_server.adapters.pytorch_embedding import PyTorchEmbeddingAdapter +from sie_server.adapters.sglang import generation as sglang_generation +from sie_server.adapters.sglang_vision_extract import adapter as sglang_vision from sie_server.adapters.siglip.adapter import SiglipAdapter from sie_server.types.inputs import Item from transformers import ( AutoConfig, + AutoProcessor, + AutoTokenizer, BertConfig, BertForSequenceClassification, BertModel, @@ -150,6 +154,156 @@ def test_direct_load_rejects_config_structure_before_model_construction( kernel_boundary.assert_not_called() +@_EXACT_TF4 +@pytest.mark.parametrize("field", _PRIVATE_FIELDS) +def test_sglang_parent_processor_guards_actual_config_fallback( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, field: str +) -> None: + tokenizer = _tokenizer(tmp_path) + _bert_config(tokenizer).save_pretrained(tmp_path) + expected = AutoProcessor.from_pretrained(tmp_path, trust_remote_code=False) + _update_config(tmp_path / "config.json", **{field: _ATTACK}) + native = AutoConfig.from_pretrained + observations = [] + + def observed_config(cls: type, *args: Any, **kwargs: Any) -> Any: + assert getattr(PretrainedConfig.__init__, "_sie_kernel_security_backport", False) + config = native(*args, **kwargs) + observations.append(config) + return config + + monkeypatch.setattr(AutoConfig, "from_pretrained", classmethod(observed_config)) + engine = Mock() + monkeypatch.setattr(sglang_vision.SGLangGenerationAdapter, "load", engine) + adapter = sglang_vision.SGLangVisionExtractAdapter(tmp_path, trust_remote_code=False) + start_loop = Mock() + monkeypatch.setattr(adapter, "_start_request_loop", start_loop) + try: + adapter.load("cuda:0") # Only the real CPU processor runs; the engine boundary is mocked. + assert observations + assert all(getattr(config, field, None) is None for config in observations) + assert type(adapter._processor) is type(expected) is BertTokenizerFast + assert adapter._processor(["alice", "bob"]) == expected(["alice", "bob"]) + engine.assert_called_once_with("cuda:0") + start_loop.assert_called_once_with() + finally: + adapter.unload() + + +@_EXACT_TF4 +@pytest.mark.parametrize("field", _STRUCTURE_FIELDS) +def test_sglang_parent_processor_rejects_config_structure_before_engine( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, field: str +) -> None: + _bert_config(_tokenizer(tmp_path)).save_pretrained(tmp_path) + _update_config(tmp_path / "config.json", **{field: {"__class__": "owned-marker"}}) + engine = Mock() + monkeypatch.setattr(sglang_vision.SGLangGenerationAdapter, "load", engine) + adapter = sglang_vision.SGLangVisionExtractAdapter(tmp_path, trust_remote_code=False) + start_loop = Mock() + monkeypatch.setattr(adapter, "_start_request_loop", start_loop) + with pytest.raises(ValueError, match="config attribute structure"): + adapter.load("cuda:0") + assert adapter._processor is None + engine.assert_not_called() + start_loop.assert_not_called() + + +@_EXACT_TF4 +def test_sglang_parent_processor_refuses_unsupported_version_before_constructor( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr("transformers.__version__", "4.57.5") + processor = Mock(side_effect=AssertionError("Processor must not run")) + engine = Mock(side_effect=AssertionError("Engine must not run")) + monkeypatch.setattr(AutoProcessor, "from_pretrained", processor) + monkeypatch.setattr(sglang_vision.SGLangGenerationAdapter, "load", engine) + adapter = sglang_vision.SGLangVisionExtractAdapter(tmp_path, trust_remote_code=False) + with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"): + adapter.load("cuda:0") + assert adapter._processor is None + processor.assert_not_called() + engine.assert_not_called() + + +def _reasoning_tokenizer(directory: Path) -> BertTokenizerFast: + tokenizer = _tokenizer(directory) + tokenizer.add_special_tokens({"additional_special_tokens": ["", ""]}) + tokenizer.save_pretrained(directory) + _bert_config(tokenizer).save_pretrained(directory) + config_path = directory / "tokenizer_config.json" + config = json.loads(config_path.read_text()) + del config["tokenizer_class"] # Exercise AutoTokenizer's actual AutoConfig fallback. + config_path.write_text(json.dumps(config)) + return tokenizer + + +@_EXACT_TF4 +@pytest.mark.parametrize("field", _PRIVATE_FIELDS) +def test_sglang_parent_reasoning_tokenizer_guards_actual_config_fallback( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, field: str +) -> None: + tokenizer = _reasoning_tokenizer(tmp_path) + _update_config(tmp_path / "config.json", **{field: _ATTACK}) + native = AutoConfig.from_pretrained + observations = [] + + def observed_config(cls: type, *args: Any, **kwargs: Any) -> Any: + assert getattr(PretrainedConfig.__init__, "_sie_kernel_security_backport", False) + config = native(*args, **kwargs) + observations.append(config) + return config + + monkeypatch.setattr(AutoConfig, "from_pretrained", classmethod(observed_config)) + ids = sglang_generation._reasoning_control_token_ids( + str(tmp_path), revision=None, trust_remote_code=False, reasoning_format="qwen3" + ) + assert observations + assert all(getattr(config, field, None) is None for config in observations) + assert ids == { + "open": tokenizer.convert_tokens_to_ids(""), + "close": tokenizer.convert_tokens_to_ids(""), + "newline": None, + } + + +@_EXACT_TF4 +@pytest.mark.parametrize("field", _STRUCTURE_FIELDS) +def test_sglang_parent_reasoning_tokenizer_rejects_structure_before_engine( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, field: str +) -> None: + _reasoning_tokenizer(tmp_path) + _update_config(tmp_path / "config.json", **{field: {"__class__": "owned-marker"}}) + reserve_port = Mock(side_effect=AssertionError("Engine resources must not be reserved")) + monkeypatch.setattr(sglang_generation._server, "find_free_port", reserve_port) + adapter = sglang_generation.SGLangGenerationAdapter( + str(tmp_path), reasoning_parser="qwen3", thinking_budget=32, trust_remote_code=False + ) + with pytest.raises(ValueError, match="config attribute structure"): + adapter.load("cuda:0") + assert adapter._thinking_budget_tokens is None + reserve_port.assert_not_called() + + +@_EXACT_TF4 +def test_sglang_parent_reasoning_tokenizer_refuses_unsupported_version_before_constructor( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr("transformers.__version__", "4.57.5") + tokenizer = Mock(side_effect=AssertionError("Tokenizer must not run")) + reserve_port = Mock(side_effect=AssertionError("Engine resources must not be reserved")) + monkeypatch.setattr(AutoTokenizer, "from_pretrained", tokenizer) + monkeypatch.setattr(sglang_generation._server, "find_free_port", reserve_port) + adapter = sglang_generation.SGLangGenerationAdapter( + str(tmp_path), reasoning_parser="qwen3", thinking_budget=32, trust_remote_code=False + ) + with pytest.raises(RuntimeError, match=r"requires transformers==4\.57\.6"): + adapter.load("cuda:0") + assert adapter._thinking_budget_tokens is None + tokenizer.assert_not_called() + reserve_port.assert_not_called() + + @_EXACT_TF4 @pytest.mark.parametrize("adapter_class", [CrossEncoderAdapter, CrossEncoderLoraAdapter]) @pytest.mark.parametrize("trust", [False, True])