Skip to content

cargo-test aborts with a stack overflow on every PR — statically_reachable_trusted_js_package_is_aot_compiled_without_route_entry, and a Python-only diff reproduces it #9196

Closed

Description

@proggeramlug

Summary

The cargo-test job is failing on every open PR with a stack overflow, not an assertion:

thread 'commands::compile::collect_modules::tests::
        statically_reachable_trusted_js_package_is_aot_compiled_without_route_entry'
       (12606) has overflowed its stack
fatal runtime error: stack overflow, aborting
process didn't exit successfully: .../perry-c8602d85b11282cb (signal: 6, SIGABRT)

The abort takes down the whole test binary, so every test that had not yet run is skipped — the same masking shape as #9108. The visible failure is one test; the cost is the tail of the run.

It is not caused by any diff

Confirmed on four PRs with unrelated contents, all the same test, all the same signal:

PR contents cargo-test
#9190 perry-runtime only stack overflow
#9191 codegen stack overflow
#9185 codegen FAILURE
#9193 scripts/ci_e2e_scope.py only — zero Rust stack overflow

#9193 is the decisive one: it changes a single Python file, cannot affect the compiled test binary at all, and reproduces the abort identically. Whatever the cause is, it is on main.

What is known

  • The test is from fix(compile): AOT-promote trusted reachable JavaScript #8529 (fix(compile): AOT-promote trusted reachable JavaScript) and long predates any of these PRs.
  • It lives in crates/perry/src/commands/compile/collect_modules/tests.rs:604.
  • It is a debug build (target/debug/build/perry/...), where stack frames are largest and no inlining trims recursion depth.
  • Stack overflow rather than assertion failure points at unbounded or deep recursion in module collection / reachability walking, not at a wrong expected value.

I have not root-caused it. This issue records that it is reproducible, that it is not diff-caused, and that a one-file Python PR is a free reproducer for whoever picks it up.

Why this is worth prioritising

Two required gates are red on main at once right now — this one and e2e-scoped (fixed in #9193). A permanently-red required check is CLAUDE.md hazard 2: it teaches reviewers to merge past red, and the next real regression lands invisibly behind it. The stack-overflow shape makes that worse than a normal failure, because it silently truncates the rest of the suite.

Suggested first steps

  1. Confirm determinism: re-run cargo test -p perry --bin perry statically_reachable_trusted on main at 84185b5656 and see whether it aborts every time or intermittently. If intermittent, the trigger is likely test ordering or a shared fixture, not the test itself.
  2. RUST_MIN_STACK bisection will say how deep the recursion is, and whether it is deep-but-bounded (a threshold problem) or unbounded (a cycle in the reachability walk — e.g. a package graph that re-enters itself when a subpath resolves back to its own root).
  3. If it is a cycle, the fix is a visited-set in the walk, and the regression test is the package shape that closes the loop.

Activity

  1. proggeramlug commented on Aug 30, 2026

    @proggeramlug
    ContributorAuthor

    Clean negative on macOS arm64 — does not reproduce there, which narrows this to platform or CI environment rather than the commit.

    Same commit (84185b5656), cargo test -p perry --bin perry, full parallel suite, default RUST_MIN_STACK:

    test result: ok. 1066 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.08s
    

    Zero occurrences of overflowed its stack. The named test also passes in isolation (--bin perry statically_reachable_trusted_js_package, 0.11 s).

    What that implies for the repro

    The likely mechanism is stack size, and it is now the first thing to test rather than a hypothesis among several. libtest gives spawned test threads a default stack — 2 MiB unless RUST_MIN_STACK overrides it — while a single-test run can land on the main thread's 8 MiB, and Linux CI containers frequently differ again from a developer Linux box.

    That gives a clean discriminator, which has to be run on a machine that actually fails:

    • Raising RUST_MIN_STACK makes it pass ⇒ the reachability walk is deep but bounded. Fix is bounding the recursion depth or converting the walk to an explicit worklist.
    • It still aborts at any stack size ⇒ a genuine cycle in the module graph. Fix is cycle detection, and the stack size is a red herring.

    Worth checking the runner before the code

    A test that passes a full parallel suite on one platform and aborts on another — with a PR touching a single Python file and no Rust reproducing it identically — is at least as likely to be a CI stack limit or container setting as a defect in the module collector. Worth confirming the failing environment's thread stack limit before bisecting perry.

    Recording the negative here so nobody re-runs it on macOS expecting a repro.

  2. proggeramlug commented on Aug 30, 2026

    @proggeramlug
    ContributorAuthor

    It does not reproduce on macOS arm64 — the platform is part of the repro.

    Same commit (84185b5656), cargo test -p perry --bin perry, full parallel suite, default RUST_MIN_STACK: 1066 passed, 0 failed, zero occurrences of "overflowed its stack", 10.08 s wall. The named test also passes in isolation. (Measured by another session on a separate machine.)

    So this is platform- or environment-specific rather than universal to the commit, and that changes where to look first. Two consequences:

    1. Check the runner before the code. A test that passes a full parallel suite on one platform and aborts on another's, with a single-Python-file PR reproducing the abort, is at least as likely to be a CI stack-limit or container setting as a perry defect. Worth diffing the Linux CI job's environment against what it was when this last passed.

    2. The stack-size discriminator is now the first experiment, not a guess. libtest gives spawned test threads a 2 MiB default stack unless RUST_MIN_STACK overrides it, while a single-test run can land on the main thread's 8 MiB — and Linux containers often differ again from both. That yields a clean fork:

    • Raising RUST_MIN_STACK makes it pass → the reachability walk is deep but bounded. The fix is bounding the recursion or converting it to an explicit worklist, and the regression test is a package graph deep enough to trip the old limit.
    • No stack size helps → it is a genuine cycle in the module graph. The fix is cycle detection, and the regression test is the package shape that closes the loop (e.g. a subpath that resolves back to its own root).

    Either way the experiment must run on the box that fails — the macOS result above cannot distinguish the two, which is the point of reporting it.

    Everything else in the original report stands: four PRs with unrelated contents, same test, same SIGABRT, and #9193 (Python only, zero Rust) is still the free reproducer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions