license: other
tags:
- security
- model-file-vulnerability
- whisper.cpp
- ggml
Uncaught C++ Exception / Process Abort via Unvalidated Length Fields in whisper.cpp's Silero-VAD 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 function: whisper_vad_init_with_params() / its header-parsing block, src/whisper.cpp:4796-4850
Reachable via public API: whisper_vad_init_from_file_with_params() (src/whisper.cpp:4731), and transitively whisper_full() whenever params.vad = true and an attacker influences vad_model_path (src/whisper.cpp:6667).
Summary
whisper_vad_init_from_file_with_params() parses a second, distinct ggml-magic binary model file (the Silero VAD model, e.g. ggml-silero-v5.1.2.bin). Unlike whisper_init_with_params_no_state() (the main ASR loader), which explicitly wraps its model-loading call in try { ... } catch (const std::exception &) { ... } specifically "so that this can be caught by langbindings (Rust via whisper-rs, Go via cgo, ...)" (see comment at src/whisper.cpp:3724), whisper_vad_init_with_params() has no such guard. Several attacker-controlled length/count fields are used directly, unchecked, in ways that can throw C++ exceptions that then propagate uncaught out of the public API and terminate the whole process:
str_len(model-type string),src/whisper.cpp:4798-4802:int32_t str_len; read_safe(loader, str_len); std::vector<char> buffer(str_len + 1, 0); loader->read(loader->context, buffer.data(), str_len); std::string model_type(buffer.data(), str_len);If the file sets
str_len = -1,str_len + 1 == 0sobufferis allocated with size 0 (buffer.data()may benullptr), while thestd::stringconstructor is then called withstr_lensign-extended toSIZE_MAX. libstdc++ detects this specific misuse and throwsstd::logic_error("basic_string: construction from null is not valid"), which is never caught βstd::terminate()βSIGABRT.n_encoder_layers,src/whisper.cpp:4822-4826:read_safe(loader, hparams.n_encoder_layers); hparams.encoder_in_channels = new int32_t[hparams.n_encoder_layers]; hparams.encoder_out_channels = new int32_t[hparams.n_encoder_layers]; hparams.kernel_sizes = new int32_t[hparams.n_encoder_layers];A negative
n_encoder_layerssign-extends to an enormoussize_tarray-new request, which always exceeds any real system's addressable/available memory and throws (std::bad_array_new_length/std::bad_alloc), again uncaught β abort.
Both are symptoms of the same missing-validation-and-missing-catch pattern (no upfront sanity check on any VAD header field, no top-level exception guard), so this is reported as one finding with two triggering fields as evidence, not two separate bugs.
Proof of concept
Two minimal files, both starting with the ggml magic:
crash_str_len_neg1.bin:magic | str_len=-1 | major/minor/patch | n_window/n_contextβ triggers thestd::logic_errorpath.crash_n_encoder_layers_neg1.bin:magic | str_len=4,"AAAA" | major/minor/patch | n_window/n_context | n_encoder_layers=-1β triggers thenew[]/allocation-failure path.
harness_vad.cpp calls the exact public API:
struct whisper_vad_context_params cparams = whisper_vad_default_context_params();
cparams.use_gpu = false;
struct whisper_vad_context * ctx = whisper_vad_init_from_file_with_params(argv[1], cparams);
if (ctx) whisper_vad_free(ctx);
Output (trace_str_len_neg1.txt):
terminate called after throwing an instance of 'std::logic_error'
what(): basic_string: construction from null is not valid
Output (trace_n_encoder_layers_neg1.txt):
==...==ERROR: AddressSanitizer: requested allocation size 0xffffffffffffffff (0x800 after adjustments...) exceeds maximum supported size of 0x10000000000
#0 ... in operator new[](unsigned long)
#1 ... in whisper_vad_init_with_params src/whisper.cpp:4824:39
==...==ABORTING
This is a genuine uncaught-allocation-failure crash, not an artifact of ASan's allocator ceiling: a request derived from a negative/corrupted length always exceeds any real system's address space regardless of sanitizer, so in a non-ASan production build the same input throws std::bad_alloc/std::bad_array_new_length uncaught and aborts identically.
Impact
Denial of service: any application that loads a VAD model file supplied or influenced by an attacker (e.g. a "bring your own VAD model" feature, or a shared/download-cached model directory) can be crashed outright by two independent single-field mutations, with no possibility for the embedding application to catch/recover (unlike the main ASR loader, which explicitly supports this via its try/catch). This is a correctness/availability bug (CWE-248 Uncaught Exception / CWE-20 Improper Input Validation), not memory corruption β reported at the appropriate (lower) severity tier.
Suggested fix
- Validate
str_len >= 0(and apply a sane upper bound) before allocating/reading/constructing the string. - Validate
n_encoder_layers > 0(and apply a sane upper bound) before thenew[]calls. - Wrap
whisper_vad_init_with_params()'s model-loading body in the sametry { ... } catch (const std::exception &)pattern already used bywhisper_init_with_params_no_state().
Dedup / novelty check
No existing GitHub issue/advisory found for uncaught exceptions in whisper_vad_init_with_params / VAD model loading (this code path was only added recently β VAD support and the whisper_vad_* API β and predates a security-focused audit). Distinct from Issue #3807 (zero-dimension hparams in the main ASR loader, which is wrapped in try/catch and instead hits a GGML_ASSERT) and from the n_dims/ne[4] stack-overflow finding filed alongside this one (different function, different flaw class: uncaught exception/DoS here vs. out-of-bounds write there).
Files in this repo
harness_vad.cppβ harness calling the real public VAD-init APImake_vad_seed.pyβ PoC/seed generatorcrash_str_len_neg1.bin,crash_n_encoder_layers_neg1.binβ minimal PoCstrace_str_len_neg1.txt,trace_n_encoder_layers_neg1.txtβ crash output