e2b-dev / e2b-dev/runtime

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

Open
#3,363 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
1.6k
Forks
438
PR merge metrics
No merged PRs in 30d

Description

Summary

GET /files (envd's file download endpoint) returns an empty 200 response for procfs/sysfs files (e.g. /proc/sys/kernel/random/boot_id) whenever the client does not negotiate gzip. The same request with Accept-Encoding: gzip returns the full file content. So the response body depends on content negotiation, and non-gzip clients silently read virtual files as empty.

The failure is silent: Content-Length: 0 on a 200 looks exactly like a legitimately empty file, and the SDKs treat it as one (files.read() returns '').

Root cause

Virtual files report size 0 from stat/Seek(0, SeekEnd) even though reading them yields content:

$ stat -c 'size=%s' /proc/sys/kernel/random/boot_id && cat /proc/sys/kernel/random/boot_id
size=0
459d0c2e-ee00-49cd-b3ff-71d4ce0b3cde

The identity path of the handler delegates to http.ServeContent, which determines the response size by seeking to the end of the file → gets 0 → sets Content-Length: 0 and copies exactly 0 bytes:

https://github.com/e2b-dev/infra/blob/afaa636b1eb7e4508998f08b2d0a887f368ad0d6/packages/envd/internal/api/download.go#L172

The gzip path doesn't size the response up front — it streams the file through gzip.NewWriter with a plain io.Copy, so it serves the real content:

https://github.com/e2b-dev/infra/blob/afaa636b1eb7e4508998f08b2d0a887f368ad0d6/packages/envd/internal/api/download.go#L151-L169

Reproduction (envd 0.6.10, prod e2b.app, base template, 2026-07-23)

Raw HTTP/1.1 to envd on localhost:49983 from inside the sandbox (no edge proxy involved; X-Access-Token header set):

Identity — empty body:

GET /files?path=/proc/sys/kernel/random/boot_id&username=user

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Disposition: inline; filename=boot_id
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Last-Modified: Mon, 13 Jul 2026 10:52:37 GMT
Vary: Accept-Encoding

<0 body bytes>

Same request + Accept-Encoding: gzip — full content:

HTTP/1.1 200 OK
Content-Disposition: inline; filename=boot_id
Content-Encoding: gzip
Content-Type: application/octet-stream
Vary: Accept-Encoding
Content-Length: 61

<61 gzip bytes, decode to "459d0c2e-ee00-49cd-b3ff-71d4ce0b3cde\n">

Control — regular file /tmp/regular.txt works fine on the identity path:

HTTP/1.1 200 OK
Content-Length: 21

regular file content

The same behavior is observable externally through the edge (https://49983-<sandboxId>.e2b.app/files?...): identity → Content-Length: 0, empty; gzip → chunked, full content.

Impact

  • Cloudflare Workers / workerd users: fetch there doesn't negotiate gzip the way undici does, so sandbox.files.read('/proc/...') (and any sysfs read) returns ''. Found while running the js-sdk test suite under workerd (e2b-dev/E2B#1586) — the two filesystem pause tests fail exactly this way.
  • Anyone using downloadUrl() with plain curl/wget (no --compressed): empty download for proc/sysfs files.
  • Node (undici) and Python (httpx) SDKs are unaffected only by accident — their HTTP clients send Accept-Encoding: gzip by default and land on the working gzip path.

Quick check from a shell, using a signed download URL:

curl "$URL"               # → empty
curl --compressed "$URL"  # → 459d0c2e-ee00-49cd-b3ff-71d4ce0b3cde

Suggested fix

Don't trust the seek-derived size when stat.Size() == 0. Since a genuinely empty file and a virtual file are indistinguishable without reading, one option: when stat.Size() == 0, read the file (optionally through an io.LimitReader cap) into memory and serve it via http.ServeContent with a bytes.Reader — an actually-empty file still yields a correct Content-Length: 0, and virtual files get their real content and a correct length. Range/conditional semantics are preserved.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/envd/internal/api/download.go, especially the identity path around the linked ServeContent call and the gzip path for comparison. Reproduce the request against a procfs or sysfs file without gzip, then verify that the identity response contains the virtual file content and correct length while regular files and genuinely empty files still behave correctly.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.