Skip to content

test(uffd): add cross-process scaffolding for gated and async ops - #2475

Merged
ValentaTomas merged 7 commits into
mainfrom
feat/uffd-test-scaffolding
May 1, 2026
Merged

ValentaTomas merged 7 commits into
mainfrom
feat/uffd-test-scaffolding

Conversation

@ValentaTomas

Copy link
Copy Markdown
Member

Summary

Pre-work for the upcoming UFFD REMOVE event handling PR. Lands as a runtime no-op on main: no production code changes, no new UFFD features enabled. Splitting it out so the REMOVE PR is just the production logic + the tests that actually exercise it.

All changes are in *_test.go files in packages/orchestrator/pkg/sandbox/uffd/userfaultfd/.

What this adds

helpers_test.go — generic test API surface

  • testConfig.gated flag + per-operation async bool
  • New operationModes: Remove, ServePause, ServeResume, Sleep
  • testHandler.servePause / serveResume hooks
  • executeAll async path: fan out goroutines, join on t.Context()
  • expectRemoved + read/write/remove ordering in checkDirtiness
  • executeRemove via unix.MADV_DONTNEED

fd_helpers_test.go — UFFD ioctl helpers

  • unregister() wrapping UFFDIO_UNREGISTER

cross_process_helpers_test.go — main↔helper plumbing

  • Two extra control pipes (fd 7 cmd, fd 8 sync) when tt.gated == true
  • Helper subprocess goroutine that can pause and restart the Serve loop on demand (drains via fdexit, then spins a fresh one)
  • unregister(uffdFd, ...) before close in cleanup
  • pageStateEntries() reworked to drain settleRequests and then take the pageTracker RLock directly, so the snapshot stays correct for future writers that don't go through settleRequests (the REMOVE handler in the follow-up)

Why land it separately

  • The REMOVE PR is going to be large; pre-merging the test API keeps that diff focused on production code + the tests that actually need the scaffolding.
  • The Remove/ServePause/ServeResume/Sleep operations and gated/async paths are not exercised by any test in this PR. They're live-but-unused — dead at runtime — and kept here only to minimise the follow-up's diff. Without them, the REMOVE PR would have to add ~200 lines of test infra alongside the actual logic.

Test plan

  • go build ./... clean
  • go vet ./pkg/sandbox/uffd/... clean
  • golangci-lint run ./pkg/sandbox/uffd/userfaultfd/... clean
  • go test ./pkg/sandbox/uffd/userfaultfd/... passes (with vm.unprivileged_userfaultfd=1)

Pre-work for the upcoming REMOVE event handling PR. Lands as a runtime
no-op on main: no production code changes, no new UFFD features enabled.

Test-helper additions (all *_test.go):

- testConfig.gated and per-op `async bool`
- operationModeRemove / ServePause / ServeResume / Sleep
- testHandler.servePause / serveResume hooks driven by control pipes
  (fd 7/8) into the helper subprocess
- helper subprocess pause/resume goroutine that tears down and restarts
  the UFFD serve loop on demand
- executeAll() async path that fans out goroutines and joins on test
  context
- expectRemoved + checkDirtiness handling for read/write/remove ordering
- executeRemove via unix.MADV_DONTNEED
- unregister() helper around UFFDIO_UNREGISTER for cleanup
- pageStateEntries() now drains settleRequests then takes the
  pageTracker RLock directly so the snapshot stays correct for future
  writers (e.g. REMOVE) that don't go through settleRequests

operationModeRemove and the gated/async paths are exercised only by the
follow-up PR; here they are dead code at runtime, kept to minimise the
diff of that PR.
@cursor

cursor Bot commented Apr 21, 2026 •

Copy link
Copy Markdown

PR Summary

Low Risk
Test-only changes add new cross-process control and concurrency paths, which could introduce flakiness but do not affect production behavior.

Overview
Extends the UFFD cross-process test harness to support deterministically pausing/resuming the helper process Serve loop and running read/write operations asynchronously, including new gating pipes and synchronization in the helper, plus safer page-state snapshotting and cleanup via UFFDIO_UNREGISTER to avoid future REMOVE-related teardown hangs.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab71d89fe7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/orchestrator/pkg/sandbox/uffd/userfaultfd/cross_process_helpers_test.go Outdated
Codex review caught a deadlock: in GO_GATED mode, handling 'P' replaced
the stop closure with one that drained `exitUffd`, but the deferred
final cleanup still pointed at the original closure. If the helper exited
between 'P' and 'R' (e.g. parent test failure, ctx cancel, SIGUSR1), the
defer ran the original cleanup again and blocked forever on `<-exitUffd`
— the channel had already been drained and the original Serve goroutine
had already exited. The hung helper would in turn hang the parent's
cmd.Wait().

Replace the two-variable (cleanup / stopServe) layout with a single
mutex-guarded `stopFn` that is reset to a no-op after firing, and have
both the gated 'P' handler and the deferred final cleanup go through
the same `stopServe` wrapper. 'R' now just installs a fresh `stopFn`
that drains the new goroutine.
Comment thread packages/orchestrator/pkg/sandbox/uffd/userfaultfd/cross_process_helpers_test.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/uffd/userfaultfd/helpers_test.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/uffd/userfaultfd/helpers_test.go Outdated
Pull out everything that's not strictly needed for "gated pause/resume +
async ops" so PR T is the smallest possible scaffolding diff and the
follow-up REMOVE PR carries those pieces alongside the production code
that actually needs them:

- helpers_test.go: drop operationModeRemove, executeRemove, expectRemoved
  and the REMOVE-aware checkDirtiness branch — none of the PR T tests
  call them, and they only become meaningful once UFFD_FEATURE_EVENT_REMOVE
  is set in the follow-up.
- fd_helpers_test.go: revert; drop unregister() helper. Without
  UFFD_FEATURE_EVENT_REMOVE, MADV_DONTNEED never queues UFFD events, so
  munmap doesn't block on un-acked events and there's nothing to
  unregister around.
- cross_process_helpers_test.go:
  - drop unregister(uffdFd, ...) call in late cleanup (same reason as
    above).
  - revert pageStateEntries() to the simple settleRequests.Lock pattern;
    the rework was forward-looking for a REMOVE handler that doesn't go
    through settleRequests and is dead code on main.
  - keep cleanup → stopFn/stopServe restructure (mandatory for the
    gated tear-down/respawn path) but restore the original
    fdExit.SignalExit error handling that the previous version had
    silently dropped.
  - revert incidental cosmetic reformats (exitUffd placement, defer
    multi-line) so the diff is purely additive in the unchanged paths.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bbdc8f9. Configure here.

Comment thread packages/orchestrator/pkg/sandbox/uffd/userfaultfd/helpers_test.go
@ValentaTomas
ValentaTomas marked this pull request as ready for review April 21, 2026 22:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bbdc8f921f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/orchestrator/pkg/sandbox/uffd/userfaultfd/helpers_test.go
- Guard executeOperation against nil servePause/serveResume hooks when a
  non-gated test misuses operationModeServePause/Resume — return a clear
  error instead of panicking on a nil function call (cursor + codex).
- Make pageStateEntries() also hold pageTracker.mu.RLock() so the
  snapshot stays correct for any future writer that doesn't go through
  settleRequests (claude — matches the PR description).
- Make gated 'P' check the gateSyncFile.Write error and propagate via
  cancel() so a write failure doesn't leave the parent's servePause
  closure blocked indefinitely on Read (claude).
- Make the gated command goroutine reject 'P'-when-paused and
  'R'-when-running so a stray or duplicate resume can't leak an
  untracked Serve goroutine and break later pauses (codex).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c100bc33c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/orchestrator/pkg/sandbox/uffd/userfaultfd/helpers_test.go
Previously executeWrite took h.mutex.Lock() but executeRead held no lock,
so an async write + async read on the same page would race on memoryArea
under go test -race even though the bytes happen to match (data.Slice is
deterministic per offset). Switch h.mutex to sync.RWMutex; reads take
RLock so concurrent reads still trigger UFFD faults in parallel, writes
take Lock so they exclude both other writers and any in-flight reads.

No active test in this PR mixes async read/write yet; this is preemptive
against the upcoming REMOVE PR's matrix tests.
@ValentaTomas
ValentaTomas merged commit dd91ea8 into main May 1, 2026
45 checks passed
@ValentaTomas
ValentaTomas deleted the feat/uffd-test-scaffolding branch May 1, 2026 21:44
ValentaTomas added a commit that referenced this pull request May 1, 2026
Resolve conflicts in cross_process_helpers_test.go and helpers_test.go
by taking the RPC-based test harness from this branch and folding in the
post-#2475 fixes from main:

- helpers_test.go: keep the RPC client/conn/cmd fields plus mutex as
  sync.RWMutex (so executeRead can RLock and executeWrite can Lock,
  matching the just-landed race fix) and the nil-guards in
  executeOperation for ServePause/ServeResume on non-gated handlers.
- cross_process_helpers_test.go: keep the net/rpc + jsonrpc harness,
  add the Service.startServe idempotency guard (early-return when a
  serve goroutine is already running) so a stray duplicate ServeResume
  RPC can't leak an untracked Serve goroutine, mirroring the running
  flag fix on main. Also add the unregister(uffdFd, ...) cleanup at
  the end of the cmd-wait t.Cleanup so a future REMOVE-events test
  doesn't hang in munmap on un-acked events.
- fd_helpers_test.go: auto-merged to keep the unregister helper from
  main (the RPC harness now uses it).
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