Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/sie_server/bundles/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions packages/sie_server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
from sie_server.adapters._types import ERR_NOT_LOADED, ComputePrecision, PoolingStrategy
from sie_server.adapters._utils import (
extract_texts,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading