Status: diagnosed, not fixed. Root cause located and confirmed; no fix is proposed here, because
the correct fix depends on a decision I could not make from the evidence (see §5).
1. Reproducer
Deterministic, 3/3, on upstream/master @ ba1f1aecb5, Release, clang-18, Linux x86-64:
build/Release/bin/slangc -target spirv-asm -experimental-feature \
-I tests/neural tests/neural/network-parameter-layout-converter-buffer.slang -o /tmp/out.spvasm
# Segmentation fault (exit 139)
The trigger is the absence of -entry. With an explicit entry point it compiles cleanly:
slangc ... -entry computeMain -stage compute ... # rc=0
slangc ... -entry roundTripMain -stage compute ... # rc=0
The test itself is unaffected in normal use — slang-test tests/neural/network-parameter-layout-converter-buffer.slang
passes 8/8, because the TEST lines pass -entry. This is why it has gone unnoticed.
2. Crash site
#0 Slang::IRInst::getOperand (this=0x0, index=1) slang-ir.h:712
#1 Slang::IRStructField::getFieldType (this=0x0) slang-ir.h:1693
#2 Slang::TupleLoweringContext::processGetTupleElement (...) slang-ir-lower-tuple-types.cpp:137
#3 Slang::TupleLoweringContext::processModule (...) slang-ir-lower-tuple-types.cpp:499
#4 Slang::lowerTuples (...) slang-ir-lower-tuple-types.cpp:517
#5 Slang::linkAndOptimizeIR (...) slang-emit.cpp:1659
processGetTupleElement does:
auto elementIndex = getIntVal(inst->getElementIndex());
SLANG_ASSERT((Index)elementIndex < loweredTupleInfo->fields.getCount()); // compiled out in Release
auto field = loweredTupleInfo->fields[(Index)elementIndex];
auto getElement = builder->emitFieldExtract(field->getFieldType(), base, field->getKey());
Instrumenting it shows, 3 times:
GetTupleElement: idx=1 fields=1 baseOp=tuple_type inFunc=convertNetworkToPortableLayout
^^ OUT OF RANGE
So the index is out of range, the debug assert is compiled out in Release, the List read runs past
the end and yields 0, and field->getFieldType() dereferences null. In a debug build this would
be a clean assertion failure; in Release it is a segfault.
3. The malformed IR
-dump-ir-before lowerTuples shows the input is already wrong before tuple lowering:
let %49 : TypePack(Int) = makeValuePack(2 : Int) <- a pack with exactly ONE element
...
call %convertOneLayerToPortableLayout1(..., 2 : Int, %elementIndex2) <- iteration 0
let %48 : Int = getTupleElement(%49, 1 : Int) <- iteration 1
call %convertOneLayerToPortableLayout1(..., %48, %elementIndex2)
The expand was unrolled for two iterations, but the pack it indexes has one element.
Iteration 1 asks for %49[1], which does not exist.
The source is the variadic-generic expansion in
source/standard-modules/neural/network-parameter-layout-converter.slang:438:
internal void convertNetworkToPortableLayout<..., let each OutDim : int>(...)
{
expand convertOneLayerToPortableLayout<T, OptimalAddress, PortableAddress>(
biasMask, inputSize, physicalOffset, logicalOffset,
optimalLayoutStorage, portableLayoutStorage,
each OutDim, elementIndex);
}
4. Root cause
maybeSpecializeExpand() in source/slang/slang-ir-specialize.cpp:3164 decides how many times to
unroll an IRExpand by looking at only the first capture:
UInt elementCount = 0;
if (auto firstTypePack = as<IRTypePack>(expandInst->getCapture(0)))
elementCount = firstTypePack->getOperandCount();
else if (auto firstValuePack = as<IRMakeValuePack>(expandInst->getCapture(0)))
elementCount = firstValuePack->getOperandCount();
It then unrolls elementCount times, binding the expand's index parameter to the literal i:
auto indexParam = expandInst->getFirstBlock()->getFirstParam();
cloneEnv.mapOldValToNew[indexParam] = subBuilder.getIntValue(subBuilder.getIntType(), i);
That single index is then substituted into every pack the pattern indexes. clonePatternValImpl
in slang-ir-lower-expand-type.cpp:31 turns each IREach into
emitGetTupleElement(type, packInst, eachIndex) using that same index, with no relation to the
arity of packInst.
So when capture 0 has arity 2 and another each-ed capture has arity 1, iteration i=1 emits
getTupleElement(shorterPack, 1). Nothing between there and lowerTuples rejects it, and
lowerTuples is where it finally dereferences null.
5. Why no fix is proposed
Per the repository's own methodology, the question to answer is whether the input shape —
an IRExpand whose each-ed captures have differing arity — is legitimate.
It is not: parallel pack expansion requires all expanded packs to have equal length. So the
defect is upstream of maybeSpecializeExpand, and there are two candidate owners, which I could not
distinguish with the evidence gathered:
- The pack is built wrong. Something upstream produced a 1-element
makeValuePack where a
2-element pack was intended (note the surviving literal 2 : Int in iteration 0, suggesting one
element was folded to a constant while the pack kept only the remainder). If so, the fix belongs
at that producer.
- The front end should have rejected it. If the two packs genuinely have different arity in the
user program, semantic checking should diagnose the mismatch rather than let it reach the IR.
Deriving elementCount differently inside maybeSpecializeExpand — say, from the shortest each-ed
pack, or by scanning the body for IREach — would stop the crash while silently changing the
meaning of the program, which would be masking a representation bug rather than fixing it. I did not
do that.
A defensible narrow hardening, independent of the above: the bound check in
processGetTupleElement is a SLANG_ASSERT, so in Release it degrades to an out-of-range read and a
null dereference. Promoting it to SLANG_RELEASE_ASSERT would turn this class of upstream bug into a
diagnosable failure instead of a segfault. That is a real improvement but is explicitly not a fix
for this bug, and is left out of the PR set so it is not mistaken for one.
6. Environment
Ubuntu 24.04, glibc 2.39, clang-18, Release,
-DSLANG_ENABLE_DXIL=OFF -DSLANG_SLANG_LLVM_FLAVOR=DISABLE -DSLANG_ENABLE_TESTS=ON.
Discovered incidentally while classifying tests by emitted SPIR-V for PR 1.
Status: diagnosed, not fixed. Root cause located and confirmed; no fix is proposed here, because
the correct fix depends on a decision I could not make from the evidence (see §5).
1. Reproducer
Deterministic, 3/3, on
upstream/master@ba1f1aecb5, Release, clang-18, Linux x86-64:build/Release/bin/slangc -target spirv-asm -experimental-feature \ -I tests/neural tests/neural/network-parameter-layout-converter-buffer.slang -o /tmp/out.spvasm # Segmentation fault (exit 139)The trigger is the absence of
-entry. With an explicit entry point it compiles cleanly:The test itself is unaffected in normal use —
slang-test tests/neural/network-parameter-layout-converter-buffer.slangpasses 8/8, because the
TESTlines pass-entry. This is why it has gone unnoticed.2. Crash site
processGetTupleElementdoes:Instrumenting it shows, 3 times:
So the index is out of range, the debug assert is compiled out in Release, the
Listread runs pastthe end and yields
0, andfield->getFieldType()dereferences null. In a debug build this wouldbe a clean assertion failure; in Release it is a segfault.
3. The malformed IR
-dump-ir-before lowerTuplesshows the input is already wrong before tuple lowering:The
expandwas unrolled for two iterations, but the pack it indexes has one element.Iteration 1 asks for
%49[1], which does not exist.The source is the variadic-generic expansion in
source/standard-modules/neural/network-parameter-layout-converter.slang:438:4. Root cause
maybeSpecializeExpand()insource/slang/slang-ir-specialize.cpp:3164decides how many times tounroll an
IRExpandby looking at only the first capture:It then unrolls
elementCounttimes, binding the expand's index parameter to the literali:That single index is then substituted into every pack the pattern indexes.
clonePatternValImplin
slang-ir-lower-expand-type.cpp:31turns eachIREachintoemitGetTupleElement(type, packInst, eachIndex)using that same index, with no relation to thearity of
packInst.So when capture 0 has arity 2 and another
each-ed capture has arity 1, iterationi=1emitsgetTupleElement(shorterPack, 1). Nothing between there andlowerTuplesrejects it, andlowerTuplesis where it finally dereferences null.5. Why no fix is proposed
Per the repository's own methodology, the question to answer is whether the input shape —
an
IRExpandwhoseeach-ed captures have differing arity — is legitimate.It is not: parallel pack expansion requires all expanded packs to have equal length. So the
defect is upstream of
maybeSpecializeExpand, and there are two candidate owners, which I could notdistinguish with the evidence gathered:
makeValuePackwhere a2-element pack was intended (note the surviving literal
2 : Intin iteration 0, suggesting oneelement was folded to a constant while the pack kept only the remainder). If so, the fix belongs
at that producer.
user program, semantic checking should diagnose the mismatch rather than let it reach the IR.
Deriving
elementCountdifferently insidemaybeSpecializeExpand— say, from the shortesteach-edpack, or by scanning the body for
IREach— would stop the crash while silently changing themeaning of the program, which would be masking a representation bug rather than fixing it. I did not
do that.
A defensible narrow hardening, independent of the above: the bound check in
processGetTupleElementis aSLANG_ASSERT, so in Release it degrades to an out-of-range read and anull dereference. Promoting it to
SLANG_RELEASE_ASSERTwould turn this class of upstream bug into adiagnosable failure instead of a segfault. That is a real improvement but is explicitly not a fix
for this bug, and is left out of the PR set so it is not mistaken for one.
6. Environment
Ubuntu 24.04, glibc 2.39, clang-18, Release,
-DSLANG_ENABLE_DXIL=OFF -DSLANG_SLANG_LLVM_FLAVOR=DISABLE -DSLANG_ENABLE_TESTS=ON.Discovered incidentally while classifying tests by emitted SPIR-V for PR 1.