Skip to content

Fix the five fuzzer crashes in #3892 - #3906

Open
aleksisch wants to merge 5 commits into
masterfrom
aleksisch/fix-fuzzer-bugs
Open

Fix the five fuzzer crashes in #3892#3906
aleksisch wants to merge 5 commits into
masterfrom
aleksisch/fix-fuzzer-bugs

Conversation

@aleksisch

@aleksisch aleksisch commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Behavior change: debug() of a void expression no longer compiles (error 30107).

This fixes the five crashes reported in #3892. Each one is a null dereference on a path a normal program never reaches, and each is fixed at the layer that owns the invariant. One commit per bug, each with a test that fails without its patch.

Three are guards. The aliasing pass walked into the field types of a struct template; inference never types those, so it read result off an unresolved call's null func. It now skips template structures, which is what const folding, export and annotation binding already do. A sealed or override field that redeclares a field from the same structure body was marked as taking its type from the parent, and inference then dereferenced a parent field that was never there; only an inherited field is marked now. typeinfo is_argument asked the enclosing function about a name while inferring a type declaration, where there is no enclosing function; it answers false there, like the sibling branch beside it and like the 52 other null-func guards in that file.

debug() of a void expression is now rejected. No tier could carry it: it crashed the const folder, made AOT emit cast<void>::from(...), which has no definition, and made the JIT give up with "failed to get IR". It reuses 30107, the code already reported for a void function argument, so one existing fixture (tests/language/invalid_table_type_mix.das) grew that code in its expect line.

The last one is codegen only. A computed goto <expr> inside a captured block must fail at runtime, the way the interpreter does. AOT instead emitted a switch over every label in every enclosing scope, so the generated lambda jumped to a label outside its scope and the C++ did not compile. The label scan now stops at the closure, so the switch carries only labels the lambda can reach and its default arm throws. The scan also ran outermost-first: the two reverse() calls around it resolve to linq's pure reverse, which returns a copy, so the scope stack was never reversed.

Where to look: debug(<void>) is the only source-level behavior change. The AOT emitter change is daslib/aot_cpp.das, visitExprGoto.

Validation, claims, ledger

Validation

  • The full AOT sweep ran locally and is green (12094 tests, 0 failed, 0 errors, 7 skipped). Per-PR CI builds only the subset, so this is the only pre-merge check of AOT outside tests/language.
  • Every test is negative-controlled. For the four crashes the patch was reverted, the compiler rebuilt, and the test observed to crash. For the AOT emitter, reverting visitExprGoto makes the emitted C++ fail to compile with error: label 'label_6' used but not defined; tests/language is compiled to C++ by test_aot_subset, which is in ALL, so that is a build failure.
  • debug() was probed with 15 argument shapes (block, function pointer, tuple, struct, array, table, variant, string, pointer, void?, float3, enum, bitfield, iterator, das_string). Each one either runs and emits AOT C++ that compiles clean, or is rejected earlier by the type system with its own diagnostic. void is the only das type with no cast<> specialization, because it is the only one with no value to cast.
  • The untracked preflight gate is red on 12 files under web/examples/glfw/ and web/test/glfw_dynlink/. They are unrelated work already in the worktree before this branch; none of them is in this PR.
  • The external codex round did not run - no codex on PATH.
  • No audit agents ran; this session is configured not to spawn them. The one checklist the diff reaches, src/parser/REVIEW.md, binds diffs that add syntax to ds2_parser.ypp or ds2_lexer.lpp; this one changes parser_impl.cpp only.

Not done

  • A literal goto label N to a label inside the same captured block is still broken. The interpreter aborts the closure and the function that invoked it, and prints nothing. It is not one of the five reported crashes and nothing here touches it.

🤖 Generated with Claude Code

aleksisch and others added 5 commits August 30, 2026 14:29
deriveAliases visits every structure's field types, a `struct template`'s
included. A template's field type expressions are never inferred, so an
unresolved call inside one -- the dim expression of `r<iterator<a>[@{0=(0,C())}]>`
in the fuzzer's repro -- reaches SourceCollector::preVisit(ExprCall) with a null
`func` and dereferences it.

Give AliasMarker a canVisitStructure that returns !isTemplate, mirroring its own
canVisitFunction isTemplate gate and ConstFolding's canVisitStructure, which
already exists for the same reason.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A sealed or override field declaration sets parentType from `type->isAuto()`,
which is right when the field it replaces was copied down from the parent and
wrong when the field was declared earlier in the same structure body. Infer then
reads parentType, looks the name up in the parent, gets nullptr back and
dereferences it.

parentType means "this field's type comes from the parent's field", so gate it
on the flag that says so.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No tier can carry it. Const folding elides a call to an empty side-effect-free
function by returning nullptr, which only ExprBlock knows how to drop; as the
argument of `debug` the null survives and ExprLooksLikeCall::visit walks into it
one pass later -- the fuzzer's SIGSEGV. AOT emits `cast<void>::from(...)`, which
has no definition, and the JIT gives up with "failed to get IR".

`debug(v())` is the only spelling that puts a void call in a subexpression
position, so reject it where a void function argument is already rejected, with
the same code and the same wording.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`is_argument` asks the enclosing function whether a name is one of its
arguments. Reached through a type declaration -- a structure field's array
dimension, say -- there is no enclosing function, so `func` is null and
findArgument dereferences it.

The sibling branch one line down already answers false when the subexpression
is not a var; answer false here too.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A `goto <expr>` inside a captured block emitted a switch over every label in
every enclosing scope, the outer function's included, so the generated lambda
jumped to a label that is not in its scope and the C++ did not compile: "use of
undeclared label 'label_6'". Failing at runtime there is the intended behavior -
the interpreter throws "jump to label N failed" - so AOT has to reach the same
runtime failure instead of failing to compile.

Stop the label scan at the closure, the rule InferTypes::findLabel already
follows. The switch then carries only the labels the lambda can reach, and its
default arm throws.

The scan also ran outermost-first: the two `reverse()` calls around it resolve to
linq's pure `reverse`, which returns a copy, so `scopes` was never reversed. Walk
it by index instead, so "stop at the closure" means the innermost one.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aleksisch
aleksisch force-pushed the aleksisch/fix-fuzzer-bugs branch from 897a4d0 to 48f429a Compare August 30, 2026 13:32
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