Skip to content

Use memfd to track sandbox memory#2522

Open
bchalios wants to merge 3 commits intomainfrom
zero-copy-pause
Open

Use memfd to track sandbox memory#2522
bchalios wants to merge 3 commits intomainfrom
zero-copy-pause

Conversation

@bchalios
Copy link
Copy Markdown
Contributor

@bchalios bchalios commented Apr 29, 2026

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
Copy link
Copy Markdown

cursor Bot commented Apr 29, 2026

PR Summary

Medium Risk
Medium risk because it changes snapshot restore and memory-diff export paths, including new FD-passing semantics between Firecracker and the UFFD handler that can fail in subtle ways at runtime.

Overview
Potential issues: Uffd.handle never closes the accepted Unix connection, and it assumes at most two received file descriptors (extra FDs would leak). copyFromMemfd reads into the mmap using a remaining-sized slice each loop, which could amplify memory pressure for very large ranges and makes error handling sensitive to short reads/EOF. The new use_memfd flag/feature-flag path changes snapshot restore behavior and memory export semantics, so mismatched Firecracker versions or partial support could cause restore failures or silent fallbacks.

Reviewed by Cursor Bugbot for commit d220659. 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
return cache, nil
}

err = cache.copyFromMemfd(ctx, memfd, ranges)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@bchalios bchalios force-pushed the zero-copy-pause branch from 4f0b47b to d09dbca Compare May 4, 2026 14:46
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

❌ 6 Tests Failed:

Tests completed Failed Passed Skipped
2579 6 2573 7
View the full list of 12 ❄️ flaky test(s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/metrics::TestSandboxMetrics

Flake rate in main: 50.00% (Passed 16 times, Failed 16 times)

Stack Traces | 5.01s run time
=== RUN   TestSandboxMetrics
=== PAUSE TestSandboxMetrics
=== CONT  TestSandboxMetrics
    sandbox_metrics_test.go:44: 
        	Error Trace:	.../api/metrics/sandbox_metrics_test.go:44
        	Error:      	Should NOT be empty, but was 0
        	Test:       	TestSandboxMetrics
--- FAIL: TestSandboxMetrics (5.01s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/metrics::TestTeamMetrics

Flake rate in main: 69.49% (Passed 18 times, Failed 41 times)

Stack Traces | 0.35s run time
=== RUN   TestTeamMetrics
=== PAUSE TestTeamMetrics
=== CONT  TestTeamMetrics
    team_metrics_test.go:61: 
        	Error Trace:	.../api/metrics/team_metrics_test.go:61
        	Error:      	Should be true
        	Test:       	TestTeamMetrics
        	Messages:   	MaxConcurrentSandboxes should be >= 0
--- FAIL: TestTeamMetrics (0.35s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestEgressFirewallAllowAllDomainsWildcard

Flake rate in main: 48.39% (Passed 16 times, Failed 15 times)

Stack Traces | 12.1s run time
=== RUN   TestEgressFirewallAllowAllDomainsWildcard
=== PAUSE TestEgressFirewallAllowAllDomainsWildcard
=== CONT  TestEgressFirewallAllowAllDomainsWildcard
    sandbox_network_out_test.go:620: Command [curl] output: event:{start:{pid:1309}}
Executing command curl in sandbox imwgrbk4bts1a7vsq5k4p
    sandbox_network_out_test.go:620: Command [curl] output: event:{data:{stdout:"HTTP/2 301 \r\nlocation: https://www.google.com/\r\ncontent-type: text/html; charset=UTF-8\r\ncontent-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-nDRrXTYVifKpLLFHmOwQZw' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle..../csp/gws/other-hp\r\ndate: Wed, 06 May 2026 11:05:27 GMT\r\nexpires: Fri, 05 Jun 2026 11:05:27 GMT\r\ncache-control: public, max-age=2592000\r\nserver: gws\r\ncontent-length: 220\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_out_test.go:620: Command [curl] output: event:{end:{exited:true status:"exit status 0"}}
    sandbox_network_out_test.go:620: Command [curl] completed successfully in sandbox iczyfgx12l6h0s60qa7tj
Executing command curl in sandbox iczyfgx12l6h0s60qa7tj
    sandbox_network_out_test.go:621: Command [curl] output: event:{start:{pid:1311}}
    sandbox_network_out_test.go:621: Command [curl] output: event:{data:{stdout:"HTTP/2 301 \r\ndate: Wed, 06 May 2026 11:05:27 GMT\r\ncontent-type: text/html\r\ncontent-length: 167\r\nlocation: https://www.cloudflare.com/\r\ncache-control: max-age=3600\r\nexpires: Wed, 06 May 2026 12:05:27 GMT\r\nset-cookie: __cf_bm=QdR2gu0zKm5zVkND7Ih7VsTmgl_DG3nkzK_tNuw5yqc-1778065527-1.0.1.1-MDbd2sgcw1QxYlyhaK8T8NyAR4JlJaddbKFm9wg2oAU4g2O9ZsFBHJLYELXibxVv4tDMcOIZklKI90FB_VVF.oOJRvE_TqAIMilJrrG6dBg; path=/; expires=Wed, 06-May-26 11:35:27 GMT; domain=.cloudflare.com; HttpOnly; Secure; SameSite=None\r\nreport-to: {\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=rxAkREQqgpwcKxlpiv85E778FFqqibCPwWL3YmNt2n3yv037RHU9EcJJnul8djvJgM9xCSBDkBJjUECxkOsaJp9ySNBHsN8MWaOb7Rm7UN%2BI4URlzWAUAp%2B5TisMdKmR\"}],\"group\":\"cf-nel\",\"max_age\":604800}\r\nnel: {\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}\r\nstrict-transport-security: max-age=15780000; includeSubDomains\r\nserver: cloudflare\r\ncf-ray: 9f77828d38b4cdc0-ORD\r\nalt-svc: h3=\":443\"; ma=86400\r\n\r\n"}}
    sandbox_network_out_test.go:621: Command [curl] output: event:{end:{exited:true status:"exit status 0"}}
    sandbox_network_out_test.go:621: Command [curl] completed successfully in sandbox iczyfgx12l6h0s60qa7tj
Executing command curl in sandbox iczyfgx12l6h0s60qa7tj
    sandbox_network_out_test.go:622: Command [curl] output: event:{start:{pid:1313}}
    sandbox_network_out_test.go:622: Command [curl] output: event:{end:{exit_code:28 exited:true status:"exit status 28" error:"exit status 28"}}
    sandbox_network_out_test.go:622: 
        	Error Trace:	.../api/sandboxes/sandbox_network_out_test.go:67
        	            				.../api/sandboxes/sandbox_network_out_test.go:622
        	Error:      	Received unexpected error:
        	            	command curl in sandbox iczyfgx12l6h0s60qa7tj failed with exit code 28
        	Test:       	TestEgressFirewallAllowAllDomainsWildcard
        	Messages:   	Expected curl to github.com to succeed with '*' wildcard
Executing command curl in sandbox i7l0544fsd37auiff6i8r
--- FAIL: TestEgressFirewallAllowAllDomainsWildcard (12.10s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestUpdateNetworkConfig

Flake rate in main: 69.70% (Passed 20 times, Failed 46 times)

Stack Traces | 42.6s run time
=== RUN   TestUpdateNetworkConfig
=== PAUSE TestUpdateNetworkConfig
=== CONT  TestUpdateNetworkConfig
Executing command curl in sandbox i0vq56nbhyrc7zcer5rtk
--- FAIL: TestUpdateNetworkConfig (42.60s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestUpdateNetworkConfig/pause_resume_preserves_allow_internet_access_false

Flake rate in main: 68.25% (Passed 20 times, Failed 43 times)

Stack Traces | 2.5s run time
=== RUN   TestUpdateNetworkConfig/pause_resume_preserves_allow_internet_access_false
Executing command curl in sandbox iglf9anj5hsq66iqwngcc
    sandbox_network_update_test.go:372: Command [curl] output: event:{start:{pid:1352}}
    sandbox_network_update_test.go:372: Command [curl] output: event:{end:{exit_code:35 exited:true status:"exit status 35" error:"exit status 35"}}
    sandbox_network_update_test.go:372: Command [curl] output: event:{start:{pid:1353}}
    sandbox_network_update_test.go:372: Command [curl] output: event:{end:{exit_code:35 exited:true status:"exit status 35" error:"exit status 35"}}
Executing command curl in sandbox iz4d18n33noh6mz78wwly
    sandbox_network_update_test.go:391: Command [curl] output: event:{start:{pid:1354}}
    sandbox_network_update_test.go:391: Command [curl] output: event:{data:{stdout:"HTTP/2 302 \r\nx-content-type-options: nosniff\r\nlocation: https://dns.google/\r\ndate: Wed, 06 May 2026 11:06:44 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:391: Command [curl] output: event:{end:{exited:true status:"exit status 0"}}
    sandbox_network_update_test.go:391: Command [curl] completed successfully in sandbox iglf9anj5hsq66iqwngcc
    sandbox_network_update_test.go:391: 
        	Error Trace:	.../api/sandboxes/sandbox_network_out_test.go:74
        	            				.../api/sandboxes/sandbox_network_update_test.go:60
        	            				.../api/sandboxes/sandbox_network_update_test.go:391
        	Error:      	An error is expected but got nil.
        	Test:       	TestUpdateNetworkConfig/pause_resume_preserves_allow_internet_access_false
        	Messages:   	https://8.8.8.8 should be blocked
--- FAIL: TestUpdateNetworkConfig/pause_resume_preserves_allow_internet_access_false (2.50s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestBindLocalhost

Flake rate in main: 48.08% (Passed 27 times, Failed 25 times)

Stack Traces | 0s run time
=== RUN   TestBindLocalhost
=== PAUSE TestBindLocalhost
=== CONT  TestBindLocalhost
--- FAIL: TestBindLocalhost (0.00s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestBindLocalhost/bind_0_0_0_0

Flake rate in main: 54.29% (Passed 16 times, Failed 19 times)

Stack Traces | 7.86s run time
=== RUN   TestBindLocalhost/bind_0_0_0_0
=== PAUSE TestBindLocalhost/bind_0_0_0_0
=== CONT  TestBindLocalhost/bind_0_0_0_0
    localhost_bind_test.go:69: Command [python] output: event:{start:{pid:1251}}
Executing command python in sandbox iw92mgm92q6v4gjn2wncr
    localhost_bind_test.go:90: 
        	Error Trace:	.../tests/envd/localhost_bind_test.go:90
        	Error:      	Not equal: 
        	            	expected: 200
        	            	actual  : 502
        	Test:       	TestBindLocalhost/bind_0_0_0_0
        	Messages:   	Unexpected status code 502 for bind address 0.0.0.0
--- FAIL: TestBindLocalhost/bind_0_0_0_0 (7.86s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestBindLocalhost/bind_localhost

Flake rate in main: 55.56% (Passed 16 times, Failed 20 times)

Stack Traces | 7.92s run time
=== RUN   TestBindLocalhost/bind_localhost
=== PAUSE TestBindLocalhost/bind_localhost
=== CONT  TestBindLocalhost/bind_localhost
    localhost_bind_test.go:69: Command [python] output: event:{start:{pid:1252}}
    localhost_bind_test.go:90: 
        	Error Trace:	.../tests/envd/localhost_bind_test.go:90
        	Error:      	Not equal: 
        	            	expected: 200
        	            	actual  : 502
        	Test:       	TestBindLocalhost/bind_localhost
        	Messages:   	Unexpected status code 502 for bind address localhost
--- FAIL: TestBindLocalhost/bind_localhost (7.92s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity

Flake rate in main: 54.35% (Passed 21 times, Failed 25 times)

Stack Traces | 0s run time
=== RUN   TestSandboxMemoryIntegrity
=== PAUSE TestSandboxMemoryIntegrity
=== CONT  TestSandboxMemoryIntegrity
--- FAIL: TestSandboxMemoryIntegrity (0.00s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity/stress-ng_verify

Flake rate in main: 56.76% (Passed 16 times, Failed 21 times)

Stack Traces | 30.7s run time
=== RUN   TestSandboxMemoryIntegrity/stress-ng_verify
=== PAUSE TestSandboxMemoryIntegrity/stress-ng_verify
=== CONT  TestSandboxMemoryIntegrity/stress-ng_verify
Executing command bash in sandbox im2tb8x1phcgejod9w5h9 (user: root)
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{start:{pid:1258}}
Executing command bash in sandbox ilttj77xosn9s64swy07o (user: root)
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Hit:1 http://deb.debian.org/debian bookworm InRelease\nHit:2 http://deb.debian.org/debian bookworm-updates InRelease\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Hit:3 http://deb.debian.org/debian-security bookworm-security InRelease\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Reading package lists..."}}
Executing command bash in sandbox irm3fklh535z6k6877wvo (user: root)
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Reading package lists..."}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Building dependency tree..."}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"\nReading state information..."}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"The following additional packages will be installed:\n  libdrm-common libdrm2 libegl-mesa0 libegl1 libgbm1 libglapi-mesa libgles2\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"  libglvnd0 libipsec-mb1 libjudydebian1 libsctp1 libwayland-client0\n  libwayland-server0 libx11-xcb1 libxcb-dri2-0 libxcb-dri3-0 libxcb-present0\n  libxcb-randr0 libxcb-sync1 libxcb-xfixes0 libxshmfence1\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Suggested packages:\n  lksctp-tools\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"The following NEW packages will be installed:\n  libdrm-common libdrm2 libegl-mesa0 libegl1 libgbm1 libglapi-mesa libgles2\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"  libglvnd0 libipsec-mb1 libjudydebian1 libsctp1 libwayland-client0\n  libwayland-server0 libx11-xcb1 libxcb-dri2-0 libxcb-dri3-0 libxcb-present0\n  libxcb-randr0 libxcb-sync1 libxcb-xfixes0 libxshmfence1 stress-ng time\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"0 upgraded, 23 newly installed, 0 to remove and 146 not upgraded.\nNeed to get 4781 kB of archives.\nAfter this operation, 25.6 MB of additional disk space will be used.\nGet:1 http://deb.debian.org/debian bookworm/main amd64 libdrm-common all 2.4.114-1 [7112 B]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:2 http://deb.debian.org/debian bookworm/main amd64 libdrm2 amd64 2.4.114-1+b1 [37.5 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:3 http://deb.debian.org/debian bookworm/main amd64 libwayland-server0 amd64 1.21.0-1 [35.9 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:4 http://deb.debian.org/debian bookworm/main amd64 libgbm1 amd64 22.3.6-1+deb12u1 [38.0 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:5 http://deb.debian.org/debian bookworm/main amd64 libglapi-mesa amd64 22.3.6-1+deb12u1 [35.7 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:6 http://deb.debian.org/debian bookworm/main amd64 libwayland-client0 amd64 1.21.0-1 [28.3 kB]\nGet:7 http://deb.debian.org/debian bookworm/main amd64 libx11-xcb1 amd64 2:1.8.4-2+deb12u2 [192 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:8 http://deb.debian.org/debian bookworm/main amd64 libxcb-dri2-0 amd64 1.15-1 [107 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:9 http://deb.debian.org/debian bookworm/main amd64 libxcb-dri3-0 amd64 1.15-1 [107 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:10 http://deb.debian.org/debian bookworm/main amd64 libxcb-present0 amd64 1.15-1 [105 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:11 http://deb.debian.org/debian bookworm/main amd64 libxcb-randr0 amd64 1.15-1 [117 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:12 http://deb.debian.org/debian bookworm/main amd64 libxcb-sync1 amd64 1.15-1 [109 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:13 http://deb.debian.org/debian bookworm/main amd64 libxcb-xfixes0 amd64 1.15-1 [109 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:14 http://deb.debian.org/debian bookworm/main amd64 libxshmfence1 amd64 1.3-1 [8820 B]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:15 http://deb.debian.org/debian bookworm/main amd64 libegl-mesa0 amd64 22.3.6-1+deb12u1 [114 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:16 http://deb.debian.org/debian bookworm/main amd64 libglvnd0 amd64 1.6.0-1 [51.8 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:17 http://deb.debian.org/debian bookworm/main amd64 libgles2 amd64 1.6.0-1 [16.8 kB]\nGet:18 http://deb.debian.org/debian bookworm/main amd64 libipsec-mb1 amd64 1.3-2 [981 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:19 http://deb.debian.org/debian bookworm/main amd64 libjudydebian1 amd64 1.0.5-5+b2 [102 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:20 http://deb.debian.org/debian bookworm/main amd64 libsctp1 amd64 1.0.19+dfsg-2 [29.7 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:21 http://deb.debian.org/debian bookworm/main amd64 libegl1 amd64 1.6.0-1 [33.7 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:22 http://deb.debian.org/debian bookworm/main amd64 stress-ng amd64 0.15.06-2 [2363 kB]\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Get:23 http://deb.debian.org/debian bookworm/main amd64 time amd64 1.9-0.2 [50.8 kB]\n"}}
Executing command bash in sandbox isvmpkpcuadeafm8ujc6l (user: root)
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stderr:"debconf: delaying package configuration, since apt-utils is not installed\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Fetched 4781 kB in 0s (21.2 MB/s)\n"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"Selecting previously unselected package libdrm-common.\r\n(Reading database ... \r"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"(Reading database ... 5%\r(Reading database ... 10%\r(Reading database ... 15%\r(Reading database ... 20%\r(Reading database ... 25%\r(Reading database ... 30%\r(Reading database ... 35%\r(Reading database ... 40%\r(Reading database ... 45%\r(Reading database ... 50%\r(Reading database ... 55%\r(Reading database ... 60%\r(Reading database ... 65%\r"}}
    sandbox_memory_integrity_test.go:141: Command [bash] output: event:{data:{stdout:"(Reading database ... 70%\r"}}
    sandbox_memory_integrity_test.go:142: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:142
        	Error:      	Received unexpected error:
        	            	failed to execute command bash in sandbox im2tb8x1phcgejod9w5h9: invalid_argument: protocol error: incomplete envelope: unexpected EOF
        	Test:       	TestSandboxMemoryIntegrity/stress-ng_verify
--- FAIL: TestSandboxMemoryIntegrity/stress-ng_verify (30.70s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity/tmpfs_hash

Flake rate in main: 58.97% (Passed 16 times, Failed 23 times)

Stack Traces | 30.7s run time
=== RUN   TestSandboxMemoryIntegrity/tmpfs_hash
=== PAUSE TestSandboxMemoryIntegrity/tmpfs_hash
=== CONT  TestSandboxMemoryIntegrity/tmpfs_hash
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{start:{pid:1258}}
Executing command bash in sandbox iajxfeles4neiip8rlok7 (user: root)
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"Hit:1 http://deb.debian.org/debian bookworm InRelease\nHit:2 http://deb.debian.org/debian bookworm-updates InRelease\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"Hit:3 http://deb.debian.org/debian-security bookworm-security InRelease\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"Reading package lists..."}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"Reading package lists..."}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"Building dependency tree..."}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"\nReading state information..."}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"The following NEW packages will be installed:\n  time\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"0 upgraded, 1 newly installed, 0 to remove and 146 not upgraded.\nNeed to get 50.8 kB of archives.\nAfter this operation, 132 kB of additional disk space will be used.\nGet:1 http://deb.debian.org/debian bookworm/main amd64 time amd64 1.9-0.2 [50.8 kB]\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stderr:"debconf: delaying package configuration, since apt-utils is not installed\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"Fetched 50.8 kB in 0s (413 kB/s)\n"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"Selecting previously unselected package time.\r\n(Reading database ... \r"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"(Reading database ... 5%\r(Reading database ... 10%\r(Reading database ... 15%\r(Reading database ... 20%\r(Reading database ... 25%\r(Reading database ... 30%\r(Reading database ... 35%\r(Reading database ... 40%\r(Reading database ... 45%\r(Reading database ... 50%\r(Reading database ... 55%\r(Reading database ... 60%\r"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"(Reading database ... 65%\r"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"(Reading database ... 70%\r"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"(Reading database ... 75%\r"}}
    sandbox_memory_integrity_test.go:33: Command [bash] output: event:{data:{stdout:"(Reading database ... 80%\r"}}
    sandbox_memory_integrity_test.go:34: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:34
        	Error:      	Received unexpected error:
        	            	failed to execute command bash in sandbox ilttj77xosn9s64swy07o: invalid_argument: protocol error: incomplete envelope: unexpected EOF
        	Test:       	TestSandboxMemoryIntegrity/tmpfs_hash
--- FAIL: TestSandboxMemoryIntegrity/tmpfs_hash (30.70s)
github.com/e2b-dev/infra/tests/integration/internal/tests/proxies::TestSandboxWithTrafficAccessTokenAutoResumeViaProxy

Flake rate in main: 43.33% (Passed 17 times, Failed 13 times)

Stack Traces | 18.7s run time
=== RUN   TestSandboxWithTrafficAccessTokenAutoResumeViaProxy
=== PAUSE TestSandboxWithTrafficAccessTokenAutoResumeViaProxy
=== CONT  TestSandboxWithTrafficAccessTokenAutoResumeViaProxy
    traffic_access_token_test.go:263: [Status code: 502] Response body: {"sandboxId":"ibvzfn3i0xy8oy7ft7b1j","message":"The sandbox is running but port is not open","port":8080,"code":502}
    traffic_access_token_test.go:263: [Status code: 502] Response body: {"sandboxId":"ibvzfn3i0xy8oy7ft7b1j","message":"The sandbox is running but port is not open","port":8080,"code":502}
Executing command ls in sandbox i6ct41zir617lnrg5newz
    traffic_access_token_test.go:263: [Status code: 502] Response body: {"sandboxId":"ibvzfn3i0xy8oy7ft7b1j","message":"The sandbox is running but port is not open","port":8080,"code":502}
    traffic_access_token_test.go:292: 
        	Error Trace:	.../tests/proxies/traffic_access_token_test.go:292
        	Error:      	Received unexpected error:
        	            	Get "http://localhost:3002": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
        	Test:       	TestSandboxWithTrafficAccessTokenAutoResumeViaProxy
--- FAIL: TestSandboxWithTrafficAccessTokenAutoResumeViaProxy (18.74s)

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
Copy link
Copy Markdown
Contributor Author

bchalios commented May 4, 2026

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
@ValentaTomas ValentaTomas self-assigned this May 4, 2026
@ValentaTomas ValentaTomas removed request for dobrac and jakubno May 4, 2026 18:00
@bchalios bchalios force-pushed the zero-copy-pause branch 2 times, most recently from d245968 to ae76b44 Compare May 5, 2026 06:59
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 830d2cd to 2015b36 Compare May 5, 2026 07:05
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 packages/orchestrator/pkg/sandbox/uffd/uffd.go:178-184 — The cleanup loop for fd := range fds { syscall.Close(fd) } at uffd.go:178-181 uses Go's single-variable range form, which iterates over indices, not values. On the NewUserfaultfdFromFd error path this closes the orchestrator's stdin (fd 0) and stdout (fd 1) — corrupting any stdout-bound logging — while leaking the actual UFFD and memfd that were received from Firecracker (the memfd in particular pins the entire guest memory mapping in the kernel).

Fix is for _, fd := range fds { syscall.Close(fd) }.

Extended reasoning...

What the bug is

In Go, for x := range slice with a single loop variable iterates over the slice's indices, not its values. To iterate over values you need for _, x := range slice. The newly added cleanup at packages/orchestrator/pkg/sandbox/uffd/uffd.go:178-184 reads:

if err != nil {
    for fd := range fds {
        syscall.Close(fd)
    }
    return fmt.Errorf("failed to create uffd: %w", err)
}

fds is []int (returned by syscall.ParseUnixRights a few lines above). With the loosened length check at line 167 (len(fds) != 1 && len(fds) != 2), fds will have length 1 or 2 here, so fd takes the values 0 and (when a memfd was passed) 1 — the indices into fds, not the FD numbers Firecracker actually handed over via SCM_RIGHTS.

Step-by-step proof

Suppose NewUserfaultfdFromFd fails after Firecracker has sent the orchestrator a UFFD fd (kernel-assigned number 42) and a memfd (kernel-assigned number 43):

  1. syscall.ParseUnixRights returns fds = [42, 43].
  2. Length check passes (len(fds) == 2).
  3. userfaultfd.NewUserfaultfdFromFd(uintptr(fds[0]), …) returns an error.
  4. The cleanup loop runs. Because of single-variable range semantics, the loop body sees fd = 0, then fd = 1.
  5. syscall.Close(0) closes the orchestrator's stdin.
  6. syscall.Close(1) closes the orchestrator's stdout — and now any zap logger writing to stdout will silently fail, or, worse, write to whatever fd the kernel reassigns 0/1 to next (a sandbox socket, a database conn, etc.). That's a real correctness/security hazard, not just lost log lines.
  7. fds 42 and 43 — the actual UFFD fd and memfd — stay open in the orchestrator's fd table for the lifetime of the process. The memfd is the costly leak: it holds an mmap of the whole guest memory region in the kernel.

Why existing code doesn't prevent it

The cleanup defer added at lines 187-198 only runs after u.handler.SetValue(uffd) is reachable — i.e., it only handles the success path's later teardown. The early-return branch at line 178 is exactly the place this leak was supposed to be addressed, so the defer can't catch it.

Impact

Two distinct failures fire on every NewUserfaultfdFromFd error after FD handover:

  • Logging/IO corruption: closing fd 0 and fd 1 corrupts the orchestrator's standard streams. The kernel reuses the lowest free fd numbers for subsequent open/socket/accept calls, so future stdout writes can land in arbitrary fds.
  • FD + memory leak: the UFFD fd and memfd are leaked. The memfd in particular pins the entire guest memory mapping until the orchestrator process exits.

The trigger is rare (any failure inside NewUserfaultfdFromFd after Firecracker has already sent the SCM_RIGHTS fds), but when it fires the consequences are severe and exactly opposite to what the cleanup was added to prevent.

How to fix

Change one line:

for _, fd := range fds {
    syscall.Close(fd)
}

This iterates over fd values instead of indices, closing the actually-received fds and leaving stdin/stdout alone.

@bchalios bchalios force-pushed the zero-copy-pause branch from 2015b36 to a148832 Compare May 5, 2026 07:14
Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go
Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go
Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go
@bchalios bchalios force-pushed the zero-copy-pause branch 3 times, most recently from 4d06cf8 to 384e6a1 Compare May 5, 2026 09:15
Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go
Comment thread packages/shared/pkg/featureflags/flags.go
@bchalios bchalios force-pushed the zero-copy-pause branch 4 times, most recently from c91c781 to 0576f1c Compare May 5, 2026 12:50
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 0576f1c. Configure here.

Comment thread packages/orchestrator/pkg/sandbox/uffd/uffd.go
bchalios added 3 commits May 6, 2026 12:56
We are making a larger change to enable memfd-backed guest memory in
Firecracker. When enabled, Firecracker passes over the memfd file
descriptor over the UFFD UDS, alongside the UFFD file descriptor.

Parse this and create a Memfd object, which we can later use to interact
with the sandbox memory.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Currently, orchestrator calls process_vm_readv() system call to copy
memory from Firecracker process into the cache backing file.

Add logic to use read directly from memfd, when that is present.

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>
@bchalios bchalios force-pushed the zero-copy-pause branch from 0576f1c to d220659 Compare May 6, 2026 10:57
@cla-bot cla-bot Bot added the cla-signed label May 6, 2026
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization has reached its monthly code review spending cap.

An organization admin can view or raise the cap at claude.ai/admin-settings/claude-code. The cap resets at the start of the next billing period.

Once the cap resets or is raised, push a new commit or reopen this pull request to trigger a review.

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.

3 participants