Conversation
amy_event_to_deltas_queue() allocated the addressed osc (and a referenced chained/mod osc) on whatever thread sent the event, without the queue lock, while FREE_OSC frees oscs on the render thread inside flush_due_deltas(), which holds it. A patch load queues frees for the old voice's oscs and then allocates the same osc numbers for the new voice, so the render thread could free an osc mid alloc_osc()/reset_osc() on the sending thread: a store through NULL or writes into freed memory (seen on a dual-core ESP32-P4 reloading a synth's patch, #1185). Those allocations were vestigial. Nothing on the ingest path touches synth[] after them, and play_delta already ensures every osc it touches (the addressed osc, chained_osc, mod_source, algo sources, breakpoint growth) under the lock. In the patch-load case the ingest allocation was freed by the queued FREE_OSC and re-allocated by play_delta anyway. Removing them takes the cross-thread allocation out entirely, rather than locking around it as #1185 does. test_voice_osc_range read synth[base] after an event whose fields were all refused, relying on ingest having allocated it; it now accepts an unallocated osc (which is at its defaults). make ctest passes. make test gives the same 90 pass / 43 fail as main here (the failures are small numeric diffs vs reference audio). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UW4jqUQ4Xc1sdoqzUPL2YV
Contributor
🎛️ AMY HW CI (AMYboard bench)Flashed this PR's AMY (LoadTestChord: 6-voice Juno ✅ PASS — the bench ran the test to completion.
Full chord settled render μs: 2610 (was 2601, Δ +0.3%) (peak 2614, 39 samples) ⬇️ Artifacts: serial log · load trace · report Self-hosted bench (amyboardci). FAIL means only that the test could not run — the load values are informational, with no threshold and no audio compare. See |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An alternative to #1185 for the same crash: a store through NULL in
reset_osc, or writes into freed memory, when a synth's patch is reloaded on a dual-core ESP32-P4.The race.
amy_event_to_deltas_queue()allocated the osc an event addresses, and any chained/mod osc it references, on whatever thread sent the event. It did this without the queue lock.FREE_OSCfrees oscs on the render thread insideflush_due_deltas(), which does hold the lock. A patch load queues frees for the old voice's oscs and then allocates the same osc numbers for the new voice. So the render thread could free an osc while the sending thread was still insidealloc_osc()/reset_osc().Why remove rather than lock. #1185 takes the lock around those calls. This PR deletes them, because they aren't needed:
synth[]after them, and the return value of the ingest ensure was ignored.play_deltaalready callsensure_osc_allocd, under the lock, for everything it touches: the addressed osc,chained_osc,mod_source, algo sources and breakpoint growth.malloc: the queuedFREE_OSCfreed it, andplay_deltaallocated it again.play_deltadid its own ensures. 85fd9de had already limited them to live-queue events, noting that "play_delta already ensures at execution for everything that plays".With them gone, ingest never allocates an osc, so there's nothing to lock. The one tradeoff: the first allocation of a new osc now always happens on the render thread, under the lock. That was already true for chained, mod and algo oscs.
Test change. Two cases in
tests/test_voice_osc_range.csend an event whose fields are all refused and then dereferencesynth[base]. They only passed because ingest had allocated that osc. They now also accept an unallocated osc, which is at its defaults.Testing
make ctestpasses, including after merging currentmain.make testfails the same 43 tests onmainand on this branch in my environment (small numeric differences against the reference audio), so the change doesn't affect them.🤖 Generated with Claude Code
https://claude.ai/code/session_01UW4jqUQ4Xc1sdoqzUPL2YV