Skip to content
Open
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
63 changes: 63 additions & 0 deletions tests/integration/chroot-edge-cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,69 @@ describe('Chroot Edge Cases', () => {
});
});

// ---------- Batch: Container escape vector tests ----------
describe('Container Escape Prevention (batched)', () => {
let batch: BatchResults;

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' },
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
// 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' },
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
{ 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' },

Copilot uses AI. Check for mistakes.
// unshare namespace creation - requires CAP_SYS_ADMIN
{ name: 'unshare_mount', command: 'unshare --mount /bin/true 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' },
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
{ name: 'umount', command: 'umount /tmp 2>&1' },
{ name: 'umount', command: 'umount /proc 2>&1' },

Copilot uses AI. Check for mistakes.
// setuid escalation - no-new-privileges should prevent
{ name: 'no_new_privs', command: 'cat /proc/self/status | grep NoNewPrivs 2>&1' },
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
{ 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' },

Copilot uses AI. Check for mistakes.
], {
allowDomains: ['localhost'],
logLevel: 'debug',
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
timeout: 120000,
});
}, 180000);

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);
});
Comment on lines +197 to +201
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.

test('should block mount after capability drop', () => {
const r = batch.get('mount_tmpfs');
expect(r.exitCode).not.toBe(0);
expect(r.stdout).toMatch(/operation not permitted|permission denied/i);
});

test('should block unshare namespace creation', () => {
const r = batch.get('unshare_mount');
expect(r.exitCode).not.toBe(0);
expect(r.stdout).toMatch(/operation not permitted|permission denied|cannot change root/i);
});

test('should block nsenter', () => {
const r = batch.get('nsenter');
expect(r.exitCode).not.toBe(0);
expect(r.stdout).toMatch(/operation not permitted|permission denied|no such file/i);
});

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);
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);

Copilot uses AI. Check for mistakes.
});

test('should have no-new-privileges set', () => {
const r = batch.get('no_new_privs');
expect(r.exitCode).toBe(0);
// NoNewPrivs: 1 means no-new-privileges is enforced
expect(r.stdout).toMatch(/NoNewPrivs:\s*1/);
});
});

// ---------- Individual: Working directory tests (different containerWorkDir options) ----------
describe('Working Directory Handling', () => {
test('should respect container-workdir in chroot mode', async () => {
Expand Down
Loading