Skip to content

Fix use-after-free in DudesCab::Finish() at teardown - #65

Closed
NLygitadm wants to merge 1 commit into
vpinball:masterfrom
NLygitadm:fix/dudescab-finish-uaf
Closed

NLygitadm wants to merge 1 commit into
vpinball:masterfrom
NLygitadm:fix/dudescab-finish-uaf

Conversation

@NLygitadm

Copy link
Copy Markdown

Problem

When a DudesCab coexists with another output controller (in my case a TeensyStripController driving an addressable backboard), quitting a table intermittently crashes the process with double free or corruption (out) / free(): invalid pointer.

Root cause

Cabinet teardown runs OutputControllerList::Finish(), which calls DudesCab::ClearDevices() and frees the shared static Device (s_devices). ~DudesCab() then calls Finish() a second time, which still holds the now-dangling m_dev and dereferences it via m_dev->AllOff() — a use-after-free. It is timing-dependent, hence intermittent. Confirmed with AddressSanitizer, which pinpointed the freed access.

Fix

Null out m_dev right after AllOff() in Finish(), so a subsequent Finish() (from the destructor) becomes a no-op. This mirrors the pattern already used in the disconnect path of UpdateOutputs() (m_dev->AllOff(); m_dev = nullptr;). No behavior change for the normal single-Finish() path: a null m_dev is already guarded throughout the class, and m_dev is re-populated on setup.

Testing

  • Built Release for Linux x64.
  • Ran on a PinCabOS cabinet with a DudesCab (LedWiz #90) + a TeensyStripController driven together: repeated table open/close, no more crashes.
  • AddressSanitizer build: the reported use-after-free at teardown is gone.

🤖 Generated with Claude Code

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>
@jsm174

jsm174 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

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

@NLygitadm

Copy link
Copy Markdown
Author

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! 🙏

@NLygitadm NLygitadm closed this Aug 31, 2026
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.

2 participants