diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index fad43378..62759734 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1387,7 +1387,8 @@ describe('docker-manager', () => { tmpfs.forEach((mount: string) => { expect(mount).toContain('noexec'); expect(mount).toContain('nosuid'); - expect(mount).toContain('size=1m'); + // Each mount must have a size limit (value varies: 1m for secrets, 65536k for /dev/shm) + expect(mount).toMatch(/size=\d+[mk]/); }); }); @@ -1409,18 +1410,20 @@ describe('docker-manager', () => { } }); - it('should include exactly 4 tmpfs mounts (mcp-logs + workDir, both normal and /host)', () => { + it('should include exactly 5 tmpfs mounts (mcp-logs + workDir both normal and /host, plus /host/dev/shm)', () => { const result = generateDockerCompose(mockConfig, mockNetworkConfig); const agent = result.services.agent; const tmpfs = agent.tmpfs as string[]; - expect(tmpfs).toHaveLength(4); + expect(tmpfs).toHaveLength(5); // Normal paths expect(tmpfs.some((t: string) => t.includes('/tmp/gh-aw/mcp-logs:'))).toBe(true); expect(tmpfs.some((t: string) => t.startsWith(`${mockConfig.workDir}:`))).toBe(true); // /host-prefixed paths (chroot always on) expect(tmpfs.some((t: string) => t.includes('/host/tmp/gh-aw/mcp-logs:'))).toBe(true); expect(tmpfs.some((t: string) => t.startsWith(`/host${mockConfig.workDir}:`))).toBe(true); + // Writable /dev/shm for POSIX semaphores (chroot makes /host/dev read-only) + expect(tmpfs.some((t: string) => t.startsWith('/host/dev/shm:'))).toBe(true); }); }); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 299b4128..4fa8c8ff 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -833,11 +833,20 @@ export function generateDockerCompose( // // Hide both normal and /host-prefixed paths since /tmp is mounted at both // /tmp and /host/tmp in chroot mode (which is always on) + // + // /host/dev/shm: /dev is bind-mounted read-only (/dev:/host/dev:ro), which makes + // /dev/shm read-only after chroot /host. POSIX semaphores and shared memory + // (used by python/black's blackd server and other tools) require a writable /dev/shm. + // A tmpfs overlay at /host/dev/shm provides a writable, isolated in-memory filesystem. + // Security: Docker containers use their own IPC namespace (no --ipc=host), so shared + // memory is fully isolated from the host and other containers. Size is capped at 64MB + // (Docker's default). noexec and nosuid flags restrict abuse vectors. tmpfs: [ '/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m', '/host/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m', `${config.workDir}:rw,noexec,nosuid,size=1m`, `/host${config.workDir}:rw,noexec,nosuid,size=1m`, + '/host/dev/shm:rw,noexec,nosuid,nodev,size=65536k', ], depends_on: { 'squid-proxy': {