Skip to content

Post-review cleanup batch: MOTU counters, shared identity tables, stale comments - #125

Merged
mrmidi merged 8 commits into
mainfrom
fix/review-cleanup-batch
Sep 18, 2026
Merged

mrmidi merged 8 commits into
mainfrom
fix/review-cleanup-batch

Conversation

@mrmidi

@mrmidi mrmidi commented Sep 18, 2026

Copy link
Copy Markdown
Owner

The cleanup backlog accumulated while reviewing #101 / #107 / #114 / #118. Eight commits, each independent and separately reviewable.

Defects

  • fix(audio): stop double-counting rxReplayEntries on the MOTU capture path — the counter was incremented in both the shared path and the MOTU block, so the MOTU capture path reported twice the real entry count. Every diagnosis that reads this counter on MOTU hardware has been reading an inflated number.
  • fix(audio): bound the MOTU profile fallback to the two models it fits — the fallback applied to any MOTU device, including models whose geometry it does not describe.
  • fix(audio): take the Loud/Mackie OUI from the shared table — a second, hand-written copy of the OUI.

Consolidation

  • refactor(audio): take every DeviceStreamModeQuirks identity from the shared table — same problem class as the OUI above: vendor/model identities written out again next to the table that already holds them.

Comments that were wrong

Three commits correcting stale comments (the MOTU offset cache, the Onyx-i start rate) and one recording what the EFC mailbox teardown wait is actually guarding — the existing comment described a different mechanism than the code implemented.

Test

  • test(audio): pin the EFC mailbox teardown race, including the UAF path — the race this suite could not previously express.

Rebased onto main after #123. No conflicts.

…path

The SPH-capture block added its own rxReplayEntries increment, but the
unconditional increment further down already counts every packet that publishes
a replay entry. A MOTU master stream therefore reported twice the replay entries
it had really published, in a counter that reaches asfw_get_audio_stream_health
and the diagnostics ring.

No behavioural change beyond the counter; motuTimingEstablished_ still latches
on the same condition.
AudioProfileRegistry::FindProfile defaulted every MOTU device that was not an
828mk2 to the UltraLite profile. Its comment defended this on the grounds that
DeviceProfiles had already matched the unit directory — but that is a different
registry; this one matches the vendor OUI alone and never consults DeviceProfiles.

Protocol v2 covers five models and their fixed chunk counts do not generalise
(Linux motu-protocol-v2.c:274-320). The 896HD and Traveler are {14,14,8}, which
happens to coincide with {14,14,0} below 176.4 kHz. The 8pre does not coincide at
all: tx {10,10,0} against rx {6,6,0}, asymmetric between directions, which a
single pcmChunks value cannot express.

Return nullptr outside the 828mk2 and UltraLite instead. Unreachable today,
because DeviceProfiles resolves kNone for the other three so they never get a
nub — but that is a guard in another layer, not a property of this function.

Also corrects MotuV2Profile's claim that geometry is shared across the v2
fixed-chunk models, which reads as an invitation to enable the 8pre by adding one
case to a name switch.
kOnyxIDefaultStartRateHz was documented as a start default that explicit user
rate selections would override. It is not: EffectiveStartClockForProfile returns
it unconditionally for the Onyx-i, discarding request.desiredClock the same way
the Duet pin does, and AVCDiscovery publishes sampleRates = {44100} so there is
no other rate to select in the first place.

The comment describes the state after the M4 48 kHz work and before c0e5da6
reverted it, so it has been wrong since that revert. Left a pointer to the
revert, since whoever restores rate switching has to undo this pin too.

Comment only.
…arding

RemoveObserver's bounded wait exists to stop Publish() being inside an observer
while that observer is destroyed. Two things about it were undocumented.

A false return is a use-after-free hazard rather than something to log: the
EfcTransport destructor reports it and then frees its lock and returns anyway,
with a Publish() potentially still inside OnResponse.

And the bound is large because the window is large. OnResponse ends in
Complete(), which calls the caller-supplied completion function, so the inflight
region spans arbitrary client work -- not a short critical section. That makes
the wait an IODelay spin of up to 100 ms on the teardown queue, and it also means
shrinking the timeout would convert waits that currently succeed into
use-after-frees.

The fix is to shrink the window, not the bound: run Complete() outside the
observer callback, leaving only the decode and sequence match inside it. Recorded
here so the obvious-looking change is not attempted first.

Comment only.
The class comment said the per-data-block SPH cache lives with the consumer that
fills it. It does not — it lives in AudioTransportControlBlock, because the
transmit side drains on the audio queue what the consumer fills on the transport
queue, which is precisely why it cannot live on either side.

The second was a doc comment left behind when the member it described moved to
the control block: three lines describing a field that is not there, immediately
above the closing brace.

Both are removed by the planned RX format extraction, which deletes this MOTU
paragraph entirely. Fixed anyway so the record is right in the meantime and the
correction survives in history.

Comment only.
DeviceStreamModeQuirks declared its own kLoudMackieVendorId = 0x000ff2 while the
same change that added the vendor-wide blocking rule also added kMackieVendorId
to AudioDeviceIds. The values agree today; two spellings of one OUI can only
drift apart. Alias the shared constant instead, keeping the file's local-name
style for the call site.
RemoveObserver's bounded wait is what stops an observer being destroyed while
Publish() is still inside it, but nothing tested either outcome.

Three cases: that it actually waits for an in-flight publish rather than
returning immediately; that it returns false when the observer outlasts the
bound, which is the use-after-free condition EfcTransport then proceeds through;
and that a removed observer is not entered by a later publish.

The failure case takes ~100 ms of real time, since IODelay sleeps under
ASFW_HOST_TEST and the bound is 2000 x 50 us. That is the point -- it confirms
the wait reaches its limit rather than short-circuiting.
…shared table

The file respelled each vendor and model id locally, so every identity it
matches on was a second copy of one the rest of the driver already holds in
DeviceProfiles::Audio. The values agree today; nothing keeps them agreeing.

Alias the shared constants instead, keeping the local names so the rules below
still read as rules. Extends the Mackie change in the previous commit to
Apogee, Focusrite and Midas, which had the same duplication for longer.

No behavioural change: every value is identical (the Apogee pair differed only
in hex case).
@mrmidi
mrmidi merged commit 0d981a2 into main Sep 18, 2026
2 checks passed
@mrmidi
mrmidi deleted the fix/review-cleanup-batch branch September 18, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant