Skip to content

CI punch list + fuzzer OOM fix + documentation reconciliation - #60

Merged
hyperpolymath merged 6 commits into
mainfrom
fix/ci-punchlist-2026-07-27
Jul 29, 2026
Merged

CI punch list + fuzzer OOM fix + documentation reconciliation#60
hyperpolymath merged 6 commits into
mainfrom
fix/ci-punchlist-2026-07-27

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Work from the 2026-07-29 session. Full narrative: dev-notes/eclexia-sitrep-2026-07-27.md.

1. Parser recursion limit — fixes the ClusterFuzzLite red

ClusterFuzzLite has been red on a reproducible finding (artifact oom-abc6a184…). It is not really an OOM: the recursive-descent parser descends the native stack, so deeply nested input aborts the process with fatal runtime error: stack overflow, which libFuzzer reports as an OOM. About 1000 nested parens — roughly 2 KB — was enough, far inside the target's 100 KB cap.

Adds MAX_RECURSION_DEPTH + with_recursion_guard() at the four recursive entry points (parse_expr_prec, parse_type, parse_block, parse_block_inner), returning a RecursionLimitExceeded error instead of dying.

The limit is 64 because it must hold on the smallest stack the parser runs on: Rust spawns threads with 2 MiB by default and both the LSP and the test harness parse off the main thread. At ~8 KiB of debug stack per level, 256 still overflowed a 2 MiB thread while passing on the main thread's 8 MiB — a discrepancy worth knowing about.

Verified: paren/type/brace nesting at 1k and 50k all survive on a 2 MiB thread (all three aborted at 1k before); all 108 .ecl files in the repo parse with zero depth-limit rejections; cargo test -p eclexia-parser 7/7 including a new regression test.

2. Scorecards startup_failure

The reusable declares contents: read at workflow level; our caller's job-level permissions: block replaced rather than extended it, so Actions rejected the file at parse time. Parse rejection produces no check run at all, which is why gh pr checks showed nothing — use gh run list --json conclusion. Same root cause as #51.

3. Cargo Audit

RUSTSEC-2026-0204 (crossbeam-epoch 0.9.18 → 0.9.20) and warning-only RUSTSEC-2026-0190 (anyhow 1.0.101 → 1.0.104).

4. CodeQL pin label

SHA 7188fc36 was commented # v3.28.1; git ls-remote resolves it to tag v4.37.1. The SHA works, so this was green and invisible. SHA kept, comment corrected. Supersedes local commit 3a140ae, which relabelled it to the equally-wrong # v3do not push that commit.

5. Documentation, human and machine

Every defect here was copy-in, not decay:

  • 0-AI-MANIFEST.a2ml pointed agents at README.adoc, gone since docs(readme): convert README.adoc -> Markdown (renders on Glama/profile/community-health) #47 converted it to Markdown — the agent entry point was dead. Now v1.1 with a correct pointer and explicit read-order.
  • ARCHITECTURE.md was a generic template stub describing src/ tests/ config/, a layout this repo does not have. Replaced with the real pipeline, directory table, resource economy (incl. Echo/Landauer), and an honest gaps list.
  • STATE.a2ml advanced June → July, with new [repo-facts] and [ci-status] blocks.
  • QUICK_STATUS.md, TOOLCHAIN_STATUS.md, .claude/CLAUDE.md: currency notes rather than fake rewrites — the 2026-02 tables are accurate as descriptions but predate Echo and undercount the workspace (53 members, not 25 crates). .claude/CLAUDE.md also still listed Nix as a packaging fallback despite the Guix-only ruling of 2026-05-18.
  • README.md: licence badge alt-text said PMPL-1.0 while its own image said MPL-2.0.

🤖 Generated with Claude Code

hyperpolymath and others added 5 commits July 27, 2026 19:16
The pinned SHA was commented '# v3.28.1' but actually resolves to tag
v4.37.1 upstream (verified via git ls-remote). Keep the working SHA,
label it honestly. Also fix guix.scm licence: pmpl-1.0-or-later is not
a Guix licence symbol and contradicts the MPL-2.0 normalisation (#45).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scorecard reusable requests contents:read at workflow level; the
caller's explicit job-level permissions block zeroed it out, so Actions
rejected the file at parse time (startup_failure, zero jobs — invisible
to gh pr checks). Same class as the Secret Scanner fix in #51.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
….104

Clears Cargo Audit red: RUSTSEC-2026-0204 (crossbeam-epoch invalid
pointer deref in fmt::Pointer) and warning RUSTSEC-2026-0190 (anyhow
unsound downcast_mut). cargo check --workspace passes locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The repo's documentation had drifted from what the code is. Every defect
here came from copy-in rather than decay.

Machine-facing:
- 0-AI-MANIFEST.a2ml pointed agents at README.adoc, which has not existed
  since PR #47 converted it to Markdown — the agent entry point was dead.
  Rewritten to v1.1 with a correct primary pointer, explicit read-order,
  and the Guix-only invariant.
- .machine_readable/6a2/STATE.a2ml advanced 2026-06-13 -> 2026-07-29:
  July milestones, open issues #43/#35/#20, plus new [repo-facts] and
  [ci-status] blocks recording measured counts and per-workflow state.

Human-facing:
- ARCHITECTURE.md was a generic template stub describing src/ tests/
  config/ and README.adoc — a layout this repo does not have. Replaced
  with the real pipeline, directory table, resource economy (including
  Echo/Landauer), metadata contract, and an honest known-gaps list.
- QUICK_STATUS.md and TOOLCHAIN_STATUS.md: refreshed headers plus a
  currency note that the 2026-02 tables predate Echo and undercount the
  workspace (53 members, not 25 crates).
- .claude/CLAUDE.md: read-this-first block with current CI state, the
  moved checkout path, and the gh-pr-checks trap; corrected packaging
  policy, which still listed Nix as a fallback despite the Guix-only
  ruling of 2026-05-18.
- README.md: licence badge alt-text said PMPL-1.0 while the badge image
  said MPL-2.0; added an Architecture quick link.

Verified: cargo check --workspace passes; counts measured by inspection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ClusterFuzzLite reported a reproducible out-of-memory in fuzz_main
(artifact oom-abc6a1849ffcc4d85f6f3524b146f4f1bc471edc). It is not
really an OOM: the recursive-descent parser descends the native stack,
so deeply nested input aborts the process with 'fatal runtime error:
stack overflow', which libFuzzer reports as an OOM. About 1000 nested
parens — roughly 2 KB — was enough, far inside the target's 100 KB cap,
so the fuzzer found it easily.

Adds MAX_RECURSION_DEPTH with a with_recursion_guard() helper that
restores depth on every exit path, applied at the four recursive entry
points: parse_expr_prec, parse_type, parse_block, and parse_block_inner.
Exceeding it now yields a RecursionLimitExceeded parse error.

The limit is 64, not a rounder larger number, because it must hold on
the smallest stack the parser runs on. Rust spawns threads with 2 MiB by
default and both the LSP and the test harness parse off the main thread;
at roughly 8 KiB of debug-build stack per level, 256 still overflowed a
2 MiB thread while passing on the main thread's 8 MiB. 64 leaves about a
4x margin.

Verified:
- paren, type and brace nesting at depths 1k and 50k all survive on a
  2 MiB thread; before the fix all three aborted at 1k.
- all 108 .ecl files in the repo parse with zero depth-limit rejections,
  so the limit rejects no legitimate program.
- cargo test -p eclexia-parser: 7/7 pass, including a new regression
  test covering expression nesting, type nesting, and the shallow case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime.
Learn more

Code Review ⚠️ Changes requested 0 resolved / 1 findings

Adds recursion depth guards to the parser to fix stack-overflow crashes in the fuzzer, but pattern parsing still lacks protection and can overflow the stack.

⚠️ Edge Case: Recursion guard omits pattern parsing — patterns still overflow stack

📄 compiler/eclexia-parser/src/expr.rs:1078-1092 📄 compiler/eclexia-parser/src/expr.rs:1118-1130 📄 compiler/eclexia-parser/src/expr.rs:1184-1194 📄 compiler/eclexia-parser/src/expr.rs:1205 📄 compiler/eclexia-parser/src/expr.rs:1220-1234

The fix guards parse_expr_prec, parse_type, parse_block and parse_block_inner, but parse_single_pattern (expr.rs:1096) recurses unbounded through nested tuple (((x))), slice [[[x]]], reference &&&x and binding patterns without ever calling with_recursion_guard. These are reachable from match arms and let patterns, so a fuzzer input such as match x { ((((…)))) => {} } will still abort the process with fatal runtime error: stack overflow — the exact class of defect this PR set out to close. Wrap the pattern entry point in the same guard, e.g. make parse_pattern call self.with_recursion_guard(|p| p.parse_pattern_inner()), so nested patterns return RecursionLimitExceeded instead of crashing, and add a nested-pattern case to the regression test.

Guard the pattern entry point so nested tuple/slice/reference/binding patterns are depth-bounded like expressions and types.
pub(crate) fn parse_pattern(&mut self) -> ParseResult<Pattern> {
    self.with_recursion_guard(|p| p.parse_pattern_inner())
}

fn parse_pattern_inner(&mut self) -> ParseResult<Pattern> {
    let mut pat = self.parse_single_pattern()?;
    // ... existing or-pattern body ...
    Ok(pat)
}
🤖 Prompt for agents
Code Review: Adds recursion depth guards to the parser to fix stack-overflow crashes in the fuzzer, but pattern parsing still lacks protection and can overflow the stack.

1. ⚠️ Edge Case: Recursion guard omits pattern parsing — patterns still overflow stack
   Files: compiler/eclexia-parser/src/expr.rs:1078-1092, compiler/eclexia-parser/src/expr.rs:1118-1130, compiler/eclexia-parser/src/expr.rs:1184-1194, compiler/eclexia-parser/src/expr.rs:1205, compiler/eclexia-parser/src/expr.rs:1220-1234

   The fix guards parse_expr_prec, parse_type, parse_block and parse_block_inner, but parse_single_pattern (expr.rs:1096) recurses unbounded through nested tuple `(((x)))`, slice `[[[x]]]`, reference `&&&x` and binding patterns without ever calling with_recursion_guard. These are reachable from `match` arms and `let` patterns, so a fuzzer input such as `match x { ((((…)))) => {} }` will still abort the process with `fatal runtime error: stack overflow` — the exact class of defect this PR set out to close. Wrap the pattern entry point in the same guard, e.g. make `parse_pattern` call `self.with_recursion_guard(|p| p.parse_pattern_inner())`, so nested patterns return RecursionLimitExceeded instead of crashing, and add a nested-pattern case to the regression test.

   Fix (Guard the pattern entry point so nested tuple/slice/reference/binding patterns are depth-bounded like expressions and types.):
   pub(crate) fn parse_pattern(&mut self) -> ParseResult<Pattern> {
       self.with_recursion_guard(|p| p.parse_pattern_inner())
   }
   
   fn parse_pattern_inner(&mut self) -> ParseResult<Pattern> {
       let mut pat = self.parse_single_pattern()?;
       // ... existing or-pattern body ...
       Ok(pat)
   }

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
@hyperpolymath
hyperpolymath marked this pull request as ready for review July 29, 2026 07:34
@hyperpolymath
hyperpolymath merged commit 14ef751 into main Jul 29, 2026
7 of 8 checks passed
@hyperpolymath
hyperpolymath deleted the fix/ci-punchlist-2026-07-27 branch July 29, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant