fix: prevent a stale teardown from affecting a newer session generation - #1231
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Follow-up commit: Since the wait in Waiters now block until the count reaches zero, i.e. until every in-flight teardown has finished. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Follow-up commit: the generation is now claimed in The guard was being claimed too late to protect the thing it was meant to protect. Claiming it at the moment a caller takes ownership of the device closes that window. A teardown that the call actually ran to completion is already finished, so invalidating it there is a no-op. |
… lock Clearing _activeSession and registering its teardown happened under two different locks, so a concurrent session creation could observe no active session and no teardown in progress and skip its wait. All three statics now share +teardownCondition, and -kill does both in one critical section.
The promise was read from the process-wide container at teardown time, so a stale -kill could stop a newer session's recording even though the reset was generation-guarded. Both steps now run only while the generation is current.
Every comment added or touched here is now at most three lines.
The stale-teardown check was only a check: a replacement could bump the generation between it and the read of the process-wide container, so the teardown could stop the replacement's recording. Read the promise inside the critical section that validates the generation instead, and make the reset conditional on the container still holding that exact promise - the stop can take up to 20 seconds, long enough for a replacement to store its own.
isSessionGenerationCurrent: released the lock before -terminate ran, so a replacement could claim the next generation and start launching in between, leaving the stale teardown to terminate the replacement's process - they share a bundle ID, so capturing an identity does not help here. Turn the check into a claim taken under the same lock as the generation bump and held across -terminate, and make the bump wait (bounded by the termination timeout) for any claim to be released.
The identity check and the reset were separate steps, so a replacement session could store its own promise in between and have it cleared by the stale teardown. Move the comparison into the container, behind its own lock.
The bounded wait fell through and bumped the generation anyway, so a session could start launching while the previous one's committed -terminate was still running. Raise FBSessionCreationException instead of handing out a generation.
+activeSession, +killActiveSessionAndWaitForTeardown and +sessionWithIdentifier: touched the global without the lock, and the last one read it twice, so it could validate one session's identifier and return its replacement.
## [16.11.4](v16.11.3...v16.11.4) (2026-08-30) ### Bug Fixes * prevent a stale teardown from affecting a newer session generation ([#1231](#1231)) ([83642a1](83642a1))
|
🎉 This PR is included in version 16.11.4 🎉 The release is available on: Your semantic-release bot 📦🚀 |
+killActiveSessionAndWaitForTeardownwaits at mostFB_KILL_WAIT_TIMEOUT_SEC(35s) for an in-progress teardown, then proceeds regardless. A worst-case teardown is already close to that budget:stopScreenRecordingWithUUID:(STOP_SCREEN_RECORDING_TIMEOUT_SEC)-fb_isTestedApplicationSameAsSystemAppWithTimeout:-fb_terminateTestedApplicationWithTimeout:plus the notification post and
-disableAlertsMonitor. So ~30s of bounded work against a 35s wait — an overrunning teardown can still be mid-flight whenPOST /sessiongives up waiting and starts a replacement session.The remaining teardown steps then mutate process-wide state on behalf of a session that is already gone:
[application terminate]— the replacement session usually runs the same bundle identifier, so "terminate the old app" terminates the new session's app. The existingisAllowedToTerminateguard only covers the terminate step overrunning its own 5s timeout, not the overall wait having expired.FBScreenRecordingContainer.sharedInstanceat teardown time, so by then it can already be the replacement session's. Both thestopScreenRecordingWithUUID:and theresetcan therefore hit the new session's recording rather than the old one.This adds a session generation counter, bumped in
+killActiveSessionAndWaitForTeardown— i.e. at the moment a caller takes ownership of the device, before it launches anything, since the launch window is itself long enough for a stale teardown to do damage.-killcaptures the generation it started with, and each step that mutates shared state re-checks it via+isSessionGenerationCurrent:. The terminate check happens inside the main-queue block rather than before dispatching, since that block is exactly where the unbounded delay occurs; the recording cleanup is skipped as a whole, covering the stop as well as the reset.Because the wait is bounded, teardowns can genuinely overlap, so
-kill's in-progress marker is a count rather than a flag — otherwise the first teardown to finish would wake waiters while another was still running._activeSession, the teardown count and the generation are all guarded by a single lock (+teardownCondition), and-killclears the active session and registers its teardown in one critical section. Without that, a concurrent session creation could observe neither an active session nor a registered teardown, skip its wait, and start launching before the old teardown had registered itself. The lock is only ever held for those short transitions — never across the teardown work, the main-queue dispatch, or the bounded wait.🤖 Generated with Claude Code