From d713ca50bb02372d730099f90b98ce96ef298ba6 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Mon, 27 Jul 2026 20:02:04 +0200 Subject: [PATCH] Fix VCF coefficient buffer undersized for 4th-order filters Found while building tools/testhost's regression suite for the Critical #5 block-size fix: VCF_CalcCoeff_LPF/_HPF/_BPF advance the coefficient pointer once per (sample, filter-section) pair, but a 4th-order filter uses FILTER_MAX_ORDER/2 = 2 cascaded 2nd-order sections per sample. VCF_SetBufsize only allocated pCoef with bufsize entries, not bufsize*sections, so any host block between 4097 and 8192 samples with a 4th-order filter selected wrote past the allocation. Confirmed with AddressSanitizer before the fix: heap-buffer-overflow, WRITE of size 8, exactly 8 bytes past a 393216-byte (8192 x sizeof(vcf_coef_t)) allocation. Fixed by sizing pCoef for the worst case. Re-verified with ASan across blockSize 4097/8192/16384/20000 - zero errors after the fix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ --- TODO.md | 1 + src/synth/vcf.c | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 789e5ad..0076495 100644 --- a/TODO.md +++ b/TODO.md @@ -11,6 +11,7 @@ Findings from a code-review pass over `src/plug` and `src/synth` (2026-07-27, br - [x] **Host block sizes larger than `SYNTH_MAX_BUFSIZE` (8192) caused a stack buffer overflow or a permanent deadlock.** `processBlock` (`src/plug/PluginProcessor.cpp:547`) now chunks rendering into `SYNTH_MAX_BUFSIZE`-sized passes instead of writing the host's full block size into a fixed 8192-sample stack buffer; `JaySynth::renderNextBlock` (`src/plug/JaySynth.cpp:977`) clamps the same way and correctly holds a pending MIDI event over between passes. - [x] **Unchecked file stream could be null, crashing on plugin construction.** `JaySynth::JaySynth` (`src/plug/JaySynth.cpp:55-57`) now guards the wavetable file read against `createInputStream()` returning null. - [x] **Parameter-changing calls raced with real-time audio rendering — no lock protected them.** `JaySynth::setParameter`/`setControl`/`setPerVoiceControl`/`setParam`/`ClearControls` (`src/plug/JaySynth.cpp`) now take `ScopedLock sl(lock)`, matching `renderNextBlock` and the note/controller handlers. +- [x] **Found while building `tools/testhost`'s regression suite for this list: the block-size fix above wasn't sufficient on its own.** `VCF_CalcCoeff_LPF`/`_HPF`/`_BPF` (`src/synth/vcf.c`) advance the coefficient pointer once per *(sample, filter-section)* pair - a 4th-order filter uses `FILTER_MAX_ORDER/2` = 2 cascaded sections per sample - but `VCF_SetBufsize` only allocated `pCoef` with `bufsize` entries, not `bufsize * sections`. Any host block between 4097 and 8192 samples (inclusive of the now-correctly-chunked range from the fix above) with a 4th-order filter selected wrote past the end of that buffer, corrupting adjacent heap memory (confirmed with AddressSanitizer: `heap-buffer-overflow`, `WRITE of size 8`, exactly 8 bytes past a 393216-byte/8192-`vcf_coef_t` allocation). Fixed by sizing the allocation for the worst case, `bufsize*(FILTER_MAX_ORDER/2)`. Verified with ASan across the full range (4097, 8192, 16384, 20000 samples) after the fix - zero errors. ## High — real-time audio-thread safety (fixed) diff --git a/src/synth/vcf.c b/src/synth/vcf.c index ad93f0f..869f38b 100644 --- a/src/synth/vcf.c +++ b/src/synth/vcf.c @@ -142,7 +142,11 @@ void VCF_SetBufsize(vcf_t *pObj, UINT32 size) return; pObj->pOut = (synth_float_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(synth_float_t))); - pObj->pCoef = (vcf_coef_t*)SynthCheckAlloc(malloc(pObj->bufsize*sizeof(vcf_coef_t))); + // VCF_CalcCoeff_*() advances the coefficient pointer once per + // (sample, filter-section) pair - a 4th-order filter uses + // FILTER_MAX_ORDER/2 cascaded 2nd-order sections per sample, so the + // buffer needs that many times bufsize entries, not just bufsize. + pObj->pCoef = (vcf_coef_t*)SynthCheckAlloc(malloc(pObj->bufsize*(FILTER_MAX_ORDER/2)*sizeof(vcf_coef_t))); pObj->pCoeff_last = pObj->pCoef; VCF_Coeff_update(pObj);