Skip to content

Commit 4f098f5

Browse files
Mossakaclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 999a1c8 commit 4f098f5

3 files changed

Lines changed: 103 additions & 5 deletions

File tree

containers/agent/entrypoint.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,26 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
167167
exit 1
168168
fi
169169

170+
# SECURITY: Mask sensitive /proc entries to prevent kernel info disclosure (#223)
171+
# /proc/kallsyms exposes kernel symbol addresses (aids ASLR bypass / exploit development)
172+
# /proc/modules lists loaded kernel modules (aids kernel exploit targeting)
173+
mount --bind /dev/null /host/proc/kallsyms 2>/dev/null || true
174+
mount --bind /dev/null /host/proc/modules 2>/dev/null || true
175+
echo "[entrypoint] Masked /proc/kallsyms and /proc/modules"
176+
177+
# Set up additional /dev entries in chroot (#223)
178+
# Since /dev is selectively mounted (only null, zero, random, urandom, tty),
179+
# we need to create pts, shm, and standard symlinks for proper operation
180+
mkdir -p /host/dev/pts /host/dev/shm
181+
mount -t devpts devpts /host/dev/pts 2>/dev/null || true
182+
mount -t tmpfs tmpfs /host/dev/shm -o mode=1777 2>/dev/null || true
183+
# Standard /dev symlinks needed by many programs
184+
ln -sf /proc/self/fd /host/dev/fd 2>/dev/null || true
185+
ln -sf /proc/self/fd/0 /host/dev/stdin 2>/dev/null || true
186+
ln -sf /proc/self/fd/1 /host/dev/stdout 2>/dev/null || true
187+
ln -sf /proc/self/fd/2 /host/dev/stderr 2>/dev/null || true
188+
echo "[entrypoint] Set up /dev/pts, /dev/shm, and standard symlinks in chroot"
189+
170190
# Copy one-shot-token library to host filesystem for LD_PRELOAD in chroot
171191
# This prevents tokens from being read multiple times by malicious code
172192
# Note: /tmp is always writable in chroot mode (mounted from host /tmp as rw)

src/docker-manager.test.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,8 +552,11 @@ describe('docker-manager', () => {
552552

553553
// Should include blanket /:/host:rw mount
554554
expect(volumes).toContain('/:/host:rw');
555-
// Should NOT include /dev/null credential hiding
556-
expect(volumes.some((v: string) => v.startsWith('/dev/null'))).toBe(false);
555+
// Should NOT include /dev/null credential hiding (but /proc masking is still present)
556+
expect(volumes.some((v: string) => v.includes('/dev/null') && v.includes('.docker/config.json'))).toBe(false);
557+
// /proc masking should still be present even with full filesystem access
558+
expect(volumes).toContain('/dev/null:/proc/kallsyms:ro');
559+
expect(volumes).toContain('/dev/null:/proc/modules:ro');
557560
});
558561

559562
it('should use blanket mount when allowFullFilesystemAccess is true in chroot mode', () => {
@@ -597,7 +600,15 @@ describe('docker-manager', () => {
597600
expect(volumes).not.toContain('/proc:/host/proc:ro');
598601
expect(volumes).not.toContain('/proc/self:/host/proc/self:ro');
599602
expect(volumes).toContain('/sys:/host/sys:ro');
600-
expect(volumes).toContain('/dev:/host/dev:ro');
603+
604+
// SECURITY: /dev is NOT blanket-mounted to prevent host block device access (#223)
605+
// Only specific safe device nodes are exposed
606+
expect(volumes).not.toContain('/dev:/host/dev:ro');
607+
expect(volumes).toContain('/dev/null:/host/dev/null:rw');
608+
expect(volumes).toContain('/dev/zero:/host/dev/zero:rw');
609+
expect(volumes).toContain('/dev/random:/host/dev/random:ro');
610+
expect(volumes).toContain('/dev/urandom:/host/dev/urandom:ro');
611+
expect(volumes).toContain('/dev/tty:/host/dev/tty:rw');
601612

602613
// Should include /etc subdirectories (read-only)
603614
expect(volumes).toContain('/etc/ssl:/host/etc/ssl:ro');
@@ -1093,6 +1104,52 @@ describe('docker-manager', () => {
10931104
expect(agent.cpu_shares).toBe(1024);
10941105
});
10951106

1107+
it('should mask /proc/kallsyms and /proc/modules for security (#223)', () => {
1108+
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
1109+
const agent = result.services.agent;
1110+
const volumes = agent.volumes as string[];
1111+
1112+
// SECURITY: Prevent kernel info disclosure
1113+
// /proc/kallsyms aids ASLR bypass, /proc/modules aids kernel exploit targeting
1114+
expect(volumes).toContain('/dev/null:/proc/kallsyms:ro');
1115+
expect(volumes).toContain('/dev/null:/proc/modules:ro');
1116+
});
1117+
1118+
it('should mask /proc/kallsyms and /proc/modules in chroot mode (#223)', () => {
1119+
const configWithChroot = {
1120+
...mockConfig,
1121+
enableChroot: true
1122+
};
1123+
const result = generateDockerCompose(configWithChroot, mockNetworkConfig);
1124+
const agent = result.services.agent;
1125+
const volumes = agent.volumes as string[];
1126+
1127+
// Container-level /proc masking should apply in chroot mode too (defense in depth)
1128+
// Additionally, entrypoint.sh masks /host/proc/kallsyms and /host/proc/modules
1129+
expect(volumes).toContain('/dev/null:/proc/kallsyms:ro');
1130+
expect(volumes).toContain('/dev/null:/proc/modules:ro');
1131+
});
1132+
1133+
it('should use selective /dev mounts in chroot mode instead of blanket mount (#223)', () => {
1134+
const configWithChroot = {
1135+
...mockConfig,
1136+
enableChroot: true
1137+
};
1138+
const result = generateDockerCompose(configWithChroot, mockNetworkConfig);
1139+
const agent = result.services.agent;
1140+
const volumes = agent.volumes as string[];
1141+
1142+
// Should NOT include blanket /dev mount (exposes host block devices)
1143+
expect(volumes).not.toContain('/dev:/host/dev:ro');
1144+
1145+
// Should include only safe device nodes
1146+
expect(volumes).toContain('/dev/null:/host/dev/null:rw');
1147+
expect(volumes).toContain('/dev/zero:/host/dev/zero:rw');
1148+
expect(volumes).toContain('/dev/random:/host/dev/random:ro');
1149+
expect(volumes).toContain('/dev/urandom:/host/dev/urandom:ro');
1150+
expect(volumes).toContain('/dev/tty:/host/dev/tty:rw');
1151+
});
1152+
10961153
it('should disable TTY by default to prevent ANSI escape sequences', () => {
10971154
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
10981155
const agent = result.services.agent;

src/docker-manager.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,14 @@ export function generateDockerCompose(
439439
`${config.workDir}/agent-logs:${effectiveHome}/.copilot/logs:rw`,
440440
];
441441

442+
// SECURITY: Mask sensitive /proc entries to prevent kernel info disclosure (#223)
443+
// /proc/kallsyms exposes kernel symbol addresses (aids ASLR bypass and exploit development)
444+
// /proc/modules lists loaded kernel modules (aids kernel exploit targeting)
445+
agentVolumes.push(
446+
'/dev/null:/proc/kallsyms:ro',
447+
'/dev/null:/proc/modules:ro',
448+
);
449+
442450
// Add chroot-related volume mounts when --enable-chroot is specified
443451
// These mounts enable chroot /host to work properly for running host binaries
444452
if (config.enableChroot) {
@@ -460,16 +468,29 @@ export function generateDockerCompose(
460468
// /opt/hostedtoolcache contains Python, Node, Ruby, Go, Java, etc.
461469
agentVolumes.push('/opt:/host/opt:ro');
462470

463-
// Special filesystem mounts for chroot (needed for devices and runtime introspection)
471+
// Special filesystem mounts for chroot (needed for runtime introspection)
464472
// NOTE: /proc is NOT bind-mounted here. Instead, a fresh container-scoped procfs is
465473
// mounted at /host/proc in entrypoint.sh via 'mount -t proc'. This provides:
466474
// - Dynamic /proc/self/exe (required by .NET CLR and other runtimes)
467475
// - /proc/cpuinfo, /proc/meminfo (required by JVM, .NET GC)
468476
// - Container-scoped only (does not expose host process info)
469477
// The mount requires SYS_ADMIN capability, which is dropped before user code runs.
478+
// Additionally, /proc/kallsyms and /proc/modules are masked in entrypoint.sh.
470479
agentVolumes.push(
471480
'/sys:/host/sys:ro', // Read-only sysfs
472-
'/dev:/host/dev:ro', // Read-only device nodes (needed by some runtimes)
481+
);
482+
483+
// SECURITY: Selective /dev mounting to prevent host block device access (#223)
484+
// A blanket /dev:/host/dev:ro mount exposes ALL host device nodes including
485+
// /dev/sda*, /dev/nvme* etc. which aids disk forensics and potential data exfiltration.
486+
// Instead, mount only the specific device nodes that runtimes need.
487+
// Additional /dev entries (pts, shm, symlinks) are set up in entrypoint.sh.
488+
agentVolumes.push(
489+
'/dev/null:/host/dev/null:rw',
490+
'/dev/zero:/host/dev/zero:rw',
491+
'/dev/random:/host/dev/random:ro',
492+
'/dev/urandom:/host/dev/urandom:ro',
493+
'/dev/tty:/host/dev/tty:rw',
473494
);
474495

475496
// User home directory for project files and Rust/Cargo (read-write)

0 commit comments

Comments
 (0)