From d1667c2f4d39a428da5a8f436784d6ab5f216b03 Mon Sep 17 00:00:00 2001 From: Lorenz Bucher Date: Fri, 27 Mar 2026 16:42:47 +0100 Subject: [PATCH 1/6] Added test case to trigger div-by-zero and fixed code --- test/test-data/malicious_numChannels0.wav | 3 +++ test/tests/MaliciousFileTests.cpp | 17 +++++++++++++++++ tinywav.c | 11 +++++++++++ 3 files changed, 31 insertions(+) create mode 100644 test/test-data/malicious_numChannels0.wav create mode 100644 test/tests/MaliciousFileTests.cpp diff --git a/test/test-data/malicious_numChannels0.wav b/test/test-data/malicious_numChannels0.wav new file mode 100644 index 0000000..20cb3d2 --- /dev/null +++ b/test/test-data/malicious_numChannels0.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ef3498de25d9111087da456271b4e8211a21fee6ad88d5f3729e9a25e92083a +size 44 diff --git a/test/tests/MaliciousFileTests.cpp b/test/tests/MaliciousFileTests.cpp new file mode 100644 index 0000000..520309a --- /dev/null +++ b/test/tests/MaliciousFileTests.cpp @@ -0,0 +1,17 @@ + +#include +#include "tinywav.h" + +#include // for memset +#include "TestCommon.hpp" + + +static auto basedir = std::string(TOSTRING(SOURCE_DIR)) + "/test/test-data/"; + +TEST_CASE("Tinywav - Test behaviour with malicious input data") +{ + TinyWav tw; + // without proper handling, this test could trigger a division by zero + REQUIRE(tinywav_open_read(&tw, std::string(basedir + "malicious_numChannels0.wav").c_str(), TW_INTERLEAVED) == -1); + tinywav_close_read(&tw); +} diff --git a/tinywav.c b/tinywav.c index edbd7b6..033d63c 100644 --- a/tinywav.c +++ b/tinywav.c @@ -195,6 +195,16 @@ int tinywav_open_read(TinyWav *tw, const char *path, TinyWavChannelFormat chanFm return -1; } + // Sanity checks + if (tw->h.NumChannels < 1 || tw->h.NumChannels > 128) { // relevant because + tinywav_close_read(tw); + return -1; + } + if (tw->h.SampleRate < 1) { + tinywav_close_read(tw); + return -1; + } + // skip over any other chunks before the "data" chunk (e.g. JUNK, INFO, bext, ...) while (fread(tw->h.Subchunk2ID, sizeof(char), 4, tw->f) == 4) { fread(&tw->h.Subchunk2Size, sizeof(uint32_t), 1, tw->f); @@ -217,6 +227,7 @@ int tinywav_open_read(TinyWav *tw, const char *path, TinyWavChannelFormat chanFm printf("[tinywav] Warning: wav file has %d bits per sample (int), which is not natively supported yet. Treating them as float; you may want to convert them manually after reading.\n", tw->h.BitsPerSample); } + // NOTE: previous sanity checks ensure div by zero is not possible here tw->numFramesInHeader = tw->h.Subchunk2Size / (tw->numChannels * tw->sampFmt); tw->totalFramesReadWritten = 0; From 00a7b5ff6bd5009c13044900d5f38b93f13721f0 Mon Sep 17 00:00:00 2001 From: Lorenz Bucher Date: Fri, 27 Mar 2026 17:36:38 +0100 Subject: [PATCH 2/6] Added alloca safeguards --- README.md | 3 +- test/tests/BasicTests.cpp | 2 +- test/tests/MaliciousFileTests.cpp | 46 +++++++++++++++++++++++++++++++ tinywav.c | 16 +++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fefd157..25e6bd2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ A minimal C library for reading and writing (32-bit float or 16-bit int) WAV aud * TinyWav takes and provides audio samples in configurable channel formats (interleaved, split, inline). WAV files always store samples in interleaved format. * TinyWav is minimal: it can only read/write RIFF WAV files with sample format `float32` or `int16`. -* TinyWav does not allocate any memory on the heap. It uses `alloca` internally, which allocates on the stack. In practice, this restricts the block size to "reasonable" values, so watch out for stack overflows. +* TinyWav does not allocate any memory on the heap. It uses `alloca` internally, which allocates on the stack. In practice, this restricts the block size to "reasonable" values, so watch out for stack overflows. + * In default `TINYWAV_USE_ALLOCA` mode, the `read()` and `write()` will return an error if the allocation exceeds 1 MegaByte, which corresponds e.g. to reading/writing 16k 32-bit sample chunks for 16-channels. * On platforms where `alloca` is not available (e.g. some DSP compilers), `TINYWAV_USE_VLA` or `TINYWAV_USE_MALLOC` can be defined. **CI/CD**: To guarantee portability, TinyWav is built and tested on several platforms, compilers & architectures: diff --git a/test/tests/BasicTests.cpp b/test/tests/BasicTests.cpp index ee686e5..fce1b28 100644 --- a/test/tests/BasicTests.cpp +++ b/test/tests/BasicTests.cpp @@ -224,7 +224,7 @@ TEST_CASE("Tinywav - Test Error Behaviour") float buffer[128]; REQUIRE(tinywav_read_f(&tw, buffer, -1) != 0); REQUIRE(tinywav_read_f(&tw, buffer, 0) == 0); - REQUIRE(tinywav_read_f(&tw, buffer, 16) == 0); // no data in file yet! + REQUIRE(tinywav_read_f(&tw, buffer, 16) == -1); // no data in file yet! tinywav_close_read(&tw); // Test data diff --git a/test/tests/MaliciousFileTests.cpp b/test/tests/MaliciousFileTests.cpp index 520309a..18bc6a0 100644 --- a/test/tests/MaliciousFileTests.cpp +++ b/test/tests/MaliciousFileTests.cpp @@ -15,3 +15,49 @@ TEST_CASE("Tinywav - Test behaviour with malicious input data") REQUIRE(tinywav_open_read(&tw, std::string(basedir + "malicious_numChannels0.wav").c_str(), TW_INTERLEAVED) == -1); tinywav_close_read(&tw); } + +TEST_CASE("Tinywav - Test Safeguards") +{ + // This test assumes TW_USE_ALLOCA is enabled + SECTION("Reading") { + TinyWav tw; + REQUIRE(tinywav_open_read(&tw, std::string(basedir + "example_32bitFloat-stereo.wav").c_str(), TW_INTERLEAVED) == 0); + int numFramesToRead = tw.numFramesInHeader; + + SECTION("try to read all samples than declared in header at once") { + float* buffer = (float*)malloc(numFramesToRead*tw.numChannels*sizeof(float)); + REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == numFramesToRead); + free(buffer); + } + SECTION("try to read more samples than declared in header") { + numFramesToRead += 4; // too much! + float* buffer = (float*)malloc(numFramesToRead*tw.numChannels*sizeof(float)); + REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == -1); + free(buffer); + } + SECTION("trigger alloca safeguard") { + float* buffer = (float*)malloc(numFramesToRead*tw.numChannels*sizeof(float)); + tw.numChannels = 32; // overwrite with another number of channels to trigger safeguard + REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == -1); + free(buffer); + } + } + + SECTION("writing") { + TinyWav tw; + REQUIRE(tinywav_open_write(&tw, 16, 8000, TW_FLOAT32, TW_INLINE, "bogus.wav") == 0); + int maxAllowedNumFrames16ch = 16*1024; // max 16ch, 16kSamples + + SECTION("try to write max samples") { + float* buffer = (float*)malloc(maxAllowedNumFrames16ch*tw.numChannels*sizeof(float)); + REQUIRE(tinywav_write_f(&tw, buffer, maxAllowedNumFrames16ch) == maxAllowedNumFrames16ch); + free(buffer); + } + SECTION("trigger alloca safeguard") { + maxAllowedNumFrames16ch += 4; // too much! + float* buffer = (float*)malloc(maxAllowedNumFrames16ch*tw.numChannels*sizeof(float)); + REQUIRE(tinywav_write_f(&tw, buffer, maxAllowedNumFrames16ch) == -1); + free(buffer); + } + } +} diff --git a/tinywav.c b/tinywav.c index 033d63c..ecc80c6 100644 --- a/tinywav.c +++ b/tinywav.c @@ -50,6 +50,9 @@ #define TW_DEALLOC(x) #endif +// Corresponds to 1MB for 32bit samples --> allows max of 16ch, 16kSamples blocksize reads/writes +static const size_t REASONABLE_MAX_ALLOCA_SIZE = 16*16*1024; // in samples + // MARK: private functions /** @returns true if the chunk of 4 characters matches the supplied string */ @@ -239,6 +242,14 @@ int tinywav_read_f(TinyWav *tw, void *data, int len) { if (tw == NULL || data == NULL || len < 0 || !tinywav_isOpen(tw)) { return -1; } + if (len > tw->numFramesInHeader) { + return -1; + } +#if TINYWAV_USE_ALLOCA + if (((size_t)tw->numChannels * len) > REASONABLE_MAX_ALLOCA_SIZE) { + return -1; + } +#endif if (tw->totalFramesReadWritten * tw->h.BlockAlign >= tw->h.Subchunk2Size) { // We are past the 'data' subchunk (size as declared in header). @@ -341,6 +352,11 @@ int tinywav_write_f(TinyWav *tw, void *f, int len) { if (tw == NULL || f == NULL || len < 0 || !tinywav_isOpen(tw)) { return -1; } +#if TINYWAV_USE_ALLOCA + if (((size_t)tw->numChannels * len) > REASONABLE_MAX_ALLOCA_SIZE) { + return -1; + } +#endif // 1. Bring samples into interleaved format // 2. write to disk From 688f869d90f5744e54ef2de88019a29d9129ad38 Mon Sep 17 00:00:00 2001 From: Lorenz Bucher Date: Fri, 27 Mar 2026 17:49:09 +0100 Subject: [PATCH 3/6] only run alloca tests if alloca feature is used --- CMakeLists.txt | 6 +++--- test/tests/MaliciousFileTests.cpp | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5184a7f..97fa143 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,13 +21,13 @@ add_library(${PROJECT_NAME} ${source_files}) if (TINYWAV_ALLOCATION MATCHES "ALLOCA") message(STATUS "Configuring tinywav to use ALLOCA for allocations") - target_compile_definitions(${PROJECT_NAME} PRIVATE TINYWAV_USE_ALLOCA=1) + target_compile_definitions(${PROJECT_NAME} PUBLIC TINYWAV_USE_ALLOCA=1) elseif(TINYWAV_ALLOCATION MATCHES "VLA") message(STATUS "Configuring tinywav to use VLA for allocations") - target_compile_definitions(${PROJECT_NAME} PRIVATE TINYWAV_USE_VLA=1) + target_compile_definitions(${PROJECT_NAME} PUBLIC TINYWAV_USE_VLA=1) elseif(TINYWAV_ALLOCATION MATCHES "MALLOC") message(STATUS "Configuring tinywav to use MALLOC for allocations") - target_compile_definitions(${PROJECT_NAME} PRIVATE TINYWAV_USE_MALLOC=1) + target_compile_definitions(${PROJECT_NAME} PUBLIC TINYWAV_USE_MALLOC=1) else() message(FATAL_ERROR "Invalid option for TINYWAV_ALLOCATION -- valid options are: ALLOCA VLA MALLOC") endif() diff --git a/test/tests/MaliciousFileTests.cpp b/test/tests/MaliciousFileTests.cpp index 18bc6a0..78638df 100644 --- a/test/tests/MaliciousFileTests.cpp +++ b/test/tests/MaliciousFileTests.cpp @@ -18,7 +18,6 @@ TEST_CASE("Tinywav - Test behaviour with malicious input data") TEST_CASE("Tinywav - Test Safeguards") { - // This test assumes TW_USE_ALLOCA is enabled SECTION("Reading") { TinyWav tw; REQUIRE(tinywav_open_read(&tw, std::string(basedir + "example_32bitFloat-stereo.wav").c_str(), TW_INTERLEAVED) == 0); @@ -35,12 +34,14 @@ TEST_CASE("Tinywav - Test Safeguards") REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == -1); free(buffer); } +#if TINYWAV_USE_ALLOCA SECTION("trigger alloca safeguard") { float* buffer = (float*)malloc(numFramesToRead*tw.numChannels*sizeof(float)); tw.numChannels = 32; // overwrite with another number of channels to trigger safeguard REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == -1); free(buffer); } +#endif } SECTION("writing") { @@ -53,11 +54,13 @@ TEST_CASE("Tinywav - Test Safeguards") REQUIRE(tinywav_write_f(&tw, buffer, maxAllowedNumFrames16ch) == maxAllowedNumFrames16ch); free(buffer); } +#if TINYWAV_USE_ALLOCA SECTION("trigger alloca safeguard") { maxAllowedNumFrames16ch += 4; // too much! float* buffer = (float*)malloc(maxAllowedNumFrames16ch*tw.numChannels*sizeof(float)); REQUIRE(tinywav_write_f(&tw, buffer, maxAllowedNumFrames16ch) == -1); free(buffer); } +#endif } } From 5b67ce7d6c46bd858c21c004a9f3f656800529f9 Mon Sep 17 00:00:00 2001 From: Lorenz Bucher Date: Fri, 27 Mar 2026 20:54:56 +0100 Subject: [PATCH 4/6] Decreased "reasonable max size" for alloca() to 500k This accomodates windows as well 500k is still quite high and risky --- README.md | 4 ++-- test/test-data/example_32bitFloat-mono.wav | 3 +++ test/tests/MaliciousFileTests.cpp | 15 ++++++--------- tinywav.c | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 test/test-data/example_32bitFloat-mono.wav diff --git a/README.md b/README.md index 25e6bd2..694ce57 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ A minimal C library for reading and writing (32-bit float or 16-bit int) WAV aud * TinyWav takes and provides audio samples in configurable channel formats (interleaved, split, inline). WAV files always store samples in interleaved format. * TinyWav is minimal: it can only read/write RIFF WAV files with sample format `float32` or `int16`. * TinyWav does not allocate any memory on the heap. It uses `alloca` internally, which allocates on the stack. In practice, this restricts the block size to "reasonable" values, so watch out for stack overflows. - * In default `TINYWAV_USE_ALLOCA` mode, the `read()` and `write()` will return an error if the allocation exceeds 1 MegaByte, which corresponds e.g. to reading/writing 16k 32-bit sample chunks for 16-channels. - * On platforms where `alloca` is not available (e.g. some DSP compilers), `TINYWAV_USE_VLA` or `TINYWAV_USE_MALLOC` can be defined. + * In default `TINYWAV_USE_ALLOCA` mode, the `read()` and `write()` will return an error if the allocation exceeds 0.5 MegaBytes, which corresponds e.g. to reading/writing 8k 32-bit sample chunks for 16 channels. + * On platforms where `alloca` is not available (e.g. some DSP compilers), `TINYWAV_USE_VLA` or `TINYWAV_USE_MALLOC` can be defined for alternate allocation. **CI/CD**: To guarantee portability, TinyWav is built and tested on several platforms, compilers & architectures: diff --git a/test/test-data/example_32bitFloat-mono.wav b/test/test-data/example_32bitFloat-mono.wav new file mode 100644 index 0000000..6685afc --- /dev/null +++ b/test/test-data/example_32bitFloat-mono.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ff730d5156328841932a92f4ffeaedecfb61c5b41bcd2d5a71b276cd2d21db6 +size 384048 diff --git a/test/tests/MaliciousFileTests.cpp b/test/tests/MaliciousFileTests.cpp index 78638df..5bf25c7 100644 --- a/test/tests/MaliciousFileTests.cpp +++ b/test/tests/MaliciousFileTests.cpp @@ -20,26 +20,24 @@ TEST_CASE("Tinywav - Test Safeguards") { SECTION("Reading") { TinyWav tw; - REQUIRE(tinywav_open_read(&tw, std::string(basedir + "example_32bitFloat-stereo.wav").c_str(), TW_INTERLEAVED) == 0); + REQUIRE(tinywav_open_read(&tw, std::string(basedir + "example_32bitFloat-mono.wav").c_str(), TW_INTERLEAVED) == 0); int numFramesToRead = tw.numFramesInHeader; - SECTION("try to read all samples than declared in header at once") { + SECTION("try to read all samples declared in header at once") { float* buffer = (float*)malloc(numFramesToRead*tw.numChannels*sizeof(float)); REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == numFramesToRead); free(buffer); } SECTION("try to read more samples than declared in header") { numFramesToRead += 4; // too much! - float* buffer = (float*)malloc(numFramesToRead*tw.numChannels*sizeof(float)); + float* buffer = nullptr; // should fail before trying to write to buffer REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == -1); - free(buffer); } #if TINYWAV_USE_ALLOCA SECTION("trigger alloca safeguard") { - float* buffer = (float*)malloc(numFramesToRead*tw.numChannels*sizeof(float)); + float* buffer = nullptr; // should fail before trying to write to buffer tw.numChannels = 32; // overwrite with another number of channels to trigger safeguard REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == -1); - free(buffer); } #endif } @@ -47,7 +45,7 @@ TEST_CASE("Tinywav - Test Safeguards") SECTION("writing") { TinyWav tw; REQUIRE(tinywav_open_write(&tw, 16, 8000, TW_FLOAT32, TW_INLINE, "bogus.wav") == 0); - int maxAllowedNumFrames16ch = 16*1024; // max 16ch, 16kSamples + int maxAllowedNumFrames16ch = 8*1024; // max 16ch, 8kSamples SECTION("try to write max samples") { float* buffer = (float*)malloc(maxAllowedNumFrames16ch*tw.numChannels*sizeof(float)); @@ -57,9 +55,8 @@ TEST_CASE("Tinywav - Test Safeguards") #if TINYWAV_USE_ALLOCA SECTION("trigger alloca safeguard") { maxAllowedNumFrames16ch += 4; // too much! - float* buffer = (float*)malloc(maxAllowedNumFrames16ch*tw.numChannels*sizeof(float)); + float* buffer = nullptr; // should fail before trying to write to buffer REQUIRE(tinywav_write_f(&tw, buffer, maxAllowedNumFrames16ch) == -1); - free(buffer); } #endif } diff --git a/tinywav.c b/tinywav.c index ecc80c6..436722c 100644 --- a/tinywav.c +++ b/tinywav.c @@ -50,8 +50,8 @@ #define TW_DEALLOC(x) #endif -// Corresponds to 1MB for 32bit samples --> allows max of 16ch, 16kSamples blocksize reads/writes -static const size_t REASONABLE_MAX_ALLOCA_SIZE = 16*16*1024; // in samples +// Corresponds to 0.5MB for 32bit samples --> allows max of 16ch, 8kSamples blocksize reads/writes +static const size_t REASONABLE_MAX_ALLOCA_SIZE = 16*8*1024; // in samples // MARK: private functions From 01c7a45a2a362f5629f29753810c6c3501a93961 Mon Sep 17 00:00:00 2001 From: Lorenz Bucher Date: Fri, 27 Mar 2026 20:59:09 +0100 Subject: [PATCH 5/6] add file exists handler before write test --- test/tests/MaliciousFileTests.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/tests/MaliciousFileTests.cpp b/test/tests/MaliciousFileTests.cpp index 5bf25c7..be01f4e 100644 --- a/test/tests/MaliciousFileTests.cpp +++ b/test/tests/MaliciousFileTests.cpp @@ -43,6 +43,10 @@ TEST_CASE("Tinywav - Test Safeguards") } SECTION("writing") { + if (TestCommon::fileExists("bogus.wav")) { + REQUIRE(std::remove("bogus.wav") == 0); + } + TinyWav tw; REQUIRE(tinywav_open_write(&tw, 16, 8000, TW_FLOAT32, TW_INLINE, "bogus.wav") == 0); int maxAllowedNumFrames16ch = 8*1024; // max 16ch, 8kSamples From 7913d4bc78fc0d7c4d75c95e6cc7f8e52cb821d0 Mon Sep 17 00:00:00 2001 From: Lorenz Bucher Date: Fri, 27 Mar 2026 21:16:01 +0100 Subject: [PATCH 6/6] close file pointers for clean handling --- test/tests/MaliciousFileTests.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/tests/MaliciousFileTests.cpp b/test/tests/MaliciousFileTests.cpp index be01f4e..f56e2c1 100644 --- a/test/tests/MaliciousFileTests.cpp +++ b/test/tests/MaliciousFileTests.cpp @@ -40,8 +40,11 @@ TEST_CASE("Tinywav - Test Safeguards") REQUIRE(tinywav_read_f(&tw, buffer, numFramesToRead) == -1); } #endif + + tinywav_close_read(&tw); } - + + SECTION("writing") { if (TestCommon::fileExists("bogus.wav")) { REQUIRE(std::remove("bogus.wav") == 0); @@ -63,5 +66,7 @@ TEST_CASE("Tinywav - Test Safeguards") REQUIRE(tinywav_write_f(&tw, buffer, maxAllowedNumFrames16ch) == -1); } #endif + + tinywav_close_write(&tw); } }