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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user