fix(e2b): harden SDK shell and cleanup boundaries

E2B starts command and PTY requests through login shells, so isolate each control shell behind a fresh randomized HOME and blank sandbox credential names before mutable profiles can run. Preserve the real remote HOME only for the requested argv.

Collapse duplicate termination state, keep failed force cleanup retryable until quiescence is observed, and make terminal state allocation cancellable. Leave numeric PGID reuse as an explicit provider-level TODO because a userspace precheck would remain TOCTOU.
This commit is contained in:
Tianyi Cui
2026-07-29 15:30:32 +08:00
parent 091af03a81
commit 81e2e1f647
21 changed files with 438 additions and 211 deletions

View File

@@ -1,11 +1,24 @@
/** Shared remote-environment scrubbing for E2B process and terminal launchers. */
import { Buffer } from 'node:buffer'
import { posix } from 'node:path'
import { e2bControlEnvs } from '@deepseek-ai/dsh-e2b'
import type { Sandbox } from '@deepseek-ai/dsh-e2b'
import { SENSITIVE_ENV_PATTERN } from '@deepseek-ai/dsh-subprocess'
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
function remoteEnvironmentEntries(raw: string): Array<readonly [string, string]> {
const entries: Array<readonly [string, string]> = []
for (const entry of raw.split('\0')) {
if (entry.length === 0) continue
const separator = entry.indexOf('=')
if (separator <= 0) continue
entries.push([entry.slice(0, separator), entry.slice(separator + 1)])
}
return entries
}
/**
* Read the remote environment through ASCII base64 so SDK callback chunking cannot corrupt UTF-8.
* @param sandbox - shared E2B execution world.
@@ -14,16 +27,29 @@ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$
*/
export async function readRemoteEnvironment(sandbox: Sandbox, signal?: AbortSignal): Promise<string> {
const result = await sandbox.commands.run(
'set -o pipefail; env -0 | base64 -w 0',
signal === undefined ? {} : { signal },
'set -o pipefail; printf \'%s\' "$PWD" | base64 -w 0; printf \'\\n\'; env -0 | base64 -w 0',
{ envs: e2bControlEnvs(), ...(signal === undefined ? {} : { signal }) },
)
const encoded = result.stdout.trim()
if (!BASE64.test(encoded)) throw new Error('subprocess-e2b: remote environment transport returned invalid base64')
const lines = result.stdout.trim().split('\n')
if (lines.length !== 2 || !lines.every(line => BASE64.test(line))) {
throw new Error('subprocess-e2b: remote environment transport returned invalid base64')
}
const [encodedHome, encodedEnvironment] = lines as [string, string]
let home: string
let raw: string
try {
return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(encoded, 'base64'))
const decoder = new TextDecoder('utf-8', { fatal: true })
home = decoder.decode(Buffer.from(encodedHome, 'base64'))
raw = decoder.decode(Buffer.from(encodedEnvironment, 'base64'))
} catch (error: unknown) {
throw new Error('subprocess-e2b: remote environment is not valid UTF-8', { cause: error })
}
if (!posix.isAbsolute(home) || home.includes('\0')) {
throw new Error(`subprocess-e2b: remote login home is invalid: ${JSON.stringify(home)}`)
}
const environment = new Map(remoteEnvironmentEntries(raw))
environment.set('HOME', home)
return [...environment].map(([name, value]) => `${name}=${value}\0`).join('')
}
/**
@@ -33,13 +59,22 @@ export async function readRemoteEnvironment(sandbox: Sandbox, signal?: AbortSign
*/
export function scrubRemoteEnvironment(raw: string): Map<string, string> {
const environment = new Map<string, string>()
for (const entry of raw.split('\0')) {
if (entry.length === 0) continue
const separator = entry.indexOf('=')
if (separator <= 0) continue
const name = entry.slice(0, separator)
for (const [name, value] of remoteEnvironmentEntries(raw)) {
if (name.startsWith('DSH_') || SENSITIVE_ENV_PATTERN.test(name)) continue
environment.set(name, entry.slice(separator + 1))
environment.set(name, value)
}
return environment
}
/**
* Isolate E2B's fixed login-shell bootstrap from user profiles and ambient credentials.
* @param raw - The complete NUL-delimited remote environment.
* @returns Explicit E2B command or PTY overrides for bootstrap-shell startup.
*/
export function bootstrapEnvironment(raw: string): Record<string, string> {
const environment: Record<string, string> = { TERM: 'dumb' }
for (const [name] of remoteEnvironmentEntries(raw)) {
if (name.startsWith('DSH_') || SENSITIVE_ENV_PATTERN.test(name)) environment[name] = ''
}
return environment
}