Skip to content

fix(web): re-check screen-mirror ownership after EnterRPC to avoid orphaning the Flipper#162

Merged
xunholy merged 1 commit into
mainfrom
fix/screen-mirror-acquire-toctou
Jun 29, 2026
Merged

fix(web): re-check screen-mirror ownership after EnterRPC to avoid orphaning the Flipper#162
xunholy merged 1 commit into
mainfrom
fix/screen-mirror-acquire-toctou

Conversation

@xunholy

@xunholy xunholy commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

A HIGH-severity acquire/disconnect TOCTOU in the web screen-mirror path that leaves the Flipper wedged + leaks a goroutine (category A/C).

handleScreenAcquire tentatively claims the mirror (screenHolder = c, mirrorActive = true) and then runs the slow EnterRPC serial round-trip outside screenMu, storing the resulting cancel/release/rpcClient without re-checking ownership. If the holding WebSocket connection drops while EnterRPC is in flight, handleWebSocket's defer calls releaseScreen, which sees a still-nil screenCancel/screenRelease and can only nil the holder. handleScreenAcquire then resumes and stores the now-orphaned release func and starts the frame stream:

  • release() is never called → Flipper left wedged in RPC mode;
  • streamFrames/heartbeat run on a live ctx → goroutine leak;
  • mirrorActive ends up false while RPC is live, so refuseIfMirrorActive wrongly admits /api/fs, /api/device, and input requests whose serial commands then collide with the active RPC session.

Fix

After EnterRPC, re-acquire screenMu and verify screenHolder == c. If the holder changed during the gap, hand the device back (cancel() + release()) and bail instead of storing orphaned state. (The Marauder acquire path is unaffected — no slow device-open inside its holder window.)

Found via the internal audit sweep.

Verification

  • TestScreen_AcquireReleasedDuringEnterRPC_HandsDeviceBack uses a gated EnterRPC to inject a holder-disconnect during the gap and asserts release() IS called and no orphaned state remains. Proven to FAIL on the pre-fix code ("Flipper left wedged in RPC mode"). The existing concurrent-acquire invariant test still passes.
  • task ci green (lint 0 / vet / build / test:full 0 fail / govulncheck clean); task eval 17/17.

Bug fix — anchors v0.780.0.

…phaning the Flipper

handleScreenAcquire tentatively claims the mirror (screenHolder = c,
mirrorActive = true) and then runs the slow EnterRPC serial round-trip OUTSIDE
screenMu, but stored the resulting cancel/release/rpcClient without re-checking
that it is still the holder. If the holding WebSocket connection drops while
EnterRPC is in flight, handleWebSocket's defer calls releaseScreen, which sees
a still-nil screenCancel/screenRelease and can only nil the holder (it can't
tear down an RPC session it doesn't yet know about). handleScreenAcquire then
resumed and blindly stored the now-orphaned release func and started the frame
stream. Consequences:
  - release() was never called -> Flipper left wedged in RPC mode;
  - streamFrames/heartbeat run on a live ctx -> goroutine leak;
  - mirrorActive ended up false while RPC was actually live, so
    refuseIfMirrorActive wrongly admitted /api/fs, /api/device, and input
    requests whose serial commands then collided with the active RPC session.

Fix: after EnterRPC, re-acquire screenMu and verify screenHolder == c. If the
holder changed during the gap, hand the device back (cancel() + release()) and
bail instead of storing orphaned state. The Marauder acquire path is not
affected — it has no slow device-open inside the holder window.

Found via the internal audit sweep.

Test: TestScreen_AcquireReleasedDuringEnterRPC_HandsDeviceBack uses a gated
EnterRPC to inject a holder-disconnect during the gap and asserts release() IS
called and no orphaned screen state remains. Proven to FAIL on the pre-fix code
("Flipper left wedged in RPC mode"). The existing concurrent-acquire invariant
test still passes.

Verified: task ci green (lint 0 / vet / build / test:full 0 fail / govulncheck
clean); task eval 17/17.
@xunholy xunholy merged commit 19a576f into main Jun 29, 2026
5 of 6 checks passed
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