#269·Whisper

Stack buffer overflow in reference whisper_model_load via unbounded n_dims

Author: professor-moodyCreated Jul 17, 2026Updated Jul 17, 2026

Affected product. Const-me/Whisper (the de-facto Windows/DirectCompute Whisper GPU port; WhisperDesktop, .NET bindings; ~10.5k stars), current master (verified on HEAD, 2026-07-15).

Vulnerability type. Stack-based buffer overflow (out-of-bounds write), CWE-787, via an unbounded per-tensor dimension count n_dims read from the model file.

Summary. The vendored reference-CPU model loader reads a per-tensor n_dims from the model file and writes n_dims 32-bit values into a fixed 3-element stack array ne[3], with no check that n_dims <= 3. A crafted ggml-*.bin model with n_dims > 3 overflows the stack array with attacker-controlled length and values. The project's own primary GPU loader guards this exact value; the vendored reference copy does not.

Technical detail.

Whisper/source/whisper.cpp, whisper_model_load (~lines 1004-1020):

int32_t n_dims;
read_safe(fin, n_dims);              // from file, UNCHECKED
...
int32_t ne[3] = { 1, 1, 1 };         // fixed 3-element stack array
for (int i = 0; i < n_dims; ++i) {
    read_safe(fin, ne[i]);           // out-of-bounds stack write for i >= 3
    nelements *= ne[i];
}

Both n_dims and each ne[i] come directly from the file, so the attacker controls the overflow length and the written 4-byte values. There is no n_dims < 1 || n_dims > 3 guard; the shape sanity check runs only after the overflow.

The guard exists in the primary loader (intra-repo asymmetry). The project's own DirectCompute GPU loader Whisper/Whisper/WhisperModel.cpp:278,364 rejects n_dims < 1 || n_dims > 3. The reference copy (marked "Copy-pasted from whisper_model_load()") never received the guard.

Reachability. The vulnerable whisper_model_load is the vendored reference-CPU path, reached via the eModelImplementation.Reference / loadReferenceCpuModel option (requires an AVX build). The default GPU loader is guarded. So exploitation requires the non-default Reference implementation; the trigger is loading an untrusted ggml-*.bin model in that mode. (We note this reachability caveat honestly; the sink itself is a clean controlled stack smash.)

Proof of concept. Witnessed with AddressSanitizer, control-vs-crafted, by compiling the verbatim reference ne[3] read loop standalone and feeding it a crafted tensor record:

CONTROL (n_dims = 3):  reads clean, 0 ASan errors.
CRAFTED (n_dims = 6):
    AddressSanitizer: stack-buffer-overflow  WRITE of size 4
    in the read_safe(ne[i]) loop (the write past the 3-element ne[]).

We can provide the crafted file, the harness, and the full ASan log. The loop is reproduced verbatim from the shipped source; the guard asymmetry with WhisperModel.cpp is source-confirmed.

Impact. A stack out-of-bounds write with attacker-controlled length and data during model load. Depending on stack layout and compiler mitigations, this ranges from a stack-canary abort / denial of service to overwrite of saved registers / return address and potential code execution. The trigger is loading an untrusted Whisper model in the reference-CPU mode.

Suggested remediation. Add if (n_dims < 1 || n_dims > 3) { /* fail */ } in the reference whisper_model_load immediately after reading n_dims, mirroring the guard already present in WhisperModel.cpp:278,364.