Skip to content

perf(orchestrator): reduce mount namespace copy overhead during sandbox startup#3006

Open
emailcannotbeblank wants to merge 2 commits into
e2b-dev:mainfrom
emailcannotbeblank:feat/reduce-sandbox-mount-copy-upstream-sync
Open

perf(orchestrator): reduce mount namespace copy overhead during sandbox startup#3006
emailcannotbeblank wants to merge 2 commits into
e2b-dev:mainfrom
emailcannotbeblank:feat/reduce-sandbox-mount-copy-upstream-sync

Conversation

@emailcannotbeblank

Copy link
Copy Markdown

Summary

This PR significantly changes and optimizes the sandbox creation path, greatly improving sandbox startup speed under high concurrency.

Problem

When creating sandboxes with 100 concurrent requests, sandbox startup is slow. A key step suffers from lock contention, causing the per-sandbox cost to exceed 400ms.

More specifically, NewProcess in packages/orchestrator/pkg/sandbox/fc/process.go is a critical function in the sandbox creation path.

Previously, this function prepared a bash-based startup flow that included commands similar to:

unshare -m ...
ip netns exec ns-xxx firecracker ...

Both commands copy the mount tree and eventually reach copy_mnt_ns in the Linux kernel file fs/namespace.c. When each thread copies the mount tree, it calls namespace_lock and tries to acquire a global lock. The kernel code is:

static inline void namespace_lock(void)
{
    down_write(&namespace_sem);
}

With 100 concurrent threads contending for the same global lock, sandbox startup latency becomes very high.

In addition:

  1. Copying a mount tree requires traversing the mount tree, which is expensive. This can be measured with:

    time unshare -m nproc

    This command can be used to test how long it takes to copy the mount tree.

  2. The previous flow performed two mount tree copies (unshare -m and ip netns exec) and one mount namespace release (the mount namespace created by unshare -m being released). All of these operations acquire the global namespace lock.

For these reasons, creating the Firecracker process has very large latency under high concurrency.

Approach

  1. Create a minimal mount tree template.

    Before creating sandboxes, the orchestrator prepares a minimal mount tree and unmounts unnecessary nodes. Later, when creating each sandbox, the mount tree is copied from this minimal template instead of being copied from the full host mount tree. This greatly reduces the time spent copying the mount tree.

  2. Optimize the Firecracker process startup flow.

    The previous unshare -m and ip netns exec ns-xxx flow is replaced with explicit setns system calls. This reduces the startup path from two mount tree copies and one mount namespace release to only one mount tree copy.

Result

This greatly improves the time required to create the Firecracker process.

In packages/orchestrator/pkg/sandbox/fc/process.go, the configure function is more than 10x faster in our tests, decreasing from over 400ms to under 40ms.

Discussion

  1. This version not only significantly improves startup speed, but also improves sandbox security.

    Previously, the host-side Firecracker process inherited a copy of the host mount tree. If an attacker compromised the Firecracker process, they could observe more of the host filesystem structure. This optimization prunes the mount tree used by the Firecracker process and reduces the attack surface.

  2. This optimization reduces the mount tree size and reduces the number of global lock acquisitions.

    Another possible optimization would be to reuse mount namespaces together with reusable network namespaces. Since different sandboxes already reuse network namespace slots, we could prepare a paired mount namespace for each network namespace. When creating a sandbox, after acquiring a network namespace slot, the sandbox could directly use the corresponding mount namespace.

    However, that approach would reduce isolation to some extent. If an attacker compromised the Firecracker process, they might be able to attack other users that later reuse the same network namespace and mount namespace pair.

This PR chooses a more conservative approach: each sandbox still gets its own mount namespace, but that namespace is copied from a minimal template and created with fewer global-lock-heavy operations.

@cla-bot

cla-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have @emailcannotbeblank on file. You can sign our CLA at https://e2b.dev/docs/cla . Once you've signed, post a comment here that says '@cla-bot check'

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@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

In packages/orchestrator/pkg/sandbox/network/mount_namespace.go, if restoring the host mount namespace fails during cleanup in createTemplate or create, unlocking the OS thread allows the Go runtime to reuse it for other goroutines, polluting them with the unshared mount namespace. Conditionally unlocking the thread only when the namespace is successfully restored ensures that a corrupted thread is discarded by the runtime.

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 on lines +151 to +173
runtime.LockOSThread()
defer runtime.UnlockOSThread()

if err := unix.Unshare(unix.CLONE_FS); err != nil {
return nil, fmt.Errorf("failed to unshare fs attributes before creating template mount namespace: %w", err)
}

hostMntNS, err := openCurrentMountNamespace()
if err != nil {
return nil, fmt.Errorf("failed to open host mount namespace: %w", err)
}
defer hostMntNS.Close()

restoreHostNS := func() {
if err := unix.Setns(int(hostMntNS.Fd()), unix.CLONE_NEWNS); err != nil {
logger.L().Error(ctx, "error resetting mount namespace back to the host namespace", zap.Error(err))
}
}

if err := unix.Unshare(unix.CLONE_NEWNS); err != nil {
return nil, fmt.Errorf("failed to unshare template mount namespace: %w", err)
}
defer restoreHostNS()

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.

critical

If restoring the host mount namespace fails during cleanup, unlocking the OS thread allows the Go runtime to reuse it for other goroutines, polluting them with the unshared mount namespace. Conditionally unlocking the thread only when the namespace is successfully restored ensures that a corrupted thread is discarded by the runtime.

	var unlockThread bool
	runtime.LockOSThread()
	defer func() {
		if unlockThread {
			runtime.UnlockOSThread()
		}
	}()

	if err := unix.Unshare(unix.CLONE_FS); err != nil {
		return nil, fmt.Errorf("failed to unshare fs attributes before creating template mount namespace: %w", err)
	}

	hostMntNS, err := openCurrentMountNamespace()
	if err != nil {
		return nil, fmt.Errorf("failed to open host mount namespace: %w", err)
	}
	defer hostMntNS.Close()

	restoreHostNS := func() {
		if err := unix.Setns(int(hostMntNS.Fd()), unix.CLONE_NEWNS); err != nil {
			logger.L().Error(ctx, "error resetting mount namespace back to the host namespace", zap.Error(err))
		} else {
			unlockThread = true
		}
	}

	if err := unix.Unshare(unix.CLONE_NEWNS); err != nil {
		unlockThread = true
		return nil, fmt.Errorf("failed to unshare template mount namespace: %w", err)
	}
	defer restoreHostNS()

Comment on lines +236 to +258
runtime.LockOSThread()
defer runtime.UnlockOSThread()

if err := unix.Unshare(unix.CLONE_FS); err != nil {
return nil, fmt.Errorf("failed to unshare fs attributes before creating mount namespace: %w", err)
}

hostMntNS, err := openCurrentMountNamespace()
if err != nil {
return nil, fmt.Errorf("failed to open host mount namespace: %w", err)
}
defer hostMntNS.Close()

restoreHostNS := func() {
if err := unix.Setns(int(hostMntNS.Fd()), unix.CLONE_NEWNS); err != nil {
logger.L().Error(ctx, "error resetting mount namespace back to the host namespace", zap.Error(err))
}
}

if err := unix.Setns(int(templateMntNS.Fd()), unix.CLONE_NEWNS); err != nil {
return nil, fmt.Errorf("failed to enter template mount namespace: %w", err)
}
defer restoreHostNS()

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.

critical

If restoring the host mount namespace fails during cleanup, unlocking the OS thread allows the Go runtime to reuse it for other goroutines, polluting them with the unshared mount namespace. Conditionally unlocking the thread only when the namespace is successfully restored ensures that a corrupted thread is discarded by the runtime.

	var unlockThread bool
	runtime.LockOSThread()
	defer func() {
		if unlockThread {
			runtime.UnlockOSThread()
		}
	}()

	if err := unix.Unshare(unix.CLONE_FS); err != nil {
		return nil, fmt.Errorf("failed to unshare fs attributes before creating mount namespace: %w", err)
	}

	hostMntNS, err := openCurrentMountNamespace()
	if err != nil {
		return nil, fmt.Errorf("failed to open host mount namespace: %w", err)
	}
	defer hostMntNS.Close()

	restoreHostNS := func() {
		if err := unix.Setns(int(hostMntNS.Fd()), unix.CLONE_NEWNS); err != nil {
			logger.L().Error(ctx, "error resetting mount namespace back to the host namespace", zap.Error(err))
		} else {
			unlockThread = true
		}
	}

	if err := unix.Setns(int(templateMntNS.Fd()), unix.CLONE_NEWNS); err != nil {
		unlockThread = true
		return nil, fmt.Errorf("failed to enter template mount namespace: %w", err)
	}
	defer restoreHostNS()

@cla-bot cla-bot Bot added the cla-signed label Jun 13, 2026
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.

1 participant