EnigmaConsultant's picture
PoC: whisper.cpp mel-filter unbounded allocation (huntr MFV)
1bf12e7 verified
|
Raw
History Blame Contribute Delete
6.19 kB
---
license: other
tags:
- security
- model-file-vulnerability
- whisper.cpp
- ggml
---
# Uncontrolled Memory Allocation via Unchecked `n_mel * n_fft` Mel-Filter Header Fields in whisper.cpp's Legacy GGML Model Loader
**Gated PoC repository for a huntr Model File Vulnerability submission.**
Access is granted on request (e.g. to `protectai-bot` for triage).
**Target format:** GGML (.ggml) – llama.cpp / ggml-org ecosystem (huntr target id `ggml`)
**Project:** ggml-org/whisper.cpp
**Affected commit:** `6fc7c33b4c3a2cec83e4b65abd5e96a890480375` (2026-07-01, current `master` at time of testing)
**Affected location:** `whisper_model_load()` β€” `src/whisper.cpp:1576-1586`; identical pattern in `parakeet_model_load()` β€” `src/parakeet.cpp:1058-1067`.
## Summary
Immediately after the hparams block, whisper.cpp reads two attacker-controlled 32-bit fields and uses their product, completely unchecked, to size a `std::vector<float>`:
```cpp
auto & filters = wctx.model.filters;
read_safe(loader, filters.n_mel);
read_safe(loader, filters.n_fft);
filters.data.resize(filters.n_mel * filters.n_fft);
loader->read(loader->context, filters.data.data(), filters.data.size() * sizeof(float));
```
(`src/whisper.cpp:1580-1584`; `struct whisper_filters { int32_t n_mel; int32_t n_fft; std::vector<float> data; };` at `src/whisper.cpp:422-427`)
There is no upper bound on `n_mel` or `n_fft` (both attacker-controlled `int32_t` read straight from the file), and no plausibility check against the actual mel-filter sizes whisper.cpp ever legitimately uses (80 or 128 for real Whisper models). A crafted file with implausible-but-small header values (e.g. `n_mel = 130048`, `n_fft = 65536`, encoded in just 8 bytes) drives a `resize()` request of `n_mel * n_fft * sizeof(float)` β‰ˆ **34 GB** from a total input file of under 100 bytes.
This is a classic amplification / uncontrolled-resource-consumption bug (CWE-789): a tiny attacker-supplied file forces a multi-gigabyte allocation attempt before any real model data has even been validated, well before the point where an application could reasonably decide "this doesn't look like a real model." On memory-constrained deployments (containers, shared hosts, serverless inference endpoints β€” common whisper.cpp deployment targets) this reliably invokes the OOM killer or otherwise denies service, and is trivially repeatable (near-zero attacker cost per attempt vs. multi-GB victim-side allocation attempt).
Reachable via the same public, documented API as the other findings in this series:
```c
whisper_context * ctx = whisper_init_from_file_with_params_no_state(path, whisper_context_default_params());
```
## Proof of Concept
Found independently via a 30-minute AFL++ coverage-guided fuzzing campaign (`afl-clang-fast++ -fsanitize=address`, seeded from a minimal valid header) against a harness calling the real public loading API β€” not hand-constructed. File: `afl_found_crash_oom_mel_filters.bin` (78 bytes).
Decoded header fields (from the crashing file): `n_audio_layer=4` (valid, MODEL_TINY), `n_mel = 130048`, `n_fft = 65536` β†’ requested allocation = 130048 Γ— 65536 Γ— 4 bytes = **34,091,302,912 bytes (~34.1 GB)**, confirmed by the ASan trace as the exact byte count `operator new` was asked for β€” i.e. this is the literal, non-overflowed mathematical product of two small header ints, not a SIZE_MAX/wraparound artifact. It would fail identically (via `std::bad_alloc`, uncaught, `SIGABRT`) on any real deployment target without terabytes of free memory, so this is not an ASan-allocator-ceiling artifact β€” it reproduces the same way with or without AddressSanitizer.
`asan_trace: afl_found_asan_trace_oom.txt`:
```
==...==ERROR: AddressSanitizer: out of memory: allocator is trying to allocate 0x7f0000000 bytes
#0 ... in operator new(unsigned long)
#1 ... in std::__new_allocator<float>::allocate(...)
#2 ... in std::allocator_traits<std::allocator<float>>::allocate(...)
#3 ... in std::_Vector_base<float, std::allocator<float>>::_M_allocate(...)
#4 ... in std::vector<float, std::allocator<float>>::_M_default_append(...)
==...==ABORTING
```
(0x7f0000000 = 34,091,302,912 = 130048 Γ— 65536 Γ— 4, confirming the trace matches `filters.data.resize(filters.n_mel * filters.n_fft)` exactly β€” this is the only single-argument `vector<float>::resize()` call in the loading path.)
`harness_whisper.cpp` β€” identical harness used for the sibling `n_dims` finding in this series, calling `whisper_init_from_file_with_params_no_state()` directly.
## Impact
Denial of service via a sub-100-byte crafted file forcing a tens-of-gigabytes allocation attempt during model loading, before any real validation of the file's legitimacy. Same underlying missing-bounds-check pattern (unchecked attacker `int32` fields multiplied directly into an allocation size) recurs verbatim in `parakeet_model_load()` (`src/parakeet.cpp:1065`, `filters.data.resize(filters.n_mel * filters.n_fb)`), so this is reported as one root cause covering both call sites, not two.
## Suggested fix
Validate `n_mel` and `n_fft` (and `n_fb` in parakeet.cpp) against sane upper bounds (e.g. a few thousand) β€” and/or against the file's actual remaining byte length β€” before calling `resize()`, in both `whisper_model_load()` and `parakeet_model_load()`.
## Dedup / novelty check
Searched GitHub issues, whisper.cpp's security advisories page, and CVE trackers for "mel filter", "n_mel n_fft allocation", "unbounded allocation whisper.cpp" β€” found only unrelated memory-usage discussions (e.g. issue #2310, about legitimate long-audio transcription using a lot of RAM at *inference* time, not malicious *model-file* headers at *load* time) and issue #3807 (a different root cause β€” zero-dimension hparams causing a null-deref/assert, not an oversized allocation). No prior disclosure of this specific `n_mel`/`n_fft` unbounded-allocation path was found.
## Files in this repo
- `harness_whisper.cpp` β€” harness calling the real public loading API
- `afl_found_crash_oom_mel_filters.bin` β€” AFL++-discovered crashing input (not hand-crafted)
- `afl_found_asan_trace_oom.txt` β€” full ASan trace