Skip to content

Commit 011bf69

Browse files
Mossakaclaude
andcommitted
test: add /proc filesystem correctness tests for chroot mode
Adds a dedicated test suite validating the dynamic procfs mount: - /proc/self/exe resolves differently for different binaries - /proc/cpuinfo, /proc/meminfo, /proc/self/status are accessible - Java program reads /proc/self/exe and verifies it contains "java" - JVM Runtime.availableProcessors() returns correct CPU count These are the core regression tests for the procfs fix (dda7c67), sourced from independent TDD test design. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3a82596 commit 011bf69

2 files changed

Lines changed: 264 additions & 0 deletions

File tree

.github/workflows/test-chroot.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,80 @@ jobs:
258258
ls -la /tmp/awf-* 2>/dev/null || true
259259
sudo cat /tmp/awf-*/squid-logs/access.log 2>/dev/null || true
260260
261+
test-chroot-procfs:
262+
name: Test Chroot /proc Filesystem
263+
runs-on: ubuntu-latest
264+
timeout-minutes: 30
265+
needs: test-chroot-languages # Run after language tests pass
266+
267+
steps:
268+
- name: Checkout repository
269+
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
270+
271+
- name: Setup Node.js
272+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
273+
with:
274+
node-version: '22'
275+
cache: 'npm'
276+
277+
- name: Setup Python
278+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
279+
with:
280+
python-version: '3.12'
281+
282+
- name: Setup Java
283+
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
284+
with:
285+
distribution: 'temurin'
286+
java-version: '21'
287+
288+
- name: Capture tool paths for chroot tests
289+
run: |
290+
if [ -n "$JAVA_HOME" ]; then
291+
echo "JAVA_HOME=${JAVA_HOME}" >> $GITHUB_ENV
292+
echo "Captured JAVA_HOME: ${JAVA_HOME}"
293+
fi
294+
295+
- name: Install dependencies
296+
run: npm ci
297+
298+
- name: Build project
299+
run: npm run build
300+
301+
- name: Build local containers
302+
run: |
303+
echo "=== Building local containers ==="
304+
docker build -t ghcr.io/github/gh-aw-firewall/squid:latest containers/squid/
305+
docker build -t ghcr.io/github/gh-aw-firewall/agent:latest containers/agent/
306+
307+
- name: Pre-test cleanup
308+
run: |
309+
echo "=== Pre-test cleanup ==="
310+
./scripts/ci/cleanup.sh || true
311+
312+
- name: Run chroot procfs tests
313+
run: |
314+
echo "=== Running chroot procfs tests ==="
315+
npm run test:integration -- --testPathPattern="chroot-procfs" --verbose
316+
env:
317+
JEST_TIMEOUT: 180000
318+
319+
- name: Post-test cleanup
320+
if: always()
321+
run: |
322+
echo "=== Post-test cleanup ==="
323+
./scripts/ci/cleanup.sh || true
324+
325+
- name: Collect logs on failure
326+
if: failure()
327+
run: |
328+
echo "=== Collecting failure logs ==="
329+
docker ps -a || true
330+
docker logs awf-squid 2>&1 || true
331+
docker logs awf-agent 2>&1 || true
332+
ls -la /tmp/awf-* 2>/dev/null || true
333+
sudo cat /tmp/awf-*/squid-logs/access.log 2>/dev/null || true
334+
261335
test-chroot-edge-cases:
262336
name: Test Chroot Edge Cases
263337
runs-on: ubuntu-latest
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* Chroot /proc Filesystem Tests
3+
*
4+
* These tests verify that the dynamic procfs mount in chroot mode provides
5+
* correct per-process /proc/self/exe resolution. This is the core regression
6+
* test for the fix in commit dda7c67, which replaced a static /proc/self
7+
* bind mount (always resolving to bash) with a dynamic mount -t proc.
8+
*
9+
* Without this fix:
10+
* - .NET CLR fails with "Cannot execute dotnet when renamed to bash"
11+
* - JVM misreads /proc/self/exe and /proc/cpuinfo
12+
* - Rustup proxy binaries appear as bash
13+
*/
14+
15+
/// <reference path="../jest-custom-matchers.d.ts" />
16+
17+
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
18+
import { createRunner, AwfRunner } from '../fixtures/awf-runner';
19+
import { cleanup } from '../fixtures/cleanup';
20+
21+
describe('Chroot /proc Filesystem Correctness', () => {
22+
let runner: AwfRunner;
23+
24+
beforeAll(async () => {
25+
await cleanup(false);
26+
runner = createRunner();
27+
});
28+
29+
afterAll(async () => {
30+
await cleanup(false);
31+
});
32+
33+
describe('/proc/self/exe resolution', () => {
34+
test('should resolve /proc/self/exe to a real path', async () => {
35+
const result = await runner.runWithSudo(
36+
'readlink /proc/self/exe',
37+
{
38+
allowDomains: ['localhost'],
39+
logLevel: 'debug',
40+
timeout: 60000,
41+
enableChroot: true,
42+
}
43+
);
44+
45+
expect(result).toSucceed();
46+
// Should resolve to an absolute path
47+
expect(result.stdout.trim()).toMatch(/^\//);
48+
}, 120000);
49+
50+
test('should resolve differently for different binaries', async () => {
51+
// The key property of the dynamic procfs mount: each process sees
52+
// its own /proc/self/exe. With the old static bind mount, all
53+
// processes would see the parent bash process.
54+
const result = await runner.runWithSudo(
55+
'bash -c "readlink /proc/self/exe" && python3 -c "import os; print(os.readlink(\'/proc/self/exe\'))"',
56+
{
57+
allowDomains: ['localhost'],
58+
logLevel: 'debug',
59+
timeout: 60000,
60+
enableChroot: true,
61+
}
62+
);
63+
64+
expect(result).toSucceed();
65+
const lines = result.stdout.trim().split('\n').filter(l => l.startsWith('/'));
66+
// bash and python should resolve to different binaries
67+
if (lines.length >= 2) {
68+
expect(lines[0]).not.toEqual(lines[lines.length - 1]);
69+
}
70+
}, 120000);
71+
});
72+
73+
describe('/proc filesystem entries', () => {
74+
test('should have /proc/cpuinfo accessible', async () => {
75+
// JVM reads /proc/cpuinfo for hardware detection
76+
const result = await runner.runWithSudo(
77+
'cat /proc/cpuinfo | head -10',
78+
{
79+
allowDomains: ['localhost'],
80+
logLevel: 'debug',
81+
timeout: 60000,
82+
enableChroot: true,
83+
}
84+
);
85+
86+
expect(result).toSucceed();
87+
expect(result.stdout).toMatch(/processor|model name|cpu|vendor/i);
88+
}, 120000);
89+
90+
test('should have /proc/meminfo accessible', async () => {
91+
// JVM uses /proc/meminfo for memory detection and heap sizing
92+
const result = await runner.runWithSudo(
93+
'cat /proc/meminfo | head -5',
94+
{
95+
allowDomains: ['localhost'],
96+
logLevel: 'debug',
97+
timeout: 60000,
98+
enableChroot: true,
99+
}
100+
);
101+
102+
expect(result).toSucceed();
103+
expect(result.stdout).toMatch(/MemTotal/);
104+
}, 120000);
105+
106+
test('should have /proc/self/status accessible', async () => {
107+
const result = await runner.runWithSudo(
108+
'cat /proc/self/status | head -5',
109+
{
110+
allowDomains: ['localhost'],
111+
logLevel: 'debug',
112+
timeout: 60000,
113+
enableChroot: true,
114+
}
115+
);
116+
117+
expect(result).toSucceed();
118+
expect(result.stdout).toMatch(/Name:/);
119+
}, 120000);
120+
});
121+
122+
describe('Java /proc/self/exe verification', () => {
123+
test('should read /proc/self/exe as java binary from JVM code', async () => {
124+
// Java program directly reads /proc/self/exe and verifies it
125+
// contains "java" not "bash" - the exact bug the procfs fix addresses
126+
const result = await runner.runWithSudo(
127+
'TESTDIR=$(mktemp -d) && ' +
128+
'cat > $TESTDIR/ProcSelf.java << \'EOF\'\n' +
129+
'import java.nio.file.Files;\n' +
130+
'import java.nio.file.Paths;\n' +
131+
'public class ProcSelf {\n' +
132+
' public static void main(String[] args) throws Exception {\n' +
133+
' String exe = Files.readSymbolicLink(Paths.get("/proc/self/exe")).toString();\n' +
134+
' System.out.println("proc_self_exe=" + exe);\n' +
135+
' if (exe.contains("java")) {\n' +
136+
' System.out.println("CORRECT: /proc/self/exe points to java");\n' +
137+
' } else {\n' +
138+
' System.out.println("UNEXPECTED: /proc/self/exe points to " + exe);\n' +
139+
' }\n' +
140+
' }\n' +
141+
'}\n' +
142+
'EOF\n' +
143+
'cd $TESTDIR && javac ProcSelf.java && java ProcSelf && rm -rf $TESTDIR',
144+
{
145+
allowDomains: ['localhost'],
146+
logLevel: 'debug',
147+
timeout: 120000,
148+
enableChroot: true,
149+
}
150+
);
151+
152+
if (result.success) {
153+
expect(result.stdout).toContain('proc_self_exe=');
154+
expect(result.stdout).toContain('CORRECT: /proc/self/exe points to java');
155+
}
156+
}, 180000);
157+
158+
test('should report correct available processors from JVM', async () => {
159+
// JVM Runtime.availableProcessors() uses /proc/cpuinfo internally
160+
const result = await runner.runWithSudo(
161+
'TESTDIR=$(mktemp -d) && ' +
162+
'cat > $TESTDIR/MemInfo.java << \'EOF\'\n' +
163+
'public class MemInfo {\n' +
164+
' public static void main(String[] args) {\n' +
165+
' Runtime rt = Runtime.getRuntime();\n' +
166+
' System.out.println("availableProcessors=" + rt.availableProcessors());\n' +
167+
' System.out.println("maxMemory=" + rt.maxMemory());\n' +
168+
' }\n' +
169+
'}\n' +
170+
'EOF\n' +
171+
'cd $TESTDIR && javac MemInfo.java && java MemInfo && rm -rf $TESTDIR',
172+
{
173+
allowDomains: ['localhost'],
174+
logLevel: 'debug',
175+
timeout: 120000,
176+
enableChroot: true,
177+
}
178+
);
179+
180+
if (result.success) {
181+
expect(result.stdout).toMatch(/availableProcessors=\d+/);
182+
expect(result.stdout).toMatch(/maxMemory=\d+/);
183+
const match = result.stdout.match(/availableProcessors=(\d+)/);
184+
if (match) {
185+
expect(parseInt(match[1])).toBeGreaterThanOrEqual(1);
186+
}
187+
}
188+
}, 180000);
189+
});
190+
});

0 commit comments

Comments
 (0)