Skip to content

Use memfd to track sandbox memory - #2522

Merged
bchalios merged 8 commits into
mainfrom
zero-copy-pause
May 19, 2026
Merged

bchalios merged 8 commits into
mainfrom
zero-copy-pause

Conversation

@bchalios

@bchalios bchalios commented Apr 29, 2026 •

Copy link
Copy Markdown
Contributor

What

In Unix OSs memfd is an anonymous file that can be used to back memory. Firecracker uses this construct when it needs to share memory with external processes (currently, when using vhost-user devices).

Currently, when we take a snapshot of the sandbox (for example, during PAUSE operations) we need to copy its memory using process_vm_readv. memfd allows us to do this in a more idiomatic way.

Why

memfd allows us to have a direct view of the sandbox memory from the orchestrator without having to copy memory across processes. Moreover, if the orchestrator holds a reference to memfd, we can post process the sandbox memory after the Firecracker process is killed. This opens up possibilities for various latency and memory utilization optimizations.

What we do in this PR is that we change the cache logic to use memfd to copy Firecracker memory into the diff file if the memfd is present.

@cursor

cursor Bot commented Apr 29, 2026 •

Copy link
Copy Markdown

PR Summary

High Risk
Touches snapshot load/pause memory-export paths and changes several core interfaces (Diff, CachePath, FileSize) to become context-aware; regressions here can break resume/pause or corrupt/omit diff uploads. New async memfd copy adds concurrency/cancellation edge cases that could hang or leak resources if copy never completes.

Overview
This PR changes pause/resume to optionally request use_memfd on snapshot load, receive a memfd over the UFFD socket, and export memory diffs by copying from that memfd (optionally on a background goroutine) instead of always using process_vm_readv. It also changes diff/cache APIs (CachePath, FileSize, Cache.Path) to take context.Context, which can break any remaining call sites/implementations and subtly change error/timeout behavior. The new async MemfdCache blocks reads on Wait and uses context.Background() in ReadAt/Slice, so a stalled copy can hang consumers even if their request context is cancelled.

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

Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go
Comment thread packages/orchestrator/pkg/sandbox/block/cache.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/block/memfd.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/sandbox.go
@bchalios
bchalios force-pushed the zero-copy-pause branch 2 times, most recently from 6d2b804 to 314abd0 Compare April 29, 2026 09:25
Comment thread packages/orchestrator/pkg/sandbox/block/cache.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/sandbox.go Outdated
@bchalios
bchalios force-pushed the zero-copy-pause branch 5 times, most recently from 1ab27f8 to 4f0b47b Compare April 29, 2026 15:56
Comment thread packages/orchestrator/pkg/sandbox/block/cache.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/block/memfd.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/block/memfd.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/block/cache.go Outdated
@bchalios
bchalios force-pushed the zero-copy-pause branch from 4f0b47b to d09dbca Compare May 4, 2026 14:46
@codecov

codecov Bot commented May 4, 2026 •

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
2652 4 2648 5
View the full list of 6 ❄️ flaky test(s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestUpdateNetworkConfig

Flake rate in main: 76.46% (Passed 318 times, Failed 1033 times)

Stack Traces | 80.1s run time
=== RUN   TestUpdateNetworkConfig
=== PAUSE TestUpdateNetworkConfig
=== CONT  TestUpdateNetworkConfig
--- FAIL: TestUpdateNetworkConfig (80.07s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestUpdateNetworkConfig/pause_resume_preserves_allow_internet_access_false

Flake rate in main: 76.99% (Passed 306 times, Failed 1024 times)

Stack Traces | 33.4s run time
=== RUN   TestUpdateNetworkConfig/pause_resume_preserves_allow_internet_access_false
    sandbox_network_update_test.go:399: Command [curl] output: event:{start:{pid:1352}}
    sandbox_network_update_test.go:399: Command [curl] output: event:{end:{exit_code:35  exited:true  status:"exit status 35"  error:"exit status 35"}}
Executing command curl in sandbox i23zulpyox1fxrtsprunz
    sandbox_network_update_test.go:399: Command [curl] output: event:{start:{pid:1353}}
    sandbox_network_update_test.go:399: Command [curl] output: event:{end:{exit_code:35  exited:true  status:"exit status 35"  error:"exit status 35"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1354}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:55:54 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1355}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:55:55 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1356}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:55:57 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1357}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:55:57 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1358}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:55:58 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1359}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:55:59 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1360}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:01 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1361}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:01 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1362}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:02 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1363}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:03 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1364}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:04 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1365}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:05 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1366}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:06 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1367}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:07 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1368}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Tue, 19 May 2026 13:56:08 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nserver: HTTP server (unknown)\r\ncontent-length: 216\r\nx-xss-protection: 0\r\nx-frame-options: SAMEORIGIN\r\nalt-svc: h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000\r\n\r\n"}}
    sandbox_network_update_test.go:28: Command [curl] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_network_update_test.go:28: Command [curl] completed successfully in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:28: Command [curl] output: event:{start:{pid:1369}}
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
Executing command curl in sandbox i35ktrm1xhzfsmrfmicpz
    sandbox_network_update_test.go:417: 
        	Error Trace:	.../api/sandboxes/sandbox_network_update_test.go:26
        	            				.../api/sandboxes/sandbox_network_update_test.go:417
        	Error:      	Condition never satisfied
        	Test:       	TestUpdateNetworkConfig/pause_resume_preserves_allow_internet_access_false
        	Messages:   	connectivity did not match expected state in time
--- FAIL: TestUpdateNetworkConfig/pause_resume_preserves_allow_internet_access_false (33.40s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/templates::TestTemplateBuildENV

Flake rate in main: 59.02% (Passed 302 times, Failed 435 times)

Stack Traces | 0s run time
=== RUN   TestTemplateBuildENV
=== PAUSE TestTemplateBuildENV
=== CONT  TestTemplateBuildENV
--- FAIL: TestTemplateBuildENV (0.00s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/templates::TestTemplateBuildENV/ENV_with_multiline_value

Flake rate in main: 59.42% (Passed 295 times, Failed 432 times)

Stack Traces | 25.9s run time
=== RUN   TestTemplateBuildENV/ENV_with_multiline_value
=== PAUSE TestTemplateBuildENV/ENV_with_multiline_value
=== CONT  TestTemplateBuildENV/ENV_with_multiline_value
    build_template_test.go:134: test-ubuntu-env-multiline: [info] Building template pvteri6hujdn8brct9is/88d3f6f5-d43d-4a7d-bf82-65bc1773b465
    build_template_test.go:134: test-ubuntu-env-multiline: [info] CACHED [base] FROM ubuntu:22.04 [ffd709f131f42dfab282de47a91dd2c139e900c1c11fc574b49b517a05ef0a32]
    build_template_test.go:134: test-ubuntu-env-multiline: [info] CACHED [base] DEFAULT USER user [90bdd4afa342293c931373351bf578872dec9179214ba3e8bf9edba311466213]
    build_template_test.go:134: test-ubuntu-env-multiline: [info] [builder 1/2] ENV MULTILINE line1
        line2
        line3 [e93da3f3765f20eb6407c336b9e4e0b9321d994ec5f6cb547743a2a4070eed23]
    build_template_test.go:134: test-ubuntu-env-multiline: [info] [builder 2/2] RUN [[ $(echo "$MULTILINE" | wc -l) -eq 3 ]] || exit 1 [477610d61cdf858776262d3331809539bcbcf16f706aac18515a57337bae1786]
    build_template_test.go:134: test-ubuntu-env-multiline: [error] Build failed: failed to run command '[[ $(echo "$MULTILINE" | wc -l) -eq 3 ]] || exit 1': exit status 1
    build_template_test.go:374: Build failed: {<nil> failed to run command '[[ $(echo "$MULTILINE" | wc -l) -eq 3 ]] || exit 1': exit status 1 0xc0002cc320}
--- FAIL: TestTemplateBuildENV/ENV_with_multiline_value (25.91s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity

Flake rate in main: 65.68% (Passed 313 times, Failed 599 times)

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

Flake rate in main: 66.18% (Passed 303 times, Failed 593 times)

Stack Traces | 48.4s run time
=== RUN   TestSandboxMemoryIntegrity/tmpfs_hash
=== PAUSE TestSandboxMemoryIntegrity/tmpfs_hash
=== CONT  TestSandboxMemoryIntegrity/tmpfs_hash
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{start:{pid:1261}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stdout:"Total memory: 985 MB\nUsed memory before tmpfs mount: 184 MB\nFree memory before tmpfs mount: 800 MB\nMemory to use in integrity test (80% of free, min 64MB): 640 MB\n"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"640+0 records in\n640+0 records out\n671088640 bytes (671 MB, 640 MiB) copied, 16.2953 s, 41.2 MB/s\n"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"\t"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"C"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"o"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"a"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"n"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"d"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"b"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"i"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"n"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"g"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"t"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"i"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"d"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:":"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"\""}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"dd"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"if=/dev/urandom"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"of=/mnt/testfile"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"bs=1M"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"count=640"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"\""}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"\n"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"\t"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"U"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"s"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"r"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"t"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"i"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"e (seconds): 0.00\n\tSystem time (seconds): 15.97\n\tPercent"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" of CPU this job got: 97%\n\tElapsed (wall clock) time (h:mm:ss"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"o"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"r"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:":"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"s"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"s"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"): 0:16.34\n\tAverage shared text size (kbytes): 0\n\tAverage unshared data size (k"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"b"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"y"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"t"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"s): 0\n\tAverage stack size (kbytes): 0\n\tAverage total size (kbytes): 0\n\tMaximum"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"r"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"s"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"i"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"d"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stdout:"Used memory after tmpfs mount and file fill: 832 MB\n"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{data:{stderr:"nt set size (kbytes): 2628\n\tAverage resident set size (kbytes): 0\n\tMajor (requiring I/O) page faults: 3\n\tMinor (reclaiming a frame) page faults: 344\n\tVoluntary context switches: 4\n\tInvoluntary context switches: 43\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"}}
    sandbox_memory_integrity_test.go:69: Command [bash] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_memory_integrity_test.go:69: Command [bash] completed successfully in sandbox iezsvwaxoljbc1b96vpf7
Executing command bash in sandbox iezsvwaxoljbc1b96vpf7 (user: root)
    sandbox_memory_integrity_test.go:73: Command [bash] output: event:{start:{pid:1278}}
    sandbox_memory_integrity_test.go:73: Command [bash] output: event:{data:{stdout:"386887e56d4bc4fc246a129a4b4c34c43b62bf06ad6a4dba3b0df60fae039e58\n"}}
    sandbox_memory_integrity_test.go:73: Command [bash] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_memory_integrity_test.go:73: Command [bash] completed successfully in sandbox iezsvwaxoljbc1b96vpf7
Executing command bash in sandbox iezsvwaxoljbc1b96vpf7 (user: root)
    sandbox_memory_integrity_test.go:98: Command [bash] output: event:{start:{pid:1281}}
    sandbox_memory_integrity_test.go:99: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:99
        	Error:      	Received unexpected error:
        	            	failed to execute command bash in sandbox iezsvwaxoljbc1b96vpf7: invalid_argument: protocol error: incomplete envelope: unexpected EOF
        	Test:       	TestSandboxMemoryIntegrity/tmpfs_hash
--- FAIL: TestSandboxMemoryIntegrity/tmpfs_hash (48.40s)

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

@bchalios
bchalios force-pushed the zero-copy-pause branch 2 times, most recently from 4ae4ebb to 9198a96 Compare May 4, 2026 15:13
@bchalios

bchalios commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Update: I've removed the logic that punches holes in the memfd, progressively after copying data into the diff file. I've ran some experiments and got some signal about this causing increase in CPU utilization and slowing down PAUSE and RESUMEs.

I think that we can proceed with adding support for memfd and revisiting after the deduplication work.

@bchalios
bchalios marked this pull request as ready for review May 4, 2026 15:19
@bchalios
bchalios requested review from dobrac and jakubno as code owners May 4, 2026 15:19
@qodo-code-review

Copy link
Copy Markdown
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@bchalios
bchalios requested a review from ValentaTomas May 4, 2026 15:26
Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go
@bchalios
bchalios force-pushed the zero-copy-pause branch 2 times, most recently from 33be29e to 8ecaf0f Compare May 19, 2026 10:12
@bchalios

Copy link
Copy Markdown
Contributor Author

maybe some metrics around this could be useful as there could be a lot of data moving around.

How do we tell that this helped, is there a better way than to just measure the whole sandbox pause?

How do we tell if the background helped?

Two things change here:

  1. We are copying from memfd instead of using process_vm_readv
  2. We are copying in the background

The amount of data the move around is the same. The only thing that changes is the source. We are just reducing the latency to our response to the PAUSE request, so I think it's reasonable that we use that latency to measure success, and look at the new spans we added to get a good idea of why that helped.

In subsequent PR that we will also deduplicate the pages we're actually saving, we will be adding metrics about how much we saved depending on the code path.

WDYT @jakubno?

@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 8ecaf0f9039830851e874ff2058d02a4c8395878. Configure here.

Comment thread packages/orchestrator/pkg/sandbox/block/memfd.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/block/device.go
Comment thread packages/orchestrator/pkg/sandbox/build/mocks/mockdiff.go
@bchalios
bchalios force-pushed the zero-copy-pause branch 2 times, most recently from 26aeaae to 2fa8bc3 Compare May 19, 2026 12:41
@jakubno

jakubno commented May 19, 2026

Copy link
Copy Markdown
Member

maybe some metrics around this could be useful as there could be a lot of data moving around.
How do we tell that this helped, is there a better way than to just measure the whole sandbox pause?
How do we tell if the background helped?

Two things change here:

  1. We are copying from memfd instead of using process_vm_readv
  2. We are copying in the background

The amount of data the move around is the same. The only thing that changes is the source. We are just reducing the latency to our response to the PAUSE request, so I think it's reasonable that we use that latency to measure success, and look at the new spans we added to get a good idea of why that helped.

In subsequent PR that we will also deduplicate the pages we're actually saving, we will be adding metrics about how much we saved depending on the code path.

WDYT @jakubno?

Thanks for clarification! Sounds good

@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: b77746e043

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return fmt.Errorf("memfd slice [%d,%d): %w", r.Start, r.Start+r.Size, err)
}

copy((*cache.mmap)[cacheOff:cacheOff+r.Size], src)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check cancellation between memfd copy chunks

When dirty contains one large contiguous range, BitsetRanges emits a single range and this copy can run for many GB without observing ctx. In async mode Close cancels the copy context and then waits for done, so a canceled upload or shutdown can still block until the entire contiguous memfd range is copied; copying in bounded chunks and checking ctx between chunks keeps cancellation effective.

Useful? React with 👍 / 👎.

bchalios and others added 8 commits May 19, 2026 15:42
We are changing Firecracker to, optionally, back the guest memory using
a memfd object. When enabled, Firecracker passes over the memfd file
descriptor over the UFFD UDS, alongside the UFFD file descriptor, using
SCM_RIGHTS.

Change the UFFD serve logic to also parse the memfd file descriptor.
When present, wrap the descriptor in a Memfd object. The object itself
provides an interface that lets users access the guest memory from the
memfd.

UFFD logic exposes the Memfd object over a newly added method of the
MemoryBackend interface, called Memfd(). The noop memory backend always
returns nil for now, as Firecracker might only use memfd when resuming
from a snapshot.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Change the ExportMemory() logic to export the memory via a MemfdCache
when Firecracker has sent us a memfd file descriptor.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Add a feature flag that controls whether the orchestrator will instruct
Firecracker to use memfd for backing the guest memory.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
The gRPC handler already injects sandbox/team/template contexts via
ctx, so team and template targeting for UseMemFdFlag already worked.
Add a sandbox-type attribute (sandbox vs build) and pass the explicit
sandboxLDContext to BoolFlag so flags can roll out to production
sandboxes separately from template-builds.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
FC < 1.14 rejects the use_memfd field on snapshot load
(deny_unknown_fields on MemoryBackend), so combining
FCSupportsMemfd(version) with the flag avoids hard-failing resumes
when the flag is flipped on across a heterogeneous fleet.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Introduce the MemfdCache wrapper (embedding *Cache) plus the
NewCacheFromMemfdAsync constructor: copy runs on a goroutine so gRPC
Pause can return as soon as the snapshot file and diff metadata are
written. The MemfdBackgroundCopyFlag gates the dispatch in
fc.ExportMemory; flag-off keeps the existing sync NewCacheFromMemfd
path untouched.

Introduce a DiffSource interface which abstracts the functionality of a
cache, so ExportMemory() now returns a DiffSource on success.

ReadAt/Slice Wait for background copies to finish before returning
results in the case of the asynchronous memfd cache.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
TestRetryableClient_ActualRetryBehavior asserted the first retry delay
was <200ms, but CI runners can add hundreds of ms of scheduling/network
overhead on top of the jittered backoff. Bump bounds to 2s to keep the
test stable while still catching order-of-magnitude regressions.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Add spans to differentiate between exporting memory from Firecracker
process vs memfd. Also, differentiate between synchronous and
synchronous memfd memory export.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
@bchalios
bchalios merged commit 12a4ed7 into main May 19, 2026
54 checks passed
@bchalios
bchalios deleted the zero-copy-pause branch May 19, 2026 14:17
ValentaTomas added a commit that referenced this pull request May 24, 2026
Adds 4 KiB memfile diff dedup behind `memfile-diff-dedup` to avoid
storing pages unchanged from the base when Firecracker reports dirty
memory at 2 MiB granularity.

Also fixes related page-granular restore correctness for empty mappings
and chunk reads, and runs integration pause/resume coverage across base,
best-effort, and direct I/O dedup modes.

Stacked on #2522.

---------

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Co-authored-by: ValentaTomas <valenta.and.thomas@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev>
kalyazin added a commit that referenced this pull request May 28, 2026
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>
kalyazin added a commit that referenced this pull request May 28, 2026
## 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:

```go
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()`:

```go
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.

---------

Signed-off-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev>
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.

5 participants