Skip to content

fix(envd): serve procfs/sysfs files on identity download path - #3364

Draft
mishushakov wants to merge 1 commit into
mainfrom
fix-issue-3363
Draft

fix(envd): serve procfs/sysfs files on identity download path#3364
mishushakov wants to merge 1 commit into
mainfrom
fix-issue-3363

Conversation

@mishushakov

Copy link
Copy Markdown
Member

Fixes #3363

Problem

GET /files returned an empty 200 for procfs/sysfs files (e.g. /proc/sys/kernel/random/boot_id) unless the client negotiated gzip. Virtual files report stat size 0 even though reading them yields content, and the identity path delegates to http.ServeContent, which sizes the response by seeking to the end of the file — so it set Content-Length: 0 and copied zero bytes. The gzip path streams with io.Copy and was unaffected, which is why undici/httpx-based SDKs worked by accident while Cloudflare Workers fetch and plain curl/wget silently read virtual files as empty.

Fix

On the identity path, when stat.Size() == 0, buffer the file (capped at 16 MiB) and serve it via http.ServeContent with a bytes.Reader:

  • genuinely empty files still get a correct Content-Length: 0
  • virtual files get their real content and a correct length
  • Range/conditional semantics are preserved (verified by a 206 test against /proc/version)
  • files that outgrow the cap (e.g. unbounded virtual files) are streamed without a known length, matching the existing gzip-path behavior

Also bumps the envd version to 0.6.11 per the behavioral-change policy.

Testing

  • New unit tests: identity download of an empty regular file (regression guard), identity download of /proc/version (Linux-only), and a Range request against /proc/version (Linux-only).
  • Verified the virtual-file test fails against the old code with exactly the reported symptom (empty body) and passes with the fix, in a Linux container.
  • Full packages/envd test suite passes on Linux (docker, golang:1.26) and macOS; make lint and make fmt are clean for the touched packages.

🤖 Generated with Claude Code

GET /files returned an empty 200 for virtual files (e.g.
/proc/sys/kernel/random/boot_id) unless the client negotiated gzip.
http.ServeContent sizes the response by seeking to the end of the file,
and virtual files report stat size 0, so the identity path served
Content-Length: 0 with no body while the gzip path streamed the real
content.

When stat reports size 0, buffer the file (capped at 16 MiB) and serve
it from a bytes.Reader so both genuinely empty files and virtual files
get a correct Content-Length, preserving Range/conditional semantics.
Files that outgrow the cap are streamed without a length, matching the
gzip path.

Fixes #3363

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 23, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Scoped change to envd file download behavior with a 16 MiB read cap; no auth or data-model changes.

Overview
Identity GET /files no longer returns an empty body for procfs/sysfs paths that report stat size 0; those reads are buffered (up to 16 MiB) so clients get the real bytes and a correct Content-Length, with oversized virtual files streamed without a known length. Genuinely empty regular files still respond with length 0. Linux tests cover empty files, /proc/version, and Range requests; envd is bumped to 0.6.11.

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

@mishushakov
mishushakov marked this pull request as draft July 23, 2026 13:56
@mishushakov

Copy link
Copy Markdown
Member Author

tbh this is an edge case and does not require immediate attention

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

❌ 7 Tests Failed:

Tests completed Failed Passed Skipped
3473 7 3466 9
View the top 3 failed test(s) by shortest run time
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_1_lists_root_directory
Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_1_lists_root_directory
=== PAUSE TestListDir/depth_1_lists_root_directory
=== CONT  TestListDir/depth_1_lists_root_directory
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_1_lists_root_directory
--- FAIL: TestListDir/depth_1_lists_root_directory (0.01s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
=== PAUSE TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
=== CONT  TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
--- FAIL: TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory) (0.01s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_0_lists_only_root_directory
Stack Traces | 0.02s run time
=== RUN   TestListDir/depth_0_lists_only_root_directory
=== PAUSE TestListDir/depth_0_lists_only_root_directory
=== CONT  TestListDir/depth_0_lists_only_root_directory
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_0_lists_only_root_directory
--- FAIL: TestListDir/depth_0_lists_only_root_directory (0.02s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir
Stack Traces | 1.08s run time
=== RUN   TestListDir
=== PAUSE TestListDir
=== CONT  TestListDir
--- FAIL: TestListDir (1.08s)
Executing command cat in sandbox ivrp3qsujmqq2vovt5ykc (user: root)
github.com/e2b-dev/infra/tests/integration/internal/tests/proxies::TestMaskRequestHostAPIParameter
Stack Traces | 5.15s run time
=== RUN   TestMaskRequestHostAPIParameter
=== PAUSE TestMaskRequestHostAPIParameter
=== CONT  TestMaskRequestHostAPIParameter
    mask_request_host_test.go:44: Command [python3] output: event:{start:{pid:1136}}
    mask_request_host_test.go:68: Command [cat] output: event:{start:{pid:1138}}
    mask_request_host_test.go:68: Command [cat] output: event:{data:{stderr:"cat: /tmp/nc_output.txt: No such file or directory\n"}}
    mask_request_host_test.go:68: Command [cat] output: event:{end:{exit_code:1 exited:true status:"exit status 1" error:"exit status 1"}}
    mask_request_host_test.go:69: 
        	Error Trace:	.../tests/proxies/mask_request_host_test.go:69
        	Error:      	Received unexpected error:
        	            	command cat in sandbox i6jd409gknqwzgq1dyijy failed with exit code 1
        	Test:       	TestMaskRequestHostAPIParameter
--- FAIL: TestMaskRequestHostAPIParameter (5.15s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/templates::TestDeleteTemplate
Stack Traces | 44.4s run time
=== RUN   TestDeleteTemplate
=== PAUSE TestDeleteTemplate
=== CONT  TestDeleteTemplate
    build_template_test.go:133: test-to-delete: [info] Building template edhb83k84sn517i6kaln/83f0295e-f58c-4c64-8082-912a1d774bc7
    build_template_test.go:133: test-to-delete: [info] [base] FROM ubuntu:22.04 [f9f564014e009a9561a82bf8c84f9314242971e833fb019936654ecba452f184]
    build_template_test.go:133: test-to-delete: [info] Base Docker image size: 30 MB
    build_template_test.go:133: test-to-delete: [info] Creating file system and pulling Docker image
    build_template_test.go:133: test-to-delete: [info] Uncompressing layer sha256:d6834b4a794c03efa2c998853e64969fa8851b11b2ade63292268872a37759d0 30 MB
    build_template_test.go:133: test-to-delete: [info] Uncompressing layer sha256:b1e8e423c107de0fe67e1c0e303b26447c3fb0a0e43cb04f5045e3cc88f7aa17 13 MB
    build_template_test.go:133: test-to-delete: [info] Uncompressing layer sha256:8c4b1b28875140ed3abacaf16ad0d696f6bef912f52d2148f261a23e3349465b 168 B
    build_template_test.go:133: test-to-delete: [info] Layers extracted
    build_template_test.go:133: test-to-delete: [info] Root filesystem structure: bin, boot, dev, etc, home, lib, lib32, lib64, libx32, media, mnt, opt, proc, root, run, sbin, srv, sys, tmp, usr, var
    build_template_test.go:133: test-to-delete: [error] Build failed: An internal error occurred. Please try again or contact support with the build ID.
    delete_template_test.go:18: Build failed: {<nil> An internal error occurred. Please try again or contact support with the build ID. <nil>}
--- FAIL: TestDeleteTemplate (44.40s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestCommandKillNextApp
Stack Traces | 271s run time
=== RUN   TestCommandKillNextApp
=== PAUSE TestCommandKillNextApp
=== CONT  TestCommandKillNextApp
    process_test.go:30: Build failed: {<nil> An internal error occurred. Please try again or contact support with the build ID. <nil>}
--- FAIL: TestCommandKillNextApp (270.79s)
View the full list of 1 ❄️ flaky test(s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_3_lists_all_directories_and_files

Flake rate in main: 30.77% (Passed 9 times, Failed 4 times)

Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_3_lists_all_directories_and_files
=== PAUSE TestListDir/depth_3_lists_all_directories_and_files
=== CONT  TestListDir/depth_3_lists_all_directories_and_files
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_3_lists_all_directories_and_files
--- FAIL: TestListDir/depth_3_lists_all_directories_and_files (0.01s)

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

@claude claude 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.

LGTM — a well-tested, contained fix for the reported bug.

What was reviewed: the zero-size buffering branch (correct Content-Length, Range/conditional handling preserved via bytes.Reader), the overflow-to-streaming fallback for files exceeding the 16MiB cap, and the gzip path (unaffected, as intended). The finder-flagged concern about the overflow branch potentially hanging on non-terminating virtual files was examined by the bug-hunting system and ruled out.

Extended reasoning...

Overview

This PR fixes a real bug (#3363): the identity (non-gzip) download path in envd used http.ServeContent directly against the file handle, which seeks to determine content length. For procfs/sysfs files that report stat.Size() == 0 but yield real content on read, this produced an empty 200 response. The fix buffers zero-stat-size files (capped at 16 MiB) into memory and serves them via a bytes.Reader, which lets http.ServeContent correctly size the response and still handle Range/conditional requests. Files exceeding the cap fall back to unbuffered streaming via io.Copy, mirroring the existing gzip path's behavior. Change is isolated to packages/envd/internal/api/download.go, with new tests and a version bump per the envd behavioral-change policy.

Security risks

No new auth, permission, or data-exposure surface — the code path already required the existing signing/auth checks earlier in GetFiles, and this change only affects how bytes already permitted to be read are packaged into the HTTP response. The main-loop bug hunting system flagged a candidate concern that the overflow-streaming branch could hang forever on a non-terminating virtual file (e.g., an unbounded /proc entry), but that was examined and ruled out — it doesn't introduce a materially different risk than the pre-existing streaming behavior on the gzip path.

Level of scrutiny

This warrants light scrutiny: it's a small, single-purpose fix to a non-auth code path in envd (in-VM daemon), backed by targeted regression tests (empty file, virtual file content, and Range request against a real procfs file) that were verified to fail on old code and pass on the fix. The blast radius is limited to file-download responses for zero-stat-size files.

Other factors

Tests are meaningful (not just happy-path assertions) and include a Linux-specific Range/conditional test against real procfs, matching the PR's own verification claims. The version bump follows the project's documented policy for envd behavioral changes. No outstanding reviewer comments to address; only automated summary from Cursor Bugbot, no bugs raised.

@ValentaTomas
ValentaTomas force-pushed the main branch 2 times, most recently from 5aad415 to d71980e Compare July 25, 2026 22:53
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.

envd: GET /files returns an empty body for procfs/sysfs files unless gzip is negotiated

1 participant