Skip to content

Heap buffer overflow (OOB write) via unvalidated n_vocab in whisper model files #4059

Description

@xobx-cherif

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 = 50357token_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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions