Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion api/src/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { describe, expect, test } from 'bun:test';
import { legacyPackagesDirectory } from './config';
import { formatBindAddress, legacyPackagesDirectory } from './config';

describe('bind address formatting', () => {
test('formats IPv4 listeners without brackets', () => {
expect(formatBindAddress('0.0.0.0', 2000)).toBe('0.0.0.0:2000');
});

test('formats IPv6 listeners with brackets for logs and URLs', () => {
expect(formatBindAddress('::', 2000)).toBe('[::]:2000');
expect(formatBindAddress('2001:db8::1', 3112)).toBe('[2001:db8::1]:3112');
});
});

describe('legacy package directory fallback', () => {
test('preserves custom legacy data directories', () => {
Expand Down
11 changes: 10 additions & 1 deletion api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,19 @@ export function legacyPackagesDirectory(raw: string | undefined): string | undef
: trimmed === '/' ? '/packages' : `${trimmed}/packages`;
}

export function formatBindAddress(host: string, port: number): string {
return host.includes(':') ? `[${host}]:${port}` : `${host}:${port}`;
}

const bindHost = process.env.SANDBOX_BIND_HOST?.trim() || '0.0.0.0';
const bindPort = safeInt(process.env.PORT, 2000);

export const config = {
hardened_sandbox_mode: process.env.CODEAPI_HARDENED_SANDBOX_MODE === 'true',
log_level: process.env.SANDBOX_LOG_LEVEL ?? 'DEBUG',
bind_address: `0.0.0.0:${process.env.PORT ?? 2000}`,
bind_host: bindHost,
bind_port: bindPort,
bind_address: formatBindAddress(bindHost, bindPort),
packages_directory: cleanDirectory(process.env.SANDBOX_PACKAGES_DIRECTORY)
?? legacyPackagesDirectory(process.env.SANDBOX_DATA_DIRECTORY)
?? '/pkgs',
Expand Down
3 changes: 1 addition & 2 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,8 @@ async function main(): Promise<void> {
validateHardenedSandboxStartup();
await initializeSandboxWorkspaceIsolation();

const [address, port] = config.bind_address.split(':');
const stopWorkspaceReaper = startWorkspaceReaper();
const server = app.listen(Number(port), address, () => {
const server = app.listen(config.bind_port, config.bind_host, () => {
logger.info({ address: config.bind_address }, 'Sandbox API started');
});

Expand Down
24 changes: 11 additions & 13 deletions docker/start-direct-sandbox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,22 @@ export SANDBOX_ROOTFS="$ROOTFS"
exec unshare --mount bash -c '
ROOTFS="${SANDBOX_ROOTFS:-/sandbox-rootfs}"

mount -o bind,ro "$ROOTFS/usr/sbin" /usr/sbin || { echo "FATAL: cannot bind /usr/sbin"; exit 1; }
mount -o bind,ro "$ROOTFS/usr/lib" /usr/lib || { echo "FATAL: cannot bind /usr/lib"; exit 1; }

if [ -d "$ROOTFS/usr/lib64" ] && ! [ -L "$ROOTFS/usr/lib64" ]; then
mount -o bind,ro "$ROOTFS/usr/lib64" /usr/lib64 2>/dev/null || \
echo "[sandbox] WARNING: could not bind /usr/lib64 - sandboxed binaries may fail to exec"
fi

mount -o bind,ro "$ROOTFS/usr/local" /usr/local || { echo "FATAL: cannot bind /usr/local"; exit 1; }
mount -o bind,ro "$ROOTFS/sandbox_api" /sandbox_api || { echo "FATAL: cannot bind /sandbox_api"; exit 1; }
mount -o bind,ro "$ROOTFS/pkgs" /pkgs || { echo "FATAL: cannot bind /pkgs"; exit 1; }
mount -o bind,ro "$ROOTFS/pkgs" /pkgs || { echo "FATAL: cannot bind /pkgs"; exit 1; }

if [ -d /host-packages ]; then
# Prefer a populated host/PVC package mount. A baked image keeps its own
# ROOTFS/pkgs as the fallback when the mount is absent or empty.
if [ -d /host-packages ] && [ "$(ls -A /host-packages 2>/dev/null)" ]; then
mount --bind /host-packages /pkgs 2>/dev/null || \
echo "WARNING: could not bind /host-packages - sandbox will run without packages"
echo "WARNING: could not bind /host-packages - using packages from $ROOTFS/pkgs"
else
echo "INFO: /host-packages is empty or missing - using packages from $ROOTFS/pkgs"
fi

mount -o bind,ro "$ROOTFS/usr/bin" /usr/bin || { echo "FATAL: cannot bind /usr/bin"; exit 1; }
# Bind the complete /usr tree once. Fedora-style merged-/usr images can
# make /usr/bin and /usr/sbin the same tree; mounting those subdirectories
# separately can hide mount(8) before startup has finished.
mount -o bind,ro "$ROOTFS/usr" /usr || { echo "FATAL: cannot bind /usr"; exit 1; }

multiarch_libdir=$(find /usr/lib -maxdepth 1 -type d -name "*-linux-gnu" -print -quit)
if [ -n "$multiarch_libdir" ]; then
Expand Down
4 changes: 4 additions & 0 deletions helm/codeapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ helm install codeapi . -f values-local.yaml

The chart includes a **package-init Job** that runs as a Helm `pre-install` hook. It automatically compiles Python, downloads Node/Bun, installs offline package sets, and registers Bash into the packages PVC before the worker pods start.

For IPv6-only or dual-stack clusters, set `workerSandbox.sandbox.bindHost` to
`"::"`. The sandbox API keeps the bind host and port separate so IPv6 addresses
are not incorrectly split on their colons.

This happens automatically on `helm install`. To force a rebuild:

```bash
Expand Down
2 changes: 2 additions & 0 deletions helm/codeapi/templates/worker-sandbox-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ spec:
value: "INFO"
- name: SANDBOX_PACKAGES_DIRECTORY
value: "/pkgs"
- name: SANDBOX_BIND_HOST
value: {{ .Values.workerSandbox.sandbox.bindHost | default "0.0.0.0" | quote }}
Comment on lines +259 to +260

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow SANDBOX_BIND_HOST through the microVM launcher

When this chart runs with the default workerSandbox.kvmEnabled: true, this new env var is set only on the outer sandbox-runner container; the microVM guest receives only variables allowed by launcher/src/main.rs:is_allowed_guest_env_key, and that allowlist currently includes PORT but not SANDBOX_BIND_HOST. As a result, setting workerSandbox.sandbox.bindHost: "::" for an IPv6-only cluster is silently dropped before api/src/config.ts runs, so the sandbox API still binds 0.0.0.0 unless KVM is disabled or the launcher allowlist is updated.

Useful? React with 👍 / 👎.

- name: SANDBOX_MAX_PROCESS_COUNT
value: {{ .Values.workerSandbox.sandbox.maxProcessCount | quote }}
- name: SANDBOX_RUN_CPU_TIME
Expand Down
2 changes: 2 additions & 0 deletions helm/codeapi/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ workerSandbox:
# Sandbox configuration (NsJail-based, uses SANDBOX_* env vars)
sandbox:
port: 2000
# Use :: for IPv6-only/dual-stack clusters; keep 0.0.0.0 for IPv4-only.
bindHost: "0.0.0.0"
maxProcessCount: 100
runCpuTime: 15000
runTimeout: 15000
Expand Down
15 changes: 15 additions & 0 deletions tests/start-direct-sandbox.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail

script="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/docker/start-direct-sandbox.sh"

grep -Fq 'mount -o bind,ro "$ROOTFS/usr" /usr' "$script"
grep -Fq '[ -d /host-packages ] && [ "$(ls -A /host-packages 2>/dev/null)" ]' "$script"
grep -Fq 'using packages from $ROOTFS/pkgs' "$script"
if grep -Fq 'mount -o bind,ro "$ROOTFS/usr/sbin" /usr/sbin' "$script" \
|| grep -Fq 'mount -o bind,ro "$ROOTFS/usr/bin" /usr/bin' "$script"; then
echo "start-direct-sandbox.sh still bind-mounts merged /usr subdirectories" >&2
exit 1
fi

echo "merged /usr mount contract passed"