Skip to content

orch: fix uffd prefault/close race condition - #2830

Merged
kalyazin merged 2 commits into
mainfrom
kalyazin/prefetch_race
May 28, 2026
Merged

kalyazin merged 2 commits into
mainfrom
kalyazin/prefetch_race

Conversation

@kalyazin

@kalyazin kalyazin commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

Fix Prefault/Close race causing ENOTTY/EBADF

Motivation

Since 2026-04-26 the orchestrator has been logging UFFD serve uffdio copy error: inappropriate ioctl for device (ENOTTY) and bad file descriptor (EBADF) from the UFFD prefetch path. The error rate increased sharply on 2026-05-27 after #2522 landed.

The root cause is a race between the prefetcher and sandbox teardown:

Prefault goroutine                  Close() (teardown)
────────────────────────────────    ──────────────────────────────
(about to acquire RLock)
                                    syscall.Close(uffd fd)   ← fd freed
                                    ← OS recycles fd number
                                      to an unrelated file
acquires RLock
calls UFFDIO_COPY(recycled fd)
→ ENOTTY  (or EBADF if not yet recycled)

Close() held no lock when it freed the uffd fd. The prefetcher runs on a non-cancellable execCtx and has no way to observe that Close() has run. If the OS recycled the fd number before the prefetcher's UFFDIO_COPY ioctl fired, the kernel returned ENOTTY because the fd now referred to a non-uffd file. The error was benign at the sandbox level (the sandbox was already being torn down) but noisy and misleading in logs.

The bug was latent from when the prefetcher was introduced in January 2026 (#1705) but only started firing after the ubuntu24 template rebuild in April populated Prefetch.Memory data, causing the prefetcher to actually run.

Fix

Close() now holds settleRequests.Lock() for the entire close sequence and is idempotent:

func (u *Userfaultfd) Close() error {
    u.settleRequests.Lock()
    defer u.settleRequests.Unlock()

    if u.closed {
        return nil
    }
    u.closed = true

    syscall.Close(u.wakeupPipe[0])
    syscall.Close(u.wakeupPipe[1])

    return u.fd.close()
}

Prefault() checks the flag immediately after acquiring settleRequests.RLock():

u.settleRequests.RLock()
defer u.settleRequests.RUnlock()

if u.closed {
    return nil
}

This gives three safety guarantees:

  1. Any Prefault caller already holding RLock when Close() runs will complete its UFFDIO_COPY against the still-valid fd before Close() can acquire the write lock.
  2. Any Prefault call that starts after Close() returns will see closed == true and return nil without touching the fd.
  3. Close() is idempotent: a second call returns immediately without touching already-freed fds, preventing accidental double-close of the wakeup pipe fds (and any unrelated fd the OS may have recycled those numbers to).

settleRequests already existed for exactly this kind of serialisation (guarding the lookup→install→state-update sequence against REMOVE batches), so no new lock is introduced. In production Serve() drains all workers via u.wg.Wait() before returning, so by the time the deferred Close() fires the lock is always uncontended.

Test

TestPrefaultConcurrentWithClose deterministically reproduces the race using a faultPhaseBeforePrefaultRLock test hook. The goroutine is parked before it acquires RLock, Close() is called to completion, then the goroutine is released. Without the fix the test fails with failed to fault page: failed uffdio copy: bad file descriptor; with the fix it returns nil.

@cla-bot cla-bot Bot added the cla-signed label May 27, 2026
@cursor

cursor Bot commented May 27, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes concurrent UFFD teardown and prefault behavior in the orchestrator sandbox path; scope is narrow and covered by a targeted regression test, but mistakes could affect sandbox lifecycle correctness.

Overview
Fixes a teardown race where Prefault could run UFFDIO_COPY on a userfaultfd already closed (and possibly recycled), which showed up in production as ENOTTY / EBADF log noise.

Close now takes the existing settleRequests write lock for the full shutdown, sets an idempotent closed flag, then closes fds; Prefault takes the read lock and returns nil without touching the fd when closed is set. A deterministic Linux regression test parks Prefault before the lock so Close can finish first.

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

@codecov

codecov Bot commented May 27, 2026 •

Copy link
Copy Markdown

❌ 6 Tests Failed:

Tests completed Failed Passed Skipped
2676 6 2670 7
View the full list of 7 ❄️ flaky test(s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestSandboxListPaginationRunningLargerLimit

Flake rate in main: 43.29% (Passed 659 times, Failed 503 times)

Stack Traces | 33.1s run time
=== RUN   TestSandboxListPaginationRunningLargerLimit
    sandbox_list_test.go:327: Created sandbox 1/12: i9aclmqs66hqh8gx22xqz
    sandbox_list_test.go:327: Created sandbox 2/12: izaquc807a05b00bw7zuc
    sandbox_list_test.go:327: Created sandbox 3/12: ixqmbfyvt8t6es77edaem
    sandbox_list_test.go:327: Created sandbox 4/12: ibo4txxiih1ogqpv2u9ds
    sandbox_list_test.go:327: Created sandbox 5/12: iym9uayi10zf2187qiy23
    sandbox_list_test.go:327: Created sandbox 6/12: ish0rj67qry1qq5i7annt
    sandbox_list_test.go:327: Created sandbox 7/12: ic4xx1fhbqeek8ru1zjds
    sandbox_list_test.go:327: Created sandbox 8/12: ia9lyj9a3fvxn04xuzdw0
    sandbox_list_test.go:327: Created sandbox 9/12: idlabg91wfuzzcz0y6bbf
    sandbox_list_test.go:327: Created sandbox 10/12: ibg1b4vk1fze4z2351yko
    sandbox_list_test.go:327: Created sandbox 11/12: imqlqd7u4o2z93iyap0g8
    sandbox_list_test.go:327: Created sandbox 12/12: ijkpswr7ahp2pcwkw09ia
--- FAIL: TestSandboxListPaginationRunningLargerLimit (33.12s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestSandboxListPaginationRunningLargerLimit/check_all_sandboxes_list

Flake rate in main: 43.63% (Passed 650 times, Failed 503 times)

Stack Traces | 0.02s run time
=== RUN   TestSandboxListPaginationRunningLargerLimit/check_all_sandboxes_list
=== PAUSE TestSandboxListPaginationRunningLargerLimit/check_all_sandboxes_list
=== CONT  TestSandboxListPaginationRunningLargerLimit/check_all_sandboxes_list
    sandbox_list_test.go:339: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:339
        	Error:      	"[{0x1ee3babbf360 6532622b 2 1982 2026-05-28 08:12:43.923386492 +0000 UTC 0.6.1 512 0x1ee3bac12148 ijkpswr7ahp2pcwkw09ia 2026-05-28 08:12:13.923386492 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd0438} {0x1ee3babbf3a0 6532622b 2 1982 2026-05-28 08:12:42.727555581 +0000 UTC 0.6.1 512 0x1ee3bac12150 imqlqd7u4o2z93iyap0g8 2026-05-28 08:12:12.727555581 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd0468} {0x1ee3babbf3e0 6532622b 2 1982 2026-05-28 08:12:32.610099174 +0000 UTC 0.6.1 512 0x1ee3bac12158 ibg1b4vk1fze4z2351yko 2026-05-28 08:12:02.610099174 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd0498} {0x1ee3babbf420 6532622b 2 1982 2026-05-28 08:12:32.310313058 +0000 UTC 0.6.1 512 0x1ee3bac12160 idlabg91wfuzzcz0y6bbf 2026-05-28 08:12:02.310313058 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd04c8} {0x1ee3babbf460 6532622b 2 1982 2026-05-28 08:12:31.702905947 +0000 UTC 0.6.1 512 0x1ee3bac12168 ia9lyj9a3fvxn04xuzdw0 2026-05-28 08:12:01.702905947 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd04f8} {0x1ee3babbf4a0 6532622b 2 1982 2026-05-28 08:12:31.147359873 +0000 UTC 0.6.1 512 0x1ee3bac12170 ic4xx1fhbqeek8ru1zjds 2026-05-28 08:12:01.147359873 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd0528} {0x1ee3babbf4e0 6532622b 2 1982 2026-05-28 08:12:29.066424937 +0000 UTC 0.6.1 512 0x1ee3bac12178 ish0rj67qry1qq5i7annt 2026-05-28 08:11:59.066424937 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd0558} {0x1ee3babbf520 6532622b 2 1982 2026-05-28 08:12:28.532387949 +0000 UTC 0.6.1 512 0x1ee3bac12180 iym9uayi10zf2187qiy23 2026-05-28 08:11:58.532387949 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd0588} {0x1ee3babbf560 6532622b 2 1982 2026-05-28 08:12:15.90565864 +0000 UTC 0.6.1 512 0x1ee3bac12188 ibo4txxiih1ogqpv2u9ds 2026-05-28 08:11:45.90565864 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd05b8} {0x1ee3babbf5a0 6532622b 2 1982 2026-05-28 08:12:14.467100445 +0000 UTC 0.6.1 512 0x1ee3bac12190 ixqmbfyvt8t6es77edaem 2026-05-28 08:11:44.467100445 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd05e8} {0x1ee3babbf5e0 6532622b 2 1982 2026-05-28 08:12:14.284023101 +0000 UTC 0.6.1 512 0x1ee3bac12198 izaquc807a05b00bw7zuc 2026-05-28 08:11:44.284023101 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd0618}]" should have 12 item(s), but has 11
        	Test:       	TestSandboxListPaginationRunningLargerLimit/check_all_sandboxes_list
--- FAIL: TestSandboxListPaginationRunningLargerLimit/check_all_sandboxes_list (0.02s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestSandboxListPaginationRunningLargerLimit/check_paginated_list

Flake rate in main: 43.63% (Passed 650 times, Failed 503 times)

Stack Traces | 0.09s run time
=== RUN   TestSandboxListPaginationRunningLargerLimit/check_paginated_list
=== PAUSE TestSandboxListPaginationRunningLargerLimit/check_paginated_list
=== CONT  TestSandboxListPaginationRunningLargerLimit/check_paginated_list
    sandbox_list_test.go:368: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:368
        	Error:      	Not equal: 
        	            	expected: 12
        	            	actual  : 11
        	Test:       	TestSandboxListPaginationRunningLargerLimit/check_paginated_list
    sandbox_list_test.go:368: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:368
        	Error:      	Not equal: 
        	            	expected: 12
        	            	actual  : 11
        	Test:       	TestSandboxListPaginationRunningLargerLimit/check_paginated_list
    sandbox_list_test.go:368: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:368
        	Error:      	Not equal: 
        	            	expected: 12
        	            	actual  : 11
        	Test:       	TestSandboxListPaginationRunningLargerLimit/check_paginated_list
    sandbox_list_test.go:368: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:368
        	Error:      	Not equal: 
        	            	expected: 12
        	            	actual  : 11
        	Test:       	TestSandboxListPaginationRunningLargerLimit/check_paginated_list
    sandbox_list_test.go:368: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:368
        	Error:      	Not equal: 
        	            	expected: 12
        	            	actual  : 11
        	Test:       	TestSandboxListPaginationRunningLargerLimit/check_paginated_list
    sandbox_list_test.go:362: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:362
        	Error:      	"[{0x1ee3ba7dc720 6532622b 2 1982 2026-05-28 08:12:14.284023101 +0000 UTC 0.6.1 512 0x1ee3bac121e0 izaquc807a05b00bw7zuc 2026-05-28 08:11:44.284023101 +0000 UTC running 2j6ly824owf4awgai1xo 0x1ee3babd0c18}]" should have 2 item(s), but has 1
        	Test:       	TestSandboxListPaginationRunningLargerLimit/check_paginated_list
--- FAIL: TestSandboxListPaginationRunningLargerLimit/check_paginated_list (0.09s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity

Flake rate in main: 58.33% (Passed 655 times, Failed 917 times)

Stack Traces | 68.1s run time
=== RUN   TestSandboxMemoryIntegrity
=== PAUSE TestSandboxMemoryIntegrity
=== CONT  TestSandboxMemoryIntegrity
    sandbox_memory_integrity_test.go:27: Build completed successfully
--- FAIL: TestSandboxMemoryIntegrity (68.06s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity/tmpfs_hash

Flake rate in main: 58.47% (Passed 645 times, Failed 908 times)

Stack Traces | 116s run time
=== RUN   TestSandboxMemoryIntegrity/tmpfs_hash
=== PAUSE TestSandboxMemoryIntegrity/tmpfs_hash
=== CONT  TestSandboxMemoryIntegrity/tmpfs_hash
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{start:{pid:1264}}
Executing command bash in sandbox iaus7nh3o1rtqxqax74p9 (user: root)
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Total memory: 985 MB\n"}}
Executing command bash in sandbox iaus7nh3o1rtqxqax74p9 (user: root)
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Used memory before tmpfs mount: 189 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Free memory before tmpfs mount: 795 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Memory to use in integrity test (80% of free, min 64MB): 636 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"636+0 records in\n636+0 records out\n666894336 bytes (667 MB, 636 MiB) copied, 2.7964 s, 238 MB/s\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\tCommand being timed: \"dd if=/dev/urandom of=/mnt/testfile bs=1M count=636\"\n\tUser time (seconds): "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"0.00\n\tSystem time (seconds): 2.78\n\tPercent of CPU this job got: 99%\n\tElapsed (wall clock) time (h:mm:ss or m:ss): 0:02.80\n\tAverage shared text size (kbytes): 0\n\tAverage unshared data size (kbytes): 0\n\tAverage stack size (kbytes): 0\n\tAverage total size (kbytes): 0\n\tMaximum resident set size (kbytes): 2612\n\tAverage resident set size (kbytes): 0\n\tMajor (requiring I/O) page faults: 3\n\tMinor (reclaiming a frame) page faults: 341\n\tVoluntary context switches: 4\n\tInvoluntary context switches: 38\n\tSwaps: 0\n\tFile system inputs: 176\n\tFile system outputs: 0\n\tSocket messages sent: 0\n\tSocket messages received: 0\n\tSignals delivered: 0\n\tPage size (bytes): 4096\n\tExit status: 0\n"}}
Executing command bash in sandbox ibhqygi7d7ka0iqe8un19 (user: root)
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Used memory after tmpfs mount and file fill: 831 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_memory_integrity_test.go:70: Command [bash] completed successfully in sandbox i15bun94zhfkjo4gyoe3f
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1280}}
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{data:{stdout:"15bcfccd01e92e97eb1f6d6e66246d32532dc1294f76f294359b7e13797b5ec2\n"}}
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_memory_integrity_test.go:80: Command [bash] completed successfully in sandbox i15bun94zhfkjo4gyoe3f
Executing command bash in sandbox ibhqygi7d7ka0iqe8un19 (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1284}}
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
Executing command bash in sandbox i15bun94zhfkjo4gyoe3f (user: root)
    sandbox_memory_integrity_test.go:110: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:81
        	            				.../hostedtoolcache/go/1.26.3.../src/runtime/asm_amd64.s:1771
        	Error:      	Received unexpected error:
        	            	failed to execute command bash in sandbox i15bun94zhfkjo4gyoe3f: unavailable: HTTP status 502 Bad Gateway
    sandbox_memory_integrity_test.go:110: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:78
        	            				.../tests/orchestrator/sandbox_memory_integrity_test.go:110
        	Error:      	Condition never satisfied
        	Test:       	TestSandboxMemoryIntegrity/tmpfs_hash
--- FAIL: TestSandboxMemoryIntegrity/tmpfs_hash (115.75s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxObjectNotFound

Flake rate in main: 43.38% (Passed 650 times, Failed 498 times)

Stack Traces | 50.2s run time
=== RUN   TestSandboxObjectNotFound
=== PAUSE TestSandboxObjectNotFound
=== CONT  TestSandboxObjectNotFound
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
Executing command bash in sandbox id1gc6x1yq5za7xn8s7kl (user: root)
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
Executing command bash in sandbox i2kw688j4f5ufos1dca48 (user: root)
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
Executing command bash in sandbox ilvdty3yy8ghh5k2poh54 (user: root)
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
Executing command bash in sandbox is819wchqhe8zyynozv04 (user: root)
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
    sandbox_object_not_found_test.go:59: sandbox creation failed due to resource exhaustion, retrying
    sandbox_object_not_found_test.go:70: failed to create sandbox after 10 retries
--- FAIL: TestSandboxObjectNotFound (50.18s)
github.com/e2b-dev/infra/tests/integration/internal/tests/proxies::TestSandboxAutoResumeViaProxy

Flake rate in main: 44.48% (Passed 649 times, Failed 520 times)

Stack Traces | 21.1s run time
=== RUN   TestSandboxAutoResumeViaProxy
=== PAUSE TestSandboxAutoResumeViaProxy
=== CONT  TestSandboxAutoResumeViaProxy
    auto_resume_test.go:97: [Status code: 502] Response body: {"sandboxId":"im5k44o3sdloj1elb1dzg","message":"The sandbox is running but port is not open","port":8000,"code":502}
    auto_resume_test.go:116: 
        	Error Trace:	.../tests/proxies/auto_resume_test.go:116
        	Error:      	Received unexpected error:
        	            	Get "http://localhost:3002": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
        	Test:       	TestSandboxAutoResumeViaProxy
--- FAIL: TestSandboxAutoResumeViaProxy (21.06s)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

Calling Close multiple times will invoke syscall.Close on the wakeupPipe file descriptors repeatedly, which can silently close unrelated active descriptors if they have been recycled by the operating system. Guarding the entire close sequence by checking and setting the closed flag under the settleRequests lock ensures idempotency and prevents accidental closure of recycled descriptors.

Comment thread packages/orchestrator/pkg/sandbox/uffd/userfaultfd/userfaultfd.go
@kalyazin
kalyazin force-pushed the kalyazin/prefetch_race branch from 1cc4dcc to b983a21 Compare May 27, 2026 15:48
@kalyazin
kalyazin marked this pull request as ready for review May 27, 2026 16:43
kalyazin added 2 commits May 28, 2026 09:04
Prefault() and Close() could race: Close() freed the uffd fd number
while a prefetcher goroutine was about to acquire settleRequests.RLock
and call UFFDIO_COPY. If the OS recycled the fd to a non-uffd file
between the close and the ioctl, the syscall returned ENOTTY (seen in
production from 2026-04-26 onward, worsening after PR #2522 added an
extra fd close per session).

Fix by making Close() acquire settleRequests.Lock() before closing the
fd and setting a `closed` flag. Prefault() checks the flag immediately
after acquiring RLock; if set, it returns nil without touching the fd.
This ensures the fd is only closed after all in-flight UFFDIO_COPY
callers have released the read-lock.

Also add faultPhaseBeforePrefaultRLock test hook so a regression test
can deterministically park Prefault before the RLock, let Close() run,
and verify the closed-check path.

Signed-off-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev>
TestPrefaultConcurrentWithClose deterministically reproduces the race
that caused ENOTTY/EBADF in production: a prefetcher goroutine is
parked at faultPhaseBeforePrefaultRLock (before acquiring
settleRequests.RLock), Close() is called to completion, and then the
goroutine is released. The test asserts Prefault returns nil rather
than an error from UFFDIO_COPY on a closed or recycled fd.

The test is in-process (no cross-process harness needed — Prefault is
called directly and short-circuits before any page-fault kernel
interaction). The setup uses a real uffd fd so Close() can safely close
a valid fd number.

Also document BeforePrefaultRLock in the testharness Point constants so
the value is named if cross-process tests ever need to park here.

Signed-off-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev>
@kalyazin
kalyazin force-pushed the kalyazin/prefetch_race branch from b983a21 to 64a02ef Compare May 28, 2026 08:04
@kalyazin
kalyazin merged commit 5c805e2 into main May 28, 2026
51 checks passed
@kalyazin
kalyazin deleted the kalyazin/prefetch_race branch May 28, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants