Repository: shader-slang/slang (2026-04-01T08:37:15Z)

Issue:
Crash when mutating through property/subscript ref accessor with interface type
## Issue Description

Using a property or subscript `ref` accessor where the value type is an interface causes a segfault at runtime when mutation is performed through the returned reference. The compiler silently accepts the code and generates SPIRV/HLSL without error, but the generated code is incorrect -- the ref accessor returns a pointer directly into the existential tuple (AnyValue-packed storage), bypassing the pack/unpack marshalling needed for dynamic dispatch.

Read-only access through the ref accessor appears to work, but any mutation through it crashes.

## Reproducer Code

```hlsl
[anyValueSize(16)]
interface IFoo {
    float getValue();
    [mutating] void setValue(float v);
}

struct ImplA : IFoo {
    float v;
    float getValue() { return v; }
    [mutating] void setValue(float nv) { v = nv; }
}

struct Container {
    IFoo stored;
    property IFoo val {
        get { return stored; }
        ref { return stored; }
    }
}

RWStructuredBuffer<float> outputBuffer;

[numthreads(1, 1, 1)]
void computeMain() {
    Container c;
    c.stored = ImplA(3.0);
    c.val.setValue(7.0);  // crashes here
    outputBuffer[0] = c.val.getValue();
}
```

**Command:**
```bash
slangc -target cpu test.slang
```

## Expected Behavior

Either a diagnostic error rejecting `ref` accessors on interface-typed properties/subscripts, or correct runtime behavior with proper AnyValue marshalling.

## Actual Behavior

Segfault (SIGSEGV) at runtime on CPU target. The same pattern also affects subscript `ref` accessors with interface types.

## Environment

- Slang Version: current main branch
- OS: macOS
- Target: CPU (compile succeeds on SPIRV/HLSL but runtime behavior untested there)

## Additional Context

The related issue #9906 (diagnostic for `__ref`/`__constref` parameters with interface types) is now resolved. Diagnostic 52010 exists and correctly rejects `__ref`/`__constref` **parameters** of interface types, but does not cover property/subscript `ref` **accessor returns**. The diagnostic should be extended to also reject ref accessors when the value type is or contains a dynamic-dispatch interface.

## Test Plan

Enable disabled tests in #10245


Discussion:

Patch:
diff --git a/source/slang/slang-check-decl.cpp b/source/slang/slang-check-decl.cpp
index e18abb7159b..7423db4e73e 100644
--- a/source/slang/slang-check-decl.cpp
+++ b/source/slang/slang-check-decl.cpp
@@ -14360,6 +14360,86 @@ Type* SemanticsDeclHeaderVisitor::_getAccessorStorageType(AccessorDecl* decl)
     }
 }
 
+// Check whether an AST type includes a dynamic-dispatch interface,
+// i.e. a non-COM, non-builtin interface that requires AnyValue packing.
+// This mirrors the IR-level `typeIncludesDynamicDispatch()` in
+// slang-ir-typeflow-specialize.cpp but operates on AST types so it
+// can be used during semantic checking.
+//
+// `visitor` is used to ensure struct field types are resolved via
+// ensureDecl before inspection.  `seenDecls` guards against infinite
+// recursion on self-recursive aggregates.
+static bool _astTypeIncludesDynamicDispatch(
+    SemanticsVisitor* visitor,
+    Type* type,
+    HashSet<Decl*>& seenDecls)
+{
+    // Peel ModifiedType wrappers (e.g. `const`, `volatile`).
+    while (auto modifiedType = as<ModifiedType>(type))
+        type = modifiedType->getBase();
+
+    if (auto declRefType = as<DeclRefType>(type))
+    {
+        if (auto interfaceDecl = as<InterfaceDecl>(declRefType->getDeclRef().getDecl()))
+        {
+            bool isCom = interfaceDecl->findModifier<ComInterfaceAttribute>() != nullptr;
+            bool isBuiltin = interfaceDecl->findModifier<BuiltinAttribute>() != nullptr;
+            if (!isCom && !isBuiltin)
+                return true;
+        }
+    }
+
+    if (auto structType = as<DeclRefType>(type))
+    {
+        if (auto structDecl = as<StructDecl>(structType->getDeclRef().getDecl()))
+        {
+            if (!seenDecls.add(structDecl))
+                return false;
+            for (auto field : structDecl->getFields())
+            {
+                visitor->ensureDecl(field, DeclCheckState::CanUseTypeOfValueDecl);
+                // Use getMemberDeclRef + getType to apply generic substitutions,
+                // so that e.g. Box<IFoo> with field `T item` resolves T to IFoo.
+                auto fieldDeclRef = visitor->getASTBuilder()
+                                        ->getMemberDeclRef(structType->getDeclRef(), field)
+                                        .as<VarDeclBase>();
+                auto fieldType = fieldDeclRef ? getType(visitor->getASTBuilder(), fieldDeclRef)
+                                              : field->type.type;
+                if (fieldType && _astTypeIncludesDynamicDispatch(visitor, fieldType, seenDecls))
+                    return true;
+            }
+            seenDecls.remove(structDecl);
+        }
+    }
+
+    if (auto arrayType = as<ArrayExpressionType>(type))
+    {
+        if (auto elemType = arrayType->getElementType())
+            return _astTypeIncludesDynamicDispatch(visitor, elemType, seenDecls);
+    }
+
+    if (auto optionalType = as<OptionalType>(type))
+    {
+        if (auto valType = optionalType->getValueType())
+            return _astTypeIncludesDynamicDispatch(visitor, valType, seenDecls);
+    }
+
+    if (auto tupleType = as<TupleType>(type))
+    {
+        for (Index i = 0; i < tupleType->getMemberCount(); i++)
+        {
+            if (auto memberType = tupleType->getMember(i))
+            {
+                if (_astTypeIncludesDynamicDispatch(visitor, memberType, seenDecls))
+                    return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+
 void SemanticsDeclHeaderVisitor::visitAccessorDecl(AccessorDecl* decl)
 {
 
@@ -14393,6 +14473,25 @@ void SemanticsDeclHeaderVisitor::visitAccessorDecl(AccessorDecl* decl)
     //
     decl->returnType.type = _getAccessorStorageType(decl);
 
+    // A `ref` accessor returns a reference to the storage location.
+    // If the storage type involves a dynamic-dispatch interface, the
+    // reference would point directly into AnyValue-packed existential
+    // storage, bypassing the pack/unpack marshalling that dynamic
+    // dispatch requires.  This leads to runtime corruption/segfaults,
+    // so we reject it at compile time.
+    if (as<RefAccessorDecl>(decl))
+    {
+        auto storageType = decl->returnType.type;
+        HashSet<Decl*> seenDecls;
+        if (storageType && _astTypeIncludesDynamicDispatch(this, storageType, seenDecls))
+        {
+            getSink()->diagnose(Diagnostics::RefAccessorWithInterfaceTypeInDynamicDispatch{
+                .valueType = storageType,
+                .location = decl->loc});
+            decl->returnType.type = getASTBuilder()->getErrorType();
+        }
+    }
+
     checkDifferentiableCallableCommon(decl);
 }
 
diff --git a/source/slang/slang-diagnostics.lua b/source/slang/slang-diagnostics.lua
index 3b6274d56d8..8f7cd24609a 100644
--- a/source/slang/slang-diagnostics.lua
+++ b/source/slang/slang-diagnostics.lua
@@ -4600,6 +4600,13 @@ err(
     span { loc = "location", message = "'~paramKind' parameter of type '~paramType:IRInst' cannot be used in a dynamic dispatch context." }
 )
 
+err(
+    "ref-accessor-with-interface-type-in-dynamic-dispatch",
+    52012,
+    "ref accessor incompatible with dynamic dispatch",
+    span { loc = "location", message = "'ref' accessor returning type '~valueType:Type' is incompatible with dynamic dispatch because interface types require AnyValue marshalling." }
+)
+
 warning(
     "mesh-output-must-be-out",
     54001,


Test patch:
diff --git a/tests/language-feature/dynamic-dispatch/diagnose-ref-interface-return.slang b/tests/language-feature/dynamic-dispatch/diagnose-ref-interface-return.slang
index 460e1ea8ad7..6fac537ec26 100644
--- a/tests/language-feature/dynamic-dispatch/diagnose-ref-interface-return.slang
+++ b/tests/language-feature/dynamic-dispatch/diagnose-ref-interface-return.slang
@@ -1,7 +1,6 @@
-// Tests disabled: diagnostic 52010 covers __ref/__constref parameters (#9906)
-// but has not been extended to ref accessor returns yet (see #10244).
-//DISABLED_TEST:SIMPLE(filecheck=CHECK): -target spirv
-//DISABLED_TEST:SIMPLE(filecheck=CHECK): -target hlsl
+// Tests that ref accessors returning interface types are rejected (#10244).
+//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK): -target spirv
+//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK): -target hlsl
 
 // Returning a reference to an interface-typed value is incompatible
 // with dynamic dispatch. Interface values are stored as existential
@@ -36,16 +35,15 @@ struct ImplB : IFoo
 }
 
 // Scenario 1: Property ref accessor with direct interface type.
-// The ref accessor implicitly returns a pointer to the existential
-// tuple, which is unsound for dynamic dispatch.
 struct ContainerPropRef
 {
     IFoo stored;
     property IFoo val
     {
         get { return stored; }
-        // CHECK: ([[# @LINE+1]]): error 52010
         ref { return stored; }
+//CHECK:^^^ ref accessor incompatible with dynamic dispatch
+//CHECK:^^^ 'ref' accessor returning type 'IFoo' is incompatible with dynamic dispatch because interface types require AnyValue marshalling.
     }
 }
 
@@ -56,14 +54,13 @@ struct CollectionSubRef
     __subscript(int i) -> IFoo
     {
         get { return items[i]; }
-        // CHECK: ([[# @LINE+1]]): error 52010
         ref { return items[i]; }
+//CHECK:^^^ ref accessor incompatible with dynamic dispatch
+//CHECK:^^^ 'ref' accessor returning type 'IFoo' is incompatible with dynamic dispatch because interface types require AnyValue marshalling.
     }
 }
 
 // Scenario 3: Property ref accessor with struct containing interface.
-// The struct has an interface-typed field, so a reference to it also
-// provides indirect access to AnyValue-packed storage.
 struct Wrapper
 {
     IFoo item;
@@ -75,14 +72,13 @@ struct ContainerStructRef
     property Wrapper val
     {
         get { return w; }
-        // CHECK: ([[# @LINE+1]]): error 52010
         ref { return w; }
+//CHECK:^^^ ref accessor incompatible with dynamic dispatch
+//CHECK:^^^ 'ref' accessor returning type 'Wrapper' is incompatible with dynamic dispatch because interface types require AnyValue marshalling.
     }
 }
 
 // Scenario 4: Property ref accessor with Tuple containing interface.
-// The check should recurse into generic compound types, just as it
-// does for __ref/__constref parameters.
 typealias TupleWithInterface = Tuple<IFoo, float>;
 
 struct ContainerTupleRef
@@ -91,8 +87,9 @@ struct ContainerTupleRef
     property TupleWithInterface val
     {
         get { return t; }
-        // CHECK: ([[# @LINE+1]]): error 52010
         ref { return t; }
+//CHECK:^^^ ref accessor incompatible with dynamic dispatch
+//CHECK:^^^ 'ref' accessor returning type 'Tuple<IFoo, float>' is incompatible with dynamic dispatch because interface types require AnyValue marshalling.
     }
 }
 
@@ -103,12 +100,119 @@ struct ContainerOptionalRef
     property Optional<IFoo> val
     {
         get { return opt; }
-        // CHECK: ([[# @LINE+1]]): error 52010
         ref { return opt; }
+//CHECK:^^^ ref accessor incompatible with dynamic dispatch
+//CHECK:^^^ 'ref' accessor returning type 'Optional<IFoo>' is incompatible with dynamic dispatch because interface types require AnyValue marshalling.
     }
 }
 
-// CHECK-NOT: error 52010
+// Scenario 6: Generic struct with interface type parameter.
+// The check must apply generic substitutions to see through Box<IFoo>.
+struct Box<T>
+{
+    T item;
+}
+
+struct ContainerGenericRef
+{
+    Box<IFoo> b;
+    property Box<IFoo> val
+    {
+        get { return b; }
+        ref { return b; }
+//CHECK:^^^ ref accessor incompatible with dynamic dispatch
+//CHECK:^^^ 'ref' accessor returning type 'Box<IFoo>' is incompatible with dynamic dispatch because interface types require AnyValue marshalling.
+    }
+}
+
+// Scenario 7: Array of interface type.
+struct ContainerArrayRef
+{
+    IFoo arr[4];
+    property IFoo[4] val
+    {
+        get { return arr; }
+        ref { return arr; }
+//CHECK:^^^ ref accessor incompatible with dynamic dispatch
+//CHECK:^^^ 'ref' accessor returning type 'IFoo[4]' is incompatible with dynamic dispatch because interface types require AnyValue marshalling.
+    }
+}
+
+// Scenario 8: Nested depth > 1 (struct containing struct containing interface).
+struct Inner
+{
+    IFoo item;
+}
+
+struct Outer
+{
+    Inner inner;
+}
+
+struct ContainerNestedRef
+{
+    Outer o;
+    property Outer val
+    {
+        get { return o; }
+        ref { return o; }
+//CHECK:^^^ ref accessor incompatible with dynamic dispatch
+//CHECK:^^^ 'ref' accessor returning type 'Outer' is incompatible with dynamic dispatch because interface types require AnyValue marshalling.
+    }
+}
+
+// --- Positive tests: these should NOT produce error 52012 ---
+
+// Positive 1: ref accessor on a concrete (non-interface) type is fine.
+struct ContainerConcreteRef
+{
+    float stored;
+    property float val
+    {
+        get { return stored; }
+        ref { return stored; }
+    }
+}
+
+// Positive 2: get-only accessor on interface type is fine (no ref).
+struct ContainerGetOnly
+{
+    IFoo stored;
+    property IFoo val
+    {
+        get { return stored; }
+    }
+}
+
+// Positive 3: get/set accessor on interface type is fine (no ref).
+struct ContainerSetOnly
+{
+    IFoo stored;
+    property IFoo val
+    {
+        get { return stored; }
+        set { stored = newValue; }
+    }
+}
+
+// Positive 4: COM interface with ref accessor is fine (COM uses vtable, not AnyValue).
+[COM("BE18F5D2-4522-4AB0-A6EE-1D157FA2B084")]
+interface ICom
+{
+    float getValue();
+}
+
+struct ContainerComRef
+{
+    ICom stored;
+    property ICom val
+    {
+        get { return stored; }
+        ref { return stored; }
+    }
+}
+
+// CHECK-NOT: error 52012
 
 IFoo factory(uint id)
 {
@@ -118,10 +222,6 @@ IFoo factory(uint id)
 
 RWStructuredBuffer<float> outputBuffer;
 
-// The diagnostic fires at the ref accessor declaration for all five
-// scenarios above. Only scenarios 1-3 are exercised below to show
-// typical consumer patterns; scenarios 4-5 (Tuple, Optional) don't
-// need runtime usage since the error is caught at declaration time.
 [numthreads(1, 1, 1)]
 void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
 {