Conversation
Cabinet teardown calls OutputControllerList::Finish() -> DudesCab::ClearDevices(), which deletes the shared static Device. ~DudesCab() then calls Finish() again and dereferences the now-dangling m_dev via m_dev->AllOff(), causing an intermittent double free / free(): invalid pointer. This only manifests when a DudesCab coexists with another output controller (e.g. a TeensyStripController) and was diagnosed with AddressSanitizer. Null out m_dev after AllOff() so the second Finish() becomes a no-op. This mirrors the pattern already used in the disconnect path in UpdateOutputs(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for this. I have a couple different strategies with ai, where I always feed it original C# source code, so we try to stay as 1:1 as possible. Can you try the latest, see #66 |
|
Thanks @jsm174 — #66 is the cleaner fix, and keeping it 1:1 with the C# source is clearly the right call. Removing the Finish() call from the destructor altogether addresses the same teardown use-after-free we hit, without the extra m_dev = nullptr; guard. I rebuilt libdof at #66 (master 0383246) and deployed it on our Linux cabinet (vpinfe / PinCabOS) with a Dude's Cab + Teensy strip controller running together — the intermittent double free / free(): invalid pointer on table exit is gone, and both controllers initialize and reconnect cleanly. Closing this one in favor of #66. Thanks for the quick turnaround! 🙏 |
Problem
When a DudesCab coexists with another output controller (in my case a
TeensyStripControllerdriving an addressable backboard), quitting a table intermittently crashes the process withdouble free or corruption (out)/free(): invalid pointer.Root cause
Cabinet teardown runs
OutputControllerList::Finish(), which callsDudesCab::ClearDevices()and frees the shared staticDevice(s_devices).~DudesCab()then callsFinish()a second time, which still holds the now-danglingm_devand dereferences it viam_dev->AllOff()— a use-after-free. It is timing-dependent, hence intermittent. Confirmed with AddressSanitizer, which pinpointed the freed access.Fix
Null out
m_devright afterAllOff()inFinish(), so a subsequentFinish()(from the destructor) becomes a no-op. This mirrors the pattern already used in the disconnect path ofUpdateOutputs()(m_dev->AllOff(); m_dev = nullptr;). No behavior change for the normal single-Finish()path: a nullm_devis already guarded throughout the class, andm_devis re-populated on setup.Testing
🤖 Generated with Claude Code