Skip to content

slangc segfaults lowering an expand whose captured packs have different arity #12796

Description

@jvepsalainen-nv

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugreproducedProvided instructions confirmed to reproduce the issue

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions