Description
The n_vocab hyperparameter read from a model file is not validated against the hardcoded special-token ids (50256–50363). A malicious model with n_vocab between 50258 and 50363 makes the logit-suppression code in whisper_process_logits write -INFINITY past the end of the logits/probs vectors sized by n_vocab, corrupting adjacent heap memory during the first transcription.
Impact: a malicious model file (e.g. downloaded from an untrusted model hub) corrupts the process heap — OOB writes of the fixed 4-byte value -INFINITY (0xFF800000) at attacker-chosen relative offsets 0–420 bytes past the end of a ~200KB heap vector, plus OOB reads whose values flow into the transcription output. Realistic impact includes crash/DoS and potential control-flow hijack on allocator metadata, since the overflow distance is attacker-controlled.
Technical analysis
whisper_model_load (src/whisper.cpp:1515) reads hparams.n_vocab unchecked; the vocab loader then sets vocab.n_vocab = model.hparams.n_vocab (src/whisper.cpp:1637) — the original cross-check against the vocab section is commented out (src/whisper.cpp:1602-1607). The whisper_vocab struct hardcodes special-token ids: token_eot = 50256, token_sot = 50257, token_translate = 50357 … token_beg = 50363 (src/whisper.cpp:443-452). These constants are only shifted when vocab.is_multilingual() (n_vocab >= 51865); for any smaller n_vocab they remain absolute.
At inference, whisper_process_logits sizes its vectors with n_logits = vocab.id_to_token.size() (= n_vocab for a consistent file) and then indexes them with the absolute constants:
logits[vocab.token_eot] = -INFINITY (src/whisper.cpp:6331 — first OOB at exactly index 50362 for a 50362-sized vector)
logits[vocab.token_not] (:6338), logits[vocab.token_sot]/token_nosp (:6353-6354), token_solm (:6358), token_translate/transcribe/prev (:6362-6364)
- the language-token suppression loop
logits[whisper_token_lang(&ctx, i)] for all 100 languages (:6368, ids 50258..50357)
With n_vocab in [50258, 50363] these writes land past the end of the logits vector; earlier OOB reads fire in whisper_full_with_state at probs[whisper_token_nosp(ctx)] (:7307) and whisper_compute_logprobs's std::max_element(logprobs.begin() + vocab.token_beg, ...) (:6455) when token_beg >= n_logits.
Boundary map (dynamically verified): n_vocab <= 50257 → stopped earlier by the always-on ggml get_rows GGML_ASSERT (safe); 50258–50363 → OOB write (this finding); >= 50364 → safe (all legitimate whisper vocab sizes are 51864–51866).
CVSS 3.1
AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H (7.1 — local vector: the victim must load the attacker-supplied model file).
POC
Steps to reproduce:
- Download the poc.
- Compile whisper with ASAN.
- run the whisper-cli with the provided model and the audio sample
poc2.zip
ASAN output
=================================================================
==47287==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7fe820c7bae8 at pc 0x7fe821c8aafc bp 0x7ffec744cde0 sp 0x7ffec744cdd8
WRITE of size 4 at 0x7fe820c7bae8 thread T0
#0 0x7fe821c8aafb in whisper_process_logits /workspace/findings/whisper.cpp/src/whisper.cpp:6331
#1 0x7fe821c95fb5 in whisper_full_with_state /workspace/findings/whisper.cpp/src/whisper.cpp:7308
#2 0x7fe821ca8e34 in whisper_full /workspace/findings/whisper.cpp/src/whisper.cpp:7911
#3 0x7fe821cab7d7 in whisper_full_parallel /workspace/findings/whisper.cpp/src/whisper.cpp:7922
#4 0x562d0f175916 in main /workspace/findings/whisper.cpp/examples/cli/cli.cpp:1318
#5 0x7fe821128fb9 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29fb9) (BuildId: 9bee4eb8329d9d71d9f59a08465751cbc1a1f7cf)
#6 0x7fe8211290d6 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a0d6) (BuildId: 9bee4eb8329d9d71d9f59a08465751cbc1a1f7cf)
#7 0x562d0f17f7f0 in _start (/workspace/findings/whisper.cpp/build/build-rel/bin/whisper-cli+0x367f0) (BuildId: 106fd1f185421015f146f716ba17d96d93f56314)
0x7fe820c7bae8 is located 0 bytes after 201448-byte region [0x7fe820c4a800,0x7fe820c7bae8)
allocated by thread T0 here:
#0 0x7fe821eb443f in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:109
#1 0x7fe821cb6ddd in std::__new_allocator<float>::allocate(unsigned long, void const*) /usr/include/c++/15/bits/new_allocator.h:151
#2 0x7fe821cb6ddd in std::allocator_traits<std::allocator<float> >::allocate(std::allocator<float>&, unsigned long) /usr/include/c++/15/bits/alloc_traits.h:614
#3 0x7fe821cb6ddd in std::_Vector_base<float, std::allocator<float> >::_M_allocate(unsigned long) /usr/include/c++/15/bits/stl_vector.h:387
#4 0x7fe821cb6ddd in std::vector<float, std::allocator<float> >::reserve(unsigned long) /usr/include/c++/15/bits/vector.tcc:79
SUMMARY: AddressSanitizer: heap-buffer-overflow /workspace/findings/whisper.cpp/src/whisper.cpp:6331 in whisper_process_logits
Shadow bytes around the buggy address:
0x7fe820c7b800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fe820c7b880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fe820c7b900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fe820c7b980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7fe820c7ba00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7fe820c7ba80: 00 00 00 00 00 00 00 00 00 00 00 00 00[fa]fa fa
0x7fe820c7bb00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fe820c7bb80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fe820c7bc00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fe820c7bc80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7fe820c7bd00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==47287==ABORTING
Description
The
n_vocabhyperparameter read from a model file is not validated against the hardcoded special-token ids (50256–50363). A malicious model withn_vocabbetween 50258 and 50363 makes the logit-suppression code inwhisper_process_logitswrite-INFINITYpast the end of thelogits/probsvectors sized byn_vocab, corrupting adjacent heap memory during the first transcription.Impact: a malicious model file (e.g. downloaded from an untrusted model hub) corrupts the process heap — OOB writes of the fixed 4-byte value
-INFINITY(0xFF800000) at attacker-chosen relative offsets 0–420 bytes past the end of a ~200KB heap vector, plus OOB reads whose values flow into the transcription output. Realistic impact includes crash/DoS and potential control-flow hijack on allocator metadata, since the overflow distance is attacker-controlled.Technical analysis
whisper_model_load(src/whisper.cpp:1515) readshparams.n_vocabunchecked; the vocab loader then setsvocab.n_vocab = model.hparams.n_vocab(src/whisper.cpp:1637) — the original cross-check against the vocab section is commented out (src/whisper.cpp:1602-1607). Thewhisper_vocabstruct hardcodes special-token ids:token_eot = 50256,token_sot = 50257,token_translate = 50357…token_beg = 50363(src/whisper.cpp:443-452). These constants are only shifted whenvocab.is_multilingual()(n_vocab >= 51865); for any smallern_vocabthey remain absolute.At inference,
whisper_process_logitssizes its vectors withn_logits = vocab.id_to_token.size()(=n_vocabfor a consistent file) and then indexes them with the absolute constants:logits[vocab.token_eot] = -INFINITY(src/whisper.cpp:6331— first OOB at exactly index 50362 for a 50362-sized vector)logits[vocab.token_not](:6338),logits[vocab.token_sot]/token_nosp(:6353-6354),token_solm(:6358),token_translate/transcribe/prev(:6362-6364)logits[whisper_token_lang(&ctx, i)]for all 100 languages (:6368, ids 50258..50357)With
n_vocabin [50258, 50363] these writes land past the end of thelogitsvector; earlier OOB reads fire inwhisper_full_with_stateatprobs[whisper_token_nosp(ctx)](:7307) andwhisper_compute_logprobs'sstd::max_element(logprobs.begin() + vocab.token_beg, ...)(:6455) whentoken_beg >= n_logits.Boundary map (dynamically verified):
n_vocab <= 50257→ stopped earlier by the always-on ggmlget_rowsGGML_ASSERT(safe); 50258–50363 → OOB write (this finding);>= 50364→ safe (all legitimate whisper vocab sizes are 51864–51866).CVSS 3.1
AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H(7.1 — local vector: the victim must load the attacker-supplied model file).POC
Steps to reproduce:
poc2.zip
ASAN output
================================================================= ==47287==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7fe820c7bae8 at pc 0x7fe821c8aafc bp 0x7ffec744cde0 sp 0x7ffec744cdd8 WRITE of size 4 at 0x7fe820c7bae8 thread T0 #0 0x7fe821c8aafb in whisper_process_logits /workspace/findings/whisper.cpp/src/whisper.cpp:6331 #1 0x7fe821c95fb5 in whisper_full_with_state /workspace/findings/whisper.cpp/src/whisper.cpp:7308 #2 0x7fe821ca8e34 in whisper_full /workspace/findings/whisper.cpp/src/whisper.cpp:7911 #3 0x7fe821cab7d7 in whisper_full_parallel /workspace/findings/whisper.cpp/src/whisper.cpp:7922 #4 0x562d0f175916 in main /workspace/findings/whisper.cpp/examples/cli/cli.cpp:1318 #5 0x7fe821128fb9 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29fb9) (BuildId: 9bee4eb8329d9d71d9f59a08465751cbc1a1f7cf) #6 0x7fe8211290d6 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a0d6) (BuildId: 9bee4eb8329d9d71d9f59a08465751cbc1a1f7cf) #7 0x562d0f17f7f0 in _start (/workspace/findings/whisper.cpp/build/build-rel/bin/whisper-cli+0x367f0) (BuildId: 106fd1f185421015f146f716ba17d96d93f56314) 0x7fe820c7bae8 is located 0 bytes after 201448-byte region [0x7fe820c4a800,0x7fe820c7bae8) allocated by thread T0 here: #0 0x7fe821eb443f in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:109 #1 0x7fe821cb6ddd in std::__new_allocator<float>::allocate(unsigned long, void const*) /usr/include/c++/15/bits/new_allocator.h:151 #2 0x7fe821cb6ddd in std::allocator_traits<std::allocator<float> >::allocate(std::allocator<float>&, unsigned long) /usr/include/c++/15/bits/alloc_traits.h:614 #3 0x7fe821cb6ddd in std::_Vector_base<float, std::allocator<float> >::_M_allocate(unsigned long) /usr/include/c++/15/bits/stl_vector.h:387 #4 0x7fe821cb6ddd in std::vector<float, std::allocator<float> >::reserve(unsigned long) /usr/include/c++/15/bits/vector.tcc:79 SUMMARY: AddressSanitizer: heap-buffer-overflow /workspace/findings/whisper.cpp/src/whisper.cpp:6331 in whisper_process_logits Shadow bytes around the buggy address: 0x7fe820c7b800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7fe820c7b880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7fe820c7b900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7fe820c7b980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x7fe820c7ba00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x7fe820c7ba80: 00 00 00 00 00 00 00 00 00 00 00 00 00[fa]fa fa 0x7fe820c7bb00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7fe820c7bb80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7fe820c7bc00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7fe820c7bc80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7fe820c7bd00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==47287==ABORTING