From 7c69ff3e1413d4956f583b8df0c1f9c630787f15 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Wed, 22 Jul 2026 16:02:23 +0200 Subject: [PATCH 1/7] fix(parity): contain fork-bombing tests in the node-suite harness Two cluster/setup tests (validation-exec-args, validation-serialization- inspect) re-exec themselves recursively under Perry. The harness's 10s timeout only kills the direct child, so orphaned workers kept forking until the macOS per-user process cap saturated and killed the suite itself mid-run (twice), leaving ~1500 orphans behind. Containment, two layers: - Run each perry test binary under its own fork budget (current user procs + 50 via a /bin/sh ulimit wrapper): legit multi-process tests fit easily, a fork bomb stalls at ~50 orphans instead of starving the harness out of fork(). - Reap orphans after every test with pkill scoped to this run's PARITY_TMP dir, so concurrent suite runs are unaffected. A global ulimit around the whole suite does not work: the harness inherits it, the bomb pins the process count at the cap, and the cleanup pkill itself can no longer fork. --- run_parity_tests.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/run_parity_tests.sh b/run_parity_tests.sh index 08f46b2906..d1db4cabe4 100755 --- a/run_parity_tests.sh +++ b/run_parity_tests.sh @@ -751,8 +751,20 @@ for test_file in "${TEST_FILES[@]}"; do # up on the 2>&1-captured stream for any test that exercises a flagged # API (dns/dgram loopback, v8 heap snapshot, …) and diff against Node. perry_tmp=$(mktemp) - run_with_timeout 10 env PERRY_STUB_DIAG=off "${parity_env[@]}" "$perry_binary" "${test_argv[@]}" > "$perry_tmp" 2>&1 + # Cap the test binary's fork budget at current-user-procs + 50: legit + # multi-process tests (cluster/child_process) fit easily, while a + # fork-bombing test (cluster re-exec loop, 2026-07-22) stalls at ~50 + # orphans instead of saturating the user process limit — which would + # starve the harness itself out of fork() and kill the whole run. + run_with_timeout 10 /bin/sh -c ' + ulimit -u $(( $(ps -u "$(id -u)" -o pid= | wc -l) + 50 )) 2>/dev/null + exec "$@"' _ env PERRY_STUB_DIAG=off "${parity_env[@]}" "$perry_binary" "${test_argv[@]}" > "$perry_tmp" 2>&1 perry_exit=$? + # Reap orphaned children of the test binary (cluster tests fork workers + # that survive the timeout kill of the direct child and can fork-bomb the + # machine — 2026-07-22). Scoped to this run's tmp dir, so concurrent + # suite runs are unaffected. + pkill -9 -f "$PARITY_TMP/" 2>/dev/null perry_output=$(cap_output < "$perry_tmp") rm -f "$perry_tmp" From 80486862bfe514e25175927f946d983b88991cb7 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Wed, 22 Jul 2026 16:34:20 +0200 Subject: [PATCH 2/7] review: guard the fork-cap against unmeasurable proc counts; escape pkill pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit review of #6762: - If ps can't measure the current process count, skip the cap entirely (fail open, commented why) instead of computing ulimit -u 50 from a zero count — a bogus low cap would silently break every legitimate multi-process test. The cap is containment, not a correctness gate; the post-test reap still runs either way. - pkill -f treats its pattern as an ERE, so escape the tmp-dir path's metacharacters (the mktemp component contains a dot) before matching. Validated: node-suite path module runs end-to-end through the updated wrapper (97.8%, same 2 known mismatches as the 2026-07-22 baseline, 0 crashes). --- run_parity_tests.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/run_parity_tests.sh b/run_parity_tests.sh index d1db4cabe4..079eff8b59 100755 --- a/run_parity_tests.sh +++ b/run_parity_tests.sh @@ -756,15 +756,27 @@ for test_file in "${TEST_FILES[@]}"; do # fork-bombing test (cluster re-exec loop, 2026-07-22) stalls at ~50 # orphans instead of saturating the user process limit — which would # starve the harness itself out of fork() and kill the whole run. + # If the proc count can't be measured, deliberately skip the cap (fail + # open): applying a bogus low limit would silently break every legit + # multi-process test, which is worse than one uncapped run. The cap is a + # containment layer, not a correctness gate — the post-test reap below + # still runs either way. Note the budget is per-UID, not per-tree, so a + # concurrent heavy job can transiently eat the +50 headroom; recomputing + # per test keeps that window small, and the worst case is one test + # classified as crash rather than a wedged machine. run_with_timeout 10 /bin/sh -c ' - ulimit -u $(( $(ps -u "$(id -u)" -o pid= | wc -l) + 50 )) 2>/dev/null + nproc_now=$(ps -u "$(id -u)" -o pid= 2>/dev/null | wc -l) + if [ "$nproc_now" -gt 0 ] 2>/dev/null; then + ulimit -u $(( nproc_now + 50 )) 2>/dev/null || true + fi exec "$@"' _ env PERRY_STUB_DIAG=off "${parity_env[@]}" "$perry_binary" "${test_argv[@]}" > "$perry_tmp" 2>&1 perry_exit=$? # Reap orphaned children of the test binary (cluster tests fork workers # that survive the timeout kill of the direct child and can fork-bomb the # machine — 2026-07-22). Scoped to this run's tmp dir, so concurrent - # suite runs are unaffected. - pkill -9 -f "$PARITY_TMP/" 2>/dev/null + # suite runs are unaffected. pkill -f treats the pattern as a regex, so + # escape the path's metacharacters (the mktemp dir contains a dot). + pkill -9 -f "$(printf '%s/' "$PARITY_TMP" | sed 's/[][\.*^$()+?{|]/\\&/g')" 2>/dev/null perry_output=$(cap_output < "$perry_tmp") rm -f "$perry_tmp" From 1495d9afd60ffdfde9f7b02289267863667fd622 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Fri, 24 Jul 2026 11:32:01 +0200 Subject: [PATCH 3/7] fix(parity): fail closed when fork limits cannot apply --- run_parity_tests.sh | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/run_parity_tests.sh b/run_parity_tests.sh index 079eff8b59..4c158eded3 100755 --- a/run_parity_tests.sh +++ b/run_parity_tests.sh @@ -756,18 +756,28 @@ for test_file in "${TEST_FILES[@]}"; do # fork-bombing test (cluster re-exec loop, 2026-07-22) stalls at ~50 # orphans instead of saturating the user process limit — which would # starve the harness itself out of fork() and kill the whole run. - # If the proc count can't be measured, deliberately skip the cap (fail - # open): applying a bogus low limit would silently break every legit - # multi-process test, which is worse than one uncapped run. The cap is a - # containment layer, not a correctness gate — the post-test reap below - # still runs either way. Note the budget is per-UID, not per-tree, so a - # concurrent heavy job can transiently eat the +50 headroom; recomputing + # If the proc count can't be measured or the cap can't be applied, abort + # this test before exec: an uncapped run could recreate the fork bomb this + # containment layer exists to prevent. The budget is per-UID, not per-tree, + # so a concurrent heavy job can transiently eat the +50 headroom; recomputing # per test keeps that window small, and the worst case is one test # classified as crash rather than a wedged machine. run_with_timeout 10 /bin/sh -c ' - nproc_now=$(ps -u "$(id -u)" -o pid= 2>/dev/null | wc -l) - if [ "$nproc_now" -gt 0 ] 2>/dev/null; then - ulimit -u $(( nproc_now + 50 )) 2>/dev/null || true + proc_list=$(ps -u "$(id -u)" -o pid= 2>/dev/null) || { + echo "failed to measure the current user process count" >&2 + exit 125 + } + nproc_now=$(printf "%s\n" "$proc_list" | awk "NF { count++ } END { print count + 0 }") || { + echo "failed to count the current user processes" >&2 + exit 125 + } + if ! [ "$nproc_now" -gt 0 ] 2>/dev/null; then + echo "invalid current user process count: $nproc_now" >&2 + exit 125 + fi + if ! ulimit -u $(( nproc_now + 50 )) 2>/dev/null; then + echo "failed to apply the per-test process limit" >&2 + exit 125 fi exec "$@"' _ env PERRY_STUB_DIAG=off "${parity_env[@]}" "$perry_binary" "${test_argv[@]}" > "$perry_tmp" 2>&1 perry_exit=$? From 5792bf76dd04b769b2c40c33f90135449850bc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 29 Jul 2026 22:36:28 +0200 Subject: [PATCH 4/7] fix(parity): count Linux tasks before limiting forks --- run_parity_tests.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/run_parity_tests.sh b/run_parity_tests.sh index 4c158eded3..6a9117d839 100755 --- a/run_parity_tests.sh +++ b/run_parity_tests.sh @@ -751,7 +751,7 @@ for test_file in "${TEST_FILES[@]}"; do # up on the 2>&1-captured stream for any test that exercises a flagged # API (dns/dgram loopback, v8 heap snapshot, …) and diff against Node. perry_tmp=$(mktemp) - # Cap the test binary's fork budget at current-user-procs + 50: legit + # Cap the test binary's fork budget at current-user-tasks + 50: legit # multi-process tests (cluster/child_process) fit easily, while a # fork-bombing test (cluster re-exec loop, 2026-07-22) stalls at ~50 # orphans instead of saturating the user process limit — which would @@ -763,7 +763,11 @@ for test_file in "${TEST_FILES[@]}"; do # per test keeps that window small, and the worst case is one test # classified as crash rather than a wedged machine. run_with_timeout 10 /bin/sh -c ' - proc_list=$(ps -u "$(id -u)" -o pid= 2>/dev/null) || { + if [ "$(uname -s)" = "Linux" ]; then + proc_list=$(ps -u "$(id -u)" -L -o lwp= 2>/dev/null) + else + proc_list=$(ps -u "$(id -u)" -o pid= 2>/dev/null) + fi || { echo "failed to measure the current user process count" >&2 exit 125 } From 277646dd322a7a668e0083471826469592b01c87 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Wed, 29 Jul 2026 22:21:45 +0200 Subject: [PATCH 5/7] fix(parity): use bash for process limits --- run_parity_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_parity_tests.sh b/run_parity_tests.sh index 6a9117d839..e8c7b6173b 100755 --- a/run_parity_tests.sh +++ b/run_parity_tests.sh @@ -762,7 +762,7 @@ for test_file in "${TEST_FILES[@]}"; do # so a concurrent heavy job can transiently eat the +50 headroom; recomputing # per test keeps that window small, and the worst case is one test # classified as crash rather than a wedged machine. - run_with_timeout 10 /bin/sh -c ' + run_with_timeout 10 "$BASH" -c ' if [ "$(uname -s)" = "Linux" ]; then proc_list=$(ps -u "$(id -u)" -L -o lwp= 2>/dev/null) else From 3ea0ac17f7fc31b1ef19259856199f3e5176d565 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 00:01:11 +0200 Subject: [PATCH 6/7] style(codegen): format loop purity match arm --- crates/perry-codegen/src/loop_purity.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/perry-codegen/src/loop_purity.rs b/crates/perry-codegen/src/loop_purity.rs index aeace93d48..364d34cb3b 100644 --- a/crates/perry-codegen/src/loop_purity.rs +++ b/crates/perry-codegen/src/loop_purity.rs @@ -121,9 +121,7 @@ fn expr_alloc_free(e: &Expr) -> bool { // Element READS never allocate — they return an existing element / a // number. Recurse so the object and index are themselves alloc-free. Expr::IndexGet { object, index } => expr_alloc_free(object) && expr_alloc_free(index), - Expr::BufferIndexGet { buffer, index } => { - expr_alloc_free(buffer) && expr_alloc_free(index) - } + Expr::BufferIndexGet { buffer, index } => expr_alloc_free(buffer) && expr_alloc_free(index), Expr::Uint8ArrayGet { array, index } => expr_alloc_free(array) && expr_alloc_free(index), // `arr[i]++` / `--`: read-modify-write of an existing numeric slot, no // growth, no allocation. From 8958b86415789836f306aa5c04c387c13e8a6a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 04:13:13 +0200 Subject: [PATCH 7/7] fix(parity): record Node enum oracle limitation --- changelog.d/6762-parity-fork-containment.md | 5 +++++ test-parity/gap_snapshot.json | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 changelog.d/6762-parity-fork-containment.md diff --git a/changelog.d/6762-parity-fork-containment.md b/changelog.d/6762-parity-fork-containment.md new file mode 100644 index 0000000000..8e003a052e --- /dev/null +++ b/changelog.d/6762-parity-fork-containment.md @@ -0,0 +1,5 @@ +### Fixed + +- Contain recursively re-executing cluster tests inside a per-test process + budget and reap their orphaned workers, preventing the Node parity suite + from exhausting the runner's process limit. diff --git a/test-parity/gap_snapshot.json b/test-parity/gap_snapshot.json index 6d2aa426ad..f5d0e46515 100644 --- a/test-parity/gap_snapshot.json +++ b/test-parity/gap_snapshot.json @@ -59,6 +59,13 @@ "category": "gap-categorical", "reason": "node --experimental-strip-types refuses this file: ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, 'TypeScript parameter property is not supported in strip-only mode'. The oracle CANNOT execute the very feature under test, so this has never verified anything. Fix by giving it a test-parity/expected/.txt fixture (expected-output mode), not by editing the test." }, + "test_gap_enum_in_function_body": { + "status": "node_fail", + "issue": null, + "added": "2026-07-30", + "category": "gap-categorical", + "reason": "node --experimental-strip-types refuses a function-body enum with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. The oracle cannot execute the TypeScript syntax under test; add an expected-output fixture to restore coverage." + }, "test_gap_fetch_instanceof_5433": { "status": "crash", "issue": "5433",