e2b-dev / e2b-dev/runtime

bug(orchestrator): Cleanup.Add has TOCTOU race — cleanup functions silently dropped after Run()

Open
#3,557 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

Cleanup.Add and Cleanup.AddPriority have a TOCTOU (time-of-check/time-of-use) race against Cleanup.run that silently discards cleanup functions without executing them, causing permanent resource leaks.

Root Cause

In packages/orchestrator/pkg/sandbox/cleanup.go:

run() sets hasRun before acquiring the lock (line 80 vs 82):

func (c *Cleanup) run(ctx context.Context) {
    c.hasRun.Store(true)   // ← written outside the lock
    c.mu.Lock()
    defer c.mu.Unlock()
    // ... drains c.cleanup and c.priorityCleanup
}

Add() reads hasRun before acquiring the lock (line 40 vs 49):

func (c *Cleanup) Add(ctx context.Context, f func(ctx context.Context) error) {
    if c.hasRun.Load() == true {   // ← read outside the lock
        // run f immediately
        return
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    c.cleanup = append(c.cleanup, f)   // ← appended after run() may have drained
}

Race Window

Goroutine A (Add):    hasRun.Load() == false  →  [not yet locked]
Goroutine B (run):    hasRun.Store(true)  →  Lock()  →  drain all cleanup  →  Unlock()
Goroutine A (Add):    Lock()  →  append(f)   ← f is now in a drained slice, never executed

sync.Once in Run() prevents run() from being called a second time, so f is permanently lost — no log, no error, no signal.

Impact

The Cleanup type is the central resource-release mechanism for Firecracker sandbox lifecycle. The package has 28 call sites across sandbox startup (sandbox.go, resume.go, reboot.go, etc.) registering operations such as:

  • Closing overlay filesystems
  • Releasing network slots
  • Removing Firecracker and UFFD socket files
  • Stopping the Firecracker process

Any one of these silently dropped during a concurrent error-path teardown leaves a leaked resource on the host until the orchestrator restarts.

Fix

Move hasRun.Store(true) inside the lock in run(), and add a double-check in Add() / AddPriority() after acquiring mu:

func (c *Cleanup) Add(ctx context.Context, f func(ctx context.Context) error) {
    // Optimistic fast path.
    if c.hasRun.Load() {
        err := f(context.WithoutCancel(ctx))
        if err != nil {
            logger.L().Error(ctx, "failed to run function after cleanup has run", zap.Error(err))
        }
        return
    }

    c.mu.Lock()
    // Double-check: run() may have completed between the Load above and here.
    if c.hasRun.Load() {
        c.mu.Unlock()
        err := f(context.WithoutCancel(ctx))
        if err != nil {
            logger.L().Error(ctx, "failed to run function after cleanup has run", zap.Error(err))
        }
        return
    }
    c.cleanup = append(c.cleanup, f)
    c.mu.Unlock()
}

func (c *Cleanup) run(ctx context.Context) {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.hasRun.Store(true)  // now set under the lock — closes the race window

    // ... rest unchanged
}

The same double-check applies to AddPriority.

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

Review Cleanup.Add, Cleanup.AddPriority, and Cleanup.run in packages/orchestrator/pkg/sandbox/cleanup.go, focusing on the lock and hasRun ordering. Confirm the change closes the TOCTOU window and that cleanup functions registered concurrently with Run() are no longer silently dropped, then exercise the package's existing tests with the race detector.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.