--- 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: 1. **`str_len` (model-type string), `src/whisper.cpp:4798-4802`:** ```cpp int32_t str_len; read_safe(loader, str_len); std::vector 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 == 0` so `buffer` is allocated with size 0 (`buffer.data()` may be `nullptr`), while the `std::string` constructor is then called with `str_len` sign-extended to `SIZE_MAX`. libstdc++ detects this specific misuse and throws `std::logic_error("basic_string: construction from null is not valid")`, which is never caught → `std::terminate()` → `SIGABRT`. 2. **`n_encoder_layers`, `src/whisper.cpp:4822-4826`:** ```cpp 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_layers` sign-extends to an enormous `size_t` array-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 the `std::logic_error` path. - `crash_n_encoder_layers_neg1.bin`: `magic | str_len=4,"AAAA" | major/minor/patch | n_window/n_context | n_encoder_layers=-1` → triggers the `new[]`/allocation-failure path. `harness_vad.cpp` calls the exact public API: ```cpp 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 the `new[]` calls. - Wrap `whisper_vad_init_with_params()`'s model-loading body in the same `try { ... } catch (const std::exception &)` pattern already used by `whisper_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 API - `make_vad_seed.py` — PoC/seed generator - `crash_str_len_neg1.bin`, `crash_n_encoder_layers_neg1.bin` — minimal PoCs - `trace_str_len_neg1.txt`, `trace_n_encoder_layers_neg1.txt` — crash output