Skip to content

Fix the five fuzzer crashes in #3892 - #3906

Merged
borisbat merged 8 commits into
masterfrom
aleksisch/fix-fuzzer-bugs
Sep 4, 2026
Merged

Fix the five fuzzer crashes in #3892#3906
borisbat merged 8 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
aleksisch force-pushed the aleksisch/fix-fuzzer-bugs branch 6 times, most recently from e9845ca to 276bf99 Compare September 4, 2026 15:23
aleksisch and others added 8 commits September 4, 2026 18:32
…ructure

A template's field types are never inferred, so the unresolved call in the
fuzzer's `r<iterator<a>[@{0=(0,C())}]>` reaches deriveAliases with a null `func`,
and its ExprMakeBlock reaches the coverage pass outside of any function. Both get
the !isTemplate gate RunFolding and ast_verify already carry.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A sealed or override declaration set parentType from `type->isAuto()`, which is
wrong when the field it replaces was declared earlier in the same body: infer
looks that name up in the parent, gets nullptr and dereferences it. Gate it on
the flag that says the type came from the parent.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Const folding elides a call to an empty pure function by returning nullptr,
which only ExprBlock knows how to drop; as `debug`'s argument the null survives
and ExprLooksLikeCall::visit walks into it - the fuzzer's SIGSEGV. AOT emits an
undefined `cast<void>::from`, and the JIT gives up. Reject it where a void
function argument is already rejected.

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 field's array dimension, say -
there is none, so `func` is null and findArgument dereferences it. Having a
function is a precondition for that branch, so it joins the condition.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`goto <expr>` in a captured block emitted a switch over every label in every
enclosing scope, so the lambda jumped to a label outside itself and the C++ did
not compile. Stop the scan at the closure, as InferTypes::findLabel does; the
default arm then throws, which is what the interpreter does.

The scan also ran outermost-first: the two reverse() calls around it resolved to
linq's pure reverse and copied, so walk `scopes` by index instead.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`default<int[2000000000]>` parses as an ExprMakeStruct and nothing on the make
path sizes it, so it reaches AllocateStack and trips `size <= 0x7fffffff` - or,
with the assertion compiled out, hangs. lint already carries that check for a
local, argument, field, global, `new` and ascend, and runs before allocateStack;
give ExprMakeStruct the same one.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visitForSource synthesizes `each(source)` and infers that call, checking only
that the source has a type. A source whose dim never reduced to a constant - an
[expr] type - then reaches inferArguments, whose assertion says the caller owes
that check. Release reports the same diagnostics either way, so it shows only in
a build with assertions.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
das keys a table by the value type's workhorse, and the interpreter and the JIT
both use it. AOT used the C++ type: equality through overload resolution, which
for BigEntityId is a vector rather than a bool, and hashing over the object's
bytes, which disagrees with hashing the workhorse. AOT is per-function, so one
table is reached by both tiers - test_cross_tier_table_hash.das is that program.

KeyHash mirrors KeyCompare and every table hash site routes through it, the
rehash on grow included. BigEntityId declares float4, its das type, instead of
the vec4f register, which the JIT has no mapping for.

Refs #3892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aleksisch
aleksisch force-pushed the aleksisch/fix-fuzzer-bugs branch from 276bf99 to ab67153 Compare September 4, 2026 15:32
@borisbat
borisbat merged commit 7af1d9e into master Sep 4, 2026
37 checks passed
@borisbat
borisbat deleted the aleksisch/fix-fuzzer-bugs branch September 4, 2026 17:09
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