From b4aa025fdfc68f7ebcca3f25160e21a477abc7cb Mon Sep 17 00:00:00 2001 From: Harsh Aggarwal Date: Fri, 3 Jul 2026 16:51:08 +0530 Subject: [PATCH 1/4] Make bound-variable recursion and tensor marshalls safe for reference-typed fields Slang's CUDA target is gaining a by-reference ABI for entry-point uniform structs that carry descriptor tables (fixed-size arrays of resources or pointer-backed tensors): such a parameter reflects as an implicit ParameterBlock sub-object (shader-slang/slang#11774). The bound-variable recursion navigated into such a field with cursor[name] and passed the resulting cursor to children. Field lookups on a reference-typed cursor auto-dereference into the sub-object, so a child marshall caching those (sub-object-relative) offsets while writing through cursor.shader_object() - still the parent object - would silently corrupt memory. Dereference once at the recursion site so children always receive a cursor whose shader_object() owns the offsets they extract, matching what the ParameterBlock fallback path already does for call_data. TensorMarshall and NativeTorchTensorMarshall additionally fail loudly (SGL_CHECK) if their own bound field is ever reference-typed, converting what would be silent corruption under a compiler/host version skew into a hard error. Tensor types themselves are never passed by reference, so the check never fires on supported shapes. This change is compatible with both current and by-reference Slang compilers (is_reference() is simply false everywhere today), and must land before slangpy bumps to a Slang containing the new ABI. --- src/slangpy_ext/utils/slangpy.cpp | 10 ++++++++++ src/slangpy_ext/utils/slangpytensor.cpp | 16 ++++++++++++++++ src/slangpy_ext/utils/slangpytorchtensor.cpp | 9 +++++++++ 3 files changed, 35 insertions(+) diff --git a/src/slangpy_ext/utils/slangpy.cpp b/src/slangpy_ext/utils/slangpy.cpp index ca4be81d0..a2d0a8c33 100644 --- a/src/slangpy_ext/utils/slangpy.cpp +++ b/src/slangpy_ext/utils/slangpy.cpp @@ -256,6 +256,16 @@ void NativeBoundVariableRuntime::write_shader_cursor_pre_dispatch( // We have children, so generate call data for each child and // store in a dictionary, then store the dictionary as the call data. ShaderCursor child_field = cursor[m_variable_name.c_str()]; + // A reference-typed field is a ConstantBuffer/ParameterBlock sub-object — + // e.g. Slang's CUDA target passes an entry-point uniform struct containing a + // fixed-size descriptor array by reference as an implicit ParameterBlock. + // Dereference before recursing so that children see a cursor whose + // shader_object() owns the offsets they extract: field lookups on a + // reference cursor auto-dereference (yielding sub-object-relative offsets), + // so a child that cached those offsets but wrote through the parent's + // shader object would silently corrupt memory. + if (child_field.is_reference()) + child_field = child_field.dereference(); for (const auto& [name, child_ref] : *m_children) { if (child_ref) { nb::object child_value = value[name.c_str()]; diff --git a/src/slangpy_ext/utils/slangpytensor.cpp b/src/slangpy_ext/utils/slangpytensor.cpp index a0d71c466..155c5f829 100644 --- a/src/slangpy_ext/utils/slangpytensor.cpp +++ b/src/slangpy_ext/utils/slangpytensor.cpp @@ -214,6 +214,22 @@ void TensorMarshall::ensure_binding_info_cached(ShaderCursor cursor, NativeBound { if (!m_cached_binding_info.primal.is_valid) { ShaderCursor field = cursor[binding->variable_name()]; + // The cached-offset fast path below assumes the tensor's fields live in + // `cursor.shader_object()` at offsets relative to that object. A + // reference-typed field (a ConstantBuffer/ParameterBlock sub-object) breaks + // that assumption: nested field lookups auto-dereference into the + // sub-object, so the cached offsets would be sub-object-relative while the + // write targets the parent object — silent corruption. Tensor types are + // never passed by reference themselves, and reference-typed *enclosing* + // structs are dereferenced before recursion (see + // NativeBoundVariableRuntime::write_shader_cursor_pre_dispatch), so fail + // loudly if one ever reaches this point. + SGL_CHECK( + !field.is_reference(), + "Tensor binding '{}' is reference-typed (a parameter-group sub-object); " + "the cached-offset writer does not support this shape", + binding->variable_name() + ); m_cached_binding_info = extract_binding_info(field); } } diff --git a/src/slangpy_ext/utils/slangpytorchtensor.cpp b/src/slangpy_ext/utils/slangpytorchtensor.cpp index 8c4d4b56f..ae1a4f767 100644 --- a/src/slangpy_ext/utils/slangpytorchtensor.cpp +++ b/src/slangpy_ext/utils/slangpytorchtensor.cpp @@ -222,6 +222,15 @@ void NativeTorchTensorMarshall::ensure_binding_info_cached( { if (!m_cached_binding_info.primal.is_valid) { ShaderCursor field = cursor[binding->variable_name()]; + // See TensorMarshall::ensure_binding_info_cached: the cached-offset writer + // requires the field's offsets to be relative to `cursor.shader_object()`, + // which a reference-typed (parameter-group sub-object) field violates. + SGL_CHECK( + !field.is_reference(), + "Torch tensor binding '{}' is reference-typed (a parameter-group sub-object); " + "the cached-offset writer does not support this shape", + binding->variable_name() + ); m_cached_binding_info = TensorMarshall::extract_binding_info(field); // Determine copy-back flags from the Slang uniform type name. From 6fde419979fd8f884037b5f9819e639223473e9b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:09:13 +0000 Subject: [PATCH 2/4] Apply automatic formatting --- src/slangpy_ext/utils/slangpy.cpp | 2 +- src/slangpy_ext/utils/slangpytensor.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slangpy_ext/utils/slangpy.cpp b/src/slangpy_ext/utils/slangpy.cpp index a2d0a8c33..5527c256c 100644 --- a/src/slangpy_ext/utils/slangpy.cpp +++ b/src/slangpy_ext/utils/slangpy.cpp @@ -256,7 +256,7 @@ void NativeBoundVariableRuntime::write_shader_cursor_pre_dispatch( // We have children, so generate call data for each child and // store in a dictionary, then store the dictionary as the call data. ShaderCursor child_field = cursor[m_variable_name.c_str()]; - // A reference-typed field is a ConstantBuffer/ParameterBlock sub-object — + // A reference-typed field is a ConstantBuffer/ParameterBlock sub-object - // e.g. Slang's CUDA target passes an entry-point uniform struct containing a // fixed-size descriptor array by reference as an implicit ParameterBlock. // Dereference before recursing so that children see a cursor whose diff --git a/src/slangpy_ext/utils/slangpytensor.cpp b/src/slangpy_ext/utils/slangpytensor.cpp index 155c5f829..f56c71d09 100644 --- a/src/slangpy_ext/utils/slangpytensor.cpp +++ b/src/slangpy_ext/utils/slangpytensor.cpp @@ -219,7 +219,7 @@ void TensorMarshall::ensure_binding_info_cached(ShaderCursor cursor, NativeBound // reference-typed field (a ConstantBuffer/ParameterBlock sub-object) breaks // that assumption: nested field lookups auto-dereference into the // sub-object, so the cached offsets would be sub-object-relative while the - // write targets the parent object — silent corruption. Tensor types are + // write targets the parent object - silent corruption. Tensor types are // never passed by reference themselves, and reference-typed *enclosing* // structs are dereferenced before recursion (see // NativeBoundVariableRuntime::write_shader_cursor_pre_dispatch), so fail From 123279b65a14fe755b83e7a84e072d358e2adab1 Mon Sep 17 00:00:00 2001 From: Harsh Aggarwal Date: Fri, 3 Jul 2026 19:51:19 +0530 Subject: [PATCH 3/4] Make NativeValueMarshall's cached-offset writer reference-aware The vectorized-array path binds a generated Array1DValueType struct wrapping the array. When T carries descriptors (tensors, buffers), Slang's CUDA by-reference ABI reflects that parameter as a ParameterBlock sub-object. NativeValueMarshall::ensure_cached navigated it with cursor[name]["value"]: the nested lookup auto-dereferences into the sub-object (making the cached offset sub-object-relative), while the write constructed a cursor on cursor.shader_object() - the parent object - producing out-of-bounds raw writes and heap corruption (test_vectorize_struct_with_tensor_array and friends on CUDA). Dereference explicitly when the bound field is reference-typed, cache the field index, and target the sub-object's ShaderObject at write time - the same pattern the call_data ParameterBlock fallback and the bound-variable recursion already use. No behavior change with compilers that pass everything by value (is_reference() is false everywhere). --- src/slangpy_ext/utils/slangpyvalue.cpp | 25 ++++++++++++++++++++++--- src/slangpy_ext/utils/slangpyvalue.h | 6 ++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/slangpy_ext/utils/slangpyvalue.cpp b/src/slangpy_ext/utils/slangpyvalue.cpp index 6f6543ff9..832d38653 100644 --- a/src/slangpy_ext/utils/slangpyvalue.cpp +++ b/src/slangpy_ext/utils/slangpyvalue.cpp @@ -16,8 +16,21 @@ void NativeValueMarshall::ensure_cached(ShaderCursor cursor, NativeBoundVariable { if (m_cached.is_valid) return; - ShaderCursor field - = binding->direct_bind() ? cursor[binding->variable_name()] : cursor[binding->variable_name()]["value"]; + ShaderCursor field = cursor[binding->variable_name()]; + // A reference-typed field is a ConstantBuffer/ParameterBlock sub-object — e.g. + // Slang's CUDA target passes an entry-point uniform struct carrying a fixed-size + // descriptor array (such as the vectorized-array wrapper Array1DValueType holding + // tensors) by reference. Nested lookups below auto-dereference into the + // sub-object, making every cached offset sub-object-relative, so the write must + // target the sub-object's ShaderObject (see write_shader_cursor_pre_dispatch); + // writing through the parent object with these offsets would corrupt memory. + m_cached.field_is_reference = field.is_reference(); + if (m_cached.field_is_reference) { + m_cached.field_index = cursor.find_field_index(binding->variable_name()); + field = field.dereference(); + } + if (!binding->direct_bind()) + field = field["value"]; m_cached.value_offset = field.offset(); m_cached.value_type_layout = field.slang_type_layout(); m_cached.writer = get_shader_cursor_writer(m_cached.value_type_layout); @@ -38,7 +51,13 @@ void NativeValueMarshall::write_shader_cursor_pre_dispatch( AccessType primal_access = binding->access().first; if (!value.is_none() && (primal_access == AccessType::read || primal_access == AccessType::readwrite)) { ensure_cached(cursor, binding); - ShaderCursor value_cursor(cursor.shader_object(), m_cached.value_type_layout, m_cached.value_offset); + // For a reference-typed field the cached offsets are relative to the + // sub-object; re-resolve it (cheap: cached field index + object lookup) and + // write there instead of into the parent object. + ShaderObject* target_object = cursor.shader_object(); + if (m_cached.field_is_reference) + target_object = cursor.get_field_by_index(m_cached.field_index).dereference().shader_object(); + ShaderCursor value_cursor(target_object, m_cached.value_type_layout, m_cached.value_offset); if (m_cached.writer) { m_cached.writer(value_cursor, value); } else { diff --git a/src/slangpy_ext/utils/slangpyvalue.h b/src/slangpy_ext/utils/slangpyvalue.h index 41040407d..494963218 100644 --- a/src/slangpy_ext/utils/slangpyvalue.h +++ b/src/slangpy_ext/utils/slangpyvalue.h @@ -35,6 +35,12 @@ class NativeValueMarshall : public NativeMarshall { slang::TypeLayoutReflection* value_type_layout = nullptr; ///< Type layout for value field. std::function writer; ///< Pre-resolved writer fn. bool direct_bind{false}; ///< direct_bind value used when populating cache. + /// True when the bound field is reference-typed (a ConstantBuffer/ParameterBlock + /// sub-object, e.g. Slang's CUDA by-reference ABI for descriptor-table-carrying + /// uniforms). value_offset is then relative to the sub-object, and writes must + /// target the sub-object's ShaderObject rather than the parent's. + bool field_is_reference{false}; + int32_t field_index{-1}; ///< Cached field index for the per-dispatch dereference. bool is_valid = false; }; From 9b7ffe19ca88476387568d004368fad4a93f6adb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:29:59 +0000 Subject: [PATCH 4/4] Apply automatic formatting --- src/slangpy_ext/utils/slangpyvalue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slangpy_ext/utils/slangpyvalue.cpp b/src/slangpy_ext/utils/slangpyvalue.cpp index 832d38653..7f3acc32b 100644 --- a/src/slangpy_ext/utils/slangpyvalue.cpp +++ b/src/slangpy_ext/utils/slangpyvalue.cpp @@ -17,7 +17,7 @@ void NativeValueMarshall::ensure_cached(ShaderCursor cursor, NativeBoundVariable if (m_cached.is_valid) return; ShaderCursor field = cursor[binding->variable_name()]; - // A reference-typed field is a ConstantBuffer/ParameterBlock sub-object — e.g. + // A reference-typed field is a ConstantBuffer/ParameterBlock sub-object - e.g. // Slang's CUDA target passes an entry-point uniform struct carrying a fixed-size // descriptor array (such as the vectorized-array wrapper Array1DValueType holding // tensors) by reference. Nested lookups below auto-dereference into the