Skip to content

fix(TypeResolver): bound nodeIsTypeRef alias recursion (stack-overflow DoS)#5

Merged
EugOT merged 2 commits into
mainfrom
fix/typeref-cyclic-alias-recursion
Jun 18, 2026
Merged

fix(TypeResolver): bound nodeIsTypeRef alias recursion (stack-overflow DoS)#5
EugOT merged 2 commits into
mainfrom
fix/typeref-cyclic-alias-recursion

Conversation

@EugOT

@EugOT EugOT commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

A deep multi-agent audit of the fork diff (bfcb30d..main, the Zig 0.16 migration + hardening work from PRs #1#4) surfaced one genuinely actionable defect, now fixed here.

🔴 Critical: stack-overflow DoS on cyclic type aliases. The nodeIsTypeRef / varDeclIsTypeRef / identifierIsTypeRef trio in src/TypeResolver.zig recursed with no depth guard. A cyclic alias such as const a = b; const b = a; (or self-referential const a = a;) in any linted file triggers unbounded recursion → stack overflow → crash. Since ziglint analyzes untrusted source, this is remotely triggerable.

PR #4's "harden resolver" added max_alias_depth = 32 but applied it only to findMethodInFileAsStructDepth, missing this sibling family. The bug is pre-existing upstream (introduced by upstream 6b11b20), so it's also a candidate upstream PR.

Fix

Thread a depth: u32 parameter through the recursive trio (via a new nodeIsTypeRefDepth), plus fieldAccessIsTypeRef. Each recursive hop increments depth; once depth >= max_alias_depth the chain returns false. The public isTypeRef() entry starts at depth 0. This mirrors the existing findMethodInFileAsStructDepth guard pattern exactly. No behavior change for non-cyclic input (32 hops is far beyond any real alias chain).

Tests (TDD)

Three regression tests added to src/TypeResolver.zig:

  • isTypeRef: cyclic alias terminates without stack overflowconst a = b; const b = a;
  • isTypeRef: self-referential alias terminates without stack overflowconst a = a;
  • findFnInCurrentModule: cyclic method alias terminates via max_alias_depth — locks in the pre-existing method-alias guard

The two isTypeRef tests crash with a 10001-frame stack overflow before the fix and pass after — proving they test the right thing.

Validation

zig build test --summary all
Build Summary: 7/7 steps succeeded; 277/277 tests passed
+- run test 277 pass (277 total)
+- zig fmt success
+- run exe ziglint success   # self-lint clean

(274 baseline + 3 new tests.)

Audit notes

The audit ran 5 review dimensions (correctness, memory-safety, robustness, performance, testing) with adversarial verification of every finding. Everything else was either already-correct fork code (verifiers confirmed PRs #1#4 fixed the bugs they flagged) or refuted false positives (arena-allocator non-leaks, bounded AST/path loops, non-material micro-perf). Two minor test gaps were noted; the higher-value one (the method-alias guard) is covered here. The remaining gap — a fault-injection test for doc_tests.zig:203's infra-error rethrow — is low severity and intentionally deferred (would require fragile I/O mocking).

Note: CodeRabbit's local CLI could not complete on this Zig 0.16-dev repo (its sandbox can't provision the toolchain — full run timed out at 60 min, --fast produced no verdict). The CodeRabbit GitHub app can review this PR server-side.

…w DoS)

WHY: ziglint analyzes untrusted source. The nodeIsTypeRef / varDeclIsTypeRef /
identifierIsTypeRef trio recursed with no depth guard, so a cyclic alias such as
`const a = b; const b = a;` (or self-referential `const a = a;`) in any linted
file caused unbounded recursion and a stack-overflow crash. PR #4's "harden
resolver" added max_alias_depth=32 but only to findMethodInFileAsStructDepth,
missing this sibling family. (Pre-existing upstream; introduced by upstream
6b11b20.)

WHAT: Thread a `depth: u32` parameter through nodeIsTypeRef (via new
nodeIsTypeRefDepth), varDeclIsTypeRef, identifierIsTypeRef, and
fieldAccessIsTypeRef, incrementing on each recursive hop and returning false once
depth >= max_alias_depth. Public isTypeRef() entry starts at depth 0. Mirrors the
existing findMethodInFileAsStructDepth guard pattern. Added regression tests:
cyclic alias, self-referential alias (both via isTypeRef), and a cyclic method
alias (via findFnInCurrentModule, locking in the pre-existing guard).

IMPACT: src/TypeResolver.zig only. No behavior change for non-cyclic input; the
32-hop cap is far beyond any real alias chain. Eliminates a remotely triggerable
linter crash on adversarial source.

VALIDATION: New tests crash (10001-frame stack overflow) before the fix and pass
after. `zig build test --summary all` -> 7/7 steps, 277/277 tests pass, zig fmt
clean, self-lint (run exe ziglint) clean. Found via multi-agent audit of the fork
diff (bfcb30d..main) with adversarial verification.
Copilot AI review requested due to automatic review settings June 18, 2026 18:00
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dca8c354-a8d7-47fd-b9fe-4a6ef04a07b5

📥 Commits

Reviewing files that changed from the base of the PR and between 74779f2 and 349f0bb.

📒 Files selected for processing (1)
  • src/TypeResolver.zig
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • EugOT/dotfiles (manual)

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Prevented stack overflows when evaluating cyclic type aliases by adding depth-limited handling.
    • Improved termination and correctness for cyclic alias and method/alias resolution cases.
  • Tests

    • Added tests to confirm cyclic alias chains terminate correctly and return expected results.
    • Added coverage for mutual and self-referential aliasing scenarios.

Walkthrough

isTypeRef in src/TypeResolver.zig is refactored to route through a new depth-limited helper nodeIsTypeRefDepth with a max_alias_depth bound. The three internal helpers—varDeclIsTypeRef, identifierIsTypeRef, and fieldAccessIsTypeRef—each gain a depth parameter and increment it on each recursive call. New tests verify termination and false return values for cyclic and self-referential aliases.

Changes

Cyclic Alias Recursion Guard

Layer / File(s) Summary
Depth-bounded isTypeRef helpers
src/TypeResolver.zig
isTypeRef delegates to new nodeIsTypeRefDepth enforcing max_alias_depth; varDeclIsTypeRef, identifierIsTypeRef, and fieldAccessIsTypeRef accept a depth parameter and recurse via nodeIsTypeRefDepth(..., depth + 1).
Cyclic alias termination tests
src/TypeResolver.zig
Tests that findFnInCurrentModule returns null on cyclic method alias chains, and that isTypeRef returns false for a = b; b = a; and a = a; without stack overflow.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • EugOT/ziglint#4: Modifies src/TypeResolver.zig to add bounded alias chasing in member/alias resolution, directly related to the same cyclic alias problem addressed here.

Poem

🐇 In alias chains that loop and twine,
I added a depth to draw the line.
"Max depth reached!" I proudly say,
No stack shall overflow today.
Cyclic tricks? I've seen them all—
false returned, no crashing fall! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main security fix: preventing stack-overflow DoS by bounding recursion depth in nodeIsTypeRef alias resolution.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, detailing the stack-overflow vulnerability, the fix implementation, test additions, and validation results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/typeref-cyclic-alias-recursion

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens TypeResolver.isTypeRef() against stack-overflow denial-of-service by bounding recursion when resolving cyclic const aliases in linted (untrusted) Zig source.

Changes:

  • Introduces a depth-bounded nodeIsTypeRefDepth helper and threads depth through the recursive type-ref checks (nodeIsTypeRef / varDeclIsTypeRef / identifierIsTypeRef / fieldAccessIsTypeRef).
  • Adds regression tests to ensure cyclic/self-referential aliases terminate without stack overflow, and to lock in existing alias-depth behavior for findFnInCurrentModule.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/TypeResolver.zig
Comment on lines 1194 to 1201
for (tree.rootDecls()) |decl_node| {
switch (tree.nodeTag(decl_node)) {
.simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => {
const var_decl = tree.fullVarDecl(decl_node) orelse continue;
const name_token = var_decl.ast.mut_token + 1;
if (!std.mem.eql(u8, tree.tokenSlice(name_token), name)) continue;
return self.varDeclIsTypeRef(tree, decl_node, module_path);
return self.varDeclIsTypeRef(tree, decl_node, module_path, depth + 1);
},

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/TypeResolver.zig`:
- Line 1187: The depth tracking in the nodeIsTypeRefDepth function is
incrementing twice per single alias resolution step, which effectively halves
the max_alias_depth guard. In the recursive call at line 1187 where
nodeIsTypeRefDepth is invoked with depth + 1, and at the corresponding location
at line 1200, remove one of these increments so that a single alias hop
(identifier -> var_decl -> init) only consumes one depth increment instead of
two. This will allow the depth guard to properly protect against deep alias
chains without producing false negatives for legitimate acyclic alias sequences.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 258e90e6-e272-419e-be6b-2a27cedcd2f0

📥 Commits

Reviewing files that changed from the base of the PR and between a2fa56a and 74779f2.

📒 Files selected for processing (1)
  • src/TypeResolver.zig
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • EugOT/dotfiles (manual)

Comment thread src/TypeResolver.zig
WHY: CodeRabbit and Copilot both flagged that the alias-recursion guard
incremented depth twice per logical hop (identifierIsTypeRef ->
varDeclIsTypeRef(depth+1) -> nodeIsTypeRefDepth(init, depth+1)), so
max_alias_depth=32 effectively capped at ~16 alias links and could false-negative
on long but acyclic alias chains.

WHAT: identifierIsTypeRef now passes `depth` (not depth+1) into varDeclIsTypeRef;
the single increment stays at the actual alias-following recursion in
varDeclIsTypeRef. One alias link now costs exactly one increment, matching the
intended "~32 hops". Termination is unchanged (every cycle still passes the
incrementing call).

IMPACT: src/TypeResolver.zig only. No safety change (still terminates, no
overflow); removes a false-negative risk for deep legitimate alias chains.

VALIDATION: zig build test --summary all -> 7/7 steps, 277/277 tests pass, fmt
clean, self-lint clean. Cyclic/self-referential alias regression tests still pass.
@EugOT EugOT merged commit f381efd into main Jun 18, 2026
7 checks passed
@EugOT EugOT deleted the fix/typeref-cyclic-alias-recursion branch June 18, 2026 18:12
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.

2 participants