Skip to content

Fix unit_sandbox_ssrf leaf-swap flake on macOS CI - #367

Merged
tylerreckart merged 1 commit into
mainfrom
fix/sandbox-ssrf-leaf-swap-test-flake
Sep 21, 2026
Merged

tylerreckart merged 1 commit into
mainfrom
fix/sandbox-ssrf-leaf-swap-test-flake

Conversation

@tylerreckart

Copy link
Copy Markdown
Owner

Problem

main is red on macos-arm64 because unit_sandbox_ssrf (the O_NOFOLLOW refuses leaf symlink swap after resolve case added in #316) fails on the loaded macOS runner. This blocks every open PR whose branch is based on current main (e.g. #342, and #338–#341 once they rebase onto main).

Root cause

The test races a planter thread (swap a regular decoy.txt for a symlink to a host file) against read_from_workspace's re-check pause:

  • reader: resolve_within_workspace → exists check → sleep(read_check_pause_ms = 80ms) → open(O_NOFOLLOW)
  • planter: sleep(20ms) → unlink → symlink

On the macos-arm64 CI runner, std::this_thread::sleep_for has coarse timer granularity and the box is heavily loaded (note unit_sandbox_quota alone takes ~373s there). The planter's 20ms sleep overshot past the reader's 80ms open, so the reader opened the still-regular decoy: ok = true, content was the workspace bytes (no secret leaked), but CHECK_FALSE(ok) failed.

I confirmed O_NOFOLLOW itself is fine on Darwin — a local probe shows open(symlink, O_NOFOLLOW) returns ELOOP (errno 62). The failure is timing, not a portability bug.

Fix

Widen the reader's pre-open window to 400ms and shrink the planter's pre-swap sleep to 5ms. The swap now reliably lands:

  • after resolve_within_workspace (resolve is a handful of syscalls, ≪1ms, so a 5ms planter sleep guarantees resolve wins → the open hits the O_NOFOLLOW refusal path rather than resolve-rejection), and
  • before the O_NOFOLLOW open (5ms ≪ 400ms, with huge margin against coarse macOS timer oversleep or scheduler jitter).

Verified locally on arm64: unit_sandbox_ssrf passes 8/8 runs (was failing every run on the macOS CI runner).

Scope

Test-only change (plus a CHANGELOG entry). No production code touched.

Made with Cursor

The TOCTOU test that swaps a regular decoy.txt for a symlink during
read_from_workspace's re-check pause used an 80ms reader window and a
20ms planter sleep. On the loaded macos-arm64 runner,
std::this_thread::sleep_for coarsely overshot, the planter swapped
after the reader's O_NOFOLLOW open, and the reader saw the
still-regular decoy -- CHECK_FALSE(ok) failed even though no host
bytes leaked (the read returned the workspace bytes, not the secret).

Widen the reader's pre-open window to 400ms and shrink the planter's
pre-swap sleep to 5ms so the swap reliably lands after
resolve_within_workspace and before the open, exercising the
O_NOFOLLOW refusal path deterministically regardless of scheduler
jitter or coarse macOS timer granularity.

Verified locally on arm64: unit_sandbox_ssrf passes 8/8 runs.
@tylerreckart
tylerreckart merged commit 0d223a4 into main Sep 21, 2026
5 checks passed
@tylerreckart
tylerreckart deleted the fix/sandbox-ssrf-leaf-swap-test-flake branch September 21, 2026 12:44
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