You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

Stack Out-of-Bounds Write via Unbounded Tensor n_dims Field 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 functions / locations:

  • whisper_model_load() β€” src/whisper.cpp:1882-1887 (main Whisper ASR model loader)
  • parakeet_model_load() β€” src/parakeet.cpp:1402-1407 (NVIDIA Parakeet ASR model loader, same file format/magic)
  • whisper_vad_model_load() β€” src/whisper.cpp:5024-5029 (Silero VAD model loader) β€” same unbounded-loop pattern confirmed by source review; not independently crash-reproduced (see Notes).

Summary

whisper.cpp loads its legacy ggml binary model format (magic 0x67676d6c, used for all ggml-*.bin Whisper/Parakeet/VAD model files) via a hand-rolled reader (whisper_model_load/parakeet_model_load/whisper_vad_model_load). Each per-tensor record in the file begins with:

int32_t n_dims;
int32_t length;
int32_t ttype;

read_safe(loader, n_dims);
read_safe(loader, length);
read_safe(loader, ttype);
...
int32_t nelements = 1;
int32_t ne[4] = { 1, 1, 1, 1 };
for (int i = 0; i < n_dims; ++i) {
    read_safe(loader, ne[i]);
    nelements *= ne[i];
}

(src/whisper.cpp:1870-1887)

n_dims is read directly from the attacker-supplied file with no upper-bound check against GGML_MAX_DIMS (4, ggml/include/ggml.h:222). The loop writes n_dims attacker-controlled int32_t values into the fixed 4-element stack array ne[4] via read_safe(loader, ne[i]) (a plain sizeof(T)-byte write to &ne[i]). Any n_dims > 4 writes past the end of ne into adjacent stack memory β€” an out-of-bounds stack write (CWE-787), fully attacker-controlled in both position (n_dims, up to INT32_MAX) and value (the ne[i] payload bytes).

Reachable directly through whisper.cpp's public, documented API β€” no special configuration needed:

whisper_context * ctx = whisper_init_from_file_with_params_no_state(path, whisper_context_default_params());

which is exactly what every application embedding whisper.cpp calls to load a user-/attacker-supplied .bin model file.

Proof of concept

crash_ndims7_whisper_model_load.bin is built as: magic(4) | 11Γ—int32 hparams (tiny model, ftype=0) | n_mel/n_fft (1,1) + 1 float | n_vocab=0 | n_dims=7, length=4, ttype=0, ne[7]Γ—int32 payload, name="test"

n_dims = 7 is the minimal trigger found (writes 3 extra int32s / 12 bytes past the 16-byte ne[4] array); n_dims = 5 and 6 did not reproduce a visible crash with this exact harness build (the overwritten bytes land in slack not subsequently dereferenced), but n_dims >= 7 deterministically corrupts a std::vector control structure (buft_list_t) declared later in the same stack frame, SEGV'ing when its destructor runs at function exit.

harness_whisper.cpp calls the exact public API:

struct whisper_context_params cparams = whisper_context_default_params();
cparams.use_gpu = false;
struct whisper_context * ctx = whisper_init_from_file_with_params_no_state(argv[1], cparams);
if (ctx) whisper_free(ctx);

Built with afl-clang-fast++ -g -O1 -fsanitize=address linked statically against whisper.cpp's own CMake-built libwhisper.a/libggml*.a (-DWHISPER_SANITIZE_ADDRESS=ON) β€” i.e. the real production parsing code, not a synthetic stub.

Running the harness on crash_ndims7_whisper_model_load.bin deterministically produces (asan_trace_whisper_model_load.txt):

AddressSanitizer:DEADLYSIGNAL
==...==ERROR: AddressSanitizer: SEGV on unknown address 0x...fff1
==...==The signal is caused by a WRITE memory access.
    #0 ... __asan::Allocator::Deallocate(...)
    ...
    #7 ... in whisper_model_load(whisper_model_loader*, whisper_context&) src/whisper.cpp:1956:1
    #8 ... in whisper_init_with_params_no_state src/whisper.cpp:3730:24
    #9 ... in whisper_init_from_file_with_params_no_state src/whisper.cpp:3659:16
    #10 ... in main harness_whisper.cpp:21

Reproduces identically (same trace, same corrupted-pointer pattern 0x????fffffff1) across independent runs β€” deterministic, not an uninitialized-memory artifact.

Second independent confirmation: parakeet.cpp

The identical unchecked loop exists verbatim in parakeet_model_load() (src/parakeet.cpp:1402-1407), reachable via the equally public parakeet_init_from_file_with_params_no_state(). crash_ndims64_parakeet_model_load.bin uses n_dims = 64 with filler 0x41414141 to guarantee corruption of a live pointer despite this function's different stack layout; the same crash class reproduces (asan_trace_parakeet_model_load.txt):

AddressSanitizer:DEADLYSIGNAL
==...==ERROR: AddressSanitizer: SEGV on unknown address (pc ...)
==...==The signal is caused by a READ memory access.
==...==Hint: this fault was caused by a dereference of a high value address

β€” consistent with corrupted 0x41414141-tainted stack data being read back as a pointer.

Why this is one finding, not three

whisper_model_load, parakeet_model_load, and whisper_vad_model_load all contain the identical copy-pasted idiom (read n_dims, loop into a fixed ne[4], no bound check). This is reported as a single root-cause vulnerability β€” a systemic missing bounds check in the shared ggml legacy tensor-header parsing convention β€” with three call sites as evidence of scope, not three separate bugs. The fix is identical in all three places: reject the file if n_dims < 1 || n_dims > GGML_MAX_DIMS.

Impact

Out-of-bounds stack write with fully attacker-controlled offset and payload during model loading, before any inference occurs. Depending on stack layout/compiler/platform this corrupts adjacent locals (observed: a std::vector control structure, producing a corrupted-pointer dereference at scope exit β€” not merely a benign abort). Any application that lets a user pick/supply a .bin/ggml model file (server, chat bot, plugin β€” a very common whisper.cpp deployment pattern) is exposed.

Suggested fix

In all three loader functions, validate immediately after reading n_dims:

if (n_dims < 1 || n_dims > GGML_MAX_DIMS) {
    WHISPER_LOG_ERROR("%s: invalid n_dims (%d) in model file\n", __func__, n_dims);
    return false;
}

Dedup / novelty check

Searched GitHub issues/advisories for ggml-org/whisper.cpp and public CVE trackers for "n_dims", "GGML_MAX_DIMS", "stack overflow model load", "tensor descriptor overflow". Found only:

  • Issue #3807 ("null pointer dereference + assertion abort via zero-dimension model parameters") β€” a different root cause (zero n_audio_state/n_audio_layer hyperparameters causing a null tensor->data and a GGML_ASSERT abort), not this bug.
  • CVE-2025-14569 (UAF in read_audio_data, audio-input handling, unrelated to model-file parsing).
  • GGUF (ggml/src/gguf.cpp) integer-overflow advisories (GHSA-vgg9-87g3-85w8, GHSA-3p4r-fq3f-q74v) β€” in llama.cpp's distinct GGUF parser, not the legacy ggml format/loader whisper.cpp uses for its own model files.

No prior disclosure of this specific unbounded-n_dims/ne[4] stack overflow was found. Repository HEAD tested is current as of 2026-07-01.

Files in this repo

  • harness_whisper.cpp, harness_parakeet.cpp β€” libFuzzer/AFL-style harnesses calling the real public API
  • make_seed_whisper.py β€” seed/PoC generator for the whisper.cpp path
  • crash_ndims7_whisper_model_load.bin β€” minimal PoC, whisper_model_load
  • crash_ndims64_parakeet_model_load.bin β€” PoC, parakeet_model_load
  • asan_trace_whisper_model_load.txt, asan_trace_parakeet_model_load.txt β€” full ASan crash traces
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support