infer: three fuzzer crashes from #4044 - #4049
Merged
Merged
Conversation
borisbat
reviewed
Sep 16, 2026
borisbat
left a comment
Collaborator
There was a problem hiding this comment.
mainly why is type-trait an ast-change?
| @@ -2716,6 +2716,7 @@ namespace das { | |||
| return new ExprConstBool(expr->at, ft != nullptr); | |||
| } else { | |||
| if (expr->trait == "safe_has_field") { | |||
Collaborator
There was a problem hiding this comment.
how so? why is this an AST change??
| } else { | ||
| if (expr->trait == "struct_safe_has_annotation") { | ||
| reportAstChanged(); | ||
| return new ExprConstBool(expr->at, false); |
| if (it == ann.end()) { | ||
| if (expr->trait == "struct_safe_has_annotation_argument") { | ||
| reportAstChanged(); | ||
| return new ExprConstBool(expr->at, false); |
Collaborator
There was a problem hiding this comment.
same again. why is this an ast change?
`struct A; class B : A; class C : B;` crashed in makeClassRtti. The
`class B : A` error is correct, and it returns from
ast_structureDeclaration before B gets its generated __rtti, so the
accepted `class C : B` wrote through a findField("__rtti") answering
null. makeClassFinalize had the identical defect one call later and
would have been the next crash.
Both now let the parent lookup pick the branch: a parent that never got
the field means the class declares its own.
aleksisch
force-pushed
the
aleksisch/fuzzer-fixes
branch
3 times, most recently
from
September 16, 2026 21:56
5f1439e to
e35cade
Compare
aleksisch
marked this pull request as ready for review
September 16, 2026 21:57
`1 &&= typeinfo safe_has_field<var48>(typeinfo is_ref(1))` crashed in buildAccessFlags on a null ExprOp2::func. The missing guard is real, but it was covering an inference bug. Root cause: four safe_* typeinfo traits returned a replacement node without reportAstChanged - safe_has_field, struct_safe_has_annotation, and both arms of struct_safe_has_annotation_argument. Every sibling branch signals; these four did not. The replacement arrives untyped, the enclosing operator takes its "failed to infer" early-out, and with no signal that pass is the last one, so func stays null with no diagnostic - which is also why the reported input never reached operator resolution and never got its 30341. It rejected valid code too: `b &&= typeinfo safe_has_field<X>(typeinfo is_ref(1))` did not compile. Guard: preVisit of ExprOp1, ExprOp2 and ExprOp3 in the access-flag pass now return early on a null func, the way ExprCall right below them already did. buildAccessFlags runs before the pass that owns the 50613 diagnostic, so without it any unsignalled rewrite crashes before it can be reported. A fifth site, is_iterable, looked like the same defect but already signals three lines above its return - left alone. The fixture holds all four traits and the reported input, and must stay free of anything that keeps signalling - a print's string builder is enough, and it grants the enclosing operator a repair pass that hides the defect. Reverting the four calls turns the expected 30341 into 50613 five times over.
`let zz : int[zz][zz]` ate 26.8 GB and died to the OOM killer. The
dimension expression lives inside the type it dimensions, so giving the
dimension's ExprVar the variable's own type gives it a type containing
that same expression - with two dimensions each infer pass squares the
previous pass's copy, 4^50.
InferTypes::visit(ExprVar*) no longer adopts a global's type while its
dimensions are unresolved (isExprType).
This also covers two shapes the report does not name, both of which OOM
on master and neither of which the globalVar self-reference guard the
issue suggests would catch:
let aa : int[bb][bb] // mutual
let bb : int[aa][aa]
let aa : int[length(bb)][length(bb)] // through a call
let bb : int[length(aa)][length(aa)]
All of them now report error[30109] in under half a second. A forward
reference that resolves in a later pass (`let b : int[MM]` before
`let MM = 3`, and a function reading b) keeps working.
aleksisch
force-pushed
the
aleksisch/fuzzer-fixes
branch
from
September 16, 2026 22:18
e35cade to
73c420f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4044 — all three crashes, one fixture each, plus the root cause behind the second one.
1.
makeClassRttinull derefclass B : Ais rejected as it should be, and that rejection returns fromast_structureDeclarationbefore B gets its generated__rtti.class C : Bis then accepted and wrote through afindField("__rtti")that answers null.makeClassRttiandmakeClassFinalizenow let the parent lookup pick the branch: a parent that never got the field means the class declares its own.makeClassFinalizehad the identical defect one call later and would have been the next crash.→
error[30134]: class can only derive from a class, one error, no crash.2. Null
ExprOp2::funcinbuildAccessFlagsThe reported crash is a missing guard, but it was hiding a real inference bug.
Root cause. Four
safe_*typeinfotraits returned a replacement node withoutreportAstChanged()—safe_has_field,struct_safe_has_annotation, and both arms ofstruct_safe_has_annotation_argument. Every sibling branch signals; these four did not. The replacement arrives untyped, the enclosing operator takes itsfailed to inferearly-out, and with no signal that pass is the last one — soExprOp2::funcstays null with no diagnostic.It broke valid programs, not just fuzzer input:
Guard.
preVisit(ExprOp1*)/preVisit(ExprOp2*)in the access-flag pass now checkfuncthe wayExprOp3andExprCallalready did.buildAccessFlagsruns before the side-effect pass that owns the50613diagnostic, so without the guard any unsignalled rewrite crashes before it can be reported.→ the fuzzer input now gives
error[30341]: no matching functions or generics: _::&&=(int const, bool const).A fifth site,
is_iterable, looked like the same defect but already signals three lines above its return — left alone.3. Self-referential fixed-array dimension (OOM, 26.8 GB)
The dimension expression lives inside the type it dimensions, so giving the dimension's
ExprVarthe variable's own type gives it a type containing that same expression. With two dimensions each infer pass squares the previous pass's copy — 4^50.InferTypes::visit(ExprVar*)no longer adopts a global's type while its dimensions are unresolved (isExprType()).This also covers two shapes the report does not name, both of which OOM'd on master and which the
globalVarself-reference guard suggested in the issue would not have caught:→
error[30109]: array dimension must be constant, twice, in 0.4s.Tests
failed_class_parent_rejected.dasexpect 30134failed_op2_int_lhs_typeinfo_rhs.dasexpect 30341failed_global_dim_self_reference.dasexpect 30109:2typeinfo_safe_has_field_reinfer.das+ 3 siblingstypeinfobranch eachfailed_macro_added_op2_must_infer.dasfuncreach the50613diagnosticOne shape per file is load-bearing for the four
typeinfofixtures: the trait has to fold in a pass where nothing else changes, and a second shape in the same file signals for the first. My first attempt was a six-arm dastest file and it passed without the fix — a tautology. Negative control: rebuilt with the fourreportAstChanged()calls reverted, all four files fail.Two things that are coverage rather than controls, stated plainly:
ast_unusedguard is not reachable by a test once the root cause is fixed — it is consistency withExprOp3/ExprCall, as the issue asked forfailed_macro_added_op2_must_infer.dasdoes not fail without the guard (buildAccessFlagsruns beforepatchAnnotations); it covers two previously untestedreportNullFuncsitesVerification
tests/language: 1759/1759--test tests: 14530 tests, 14514 passed, 0 failed, 0 errors, 16 skipped--verifyclean overtests/language(410 files); lint clean on the new files;src/ast/REVIEW.dasgate OKtest_aot_subsetbuilds with the new fixtures; thefailed_*files are excluded by the existing filter attests/aot/CMakeLists.txt:334, so no CMake change🤖 Generated with Claude Code