Skip to content

Commit 71c996b

Browse files
Mossakaclaude
andauthored
test: add Java and .NET chroot integration tests (#569)
* test: add Java and .NET chroot integration tests Validates the procfs fix (dda7c67) that replaced the static /proc/self bind mount with a dynamic `mount -t proc`, unblocking .NET CLR and JVM runtimes that read /proc/self/exe for binary introspection. Changes: - Add DOTNET_ROOT to criticalEnvVars in awf-runner.ts so it survives sudo - Add actions/setup-java and actions/setup-dotnet to test-chroot.yml - Add Java language tests: version check, compile+run Hello World, stdlib - Add .NET language tests: version check, dotnet --info, create+run app - Add .NET package manager tests: list SDKs/runtimes, NuGet restore, blocked-domain test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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> * fix: relax procfs test assertion to handle debug output in stdout The stdout from awf includes [entrypoint] debug log lines when logLevel is 'debug'. The /proc/self/exe test was asserting the entire trimmed stdout starts with '/', but it starts with debug output. Match for known binary paths instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add external package to NuGet blocked-domain test A bare 'dotnet restore' on a default console project succeeds from the local SDK cache without hitting NuGet. Adding Newtonsoft.Json as an external dependency forces a network fetch, which correctly fails when NuGet domains are not whitelisted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e147717 commit 71c996b

5 files changed

Lines changed: 486 additions & 3 deletions

File tree

.github/workflows/test-chroot.yml

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,38 @@ jobs:
3636
with:
3737
go-version: '1.22'
3838

39-
- name: Capture GOROOT for chroot tests
40-
id: go-env
39+
- name: Setup Java
40+
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
41+
with:
42+
distribution: 'temurin'
43+
java-version: '21'
44+
45+
- name: Setup .NET
46+
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
47+
with:
48+
dotnet-version: '8.0'
49+
50+
- name: Capture tool paths for chroot tests
51+
id: tool-paths
4152
run: |
4253
# Go on GitHub Actions uses trimmed binaries that require GOROOT
43-
# Capture it here so we can pass it to chroot tests
4454
GOROOT_VALUE=$(go env GOROOT)
4555
echo "GOROOT=${GOROOT_VALUE}" >> $GITHUB_OUTPUT
4656
echo "GOROOT=${GOROOT_VALUE}" >> $GITHUB_ENV
4757
echo "Captured GOROOT: ${GOROOT_VALUE}"
4858
59+
# Java: JAVA_HOME is needed so entrypoint can add $JAVA_HOME/bin to PATH
60+
if [ -n "$JAVA_HOME" ]; then
61+
echo "JAVA_HOME=${JAVA_HOME}" >> $GITHUB_ENV
62+
echo "Captured JAVA_HOME: ${JAVA_HOME}"
63+
fi
64+
65+
# .NET: DOTNET_ROOT is needed so entrypoint can add to PATH and set DOTNET_ROOT
66+
if [ -n "$DOTNET_ROOT" ]; then
67+
echo "DOTNET_ROOT=${DOTNET_ROOT}" >> $GITHUB_ENV
68+
echo "Captured DOTNET_ROOT: ${DOTNET_ROOT}"
69+
fi
70+
4971
- name: Verify host tools are available
5072
run: |
5173
echo "=== Verifying host tools ==="
@@ -55,6 +77,10 @@ jobs:
5577
echo "pip: $(pip3 --version)"
5678
echo "Go: $(go version)"
5779
echo "GOROOT: $GOROOT"
80+
echo "Java: $(java --version 2>&1 | head -1)"
81+
echo "JAVA_HOME: $JAVA_HOME"
82+
echo "dotnet: $(dotnet --version 2>&1)"
83+
echo "DOTNET_ROOT: $DOTNET_ROOT"
5884
echo "Git: $(git --version)"
5985
echo "curl: $(curl --version | head -1)"
6086
@@ -138,6 +164,11 @@ jobs:
138164
distribution: 'temurin'
139165
java-version: '21'
140166

167+
- name: Setup .NET
168+
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
169+
with:
170+
dotnet-version: '8.0'
171+
141172
- name: Capture tool paths for chroot tests
142173
id: tool-paths
143174
run: |
@@ -162,6 +193,12 @@ jobs:
162193
echo "Captured JAVA_HOME: ${JAVA_HOME}"
163194
fi
164195
196+
# .NET: DOTNET_ROOT is needed so entrypoint can add to PATH and set DOTNET_ROOT
197+
if [ -n "$DOTNET_ROOT" ]; then
198+
echo "DOTNET_ROOT=${DOTNET_ROOT}" >> $GITHUB_ENV
199+
echo "Captured DOTNET_ROOT: ${DOTNET_ROOT}"
200+
fi
201+
165202
- name: Verify host tools are available
166203
run: |
167204
echo "=== Verifying host tools ==="
@@ -178,6 +215,8 @@ jobs:
178215
echo "CARGO_HOME: $CARGO_HOME"
179216
echo "Java: $(java --version 2>&1 | head -1)"
180217
echo "JAVA_HOME: $JAVA_HOME"
218+
echo "dotnet: $(dotnet --version 2>&1)"
219+
echo "DOTNET_ROOT: $DOTNET_ROOT"
181220
182221
- name: Install dependencies
183222
run: npm ci
@@ -219,6 +258,80 @@ jobs:
219258
ls -la /tmp/awf-* 2>/dev/null || true
220259
sudo cat /tmp/awf-*/squid-logs/access.log 2>/dev/null || true
221260
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+
222335
test-chroot-edge-cases:
223336
name: Test Chroot Edge Cases
224337
runs-on: ubuntu-latest

tests/fixtures/awf-runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export class AwfRunner {
182182
'GOROOT',
183183
'CARGO_HOME',
184184
'JAVA_HOME',
185+
'DOTNET_ROOT',
185186
].filter(v => process.env[v]);
186187

187188
if (criticalEnvVars.length > 0) {

tests/integration/chroot-languages.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,117 @@ describe('Chroot Language Support', () => {
177177
}, 120000);
178178
});
179179

180+
describe('Java', () => {
181+
test('should execute java --version from host via chroot', async () => {
182+
// Validates JVM starts correctly - before procfs fix, JVM would fail
183+
// because /proc/self/exe resolved to bash instead of the java binary
184+
const result = await runner.runWithSudo('java --version 2>&1 || java -version 2>&1', {
185+
allowDomains: ['github.com'],
186+
logLevel: 'debug',
187+
timeout: 60000,
188+
enableChroot: true,
189+
});
190+
191+
expect(result).toSucceed();
192+
expect(result.stdout + result.stderr).toMatch(/openjdk|java|version/i);
193+
}, 120000);
194+
195+
test('should compile and run Java Hello World', async () => {
196+
// Full javac + java toolchain validation through chroot
197+
const result = await runner.runWithSudo(
198+
'TESTDIR=$(mktemp -d) && ' +
199+
'echo \'public class Hello { public static void main(String[] args) { System.out.println("Hello from Java"); } }\' > $TESTDIR/Hello.java && ' +
200+
'cd $TESTDIR && javac Hello.java && java Hello && rm -rf $TESTDIR',
201+
{
202+
allowDomains: ['github.com'],
203+
logLevel: 'debug',
204+
timeout: 120000,
205+
enableChroot: true,
206+
}
207+
);
208+
209+
expect(result).toSucceed();
210+
expect(result.stdout).toContain('Hello from Java');
211+
}, 180000);
212+
213+
test('should access Java standard library (java.util)', async () => {
214+
// Validates JVM class loading works beyond trivial hello world
215+
const result = await runner.runWithSudo(
216+
'TESTDIR=$(mktemp -d) && ' +
217+
'cat > $TESTDIR/TestUtil.java << \'EOF\'\n' +
218+
'import java.util.Arrays;\n' +
219+
'import java.util.List;\n' +
220+
'public class TestUtil {\n' +
221+
' public static void main(String[] args) {\n' +
222+
' List<String> items = Arrays.asList("a", "b", "c");\n' +
223+
' System.out.println("List size: " + items.size());\n' +
224+
' }\n' +
225+
'}\n' +
226+
'EOF\n' +
227+
'cd $TESTDIR && javac TestUtil.java && java TestUtil && rm -rf $TESTDIR',
228+
{
229+
allowDomains: ['github.com'],
230+
logLevel: 'debug',
231+
timeout: 120000,
232+
enableChroot: true,
233+
}
234+
);
235+
236+
if (result.success) {
237+
expect(result.stdout).toContain('List size: 3');
238+
}
239+
}, 180000);
240+
});
241+
242+
describe('.NET', () => {
243+
test('should execute dotnet --version from host via chroot', async () => {
244+
// Primary regression test for the /proc/self/exe fix.
245+
// Before the fix, .NET CLR failed with "Cannot execute dotnet when renamed to bash"
246+
const result = await runner.runWithSudo('dotnet --version', {
247+
allowDomains: ['github.com'],
248+
logLevel: 'debug',
249+
timeout: 60000,
250+
enableChroot: true,
251+
});
252+
253+
expect(result).toSucceed();
254+
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
255+
}, 120000);
256+
257+
test('should show dotnet runtime information', async () => {
258+
const result = await runner.runWithSudo('dotnet --info 2>&1 | head -30', {
259+
allowDomains: ['github.com'],
260+
logLevel: 'debug',
261+
timeout: 60000,
262+
enableChroot: true,
263+
});
264+
265+
expect(result).toSucceed();
266+
expect(result.stdout + result.stderr).toMatch(/\.NET|SDK|Runtime/i);
267+
}, 120000);
268+
269+
test('should create and run a .NET console app', async () => {
270+
// Full toolchain test: project creation, NuGet restore, build, and run
271+
const result = await runner.runWithSudo(
272+
'TESTDIR=$(mktemp -d) && cd $TESTDIR && ' +
273+
'dotnet new console -o testapp --no-restore && ' +
274+
'cd testapp && dotnet restore && dotnet run && ' +
275+
'rm -rf $TESTDIR',
276+
{
277+
allowDomains: ['api.nuget.org', 'nuget.org', 'dotnetcli.azureedge.net'],
278+
logLevel: 'debug',
279+
timeout: 180000,
280+
enableChroot: true,
281+
}
282+
);
283+
284+
// May fail if NuGet connectivity varies in CI
285+
if (result.success) {
286+
expect(result.stdout).toContain('Hello, World!');
287+
}
288+
}, 240000);
289+
});
290+
180291
describe('Basic System Binaries', () => {
181292
test('should access standard Unix utilities', async () => {
182293
const result = await runner.runWithSudo(

tests/integration/chroot-package-managers.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,74 @@ describe('Chroot Package Manager Support', () => {
185185
}, 120000);
186186
});
187187

188+
describe('.NET (dotnet/nuget)', () => {
189+
test('should list installed .NET SDKs (offline)', async () => {
190+
const result = await runner.runWithSudo('dotnet --list-sdks', {
191+
allowDomains: ['localhost'],
192+
logLevel: 'debug',
193+
timeout: 60000,
194+
enableChroot: true,
195+
});
196+
197+
expect(result).toSucceed();
198+
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
199+
}, 120000);
200+
201+
test('should list installed .NET runtimes (offline)', async () => {
202+
const result = await runner.runWithSudo('dotnet --list-runtimes', {
203+
allowDomains: ['localhost'],
204+
logLevel: 'debug',
205+
timeout: 60000,
206+
enableChroot: true,
207+
});
208+
209+
expect(result).toSucceed();
210+
expect(result.stdout).toMatch(/Microsoft\.\w+/);
211+
}, 120000);
212+
213+
test('should create and build a .NET project with NuGet restore', async () => {
214+
// Tests NuGet package restore through the firewall
215+
const result = await runner.runWithSudo(
216+
'TESTDIR=$(mktemp -d) && cd $TESTDIR && ' +
217+
'dotnet new console -o buildtest --no-restore && ' +
218+
'cd buildtest && dotnet restore && dotnet build --no-restore && ' +
219+
'rm -rf $TESTDIR',
220+
{
221+
allowDomains: ['api.nuget.org', 'nuget.org', 'dotnetcli.azureedge.net'],
222+
logLevel: 'debug',
223+
timeout: 180000,
224+
enableChroot: true,
225+
}
226+
);
227+
228+
if (result.success) {
229+
expect(result.stdout + result.stderr).toMatch(/Build succeeded/i);
230+
}
231+
}, 240000);
232+
233+
test('should be blocked from NuGet without domain whitelisting', async () => {
234+
// A bare 'dotnet restore' on a default console project may succeed from
235+
// the local SDK cache. Adding an external package forces a network fetch.
236+
const result = await runner.runWithSudo(
237+
'TESTDIR=$(mktemp -d) && cd $TESTDIR && ' +
238+
'dotnet new console -o blocktest --no-restore 2>&1 && ' +
239+
'cd blocktest && ' +
240+
'dotnet add package Newtonsoft.Json --no-restore 2>&1 && ' +
241+
'dotnet restore 2>&1; ' +
242+
'EXIT=$?; rm -rf $TESTDIR; exit $EXIT',
243+
{
244+
allowDomains: ['localhost'],
245+
logLevel: 'debug',
246+
timeout: 90000,
247+
enableChroot: true,
248+
}
249+
);
250+
251+
// dotnet restore should fail because NuGet API is not allowed
252+
expect(result).toFail();
253+
}, 150000);
254+
});
255+
188256
describe('Ruby (gem/bundler)', () => {
189257
test('should execute ruby from host via chroot', async () => {
190258
const result = await runner.runWithSudo('ruby --version', {

0 commit comments

Comments
 (0)