diff --git a/src/whisper.cpp b/src/whisper.cpp index 2a95bdb1e..c777e6734 100644 --- a/src/whisper.cpp +++ b/src/whisper.cpp @@ -3189,6 +3189,14 @@ static bool log_mel_spectrogram( int64_t stage_1_pad = WHISPER_SAMPLE_RATE * 30; int64_t stage_2_pad = frame_size / 2; + // The reflective padding below reads samples[1 .. stage_2_pad], so an input + // shorter than stage_2_pad + 1 would read past the end of the buffer. + if (n_samples < stage_2_pad + 1) { + WHISPER_LOG_ERROR("%s: audio too short: %d samples, need at least %d\n", + __func__, n_samples, (int) stage_2_pad + 1); + return false; + } + // Initialize a vector and copy data from C array to it. std::vector samples_padded; samples_padded.resize(n_samples + stage_1_pad + stage_2_pad * 2); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 74a5b1429..32fc42bd6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -177,3 +177,13 @@ add_parakeet_transcription_test( tests/parakeet-expected-diffusion-output.txt 0.95) +# log_mel_spectrogram out-of-bounds read on very short audio (issue #3923) +set(MEL_OOB_TEST test-log-mel-oob) +add_executable(${MEL_OOB_TEST} ${MEL_OOB_TEST}.cpp) +target_include_directories(${MEL_OOB_TEST} PRIVATE ../include ../ggml/include ../examples) +target_link_libraries(${MEL_OOB_TEST} PRIVATE common) +target_compile_definitions(${MEL_OOB_TEST} PRIVATE + WHISPER_MODEL_PATH="${PROJECT_SOURCE_DIR}/models/for-tests-ggml-tiny.bin") +add_test(NAME ${MEL_OOB_TEST} COMMAND ${MEL_OOB_TEST}) +set_tests_properties(${MEL_OOB_TEST} PROPERTIES LABELS "unit;gh") + diff --git a/tests/test-log-mel-oob.cpp b/tests/test-log-mel-oob.cpp new file mode 100644 index 000000000..a0da263fa --- /dev/null +++ b/tests/test-log-mel-oob.cpp @@ -0,0 +1,35 @@ +#include "whisper.h" + +#include +#include + +#ifdef NDEBUG +#undef NDEBUG +#endif +#include + +// Regression test for the heap out-of-bounds read in log_mel_spectrogram(). +// Reflective padding reads samples[1 .. WHISPER_N_FFT/2], so an input shorter +// than WHISPER_N_FFT/2 + 1 (201) samples used to read past the end of the +// caller's buffer. Such inputs must now be rejected with a non-zero return. + +int main() { + std::string model_path = WHISPER_MODEL_PATH; + + struct whisper_context_params cparams = whisper_context_default_params(); + struct whisper_context * ctx = whisper_init_from_file_with_params(model_path.c_str(), cparams); + assert(ctx != nullptr); + + std::vector samples(256, 0.0f); + + // Shorter than the reflective-pad window: must be rejected, not read OOB. + assert(whisper_pcm_to_mel(ctx, samples.data(), 4, 1) != 0); + assert(whisper_pcm_to_mel(ctx, samples.data(), 200, 1) != 0); + + // Long enough to pad safely: must still succeed. + assert(whisper_pcm_to_mel(ctx, samples.data(), 201, 1) == 0); + + whisper_free(ctx); + + return 0; +}