Skip to content

fix(uffd): re-read page state inside worker goroutine under settleRequests.RLock#2514

Closed
ValentaTomas wants to merge 1 commit intofeat/uffd-page-tracker-and-remove-eventsfrom
fix/uffd-stale-source-race-on-main
Closed

fix(uffd): re-read page state inside worker goroutine under settleRequests.RLock#2514
ValentaTomas wants to merge 1 commit intofeat/uffd-page-tracker-and-remove-eventsfrom
fix/uffd-stale-source-race-on-main

Conversation

@ValentaTomas
Copy link
Copy Markdown
Member

This PR is the fix side of a stacked pair targeting main. It depends
on PR #2513 (feat/uffd-page-tracker-and-remove-events), which lifts the
minimum production subset of UFFD code from #1896 plus the test harness
and the deterministic race tests that demonstrate this bug. Merge this
stack as a unit
: PR #2513 first, then this PR. Otherwise PR #2513's
TestStaleSourceRaceMissingAndRemove fails on main and CI goes red.

The bug

The Serve() loop on PR #2513 (and on feat/free-page-reporting) reads
pageTracker state and captures source = u.src in the parent loop,
then dispatches a worker goroutine. A UFFD_EVENT_REMOVE for the same
page that arrives in the window between the parent-loop state read and
the worker actually acquiring settleRequests.RLock() silently leaves
the worker with a stale source = u.src snapshot. The worker then
UFFDIO_COPYs u.src bytes into a page the kernel has just
MADV_DONTNEED'd, leaving pageTracker == removed but the kernel page
re-mapped with stale src data. Observably this also deadlocks parent
madvise() in the orchestrator unit test suite under tight enough
interleavings.

The fix

Move the pageTracker.get(addr) lookup and the source = u.src capture
inside the worker goroutine, after acquiring
settleRequests.RLock(). Now the read+act+commit sequence
(state lookup → faultPage → setState) is atomic with respect to any
concurrent REMOVE batch (which takes settleRequests.Lock()). One file
changed, +30 / -21 lines.

Verification

Run on this branch (sudo, Linux, Go 1.25.9):

  • All three race tests from PR feat(uffd): add REMOVE-event handling, pageTracker removed state, and deterministic race tests #2513 pass:
    • TestStaleSourceRaceMissingAndRemove (4k + hugepage) — was
      got 0xc3 pre-fix; now zero-fills as expected.
    • TestNoMadviseDeadlockWithInflightCopy (4k + hugepage).
    • TestFaultedShortCircuitOrdering (4k + hugepage).
  • Single run: 0.302s test wallclock / 1.6s total.
  • -count=20 -timeout=30s soak across all three: passes in 5.5s.
  • Full pkg/sandbox/uffd/userfaultfd/... suite (incl. TestRemove*,
    TestRemoveThenWriteGated, TestWriteThenRemoveGated,
    TestMissing*, TestMissingWrite*, TestAsyncWriteProtection,
    TestSerial*, TestParallel*): all pass post-fix in 0.93s.

Related

Stacking

Test plan

  • go build ./... clean.
  • go vet ./... clean.
  • sudo go test -run 'Race|Deadlock|Ordering' -count=1 — all pass.
  • sudo go test -run 'Race|Deadlock|Ordering' -count=20 -timeout=30s — soak passes (5.5s).
  • sudo go test ./pkg/sandbox/uffd/userfaultfd/... — full suite passes.

…uests.RLock

The Serve() loop previously read pageTracker state and captured
`source = u.src` in the parent loop, then dispatched a worker
goroutine. A REMOVE event for the same page that arrived between the
state read and the worker actually acquiring settleRequests.RLock()
would silently leave the worker with a stale `source = u.src`
snapshot. The worker would then UFFDIO_COPY src bytes into a page the
kernel had just MADV_DONTNEED'd, leaving pageTracker == removed and
the kernel page mapped with stale src data — and observably
deadlocking parent madvise() in the orchestrator unit-test suite.

Move the state lookup and source capture inside the goroutine, after
RLock(). The read+act+commit sequence is now atomic with respect to
the REMOVE batch (which takes settleRequests.Lock()).

# Conflicts:
#	packages/orchestrator/pkg/sandbox/uffd/userfaultfd/userfaultfd.go
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 28, 2026

PR Summary

Medium Risk
Touches the hot-path userfaultfd page-fault handling and locking, so mistakes could introduce deadlocks or regress fault throughput/latency under concurrency.

Overview
Fixes a race between UFFD_EVENT_REMOVE handling and concurrent page-fault workers by moving the pageTracker state check and u.src capture into the worker goroutine after acquiring settleRequests.RLock(), ensuring a consistent state/source decision before issuing UFFDIO_* and committing faulted.

Reviewed by Cursor Bugbot for commit 8a88dbf. Bugbot is set up for automated code reviews on this repo. Configure here.

@ValentaTomas
Copy link
Copy Markdown
Member Author

Closing in favor of #2512. We're keeping the Stack A form (#2511#2475 → main). #2513/#2514 were a duplicate Option-3 stack opened in parallel; superseded.

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