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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Sandbox-runner liveness checks
run: tests/sandbox_runner_healthcheck.sh

- name: File-generation runtime checks
run: tests/file_generation_runtime_static.sh

- name: Validate sandbox Dockerfiles
run: |
docker buildx build --check -f api/Dockerfile .
Expand Down
7 changes: 7 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ RUN apt-get update && \
file \
jq \
poppler-utils \
libreoffice \
ffmpeg \
libpango-1.0-0 \
libpangoft2-1.0-0 \
shared-mime-info \
fonts-dejavu \
fonts-liberation \
libnl-route-3-200 \
Expand Down Expand Up @@ -211,6 +216,8 @@ RUN dnf install -y --setopt=install_weak_deps=False \
ca-certificates \
&& dnf clean all

RUN mkdir -p /etc/alternatives /etc/fonts /etc/libreoffice /var/cache/fontconfig

COPY --from=launcher-builder /launcher/target/release/sandbox-launcher /usr/local/bin/launcher

COPY launcher/entrypoint.sh /usr/local/bin/launcher-entrypoint.sh
Expand Down
4 changes: 2 additions & 2 deletions api/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions api/config/sandbox.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ mount {
mandatory: false
}

# Native document/media tools need their read-only package registries and
# font configuration. Keep the allowlist narrow instead of exposing /etc.
mount { src: "/etc/alternatives" dst: "/etc/alternatives" is_bind: true rw: false mandatory: false }
mount { src: "/etc/fonts" dst: "/etc/fonts" is_bind: true rw: false mandatory: false }
mount { src: "/etc/libreoffice" dst: "/etc/libreoffice" is_bind: true rw: false mandatory: false }
mount { src: "/var/cache/fontconfig" dst: "/var/cache/fontconfig" is_bind: true rw: false mandatory: false }

# Static /etc/hosts so "localhost" resolves for tool-call-server connections.
# Uses inline content instead of bind-mounting the host's /etc/hosts, which
# in Kubernetes leaks pod name, internal IP, and deployment metadata.
Expand Down
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0",
"express": "^4.22.2",
"nanoid": "^3.3.7",
"nanoid": "^3.3.18",
"pino": "^10.3.0",
"prom-client": "^15.1.3",
"semver": "^7.8.0"
Expand Down
7 changes: 5 additions & 2 deletions api/src/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ if grep -q virtiofs /proc/filesystems 2>/dev/null; then
fi
fi

# Restrict dmesg access (requires CAP_SYSLOG to read kernel ring buffer)
echo 1 > /proc/sys/kernel/dmesg_restrict 2>/dev/null || true
# Only change dmesg policy inside the dedicated guest kernel. Direct mode must
# not attempt to mutate the host kernel's sysctl namespace.
if [ "${KVM_ENABLED:-true}" = "true" ]; then
echo 1 > /proc/sys/kernel/dmesg_restrict 2>/dev/null || true
fi

# Remove Kubernetes/containerd proc masks so NsJail can mount fresh procfs.
# Container runtimes add submounts to /proc (kcore, keys, timer_list, etc.)
Expand Down
26 changes: 25 additions & 1 deletion api/src/nsjail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,29 @@ describe('NsJail args', () => {
expect(valueAfter(args, '--config')).toBe('/tmp/nsjail-job-xyz.cfg');
});

test('exposes only the package bundle checksum alongside the selected runtime', async () => {
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'nsjail-bundle-checksum-'));
const originalPackagesDirectory = config.packages_directory;
config.packages_directory = tmp;
try {
const checksum = path.join(tmp, '.bundle.sha256');
await fsp.writeFile(checksum, '0'.repeat(64));
const args = buildArgs({
logPath: '/tmp/nsjail-test.log',
pkgdir: path.join(tmp, 'python/3.14.4'),
timeout: 1000,
memoryLimit: -1,
envVars: {},
command: ['/bin/bash', path.join(tmp, 'python/3.14.4/run'), 'main.py'],
identity: { slot: 0, uid: 65534, gid: 65534, perJobUid: false },
});
expect(hasArgPair(args, '-R', `${checksum}:${checksum}`)).toBe(true);
} finally {
config.packages_directory = originalPackagesDirectory;
await fsp.rm(tmp, { recursive: true, force: true });
}
});

test('does not export TOOL_CALL_SOCKET into the jail (preamble references the literal path)', () => {
const args = buildArgs({
logPath: '/tmp/nsjail-test.log',
Expand Down Expand Up @@ -345,7 +368,7 @@ describe('NsJail seccomp policy', () => {

test('KILLs the new mount API family (Linux 5.2+)', () => {
const policy = seccompPolicy();
for (const name of ['move_mount', 'open_tree', 'fsopen', 'fsmount', 'fspick']) {
for (const name of ['move_mount', 'open_tree', 'mount_setattr', 'fsopen', 'fsmount', 'fspick']) {
expect(policy).toMatch(new RegExp(`\\b${name}\\b[,\\s]`));
}
});
Expand All @@ -357,6 +380,7 @@ describe('NsJail seccomp policy', () => {
expect(policy).toContain('#define fsopen 430');
expect(policy).toContain('#define fsmount 432');
expect(policy).toContain('#define fspick 433');
expect(policy).toContain('#define mount_setattr 442');
});

test('KILLs AF_VSOCK in the socket(domain) filter', () => {
Expand Down
13 changes: 10 additions & 3 deletions api/src/nsjail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const sharedSyscallDefines = [
'#define fsopen 430',
'#define fsmount 432',
'#define fspick 433',
'#define mount_setattr 442',
/* pidfd_* are Linux 5.1+/5.3+ — newer than Kafel's bundled symbol
* table on the pinned NsJail snapshot, so define numerically. Same
* number on x86_64 and arm64. */
Expand Down Expand Up @@ -111,9 +112,10 @@ const SECCOMP_POLICY = [
' add_key, request_key, keyctl,',
' mount, umount2, pivot_root,',
/* New mount API (Linux 5.2+) — orthogonal to mount(2) and not covered by
* the line above. open_tree+move_mount can replicate a bind-mount; fsopen/
* fsmount/fspick form the new filesystem-context flow. Block all five. */
' move_mount, open_tree, fsopen, fsmount, fspick,',
* the line above. open_tree+mount_setattr+move_mount can replicate a
* read-only bind-mount; fsopen/fsmount/fspick form the filesystem-context
* flow. The runner needs these before NsJail starts, but sandboxed code does not. */
' move_mount, open_tree, mount_setattr, fsopen, fsmount, fspick,',
' swapon, swapoff, reboot,',
' init_module, finit_module, delete_module,',
/* setns joins an existing namespace via fd. Unshare is already blocked
Expand Down Expand Up @@ -668,6 +670,11 @@ export function buildArgs(opts: BuildArgsOptions): string[] {
'-R', `${pkgdir}:${pkgdir}`,
];

const bundleChecksum = path.join(config.packages_directory, '.bundle.sha256');
if (fs.existsSync(bundleChecksum)) {
args.push('-R', `${bundleChecksum}:${bundleChecksum}`);
}

if (config.use_cgroupv2) {
args.push('--use_cgroupv2');
}
Expand Down
45 changes: 42 additions & 3 deletions apparmor/sandbox-nsjail
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,51 @@ profile sandbox-nsjail flags=(attach_disconnected,mediate_deleted) {
# ==========================================================================
# File access rules
# ==========================================================================


/ r,

# Full access to sandbox working directories
/pkgs/** rwlk,
/pkgs/ rw,
/pkgs/** rwmlkix,
/host-packages/ r,
/host-packages/** r,
/sandbox_api/ rw,
/sandbox_api/** rix,
/sandbox-rootfs/ r,
/sandbox-rootfs/** r,
/tmp/ rw,
/tmp/** rwlk,
/mnt/ rw,
/mnt/** rwlk,
/run/mount/ rw,
/run/mount/** rwk,
/run/user/ rw,
/run/user/** rwk,

# util-linux mount and libc identity lookups used during direct startup
/etc/fstab r,
/etc/passwd r,
/etc/group r,
/etc/nsswitch.conf r,
/etc/authselect/nsswitch.conf r,
/etc/host.conf r,
/etc/hosts r,
/etc/resolv.conf r,
/etc/gai.conf r,
/etc/services r,

# Read-only registries required by native document and media tools
/etc/alternatives/ r,
/etc/alternatives/** r,
/etc/fonts/ r,
/etc/fonts/** r,
/etc/libreoffice/ r,
/etc/libreoffice/** r,
/var/cache/fontconfig/ r,
/var/cache/fontconfig/** r,

# NsJail binary and config
/usr/sbin/nsjail rix,
/sandbox_api/** r,
/sandbox_api/config/** r,

# Runtime executables (Python, Node, Bun, etc.)
Expand All @@ -69,6 +105,9 @@ profile sandbox-nsjail flags=(attach_disconnected,mediate_deleted) {

# Proc filesystem (read-only for most, some writes for cgroups)
/proc/** r,
/proc/[0-9]*/uid_map rw,
/proc/[0-9]*/gid_map rw,
/proc/[0-9]*/setgroups rw,
/proc/sys/kernel/random/uuid r,

# Cgroups v2 (NsJail uses these for resource limits)
Expand Down
Loading