Records the critical fixes already applied (commit a65b71a) and the
remaining high/memory/patch-export/design findings from the code
review, for later reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
6.1 KiB
6.1 KiB
Hardening todo
Findings from a code-review pass over src/plug and src/synth (2026-07-27, branch hardening).
Critical — crashes, memory corruption (fixed, commit a65b71a)
- NRPN controller ID caused an out-of-bounds write on the audio thread.
JaySynth::handleController(src/plug/JaySynth.cpp:899) now bounds-checksmidiCC_info.IDagainstNUM_MIDI_CONTROLLERSbefore indexinglast_midiCC_info[](NRPN IDs can reach 16383, array is sized 1024). - Negative controller IDs from patch files could corrupt memory.
JaySynthMidiCC::add/remove(src/plug/JaySynthMidiCC.cpp:433-460) now also reject< 0, not just out-of-range-high. - Null-pointer dereference on malformed/unparsable patch or bank files.
bankImportXmland the.xmpbranch ofloadPatchFromFile(src/plug/PluginProcessor.cpp:860-885, 949-975) now null-check XML elements aftergetFirstChildElement()/XmlDocument::parse()before dereferencing. - No size validation before casting raw file bytes to
fxBank/fxProgramstructs.loadBankFromFile/loadPatchFromFile(src/plug/PluginProcessor.cpp:834-858, 930-957) now check the loaded file is at least header-sized and that the embedded chunk size fits within it before reading through the raw pointer. - 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 intoSYNTH_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. - Unchecked file stream could be null, crashing on plugin construction.
JaySynth::JaySynth(src/plug/JaySynth.cpp:55-57) now guards the wavetable file read againstcreateInputStream()returning null. - Parameter-changing calls raced with real-time audio rendering — no lock protected them.
JaySynth::setParameter/setControl/setPerVoiceControl/setParam/ClearControls(src/plug/JaySynth.cpp) now takeScopedLock sl(lock), matchingrenderNextBlockand the note/controller handlers.
High — real-time audio-thread safety
- Every note/CC/voice-count change allocates Strings and posts a message from the audio thread.
JaySynthActionListener::call*(src/plug/PluginProcessor.h:41-127) build messages via chainedStringconcatenation, invoked synchronously fromsynthChangedinsidenoteOn/noteOff/handleController/Voicestart/renderNextBlock— all on the audio thread whenever the GUI is open. Fix: pass typed structs instead of encoding into Strings, or hop to the message thread before allocating. - ~450KB–900KB of stack arrays allocated per voice per audio block.
VoiceProcessDataV(src/synth/voice.c:836-858) declares ~11SYNTH_MAX_BUFSIZE-sized locals, called once per active voice per block from the worker threads. Fix: hoist these intovoice_common_t/voice_tas pre-allocated buffers (same pattern already used byvcf.c/env.c/lfo.c/blit.c). SynthDebuguses unboundedvsprintfinto a fixed 1024-byte buffer plus blocking I/O, called fromhandleMidiEventinside the audio-thread render path (src/synth/synth_debug.c:27-38). Fix: usevsnprintfwith the buffer size, and consider a lock-free ring buffer instead of blocking I/O for debug builds.
Memory management
new[]/deletemismatches (UB) inJaySynth's destructor —m_pVoices,pPer_voice_controls,pCurrNoteInfos,humanize_voice_param[i],ppHumanizedSliders[i],m_ppAudioThread,m_ppEventAudioThreadRdyall allocated withnew T[n]but freed with scalardelete(src/plug/JaySynth.cpp:62/186, 77/184, 80/185, 100-101/173-174, 124-125/160,191). Fix: change todelete[]at each site.mallocis never null-checked across the entire C DSP core (env.c,lfo.c,vcf.c,vco.c,blit.c,wavetable.c,voice.c,param_scale.c). Fix: check and fail gracefully (orabort()with a diagnostic) rather than crashing on first audio callback.
Patch/Bank import-export
- Legacy patch import decodes the wrong XML node — uses outer
xmlinstead of loop variablexml2in the legacy branch (src/plug/PluginProcessor.cpp:959-991), unlike every other branch; currently masked but re-decodes redundantly and is a latent bug if the legacy format gains child elements. - "Regular" (non-chunk)
.fxb/.fxpfiles are silently accepted but never loaded —is_regularis computed and never used (src/plug/PluginProcessor.cpp:834-852,930-948). Fix: either implement the regular-format path or surface an error to the user. .fxp/.fxbsaving is entirely unimplemented (savePatchToFile/saveBankToFile,src/plug/PluginProcessor.cpp:1027-1067, both marked// ToDo) while loading is implemented — asymmetric format support.patchDecodeXmlandpatchDecodeXml_legacyare near-duplicated, including a copy-pasted legacy frequency→cents conversion special case (src/plug/PluginProcessor.cpp:749-765,767-783). Fix: factor out the shared per-parameter decode loop.
Design
JaySynthis a god object (src/plug/JaySynth.h) — owns parameters, MIDI CC/NRPN, MIDI clock, 3 humanize subsystems, unison, monophonic orchestration, voice stealing, and the thread pool, all via direct field access with no internal boundaries. This is very plausibly why the parameter-locking bug (now fixed) was missed in the first place — no seam enforces consistent locking discipline. Consider splitting into a handful of focused collaborators (e.g. MIDI dispatch, voice allocation, humanize/unison modes) thatJaySynthcomposes.void *the_editor(src/plug/PluginProcessor.h:394) duplicates JUCE's own editor tracking, is never cleared when the editor is destroyed, and every other call site correctly usesgetEditor(this)instead — currently harmless but a dangling-pointer trap. Fix: remove the field and routecreateEditor()throughgetEditor(this)like everywhere else.