Conversation
Add 6 new integration tests for container escape prevention: - pivot_root syscall blocked by seccomp profile - mount blocked after capability drop (CAP_SYS_ADMIN dropped) - unshare namespace creation blocked without CAP_SYS_ADMIN - nsenter blocked without CAP_SYS_ADMIN - umount/umount2 blocked by seccomp profile - no-new-privileges flag verified via /proc/self/status Tests are batched into a single container invocation for efficiency. Fixes #762 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (1 files)
Coverage comparison generated by |
There was a problem hiding this comment.
Pull request overview
Adds a new batched integration test suite to validate common container/chroot escape prevention mechanisms in a single container invocation.
Changes:
- Adds 6 integration tests covering
pivot_root,mount,unshare,nsenter,umount, andNoNewPrivs. - Executes the checks via a single
runBatch(...)call to reduce container startup overhead. - Asserts failure modes and output patterns for each escape vector.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // pivot_root - blocked in seccomp profile | ||
| { name: 'pivot_root', command: 'mkdir -p /tmp/newroot /tmp/putold && pivot_root /tmp/newroot /tmp/putold 2>&1 || unshare --mount pivot_root /tmp/newroot /tmp/putold 2>&1' }, | ||
| // mount after capability drop - mount syscall allowed in seccomp but CAP_SYS_ADMIN should be dropped | ||
| { name: 'mount_tmpfs', command: 'mount -t tmpfs tmpfs /tmp/test-mount-$$ 2>&1' }, |
There was a problem hiding this comment.
This mount is likely to fail because the mountpoint /tmp/test-mount-$$ is not created first. That can produce a false positive (test passes even if CAP_SYS_ADMIN were not dropped) with errors like 'mount point does not exist'. Create the directory in the command (e.g., mkdir -p ... && mount ...) so the failure is actually attributable to the intended protection.
| { name: 'mount_tmpfs', command: 'mount -t tmpfs tmpfs /tmp/test-mount-$$ 2>&1' }, | |
| { name: 'mount_tmpfs', command: 'mkdir -p /tmp/test-mount-$$ && mount -t tmpfs tmpfs /tmp/test-mount-$$ 2>&1' }, |
| // nsenter - requires CAP_SYS_ADMIN | ||
| { name: 'nsenter', command: 'nsenter --mount --target 1 /bin/true 2>&1' }, | ||
| // umount - blocked in seccomp profile | ||
| { name: 'umount', command: 'umount /tmp 2>&1' }, |
There was a problem hiding this comment.
umount /tmp is often not a mount point, so it may fail with messages like 'not mounted' or 'invalid argument' rather than a permission/seccomp-related error. This can make the test flaky or fail for the wrong reason. Consider targeting a path that is reliably a mount point inside the container (or adjust the expected output regex to include common non-permission failures if that's acceptable for this check).
| { name: 'umount', command: 'umount /tmp 2>&1' }, | |
| { name: 'umount', command: 'umount /proc 2>&1' }, |
| test('should block umount/umount2', () => { | ||
| const r = batch.get('umount'); | ||
| expect(r.exitCode).not.toBe(0); | ||
| expect(r.stdout).toMatch(/operation not permitted|permission denied|not permitted/i); |
There was a problem hiding this comment.
umount /tmp is often not a mount point, so it may fail with messages like 'not mounted' or 'invalid argument' rather than a permission/seccomp-related error. This can make the test flaky or fail for the wrong reason. Consider targeting a path that is reliably a mount point inside the container (or adjust the expected output regex to include common non-permission failures if that's acceptable for this check).
| expect(r.stdout).toMatch(/operation not permitted|permission denied|not permitted/i); | |
| expect(r.stdout).toMatch(/operation not permitted|permission denied|not permitted|not mounted|invalid argument/i); |
| beforeAll(async () => { | ||
| batch = await runBatch(runner, [ | ||
| // pivot_root - blocked in seccomp profile | ||
| { name: 'pivot_root', command: 'mkdir -p /tmp/newroot /tmp/putold && pivot_root /tmp/newroot /tmp/putold 2>&1 || unshare --mount pivot_root /tmp/newroot /tmp/putold 2>&1' }, |
There was a problem hiding this comment.
This command doesn’t reliably exercise pivot_root as an escape vector: (1) pivot_root requires specific preconditions (new root typically needs to be a mount point), so it can fail with EINVAL (invalid argument) even when protections are not the cause; and (2) the || unshare --mount pivot_root ... fallback further mixes failure modes. As written, the test can pass due to invalid setup rather than seccomp/capability enforcement. To make this a true protection test, set up the required preconditions (so it would succeed in an unsafe configuration), then assert on a failure mode that maps to the intended protection (or adjust comments/assertions to avoid claiming seccomp specifically).
| test('should block pivot_root syscall', () => { | ||
| const r = batch.get('pivot_root'); | ||
| expect(r.exitCode).not.toBe(0); | ||
| expect(r.stdout).toMatch(/operation not permitted|permission denied|invalid argument|no such file/i); | ||
| }); |
There was a problem hiding this comment.
This command doesn’t reliably exercise pivot_root as an escape vector: (1) pivot_root requires specific preconditions (new root typically needs to be a mount point), so it can fail with EINVAL (invalid argument) even when protections are not the cause; and (2) the || unshare --mount pivot_root ... fallback further mixes failure modes. As written, the test can pass due to invalid setup rather than seccomp/capability enforcement. To make this a true protection test, set up the required preconditions (so it would succeed in an unsafe configuration), then assert on a failure mode that maps to the intended protection (or adjust comments/assertions to avoid claiming seccomp specifically).
| // umount - blocked in seccomp profile | ||
| { name: 'umount', command: 'umount /tmp 2>&1' }, | ||
| // setuid escalation - no-new-privileges should prevent | ||
| { name: 'no_new_privs', command: 'cat /proc/self/status | grep NoNewPrivs 2>&1' }, |
There was a problem hiding this comment.
Prefer avoiding cat | grep here; it adds an unnecessary process and can be simplified to a single grep reading the file directly (and optionally anchor the match to reduce accidental matches). This also makes the command’s exit code semantics clearer.
| { name: 'no_new_privs', command: 'cat /proc/self/status | grep NoNewPrivs 2>&1' }, | |
| { name: 'no_new_privs', command: 'grep "^NoNewPrivs:" /proc/self/status 2>&1' }, |
| { name: 'no_new_privs', command: 'cat /proc/self/status | grep NoNewPrivs 2>&1' }, | ||
| ], { | ||
| allowDomains: ['localhost'], | ||
| logLevel: 'debug', |
There was a problem hiding this comment.
Setting logLevel: 'debug' in integration tests can significantly increase CI noise and make failures harder to scan. Consider using the default log level (or only enabling debug logging when a test fails / via an env flag) so routine runs stay quiet.
🦀 Rust Build Test Results
Overall: ✅ PASS
|
|
Smoke Test Results ✅ GitHub MCP: #1159 fix(security): eliminate TOCTOU race conditions in ssl-bump.ts, #1158 fix(security): stop logging partial token values Overall: PASS
|
Build Test: Bun
Overall: PASS ✅ Bun version: 1.3.10
|
Build Test: Deno Results
Overall: ✅ PASS Deno version: 2.7.4
|
Java Build Test Results
Overall: PASS ✅ All projects compiled and all tests passed successfully.
|
Build Test: Node.js Results
Overall: ✅ PASS
|
|
PR titles: fix(security): eliminate TOCTOU race conditions in ssl-bump.ts | fix(security): stop logging partial token values
|
Go Build Test Results ✅
Overall: PASS
|
C++ Build Test Results
Overall: PASS ✅ All C++ projects configured and built successfully with GCC 13.3.0.
|
Smoke Test Results ✅ PASSLast 2 merged PRs:
Overall: PASS —
|
.NET Build Test Results
Overall: PASS Run outputhello-world: json-parse: {
"Name": "AWF Test",
"Version": 1,
"Success": true
}
Name: AWF Test, Success: True
|
Summary
Fixes #762
Test Coverage Added
pivot_rootmount -t tmpfsunshare --mountnsenterumountno-new-privilegesflag verifiedTest plan
🤖 Generated with Claude Code