Skip to content

fix(orchestrator): fail builds loudly when memfile.header cannot be written#3251

Open
AdaAibaby wants to merge 4 commits into
e2b-dev:mainfrom
AdaAibaby:fix/memfile-header-missing-local-storage
Open

fix(orchestrator): fail builds loudly when memfile.header cannot be written#3251
AdaAibaby wants to merge 4 commits into
e2b-dev:mainfrom
AdaAibaby:fix/memfile-header-missing-local-storage

Conversation

@AdaAibaby

Copy link
Copy Markdown
Contributor

Problem

Issue #3226: user-built templates in Local storage mode never have memfile.header written to disk. The template works within the first ~3 minutes (served from in-memory cache), but once the cache TTL expires and the template is reloaded from disk, NewStorage falls back to a fake header with oldMemfileHugePageSize = 2 MiB block size. This wrong block size causes VM restores to fail with "offset is beyond the end of mapping, but normalize fix is not applied" warnings and ultimately "object does not exist" errors on snapfile access.

Root cause (symptomatic)

runV3/runV4 goroutine 1 writes memfile.header. It first waits on MemorySnapshot.DiffHeader (a SetOnce future). If that future resolves to (nil, nil), the goroutine silently returns nil — memfile.header is never written, the upload is reported as successful, and the template is permanently broken after cache eviction.

A nil DiffHeader is only valid for filesystem-only snapshots (which use NewResolvedDiffHeader(nil)). For full memory snapshots it must always resolve to a real header — a nil result indicates a silent failure in the background header goroutine (pauseProcessMemory).

Fix

  1. runV3 and runV4: Add an explicit guard before if h == nil { return nil } — for non-filesystem-only snapshots, a nil DiffHeader is now an upload error. Builds fail immediately with a clear message instead of producing a silently corrupted template.

  2. NewStorage fallback: Upgrade the log from Debug to Warn when the headerless legacy fallback is used. This makes the symptom visible in operator logs and helps identify templates affected by the upload bug.

Why not a full root-cause fix?

The exact trigger for DiffHeader resolving to nil on a full memory snapshot is not yet identified from static analysis alone. The changes in this PR make the failure loud and observable (build error + warning log) rather than silent and delayed (~3 min), enabling faster diagnosis. Closes #3226.

Test plan

  • Build a template in Local storage mode; verify memfile.header is present in LOCAL_TEMPLATE_STORAGE_BASE_PATH/{buildId}/ after successful build
  • Verify filesystem-only snapshots still work (they skip memfile upload via FilesystemSnapshot == true)
  • If the nil-DiffHeader trigger is reproduced, verify the build now fails with the new error message instead of silently missing the header

…ritten

memfile.header is written by a goroutine that waits on the DiffHeader
SetOnce future. The future can only be nil for filesystem-only snapshots
(NoDiff); for full memory snapshots it must always resolve to a real
header. If it resolves nil for a non-filesystem-only snapshot the header
file is silently skipped and the template is permanently unresumable after
its in-memory cache entry is evicted (the reload path falls back to a
wrong 2 MiB block size, causing VM restore failures with offset-beyond-end-of-mapping
errors).

Add an explicit check in runV3 and runV4 so such a build fails loudly
with a clear error instead of silently producing a corrupted template.

Also upgrade the legacy-fallback log in NewStorage from Debug to Warn so
operators can see when the headerless code path is hit; this is
symptomatic of the upload bug described in issue e2b-dev#3226 and lets users
identify affected templates without waiting for VM restore failures.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds validation checks in both build_upload_v3.go and build_upload_v4.go to return an error if a memfile diff header resolves to nil for non-filesystem-only snapshots, and updates a log level from Debug to Warn in storage.go for legacy headerless fallbacks. The reviewer noted a potential nil pointer dereference panic in other goroutines that wait on these headers and call u.layerSizeMetadata(h) without checking if h is nil, suggesting that similar nil checks should be added there.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/orchestrator/pkg/sandbox/build_upload_v3.go

@jakubno jakubno left a comment

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.

please fix lint issues

Comment thread packages/orchestrator/pkg/sandbox/template/storage.go Outdated
- Revert legacy-fallback log level from Warn back to Debug; this code
  path is expected for old templates and should not page operators
- Shorten the log message to match the original terse style
- Use errors.New instead of fmt.Errorf for the static error string in
  runV3 and runV4 (fixes perfsprint lint)
@AdaAibaby

Copy link
Copy Markdown
Contributor Author

please fix lint issues

done.

@jakubno jakubno left a comment

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.

Checking the code it seems full-snapshot code cannot produce (nil, nil)
pauseProcessMemory resolves DiffHeader as either:
non-nil header, nil error
nil header, non-nil error

This doesn't fix the connected issue

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

ℹ️ 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".

Comment thread packages/orchestrator/pkg/sandbox/build_upload_v3.go Outdated
jakubno's review correctly identified that the (nil, nil) guard added in
the previous commit is dead code: pauseProcessMemory's goroutine always
resolves DiffHeader to either (non-nil header, nil error) via ToDiffHeader
or (nil, non-nil error) on failure, never (nil, nil) for full-memory
snapshots.

The real failure mode in issue e2b-dev#3226 was that two independent goroutines
uploaded the memfile header and body concurrently. If the body goroutine
completed and the header goroutine later failed, the errgroup error
triggered cleanup, but a transient cleanup failure left the body in
storage without a header. Loading that template after cache eviction
produces the synthetic V1 fallback with Size=physicalMemfileSize instead
of the virtual RAM size, causing the "mappingEnd vs offset" crash.

Fix: merge the per-file header and body goroutines in runV3 into one,
writing the header first. If the header write fails the body is never
uploaded (clean state). If the body upload fails after a successful header
write, cleanup removes an easily-detectable orphaned header rather than a
silent orphaned body.

runV4's uploadFramed already handles both header and body atomically in a
single call (body first is required there since the V4 header includes the
body's frame table). Remove the dead guard from its memfile goroutine too.

Fixes e2b-dev#3226
@AdaAibaby AdaAibaby force-pushed the fix/memfile-header-missing-local-storage branch from f557af2 to b21997a Compare July 10, 2026 15:14

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

ℹ️ 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".

Comment thread packages/orchestrator/pkg/sandbox/build_upload_v3.go Outdated
… memfile header

- Merge memfile header+body into one goroutine per file type; upload the
  body first, then write the header to storage. PollRemoteStorageForHeader
  on peer orchestrators treats the header's appearance as the durability
  signal for the whole layer, so writing the header first (as in b21997a)
  lets child builds proceed against a body that has not yet landed.

- Return an explicit error when MemorySnapshot.DiffHeader resolves to nil
  for a non-filesystem snapshot. Previously the header goroutine returned
  nil silently (if h == nil { return nil }), allowing the body to land in
  storage without a header and causing the mis-sizing logged in e2b-dev#3226.
  Filesystem-only snapshots still return nil (intentional NoDiff path).

- Add Info-level logs at the two key points in the header-resolution
  pipeline: when pauseProcessMemory's goroutine calls setHeader (sandbox.go)
  and when runV3's goroutine receives the resolved value (build_upload_v3.go).
  Together these logs make the root-cause observable on the next reproduction
  without requiring a core dump or additional instrumentation.
@AdaAibaby

AdaAibaby commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Checking the code it seems full-snapshot code cannot produce (nil, nil) pauseProcessMemory resolves DiffHeader as either: non-nil header, nil error nil header, non-nil error

This doesn't fix the connected issue

Agreed — (nil, nil) should be analytically unreachable for full-memory snapshots,
so the original if h == nil { return nil } was dead code on that path.

Updated in 8a4cf57: the nil case now returns an explicit error for non-filesystem
snapshots instead of silently succeeding, so if this ever does trigger the build
fails loudly with the build_id rather than leaving a header-less template.

Also added Info-level logs at both ends of the resolution chain
(pauseProcessMemory's setHeader call and runV3's WaitWithContext return) to make
the actual runtime values observable on the next reproduction — that should tell
us whether metaOut never resolved, ToDiffHeader returned unexpectedly, or
something else entirely.

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.

Every template I build ends up unusable after a few minutes — memfile.header is missing on disk. Am I doing something wrong?

4 participants