Skip to content

fuzzer crashes from #3959: fixed-array count underflow, unchecked function result size, a piped-call null deref, and the ambiguous AOT cvt_ surface - #3989

Merged
borisbat merged 4 commits into
masterfrom
aleksisch/fuzzer-fizes
Sep 10, 2026
Merged

fuzzer crashes from #3959: fixed-array count underflow, unchecked function result size, a piped-call null deref, and the ambiguous AOT cvt_ surface#3989
borisbat merged 4 commits into
masterfrom
aleksisch/fuzzer-fizes

Conversation

@aleksisch

@aleksisch aleksisch commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Grammar-fuzzer crashes from #3959. Four defects, one commit each with its own regression fixture.

Closes #3959

What each commit fixes

ast: a non-positive fixed-array dimension is not an element count — issue item 6.
getCountOf64 multiplied by uint64_t(fixedDim) for every node of a fixed-array chain.
dimAuto is -1 and dimConst is -2, so an unresolved dimension wrapped the product and the
size read as enormous rather than unknown: 2^64-48 for the reported repro. Such a dimension
now contributes no count, matching what a dimension of 0 already did and what the
getSizeOf64(bool&) overload has always answered for the same input.

lint: an oversized function result is diagnosed, not asserted on — issue item 1.
The size gate covered locals, arguments, block arguments, structures, aliases, new and
ascend, but never a function result, so an oversized result reached Program::simulate
where the debug-info walk sizes it and asserts.

infer: a piped call stops padding in a default whose parameter type never resolves
issue item 5. This one was a SIGSEGV in stock Release, not only an assertion failure.
isFunctionCompatiblePipedAt admits a piped-call candidate after checking only that the
parameters it shifts across have defaults, never that their types match; instancing is then
handed an argument lookup never matched, inferGenericType answers null, and the next
statement writes through it under an assert that DAS_NO_ASSERTIONS compiles out.

The fix is at the lookup, not the symptom: padded defaults are matched once resolved, but only
where the parameter type is still auto or an alias. A parameter with a concrete type is left to
flow through on purpose — its mismatch is a real user error and the instanced function reports
it far better (error[30161], both types named) than a lookup miss would.
DAS_ASSERTF(resT, ...) therefore stays: with its only known producer closed off it means
what it says again, and a null from any other path is a compiler defect that must surface as
one. The other assert on that path is deleted, because its claim is false — the no-progress
exit is reachable whenever a concrete parameter's default mismatches, and falling through is
what produces 30161.

aot: every cvt_ inline answers in its vector type, not vec4f — issue item 2.
Every vec2/vec3/vec4 flavor converts to and from vec4f, so a cvt_ answering in
vec4f is fine alone and ambiguous the moment its result becomes the next conversion's
argument: x |> uint3 |> int3 emits cvt_pass(cvt_uint3(..)) and all six cvt_pass
overloads accept a vec4f equally. A single conversion compiled and a constant chain folded
away before codegen, so only a runtime value through two conversions reached it.
include/daScript/simulate/REVIEW.md carries the rule so a cvt_ added later cannot
reintroduce it.

The two items with no code change

  • Item 3 (JIT panic on unreachable code after return) was already fixed by 0e93e8a9c,
    which landed after the base the report was built from. The repro compiles and runs under
    -jit today.
  • Item 4 (fixed-array canonical form) does not reproduce on this base. Built at the
    reported 5c734c4d6 it does; the non-canonical node is a bool[2] carrying const on both
    the array node and its element. No live path was found here and no fix is guessed at.

getCountOf64 multiplied the running product by uint64_t(fixedDim) for every
node of a fixed-array chain. dimAuto is -1 and dimConst is -2, so an
unresolved or already-rejected dimension wrapped the product, and the size
that fell out read as enormous rather than unknown: pointer arithmetic on
class<block[]> reached getSizeOf with 18446744073709551568, which is 2^64-48.

Such a dimension now contributes no count at all, which is what a dimension
of 0 - the other spelling the parser rejects - already did, and what the
getSizeOf64(bool&) overload has always answered for the same input. The two
disagreed before this: the flagged one said 0, the flagless one 2^64-48.
The size gate ran on locals, arguments, block arguments, structures, aliases,
ascend and new, but never on a function result. A result too big to size
therefore reached Program::simulate, where makeFunctionDebugInfo walks it and
getSizeOf/getStride assert - and in a build with DAS_NO_ASSERTIONS it walked
on with a truncated size instead.

preVisit(Function*) now sizes the result the way preVisitArgument sizes an
argument, skipping a result that is still auto or an alias, so a generic that
has not been instanced is left alone.

The fixture carries both shapes: the reported one, whose make-value on the path
is diagnosed too, and one where nothing but the result carries the size.
@aleksisch
aleksisch force-pushed the aleksisch/fuzzer-fizes branch from 73d0e38 to 4b5d543 Compare September 10, 2026 08:28
…ever resolves

A piped call that lands past the first parameter pads the defaults of the
parameters it shifts across. isFunctionCompatiblePipedAt admits the candidate
on those defaults existing - it type-checks only the arguments the caller
actually wrote - and tryPipedCallPadding then splices them into the call. So
instancing is handed an argument lookup never matched, the alias loop gives up
without progress, and inferGenericType answers null for it. The next statement
writes through that null under DAS_ASSERTF, which DAS_NO_ASSERTIONS compiles
out, so a stock Release daslang segfaults rather than reporting anything.

The padded defaults are now matched once they are resolved, but only where the
parameter type is still auto or an alias - a type that never resolves can
neither be matched nor instanced, so the candidate is dropped and the call
reports a lookup miss. A parameter with a concrete type deliberately stays: its
mismatch is a real user error, and the instanced function names it far better
(error[30161], both types spelled out) than a lookup miss would. Matching those
too was tried and rejected for exactly that reason.

With the null's only producer closed off, DAS_ASSERTF(resT, ...) keeps its
meaning and stays: if some other path ever yields that null, it is a compiler
defect and must be seen as one, not reported to the user as a fault in their
code.

The other assert on that path goes, because what it claims is false. The
no-progress exit is reachable whenever a concrete parameter's default does not
match, and falling through is what lets the instanced function produce
error[30161]; asserting there fires on a legitimate diagnostic.

failed_generic_unknown_arg_type pins the unresolved shape - without the lookup
match it aborts on the resT assert - and failed_generic_default_type_mismatch
pins the concrete one, which aborts if the deleted assert is put back.
@aleksisch
aleksisch force-pushed the aleksisch/fuzzer-fizes branch 2 times, most recently from e7c5ae2 to 9b37275 Compare September 10, 2026 15:36
Every vec2/vec3/vec4 flavor converts from vec4f, so the six cvt_pass overloads
could not be resolved once their argument was another cvt_'s result - which is
what the emitter writes for a conversion chain:

    int3 v = cvt_pass(cvt_uint3(das_alias<Point3>::from(getSamplePoint3())));

A single conversion compiled and a constant chain folded away before codegen,
so only a runtime value through two conversions reached it.

cvt_pass collapses to one function taking and returning vec4f, which is the
identity a pass-through conversion should be. Retyping the cvt_ results to
their concrete vector type would also resolve the call and is the wrong trade:
vec4f is the SIMD register while vec2/vec3/vec4 are structs of scalars, so a
concrete return spills the lanes through v_extract_* and the next conversion
reloads them - one cvttps2dq becomes a dozen instructions of shuffles and
stack traffic. All twelve cvt_ keep their vec4f result.

vec_constructors runs the chains on globals the folder cannot reach, two links
and three; the AOT lane compiles the file, so the ambiguity is a build failure
there rather than a wrong value.
@aleksisch
aleksisch force-pushed the aleksisch/fuzzer-fizes branch from 9b37275 to 92e56e6 Compare September 10, 2026 15:41
@borisbat
borisbat merged commit 46b467f into master Sep 10, 2026
31 checks passed
@borisbat
borisbat deleted the aleksisch/fuzzer-fizes branch September 10, 2026 19:23
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.

Fuzzer crashes (2)

2 participants