From 4f098f5bfb1e7ce1dfcaaae7bd8b4a48005ef5f0 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Thu, 12 Feb 2026 05:26:48 +0000 Subject: [PATCH] fix: mask /proc info and selective /dev mounting (#223) Address Phase 3 filesystem exploit findings: 1. Mask /proc/kallsyms and /proc/modules in both container modes: - Non-chroot: Docker volume mounts /dev/null over /proc/kallsyms and /proc/modules - Chroot: bind-mount /dev/null over /host/proc/kallsyms and /host/proc/modules This prevents kernel symbol address disclosure (ASLR bypass) and module enumeration. 2. Replace blanket /dev:/host/dev:ro with selective device mounts in chroot mode: - Only mount /dev/null, /dev/zero, /dev/random, /dev/urandom, /dev/tty - Set up /dev/pts, /dev/shm, and standard symlinks (fd, stdin, stdout, stderr) in entrypoint This prevents host block device (/dev/sda*) exposure. Co-Authored-By: Claude Opus 4.6 --- containers/agent/entrypoint.sh | 20 +++++++++++ src/docker-manager.test.ts | 63 ++++++++++++++++++++++++++++++++-- src/docker-manager.ts | 25 ++++++++++++-- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 77a25f10..464647a1 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -167,6 +167,26 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then exit 1 fi + # SECURITY: Mask sensitive /proc entries to prevent kernel info disclosure (#223) + # /proc/kallsyms exposes kernel symbol addresses (aids ASLR bypass / exploit development) + # /proc/modules lists loaded kernel modules (aids kernel exploit targeting) + mount --bind /dev/null /host/proc/kallsyms 2>/dev/null || true + mount --bind /dev/null /host/proc/modules 2>/dev/null || true + echo "[entrypoint] Masked /proc/kallsyms and /proc/modules" + + # Set up additional /dev entries in chroot (#223) + # Since /dev is selectively mounted (only null, zero, random, urandom, tty), + # we need to create pts, shm, and standard symlinks for proper operation + mkdir -p /host/dev/pts /host/dev/shm + mount -t devpts devpts /host/dev/pts 2>/dev/null || true + mount -t tmpfs tmpfs /host/dev/shm -o mode=1777 2>/dev/null || true + # Standard /dev symlinks needed by many programs + ln -sf /proc/self/fd /host/dev/fd 2>/dev/null || true + ln -sf /proc/self/fd/0 /host/dev/stdin 2>/dev/null || true + ln -sf /proc/self/fd/1 /host/dev/stdout 2>/dev/null || true + ln -sf /proc/self/fd/2 /host/dev/stderr 2>/dev/null || true + echo "[entrypoint] Set up /dev/pts, /dev/shm, and standard symlinks in chroot" + # Copy one-shot-token library to host filesystem for LD_PRELOAD in chroot # This prevents tokens from being read multiple times by malicious code # Note: /tmp is always writable in chroot mode (mounted from host /tmp as rw) diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 9ef80eff..80cb7ab8 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -552,8 +552,11 @@ describe('docker-manager', () => { // Should include blanket /:/host:rw mount expect(volumes).toContain('/:/host:rw'); - // Should NOT include /dev/null credential hiding - expect(volumes.some((v: string) => v.startsWith('/dev/null'))).toBe(false); + // Should NOT include /dev/null credential hiding (but /proc masking is still present) + expect(volumes.some((v: string) => v.includes('/dev/null') && v.includes('.docker/config.json'))).toBe(false); + // /proc masking should still be present even with full filesystem access + expect(volumes).toContain('/dev/null:/proc/kallsyms:ro'); + expect(volumes).toContain('/dev/null:/proc/modules:ro'); }); it('should use blanket mount when allowFullFilesystemAccess is true in chroot mode', () => { @@ -597,7 +600,15 @@ describe('docker-manager', () => { expect(volumes).not.toContain('/proc:/host/proc:ro'); expect(volumes).not.toContain('/proc/self:/host/proc/self:ro'); expect(volumes).toContain('/sys:/host/sys:ro'); - expect(volumes).toContain('/dev:/host/dev:ro'); + + // SECURITY: /dev is NOT blanket-mounted to prevent host block device access (#223) + // Only specific safe device nodes are exposed + expect(volumes).not.toContain('/dev:/host/dev:ro'); + expect(volumes).toContain('/dev/null:/host/dev/null:rw'); + expect(volumes).toContain('/dev/zero:/host/dev/zero:rw'); + expect(volumes).toContain('/dev/random:/host/dev/random:ro'); + expect(volumes).toContain('/dev/urandom:/host/dev/urandom:ro'); + expect(volumes).toContain('/dev/tty:/host/dev/tty:rw'); // Should include /etc subdirectories (read-only) expect(volumes).toContain('/etc/ssl:/host/etc/ssl:ro'); @@ -1093,6 +1104,52 @@ describe('docker-manager', () => { expect(agent.cpu_shares).toBe(1024); }); + it('should mask /proc/kallsyms and /proc/modules for security (#223)', () => { + const result = generateDockerCompose(mockConfig, mockNetworkConfig); + const agent = result.services.agent; + const volumes = agent.volumes as string[]; + + // SECURITY: Prevent kernel info disclosure + // /proc/kallsyms aids ASLR bypass, /proc/modules aids kernel exploit targeting + expect(volumes).toContain('/dev/null:/proc/kallsyms:ro'); + expect(volumes).toContain('/dev/null:/proc/modules:ro'); + }); + + it('should mask /proc/kallsyms and /proc/modules in chroot mode (#223)', () => { + const configWithChroot = { + ...mockConfig, + enableChroot: true + }; + const result = generateDockerCompose(configWithChroot, mockNetworkConfig); + const agent = result.services.agent; + const volumes = agent.volumes as string[]; + + // Container-level /proc masking should apply in chroot mode too (defense in depth) + // Additionally, entrypoint.sh masks /host/proc/kallsyms and /host/proc/modules + expect(volumes).toContain('/dev/null:/proc/kallsyms:ro'); + expect(volumes).toContain('/dev/null:/proc/modules:ro'); + }); + + it('should use selective /dev mounts in chroot mode instead of blanket mount (#223)', () => { + const configWithChroot = { + ...mockConfig, + enableChroot: true + }; + const result = generateDockerCompose(configWithChroot, mockNetworkConfig); + const agent = result.services.agent; + const volumes = agent.volumes as string[]; + + // Should NOT include blanket /dev mount (exposes host block devices) + expect(volumes).not.toContain('/dev:/host/dev:ro'); + + // Should include only safe device nodes + expect(volumes).toContain('/dev/null:/host/dev/null:rw'); + expect(volumes).toContain('/dev/zero:/host/dev/zero:rw'); + expect(volumes).toContain('/dev/random:/host/dev/random:ro'); + expect(volumes).toContain('/dev/urandom:/host/dev/urandom:ro'); + expect(volumes).toContain('/dev/tty:/host/dev/tty:rw'); + }); + it('should disable TTY by default to prevent ANSI escape sequences', () => { const result = generateDockerCompose(mockConfig, mockNetworkConfig); const agent = result.services.agent; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 22f517ea..0c314694 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -439,6 +439,14 @@ export function generateDockerCompose( `${config.workDir}/agent-logs:${effectiveHome}/.copilot/logs:rw`, ]; + // SECURITY: Mask sensitive /proc entries to prevent kernel info disclosure (#223) + // /proc/kallsyms exposes kernel symbol addresses (aids ASLR bypass and exploit development) + // /proc/modules lists loaded kernel modules (aids kernel exploit targeting) + agentVolumes.push( + '/dev/null:/proc/kallsyms:ro', + '/dev/null:/proc/modules:ro', + ); + // Add chroot-related volume mounts when --enable-chroot is specified // These mounts enable chroot /host to work properly for running host binaries if (config.enableChroot) { @@ -460,16 +468,29 @@ export function generateDockerCompose( // /opt/hostedtoolcache contains Python, Node, Ruby, Go, Java, etc. agentVolumes.push('/opt:/host/opt:ro'); - // Special filesystem mounts for chroot (needed for devices and runtime introspection) + // Special filesystem mounts for chroot (needed for runtime introspection) // NOTE: /proc is NOT bind-mounted here. Instead, a fresh container-scoped procfs is // mounted at /host/proc in entrypoint.sh via 'mount -t proc'. This provides: // - Dynamic /proc/self/exe (required by .NET CLR and other runtimes) // - /proc/cpuinfo, /proc/meminfo (required by JVM, .NET GC) // - Container-scoped only (does not expose host process info) // The mount requires SYS_ADMIN capability, which is dropped before user code runs. + // Additionally, /proc/kallsyms and /proc/modules are masked in entrypoint.sh. agentVolumes.push( '/sys:/host/sys:ro', // Read-only sysfs - '/dev:/host/dev:ro', // Read-only device nodes (needed by some runtimes) + ); + + // SECURITY: Selective /dev mounting to prevent host block device access (#223) + // A blanket /dev:/host/dev:ro mount exposes ALL host device nodes including + // /dev/sda*, /dev/nvme* etc. which aids disk forensics and potential data exfiltration. + // Instead, mount only the specific device nodes that runtimes need. + // Additional /dev entries (pts, shm, symlinks) are set up in entrypoint.sh. + agentVolumes.push( + '/dev/null:/host/dev/null:rw', + '/dev/zero:/host/dev/zero:rw', + '/dev/random:/host/dev/random:ro', + '/dev/urandom:/host/dev/urandom:ro', + '/dev/tty:/host/dev/tty:rw', ); // User home directory for project files and Rust/Cargo (read-write)