diff --git a/BUILD.gn b/BUILD.gn index 447c73d22db..443fb9cc572 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -215,6 +215,7 @@ vvl_sources = [ "layers/gpuav/instrumentation/descriptor_checks.h", "layers/gpuav/instrumentation/mesh_shading.cpp", "layers/gpuav/instrumentation/post_process_descriptor_indexing.cpp", + "layers/gpuav/instrumentation/poison_value.cpp", "layers/gpuav/instrumentation/ray_query.cpp", "layers/gpuav/instrumentation/ray_hit_object.cpp", "layers/gpuav/instrumentation/register_validation.h", @@ -242,6 +243,8 @@ vvl_sources = [ "layers/gpuav/spirv/buffer_device_address_pass.h", "layers/gpuav/spirv/mesh_shading_pass.cpp", "layers/gpuav/spirv/mesh_shading_pass.h", + "layers/gpuav/spirv/poison_pass.cpp", + "layers/gpuav/spirv/poison_pass.h", "layers/gpuav/spirv/post_process_descriptor_indexing_pass.cpp", "layers/gpuav/spirv/post_process_descriptor_indexing_pass.h", "layers/gpuav/spirv/vertex_attribute_fetch_oob_pass.cpp", diff --git a/layers/CMakeLists.txt b/layers/CMakeLists.txt index 7644ba27dba..2dc06b7c40a 100644 --- a/layers/CMakeLists.txt +++ b/layers/CMakeLists.txt @@ -340,6 +340,7 @@ target_sources(vvl PRIVATE gpuav/instrumentation/descriptor_checks.cpp gpuav/instrumentation/mesh_shading.cpp gpuav/instrumentation/post_process_descriptor_indexing.cpp + gpuav/instrumentation/poison_value.cpp gpuav/instrumentation/ray_query.cpp gpuav/instrumentation/ray_hit_object.cpp gpuav/instrumentation/register_validation.h diff --git a/layers/VkLayer_khronos_validation.json.in b/layers/VkLayer_khronos_validation.json.in index df2ec8b8144..2ff1a15b931 100644 --- a/layers/VkLayer_khronos_validation.json.in +++ b/layers/VkLayer_khronos_validation.json.in @@ -903,6 +903,21 @@ ] } }, + { + "key": "gpuav_poison_value", + "label": "Poison value (uninitialized variable detection)", + "description": "Instrument shaders to detect uses of uninitialized values.", + "type": "BOOL", + "view": "DEBUG", + "default": true, + "dependence": { + "mode": "ALL", + "settings": [ + { "key": "gpuav_enable", "value": true }, + { "key": "gpuav_shader_instrumentation", "value": true } + ] + } + }, { "key": "gpuav_shared_memory_data_race", "label": "Shared Memory Data Race Detection", diff --git a/layers/gpuav/core/gpuav_record.cpp b/layers/gpuav/core/gpuav_record.cpp index 32b1e991395..f937688491e 100644 --- a/layers/gpuav/core/gpuav_record.cpp +++ b/layers/gpuav/core/gpuav_record.cpp @@ -167,6 +167,7 @@ void Validator::PreCallRecordBeginCommandBuffer(VkCommandBuffer commandBuffer, c RegisterRayQueryValidation(*this, gpuav_cb_state); RegisterRayHitObjectValidation(*this, gpuav_cb_state); RegisterSharedMemoryDataRaceValidation(*this, gpuav_cb_state); + RegisterPoisonValueValidation(*this, gpuav_cb_state); RegisterSanitizer(*this, gpuav_cb_state); debug_printf::RegisterDebugPrintf(*this, gpuav_cb_state); debug_descriptor::RegisterDebugDescriptor(*this, gpuav_cb_state); diff --git a/layers/gpuav/core/gpuav_settings.cpp b/layers/gpuav/core/gpuav_settings.cpp index 792f66b600b..2f2f5ba0bd8 100644 --- a/layers/gpuav/core/gpuav_settings.cpp +++ b/layers/gpuav/core/gpuav_settings.cpp @@ -33,7 +33,8 @@ bool GpuAVSettings::IsShaderInstrumentationEnabled() const { return shader_instrumentation.descriptor_checks || shader_instrumentation.buffer_device_address || shader_instrumentation.ray_query || shader_instrumentation.ray_hit_object || shader_instrumentation.mesh_shading || shader_instrumentation.post_process_descriptor_indexing || shader_instrumentation.vertex_attribute_fetch_oob || - shader_instrumentation.sanitizer || shader_instrumentation.shared_memory_data_race; + shader_instrumentation.poison_value || shader_instrumentation.sanitizer || + shader_instrumentation.shared_memory_data_race; } bool GpuAVSettings::IsSpirvModified() const { return IsShaderInstrumentationEnabled() || debug_printf_enabled || debug_descriptor_enabled; @@ -48,6 +49,7 @@ void GpuAVSettings::DisableShaderInstrumentationAndOptions() { shader_instrumentation.mesh_shading = false; shader_instrumentation.post_process_descriptor_indexing = false; shader_instrumentation.vertex_attribute_fetch_oob = false; + shader_instrumentation.poison_value = false; shader_instrumentation.sanitizer = false; shader_instrumentation.shared_memory_data_race = false; // Because of this setting, cannot really have an "enabled" parameter to pass to this method @@ -132,6 +134,7 @@ void GpuAVSettings::TracyLogSettings() const { VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(mesh_shading); VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(post_process_descriptor_indexing); VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(vertex_attribute_fetch_oob); + VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(poison_value); VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(sanitizer); VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(shared_memory_data_race); VVL_TRACY_PRINT_GPUAV_SETTING(debug_printf_only); diff --git a/layers/gpuav/core/gpuav_settings.h b/layers/gpuav/core/gpuav_settings.h index b50340941da..186e8d4e0ad 100644 --- a/layers/gpuav/core/gpuav_settings.h +++ b/layers/gpuav/core/gpuav_settings.h @@ -66,6 +66,7 @@ struct GpuAVSettings { bool mesh_shading = true; bool post_process_descriptor_indexing = true; bool vertex_attribute_fetch_oob = true; + bool poison_value = true; bool sanitizer = true; bool shared_memory_data_race = true; } shader_instrumentation; diff --git a/layers/gpuav/instrumentation/gpuav_shader_instrumentor.cpp b/layers/gpuav/instrumentation/gpuav_shader_instrumentor.cpp index 78b7080fb61..7058195b879 100644 --- a/layers/gpuav/instrumentation/gpuav_shader_instrumentor.cpp +++ b/layers/gpuav/instrumentation/gpuav_shader_instrumentor.cpp @@ -32,6 +32,7 @@ #include "gpuav/shaders/gpuav_error_codes.h" #include "gpuav/shaders/gpuav_error_header.h" #include "gpuav/spirv/log_error_pass.h" +#include "gpuav/spirv/poison_pass.h" #include "error_message/spirv_logging.h" #include #include @@ -1637,6 +1638,14 @@ bool GpuShaderInstrumentor::InstrumentShader(const vvl::span& in bool modified = false; + // Poison pass runs first so its checks are outermost: later passes' if/else guards end up + // inside the poison conditional, meaning root-cause "uninitialized value" errors are reported + // before downstream checks (e.g. BDA OOB) that would mask them. + if (gpuav_settings.shader_instrumentation.poison_value) { + spirv::PoisonPass pass(module); + modified |= pass.Run(); + } + // If descriptor indexing is enabled, enable length checks and updated descriptor checks if (gpuav_settings.shader_instrumentation.descriptor_checks) { if (interface.descriptor_mode == vvl::DescriptorModeClassic) { diff --git a/layers/gpuav/instrumentation/poison_value.cpp b/layers/gpuav/instrumentation/poison_value.cpp new file mode 100644 index 00000000000..343df19154f --- /dev/null +++ b/layers/gpuav/instrumentation/poison_value.cpp @@ -0,0 +1,95 @@ +/* Copyright (c) 2026 LunarG, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "generated/spirv_grammar_helper.h" +#include "gpuav/core/gpuav.h" +#include "gpuav/resources/gpuav_state_trackers.h" +#include "gpuav/shaders/gpuav_error_codes.h" +#include "gpuav/shaders/gpuav_error_header.h" + +namespace gpuav { + +void RegisterPoisonValueValidation(Validator& gpuav, CommandBufferSubState& cb) { + if (!gpuav.gpuav_settings.shader_instrumentation.poison_value) { + return; + } + + cb.on_instrumentation_error_logger_register_functions.emplace_back( + [](Validator& gpuav, CommandBufferSubState& cb, const LastBound& last_bound) { + CommandBufferSubState::InstrumentationErrorLogger inst_error_logger = + [](Validator& gpuav, const Location& loc, const uint32_t* error_record, + const InstrumentedShader* instrumented_shader, std::string& out_error_msg, std::string& out_vuid_msg) { + using namespace glsl; + bool error_found = false; + if (GetErrorGroup(error_record) != kErrorGroup_InstPoisonValue) { + return error_found; + } + error_found = true; + + const uint32_t error_sub_code = GetSubError(error_record); + std::ostringstream strm; + + switch (error_sub_code) { + case kErrorSubCode_PoisonValue_UninitializedVariable: { + const uint32_t trigger_opcode = error_record[kInst_LogError_ParameterOffset_0]; + + if (trigger_opcode == spv::OpBranchConditional || trigger_opcode == spv::OpSwitch) { + strm << "Undefined behavior: poison value used in " << string_SpvOpcode(trigger_opcode) << "."; + strm << " A poison value was used as a branch condition."; + out_vuid_msg = "SPIRV-PoisonValue-BranchOnPoison"; + } else if (trigger_opcode == spv::OpAccessChain || trigger_opcode == spv::OpInBoundsAccessChain || + trigger_opcode == spv::OpPtrAccessChain) { + strm << "Undefined behavior: poison value used in " << string_SpvOpcode(trigger_opcode) << "."; + strm << " A poison value was used as an index in an access chain."; + out_vuid_msg = "SPIRV-PoisonValue-AddressFromPoison"; + } else if (trigger_opcode == spv::OpStore) { + strm << "Warning: poison value stored to an externally-visible variable" + << " (not undefined behavior per the SPIR-V spec, but likely an application bug)."; + out_vuid_msg = "SPIRV-PoisonValue-ExternalStoreOfPoison"; + } else if (trigger_opcode == spv::OpReturnValue) { + strm << "Warning: poison value returned from a function" + << " (not undefined behavior per the SPIR-V spec, but likely an application bug)."; + out_vuid_msg = "SPIRV-PoisonValue-ReturnOfPoison"; + } else { + assert(false && "unexpected opcode for poison value error"); + out_vuid_msg = "SPIRV-PoisonValue-UninitializedVariable"; + } + } break; + case kErrorSubCode_PoisonValue_StoreToFunctionParam: { + strm << "Warning: poison value stored to a function out/inout parameter" + << " (not undefined behavior per the SPIR-V spec, but likely an application bug)."; + out_vuid_msg = "SPIRV-PoisonValue-StoreToFunctionParam"; + } break; + case kErrorSubCode_PoisonValue_PoisonPointerDereference: { + const uint32_t trigger_opcode = error_record[kInst_LogError_ParameterOffset_0]; + strm << "Undefined behavior: dereference of a poison pointer in " << string_SpvOpcode(trigger_opcode) + << "."; + out_vuid_msg = "SPIRV-PoisonValue-PoisonPointerDereference"; + } break; + default: + error_found = false; + break; + } + + out_error_msg += strm.str(); + return error_found; + }; + + return inst_error_logger; + }); +} + +} // namespace gpuav diff --git a/layers/gpuav/instrumentation/register_validation.h b/layers/gpuav/instrumentation/register_validation.h index 5fd493f1b3a..067698285b2 100644 --- a/layers/gpuav/instrumentation/register_validation.h +++ b/layers/gpuav/instrumentation/register_validation.h @@ -23,6 +23,7 @@ void RegisterPostProcessingValidation(Validator& gpuav, CommandBufferSubState& c void RegisterRayQueryValidation(Validator& gpuav, CommandBufferSubState& cb); void RegisterRayHitObjectValidation(Validator& gpuav, CommandBufferSubState& cb); void RegisterMeshShadingValidation(Validator& gpuav, CommandBufferSubState& cb); +void RegisterPoisonValueValidation(Validator& gpuav, CommandBufferSubState& cb); void RegisterSanitizer(Validator& gpuav, CommandBufferSubState& cb); void RegisterVertexAttributeFetchOobValidation(Validator& gpuav, CommandBufferSubState& cb); void RegisterSharedMemoryDataRaceValidation(Validator& gpuav, CommandBufferSubState& cb); diff --git a/layers/gpuav/shaders/gpuav_error_codes.h b/layers/gpuav/shaders/gpuav_error_codes.h index d7830189a97..517d6303546 100644 --- a/layers/gpuav/shaders/gpuav_error_codes.h +++ b/layers/gpuav/shaders/gpuav_error_codes.h @@ -43,6 +43,7 @@ const int kErrorGroup_GpuPreBuildAccelerationStructures = 12; const int kErrorGroup_InstMeshShading = 13; const int kErrorGroup_InstRayHitObject = 14; const int kErrorGroup_SharedMemoryDataRace = 15; +const int kErrorGroup_InstPoisonValue = 16; // We just take ExecutionModel and normalize it so we only use 5 bits to store it const int kExecutionModel_Vertex = 0; @@ -120,6 +121,12 @@ const int kErrorSubCode_RayHitObject_SkipAABBsWithPipelineSkipTriangles = 14; const int kErrorSubCode_RayHitObject_TimeOutOfRange = 15; const int kErrorSubCode_RayHitObject_SBTIndexExceedsLimit = 16; +// Poison value (uninitialized / poisoned SSA uses) +// +const int kErrorSubCode_PoisonValue_UninitializedVariable = 1; +const int kErrorSubCode_PoisonValue_StoreToFunctionParam = 2; +const int kErrorSubCode_PoisonValue_PoisonPointerDereference = 3; + // Shared Memory Data Race const int kErrorSubCode_SharedMemoryDataRace_RaceOnStore = 1; const int kErrorSubCode_SharedMemoryDataRace_RaceOnLoad = 2; diff --git a/layers/gpuav/shaders/instrumentation/poison_value.comp b/layers/gpuav/shaders/instrumentation/poison_value.comp new file mode 100644 index 00000000000..55724f3ce96 --- /dev/null +++ b/layers/gpuav/shaders/instrumentation/poison_value.comp @@ -0,0 +1,31 @@ +// Copyright (c) 2026 LunarG, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// NOTE: This file doesn't contain any entrypoints and should be compiled with the --no-link option for glslang + +#version 450 +#extension GL_GOOGLE_include_directive : enable +#include "common_descriptor_sets.h" +#include "error_payload.h" + +void inst_poison_report(const bool is_poison, const uint inst_offset, const uint trigger_opcode, const uint error_sub_code) { + if (is_poison) { + error_payload = ErrorPayload( + inst_offset, + SpecConstantLinkShaderId | (kErrorGroup_InstPoisonValue << kErrorGroup_Shift) | (error_sub_code << kErrorSubCode_Shift), + trigger_opcode, + 0, + 0); + } +} diff --git a/layers/gpuav/spirv/CMakeLists.txt b/layers/gpuav/spirv/CMakeLists.txt index 7b6cf83e44e..38ddd8b59a8 100644 --- a/layers/gpuav/spirv/CMakeLists.txt +++ b/layers/gpuav/spirv/CMakeLists.txt @@ -31,6 +31,8 @@ target_sources(gpu_av_spirv PRIVATE ray_hit_object_pass.cpp sanitizer_pass.h sanitizer_pass.cpp + poison_pass.h + poison_pass.cpp shared_memory_data_race_pass.h shared_memory_data_race_pass.cpp debug_descriptor_pass.h diff --git a/layers/gpuav/spirv/poison_pass.cpp b/layers/gpuav/spirv/poison_pass.cpp new file mode 100644 index 00000000000..e860a114253 --- /dev/null +++ b/layers/gpuav/spirv/poison_pass.cpp @@ -0,0 +1,1659 @@ +/* Copyright (c) 2026 LunarG, Inc. + * Copyright (c) 2026 The Khronos Group Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ============================================================================= +// Poison Pass - Poison Value Propagation and UB Detection +// ============================================================================= +// +// This pass instruments SPIR-V shaders to model the propagation of "poison" +// values and detect undefined behavior caused by their use. In SPIR-V, a +// poison value is a value that the spec says has no defined meaning - using +// it in certain ways is UB. +// +// The pass is a general poison-tracking framework. Currently, the only seed +// source is uninitialized variables (Function/Private storage class without +// an initializer), but the architecture supports adding other poison sources +// in the future (see "Future Poison Sources" below). +// +// OVERVIEW +// -------- +// The pass processes each function independently in four phases: +// +// 1. Seed identification - find initial poison sources +// 2. ComputePotentiallyPoisonSet - static dataflow analysis +// 3. CreateShadowVariables - allocate runtime shadow state +// 4. InstrumentFunction - insert shadow tracking and error checks +// +// Each function's analysis is self-contained. Cross-function poison flow is +// handled conservatively at call boundaries (see "Function Calls" below). +// +// SHADOW VARIABLES +// ---------------- +// For each variable that may contain poison, a parallel "shadow" variable is +// created. The shadow has the same composite structure as the original but +// with all leaf types replaced by bool: +// +// uint -> bool +// vec4 -> bvec4 +// float[3] -> bool[3] +// struct{uint a; vec2 b;} -> struct{bool a; bvec2 b;} +// mat3 -> bvec3[3] (array of column shadows) +// +// A shadow value of `true` means "clean" and `false` means "poison". Shadow +// variables for uninitialized sources are initialized to OpConstantNull (all +// false / all poison). When a store writes to a tracked variable, the +// corresponding shadow is updated; when a load reads from it, the shadow +// is loaded and propagated through dependent instructions. +// +// INSTRUCTION CATEGORIES +// ---------------------- +// Instructions are classified into three categories based on how they +// interact with poison: +// +// Pass-through (data movement): +// OpLoad, OpStore (to local), OpSelect, OpPhi, OpCopyObject, OpCopyLogical, +// OpCompositeExtract, OpCompositeInsert, OpCompositeConstruct, +// OpCompositeConstructReplicateEXT, +// OpVectorShuffle, OpVectorExtractDynamic, OpVectorInsertDynamic. +// Shadow follows the data naturally, preserving per-component tracking. +// +// Propagation (computation): +// Arithmetic, bitwise, comparison, conversion, dot product, etc. +// If any input operand is poison, the output is poison (logical AND +// of all operand shadows). When operand and result types differ +// (e.g. dot(vec4, vec4)->float), each operand shadow is reduced to +// scalar bool and broadcast to match the result shadow type. +// No error is reported - poison silently propagates. +// +// UB triggers (error reporting): +// These instructions cause undefined behavior if given poison input: +// - OpBranchConditional / OpSwitch with poison condition/selector +// - OpAccessChain / OpPtrAccessChain with poison index +// An error is reported and the poison condition is replaced with a safe +// constant to prevent the compiler from optimizing away the error +// reporting code. +// +// Likely-bug warnings (error reporting, not spec UB): +// These are not undefined behavior per the SPIR-V spec, but are almost +// certainly application bugs: +// - OpStore to non-local (externally-visible) storage +// - OpStore to a function parameter (out/inout) +// - OpReturnValue of a poison value +// +// STATIC ANALYSIS (ComputePotentiallyPoisonSet) +// --------------------------------------------- +// To avoid instrumenting every instruction, a worklist-based dataflow analysis +// determines which SSA values *could possibly* be poison. Only these values +// receive shadow tracking at runtime. +// +// The analysis seeds the worklist with the result IDs of all OpLoad +// instructions that read from poison-source variables, then propagates any +// instruction whose operand is a known-poison value produces a +// potentially-poison result. Special cases: +// +// - OpStore of poison to a local variable "contaminates" that variable, +// adding it to the tracked set and re-seeding loads from it. +// - OpFunctionCall arguments are checked both as values (for pass-by-value) +// and as pointers (for pass-by-reference). +// +// Values NOT in the potentially-poison set are assumed clean and receive no +// shadow instrumentation. This is an optimization - the shadow variable +// mechanism would produce the same results if applied universally. +// +// FUNCTION CALLS +// -------------- +// Cross-function poison flow is handled conservatively without rewriting +// function signatures: +// +// Caller side (OpFunctionCall): +// - The call result's shadow is the logical AND of all argument shadows, +// each reduced to scalar bool. If any argument is poison, the result +// is treated as poison. This could lead to false positives - the callee +// might not actually use the poison argument in computing its return +// value. +// - After the call, shadow variables for any pointer arguments (which +// correspond to out/inout parameters) are set to "clean" (all true). +// The callee may have written to these variables, so subsequent loads +// in the caller should not see stale poison state. +// +// Callee side: +// - Function parameters (OpFunctionParameter) are NOT treated as poison +// sources. Only the callee's own local variables are tracked. +// - OpReturnValue of a poison value is a UB trigger. +// - OpStore to a function parameter pointer (out/inout) of a poison +// value is a UB trigger with a dedicated error sub-code. +// +// KNOWN LIMITATIONS +// ----------------- +// - Per-function analysis: the pass cannot track poison across function +// boundaries precisely. The conservative caller-side approach may +// produce false positives (treating clean call results as poison when +// only an unused argument was poison). +// - Limited domination analysis: a direct store to the whole variable in +// the entry block before any access is treated as an initializer, and +// function call pointer arguments are assumed to initialize the variable. +// Beyond these cases, stores before loads are not recognized as +// initializers (e.g. stores in non-entry blocks, partial stores via +// access chains). +// - Cooperative matrix instructions are skipped (opaque types with +// implementation-dependent component counts). +// +// CURRENT POISON SOURCES +// ---------------------- +// - Uninitialized variables: OpVariable in Function or Private storage +// class without an initializer operand. +// +// FUTURE POISON SOURCES (not yet seeded by this pass) +// --------------------------------------------------- +// The following are additional sources of poison defined by the SPIR-V +// specification that this pass does not yet seed. If seeded, the existing +// dataflow analysis and instrumentation would handle them automatically. +// - OpFRem / OpFMod with a 0 divisor -> result is poison. +// - Shift instructions (OpShiftLeftLogical, etc.) with shift amount >= +// bit width -> result is poison. +// - OpBitFieldInsert / OpBitFieldSExtract / OpBitFieldUExtract with +// Count or Offset out of the valid range -> result is poison. +// - OpEmitVertex / OpEmitStreamVertex: output variables become poison +// after emission (geometry shaders). +// - Fast math flags (NotNaN, NotInf): if the assumption is violated, +// the result is poison. +// - OpDemoteToHelperInvocation: subsequent loads of HelperInvocation +// built-in may return poison. +// - OpGroupNonUniformBallotFindLSB / OpGroupNonUniformBallotFindMSB +// with no bits set -> result is poison. +// + +#include "poison_pass.h" +#include "module.h" +#include "type_manager.h" +#include "gpuav/shaders/gpuav_error_codes.h" +#include +#include +#include + +#include "generated/gpuav_offline_spirv.h" + +namespace gpuav { +namespace spirv { + +const static OfflineModule kOfflineModule = {instrumentation_poison_value_comp, instrumentation_poison_value_comp_size, + UseErrorPayloadVariable}; +const static OfflineFunction kOfflineFunction = {"inst_poison_report", instrumentation_poison_value_comp_function_0_offset}; + +PoisonPass::PoisonPass(Module& module) : Pass(module, kOfflineModule) {} + +void PoisonPass::PrintDebugInfo() const { std::cout << "PoisonPass instrumentation count: " << instrumentations_count_ << '\n'; } + +// ============================================================================= +// Phase 1: Static Analysis +// ============================================================================= + +void PoisonPass::FindUninitializedVariables(Function& function) { + uninitialized_var_ids_.clear(); + all_local_var_ids_.clear(); + contaminated_var_ids_.clear(); + + auto check_variable = [&](const Instruction& inst) { + auto sc = static_cast(inst.Word(3)); + if (sc == spv::StorageClassFunction || sc == spv::StorageClassPrivate) { + all_local_var_ids_.insert(inst.ResultId()); + if (inst.Length() <= 4) { + uninitialized_var_ids_.insert(inst.ResultId()); + } + } + }; + + for (auto& block : function.blocks_) { + for (auto& inst : block->instructions_) { + if (inst->Opcode() == spv::OpVariable) { + check_variable(*inst); + } + } + } + + // Module-level Private variables + for (const auto& inst : module_.types_values_constants_) { + if (inst->Opcode() == spv::OpVariable) { + check_variable(*inst); + } + } + + // A direct store to the whole variable in the entry block before any other + // access effectively initializes it. This is a common pattern when compilers + // emit OpVariable without an initializer followed by an immediate OpStore. + if (!uninitialized_var_ids_.empty() && !function.blocks_.empty()) { + BasicBlock& entry_block = *function.blocks_.front(); + std::unordered_set accessed_before_store; + + for (auto& inst : entry_block.instructions_) { + if (inst->Opcode() == spv::OpStore) { + uint32_t ptr_id = inst->Operand(0); + if (uninitialized_var_ids_.count(ptr_id) && !accessed_before_store.count(ptr_id)) { + uninitialized_var_ids_.erase(ptr_id); + continue; + } + } + + inst->ForEachIdOperand([&](uint32_t id) { + if (uninitialized_var_ids_.count(id)) { + accessed_before_store.insert(id); + } + }); + } + } +} + +void PoisonPass::ComputePotentiallyPoisonSet(Function& function) { + potentially_poison_ids_.clear(); + + // Forward pass: build pointer-to-variable map and collect loads per variable. + // Built from all_local_var_ids_ so contamination detection can resolve pointers + // to any local variable, not just currently tracked ones. + std::unordered_set tracked_vars = uninitialized_var_ids_; + std::unordered_map ptr_to_var; // pointer id -> base variable id + std::unordered_map> var_to_loads; // variable id -> load result ids + + auto resolve_var = [&](uint32_t ptr_id) -> uint32_t { + if (all_local_var_ids_.count(ptr_id)) return ptr_id; + auto it = ptr_to_var.find(ptr_id); + return it != ptr_to_var.end() ? it->second : 0; + }; + + for (auto& block : function.blocks_) { + for (auto& inst : block->instructions_) { + const auto op = static_cast(inst->Opcode()); + if (op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain || op == spv::OpPtrAccessChain || + op == spv::OpCopyObject) { + uint32_t base_id = inst->Operand(0); + if (all_local_var_ids_.count(base_id)) { + ptr_to_var[inst->ResultId()] = base_id; + } else { + auto it = ptr_to_var.find(base_id); + if (it != ptr_to_var.end()) { + ptr_to_var[inst->ResultId()] = it->second; + } + } + } else if (op == spv::OpLoad) { + uint32_t var_id = resolve_var(inst->Operand(0)); + if (var_id != 0) { + var_to_loads[var_id].push_back(inst->ResultId()); + } + } else { + // Many instructions can consume a tracked pointer without deriving a new + // pointer (OpStore, OpFunctionCall, OpExtInst for debug info, ray query ops, + // atomics, etc.). We only care about pointer-deriving instructions, which + // are exhaustively handled in the if-chain above (OpAccessChain, + // OpInBoundsAccessChain, OpPtrAccessChain, OpCopyObject). + } + } + } + + auto seed_loads = [&](uint32_t var_id, std::vector& worklist) { + auto it = var_to_loads.find(var_id); + if (it == var_to_loads.end()) return; + for (uint32_t load_id : it->second) { + if (potentially_poison_ids_.insert(load_id).second) { + worklist.push_back(load_id); + } + } + }; + + std::vector worklist; + for (uint32_t var_id : uninitialized_var_ids_) { + seed_loads(var_id, worklist); + } + + // Forward propagation of poison through value-producing instructions + while (!worklist.empty()) { + uint32_t poison_id = worklist.back(); + worklist.pop_back(); + + for (auto& block : function.blocks_) { + for (auto& inst : block->instructions_) { + const auto op = static_cast(inst->Opcode()); + + // Poison stored into a local variable contaminates it + if (op == spv::OpStore) { + uint32_t value_id = inst->Operand(1); + if (value_id == poison_id) { + uint32_t var_id = resolve_var(inst->Operand(0)); + if (var_id != 0 && !tracked_vars.count(var_id)) { + tracked_vars.insert(var_id); + contaminated_var_ids_.insert(var_id); + seed_loads(var_id, worklist); + } + } + continue; + } + + uint32_t result = inst->ResultId(); + if (result == 0 || potentially_poison_ids_.count(result)) continue; + + // Poison sources, not propagation targets + if (op == spv::OpVariable) continue; + // No result ID / not data flow + if (op == spv::OpLabel) continue; + if (op == spv::OpBranch || op == spv::OpBranchConditional || op == spv::OpSwitch) continue; + if (op == spv::OpSelectionMerge || op == spv::OpLoopMerge) continue; + if (op == spv::OpReturn || op == spv::OpReturnValue || op == spv::OpUnreachable) continue; + // Opaque types with implementation-dependent component counts + if (op == spv::OpCooperativeMatrixLoadKHR || op == spv::OpCooperativeMatrixStoreKHR || + op == spv::OpCooperativeMatrixMulAddKHR || op == spv::OpCooperativeMatrixLengthKHR) + continue; + + // glslang passes function args by pointer, so check both value and pointer args + if (op == spv::OpFunctionCall) { + for (uint32_t w = 4; w < inst->Length(); w++) { + uint32_t arg_id = inst->Word(w); + if (arg_id == poison_id) { + potentially_poison_ids_.insert(result); + worklist.push_back(result); + break; + } + uint32_t var_id = resolve_var(arg_id); + if (var_id != 0 && tracked_vars.count(var_id)) { + potentially_poison_ids_.insert(result); + worklist.push_back(result); + break; + } + } + continue; + } + + bool uses_poison = false; + inst->ForEachIdOperand([&](uint32_t id) { + if (id == poison_id) { + uses_poison = true; + } + }); + if (uses_poison) { + if (potentially_poison_ids_.insert(result).second) { + worklist.push_back(result); + } + } + } + } + } + + // Expand uninitialized_var_ids_ to include contaminated variables so they get shadow variables + uninitialized_var_ids_.insert(contaminated_var_ids_.begin(), contaminated_var_ids_.end()); +} + +// ============================================================================= +// Phase 2: Shadow Type/Variable Creation +// ============================================================================= + +// Maps an original SPIR-V type to its shadow type (same composite structure, all leaves become bool). +// Results are cached in shadow_type_cache_. +uint32_t PoisonPass::GetOrCreateShadowType(uint32_t type_id) { + auto it = shadow_type_cache_.find(type_id); + if (it != shadow_type_cache_.end()) return it->second; + + const Type* type = type_manager_.FindTypeById(type_id); + if (!type) { + shadow_type_cache_[type_id] = type_manager_.GetTypeBool().Id(); + return type_manager_.GetTypeBool().Id(); + } + + uint32_t shadow_id = 0; + switch (type->spv_type_) { + case SpvType::kBool: + case SpvType::kInt: + case SpvType::kFloat: + shadow_id = type_manager_.GetTypeBool().Id(); + break; + case SpvType::kVector: { + uint32_t count = type->meta_.vector.component_count; + shadow_id = type_manager_.GetTypeVector(type_manager_.GetTypeBool(), count).Id(); + break; + } + case SpvType::kVectorIdEXT: { + const Constant* count = type_manager_.FindConstantById(type->inst_.Word(3)); + if (count) { + shadow_id = type_manager_.GetTypeVectorIdEXT(type_manager_.GetTypeBool(), *count).Id(); + } else { + shadow_id = type_manager_.GetTypeBool().Id(); + } + break; + } + case SpvType::kMatrix: { + uint32_t col_type_id = type->inst_.Word(2); + uint32_t col_count = type->inst_.Word(3); + uint32_t shadow_col_id = GetOrCreateShadowType(col_type_id); + const Type* shadow_col = type_manager_.FindTypeById(shadow_col_id); + const Constant& col_count_const = type_manager_.GetConstantUInt32(col_count); + if (shadow_col) { + shadow_id = type_manager_.GetTypeArray(*shadow_col, col_count_const, false).Id(); + } else { + shadow_id = type_manager_.GetTypeBool().Id(); + } + break; + } + case SpvType::kArray: { + uint32_t elem_type_id = type->inst_.Word(2); + uint32_t shadow_elem_id = GetOrCreateShadowType(elem_type_id); + const Type* shadow_elem = type_manager_.FindTypeById(shadow_elem_id); + const Constant* length = type_manager_.FindConstantById(type->inst_.Word(3)); + if (shadow_elem && length) { + shadow_id = type_manager_.GetTypeArray(*shadow_elem, *length, false).Id(); + } else { + shadow_id = type_manager_.GetTypeBool().Id(); + } + break; + } + case SpvType::kStruct: { + std::vector member_shadow_ids; + for (uint32_t i = 2; i < type->inst_.Length(); i++) { + member_shadow_ids.push_back(GetOrCreateShadowType(type->inst_.Word(i))); + } + const uint32_t struct_type_id = module_.TakeNextId(); + auto new_inst = std::make_unique(static_cast(member_shadow_ids.size() + 2), spv::OpTypeStruct); + std::vector fill_words = {struct_type_id}; + fill_words.insert(fill_words.end(), member_shadow_ids.begin(), member_shadow_ids.end()); + new_inst->Fill(fill_words); + shadow_id = type_manager_.AddType(std::move(new_inst), SpvType::kStruct).Id(); + break; + } + default: + shadow_id = type_manager_.GetTypeBool().Id(); + break; + } + + shadow_type_cache_[type_id] = shadow_id; + return shadow_id; +} + +const PoisonPass::ShadowPtrInfo* PoisonPass::FindShadowPointer(uint32_t ptr_id) const { + auto it = shadow_pointer_map_.find(ptr_id); + return it != shadow_pointer_map_.end() ? &it->second : nullptr; +} + +// Forward-propagate shadow pointers through pointer-deriving instructions. +// Called for every instruction in the main loop before load/store handling. +// shadow_pointer_map_ is seeded from shadow_var_map_ at the start of each function, +// then this function propagates through: +// OpAccessChain / OpInBoundsAccessChain / OpPtrAccessChain - mirror with shadow access chain +// OpCopyObject - propagate shadow pointer directly +void PoisonPass::PropagateShadowPointer(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const auto opcode = static_cast(inst.Opcode()); + const uint32_t result_id = inst.ResultId(); + + if (opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain || opcode == spv::OpPtrAccessChain) { + uint32_t base_id = inst.Operand(0); + const ShadowPtrInfo* base_shadow = FindShadowPointer(base_id); + if (!base_shadow) return; + + // Walk the shadow type through the access chain indices. + // OpPtrAccessChain has an Element operand (Word 4) that indexes at the pointer level + // without descending into the type, followed by regular indices at Word 5+. + // OpAccessChain/OpInBoundsAccessChain have regular indices starting at Word 4. + const Type* current_type = type_manager_.FindTypeById(base_shadow->shadow_pointee_type_id); + assert(current_type); + + const uint32_t first_type_index = (opcode == spv::OpPtrAccessChain) ? 5 : 4; + const uint32_t num_type_indices = inst.Length() - first_type_index; + for (uint32_t i = 0; i < num_type_indices; i++) { + switch (current_type->spv_type_) { + case SpvType::kArray: + case SpvType::kRuntimeArray: + case SpvType::kVector: + case SpvType::kVectorIdEXT: + case SpvType::kMatrix: + current_type = type_manager_.FindTypeById(current_type->inst_.Word(2)); + break; + case SpvType::kStruct: { + const Constant* idx_const = type_manager_.FindConstantById(inst.Word(first_type_index + i)); + assert(idx_const); + uint32_t member_idx = idx_const->GetValueUint32(); + assert(member_idx + 2 < current_type->inst_.Length()); + current_type = type_manager_.FindTypeById(current_type->inst_.Word(2 + member_idx)); + break; + } + default: + assert(false); + break; + } + assert(current_type); + } + + const Type& ptr_type = type_manager_.GetTypePointer(base_shadow->shadow_sc, *current_type); + uint32_t new_ptr = module_.TakeNextId(); + std::vector words = {ptr_type.Id(), new_ptr, base_shadow->shadow_ptr_id}; + for (uint32_t i = 4; i < inst.Length(); i++) { + words.push_back(inst.Word(i)); + } + block.CreateInstruction(opcode, words, inst_it); + shadow_pointer_map_[result_id] = {new_ptr, current_type->Id(), base_shadow->shadow_sc, base_shadow->var_id}; + return; + } + + if (opcode == spv::OpCopyObject) { + if (const ShadowPtrInfo* src_shadow = FindShadowPointer(inst.Operand(0))) { + shadow_pointer_map_[result_id] = *src_shadow; + } + return; + } +} + +// Broadcast a scalar bool to match a composite shadow type using OpCompositeConstruct. +// For scalar bool input, just returns it if the target is already bool. +uint32_t PoisonPass::BroadcastShadow(BasicBlock& block, InstructionIt* inst_it, uint32_t scalar_shadow_id, + uint32_t shadow_type_id) { + const Type* shadow_type = type_manager_.FindTypeById(shadow_type_id); + if (!shadow_type || shadow_type->spv_type_ == SpvType::kBool) { + return scalar_shadow_id; + } + + if (scalar_shadow_id == constant_false_id_) { + return type_manager_.GetConstantNull(*shadow_type).Id(); + } + if (scalar_shadow_id == constant_true_id_) { + return GetAllTrueConstant(shadow_type_id); + } + + if (shadow_type->spv_type_ == SpvType::kVector || shadow_type->spv_type_ == SpvType::kVectorIdEXT) { + uint32_t count = shadow_type->meta_.vector.component_count; + uint32_t result_id = module_.TakeNextId(); + std::vector words = {shadow_type_id, result_id}; + for (uint32_t i = 0; i < count; i++) words.push_back(scalar_shadow_id); + block.CreateInstruction(spv::OpCompositeConstruct, words, inst_it); + return result_id; + } + + if (shadow_type->spv_type_ == SpvType::kArray) { + uint32_t elem_type_id = shadow_type->inst_.Word(2); + uint32_t elem_shadow = BroadcastShadow(block, inst_it, scalar_shadow_id, elem_type_id); + const Constant* length_const = type_manager_.FindConstantById(shadow_type->inst_.Word(3)); + if (!length_const) return scalar_shadow_id; + uint32_t length = length_const->GetValueUint32(); + uint32_t result_id = module_.TakeNextId(); + std::vector words = {shadow_type_id, result_id}; + for (uint32_t i = 0; i < length; i++) words.push_back(elem_shadow); + block.CreateInstruction(spv::OpCompositeConstruct, words, inst_it); + return result_id; + } + + if (shadow_type->spv_type_ == SpvType::kStruct) { + uint32_t result_id = module_.TakeNextId(); + std::vector words = {shadow_type_id, result_id}; + for (uint32_t i = 2; i < shadow_type->inst_.Length(); i++) { + words.push_back(BroadcastShadow(block, inst_it, scalar_shadow_id, shadow_type->inst_.Word(i))); + } + block.CreateInstruction(spv::OpCompositeConstruct, words, inst_it); + return result_id; + } + + return scalar_shadow_id; +} + +// Reduce a composite shadow value to a scalar bool (true = all clean, false = any poison) +uint32_t PoisonPass::ReduceShadowToScalar(BasicBlock& block, InstructionIt* inst_it, uint32_t shadow_id, uint32_t shadow_type_id) { + const Type& bool_type = type_manager_.GetTypeBool(); + const Type* shadow_type = type_manager_.FindTypeById(shadow_type_id); + if (!shadow_type || shadow_type->spv_type_ == SpvType::kBool) { + return shadow_id; + } + + if (shadow_type->spv_type_ == SpvType::kVector || shadow_type->spv_type_ == SpvType::kVectorIdEXT) { + uint32_t result_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpAll, {bool_type.Id(), result_id, shadow_id}, inst_it); + return result_id; + } + + if (shadow_type->spv_type_ == SpvType::kArray) { + uint32_t elem_type_id = shadow_type->inst_.Word(2); + const Constant* length_const = type_manager_.FindConstantById(shadow_type->inst_.Word(3)); + if (!length_const) return constant_false_id_; + uint32_t length = length_const->GetValueUint32(); + uint32_t combined = constant_true_id_; + for (uint32_t i = 0; i < length; i++) { + uint32_t extracted_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpCompositeExtract, {elem_type_id, extracted_id, shadow_id, i}, inst_it); + uint32_t elem_scalar = ReduceShadowToScalar(block, inst_it, extracted_id, elem_type_id); + if (combined == constant_true_id_) { + combined = elem_scalar; + } else { + uint32_t and_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpLogicalAnd, {bool_type.Id(), and_id, combined, elem_scalar}, inst_it); + combined = and_id; + } + } + return combined; + } + + if (shadow_type->spv_type_ == SpvType::kStruct) { + uint32_t combined = constant_true_id_; + for (uint32_t i = 2; i < shadow_type->inst_.Length(); i++) { + uint32_t member_type_id = shadow_type->inst_.Word(i); + uint32_t extracted_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpCompositeExtract, {member_type_id, extracted_id, shadow_id, i - 2}, inst_it); + uint32_t member_scalar = ReduceShadowToScalar(block, inst_it, extracted_id, member_type_id); + if (combined == constant_true_id_) { + combined = member_scalar; + } else { + uint32_t and_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpLogicalAnd, {bool_type.Id(), and_id, combined, member_scalar}, inst_it); + combined = and_id; + } + } + return combined; + } + + return constant_false_id_; +} + +void PoisonPass::CreateShadowVariables(Function& function) { + shadow_var_map_.clear(); + shadow_value_map_.clear(); + shadow_value_type_map_.clear(); + + const Type& bool_type = type_manager_.GetTypeBool(); + + // false = "poison" (uninitialized), true = "clean" (initialized) + constant_false_id_ = type_manager_.GetConstantNull(bool_type).Id(); + + // Search for an existing OpConstantTrue in the module type/constant section + constant_true_id_ = 0; + for (const auto& inst : module_.types_values_constants_) { + if (inst->Opcode() == spv::OpConstantTrue) { + constant_true_id_ = inst->ResultId(); + break; + } + } + + if (constant_true_id_ == 0) { + uint32_t id = module_.TakeNextId(); + auto new_inst = std::make_unique(3, spv::OpConstantTrue); + new_inst->Fill({bool_type.Id(), id}); + constant_true_id_ = id; + type_manager_.AddConstant(std::move(new_inst), bool_type); + } + + BasicBlock& entry_block = *function.blocks_.front(); + + for (uint32_t var_id : uninitialized_var_ids_) { + const Instruction* var_inst = function.FindInstruction(var_id); + if (!var_inst) { + const Variable* var = type_manager_.FindVariableById(var_id); + if (var) var_inst = &var->inst_; + } + if (!var_inst) continue; + + auto orig_sc = static_cast(var_inst->Word(3)); + + const Type* ptr_type = type_manager_.FindTypeById(var_inst->TypeId()); + if (!ptr_type || ptr_type->spv_type_ != SpvType::kPointer) continue; + uint32_t pointee_type_id = ptr_type->inst_.Word(3); + + uint32_t shadow_pointee_type_id = GetOrCreateShadowType(pointee_type_id); + + spv::StorageClass shadow_sc = (orig_sc == spv::StorageClassFunction) ? spv::StorageClassFunction : spv::StorageClassPrivate; + + const Type* spt = type_manager_.FindTypeById(shadow_pointee_type_id); + if (!spt) continue; + uint32_t shadow_ptr_type_id = type_manager_.GetTypePointer(shadow_sc, *spt).Id(); + + bool is_contaminated = contaminated_var_ids_.count(var_id) != 0; + uint32_t init_id; + if (is_contaminated) { + // Contaminated variables were originally initialized by the program; + // they only become poison later when a poison value is stored into them. + // Start clean (all-true) to avoid false positives on loads before contamination. + init_id = GetAllTrueConstant(shadow_pointee_type_id); + } else { + // Truly uninitialized variables start as poison (all-false). + init_id = type_manager_.GetConstantNull(*spt).Id(); + } + + uint32_t shadow_var_id; + if (shadow_sc == spv::StorageClassFunction) { + shadow_var_id = module_.TakeNextId(); + auto inject_it = entry_block.instructions_.begin(); + if (inject_it != entry_block.instructions_.end() && (*inject_it)->Opcode() == spv::OpLabel) { + ++inject_it; + } + while (inject_it != entry_block.instructions_.end() && (*inject_it)->Opcode() == spv::OpVariable) { + ++inject_it; + } + entry_block.CreateInstruction( + spv::OpVariable, {shadow_ptr_type_id, shadow_var_id, static_cast(shadow_sc), init_id}, &inject_it); + } else { + auto var_inst_new = std::make_unique(5, spv::OpVariable); + shadow_var_id = module_.TakeNextId(); + var_inst_new->Fill({shadow_ptr_type_id, shadow_var_id, static_cast(shadow_sc), init_id}); + type_manager_.AddVariable(std::move(var_inst_new), *spt); + module_.AddInterfaceVariables(shadow_var_id, shadow_sc); + } + + shadow_var_map_[var_id] = {shadow_var_id, shadow_pointee_type_id, shadow_sc}; + } +} + +// ============================================================================= +// Phase 3: Instrumentation +// ============================================================================= + +// Returns the shadow for a value. If the value was instrumented, returns its tracked shadow. +// For untracked values, returns a constant: all-true (clean) or all-false (poison). +// expected_shadow_type_id is used by OpPhi to get a type-matched composite constant +// instead of a scalar bool, avoiding type mismatches in the phi's shadow operands. +uint32_t PoisonPass::GetShadowValue(uint32_t id, uint32_t expected_shadow_type_id) { + auto it = shadow_value_map_.find(id); + if (it != shadow_value_map_.end()) return it->second; + if (expected_shadow_type_id != 0) { + if (IsPotentiallyPoison(id)) { + const Type* type = type_manager_.FindTypeById(expected_shadow_type_id); + return type ? type_manager_.GetConstantNull(*type).Id() : constant_false_id_; + } + return GetAllTrueConstant(expected_shadow_type_id); + } + if (IsPotentiallyPoison(id)) return constant_false_id_; + return constant_true_id_; +} + +// Returns the shadow type for a value. Defaults to scalar bool for untracked values. +uint32_t PoisonPass::GetShadowType(uint32_t id) { + auto it = shadow_value_type_map_.find(id); + if (it != shadow_value_type_map_.end()) return it->second; + return type_manager_.GetTypeBool().Id(); +} + +void PoisonPass::SetShadowValue(uint32_t value_id, uint32_t shadow_id, uint32_t shadow_type_id) { + shadow_value_map_[value_id] = shadow_id; + shadow_value_type_map_[value_id] = shadow_type_id; +} + +// Returns (or creates) an OpConstantComposite where every leaf is true (all clean). +// Used to initialize contaminated shadow variables and to represent "definitely not poison" +// for composite types. +uint32_t PoisonPass::GetAllTrueConstant(uint32_t shadow_type_id) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + if (shadow_type_id == bool_type_id) return constant_true_id_; + + auto it = all_true_constants_.find(shadow_type_id); + if (it != all_true_constants_.end()) return it->second; + + const Type* shadow_type = type_manager_.FindTypeById(shadow_type_id); + if (!shadow_type) return constant_true_id_; + + uint32_t id = module_.TakeNextId(); + + if (shadow_type->spv_type_ == SpvType::kVector || shadow_type->spv_type_ == SpvType::kVectorIdEXT) { + uint32_t count = shadow_type->meta_.vector.component_count; + auto new_inst = std::make_unique(3 + count, spv::OpConstantComposite); + std::vector words = {shadow_type_id, id}; + for (uint32_t i = 0; i < count; i++) words.push_back(constant_true_id_); + new_inst->Fill(words); + type_manager_.AddConstant(std::move(new_inst), *shadow_type); + all_true_constants_[shadow_type_id] = id; + return id; + } + + if (shadow_type->spv_type_ == SpvType::kArray) { + uint32_t elem_type_id = shadow_type->inst_.Word(2); + uint32_t elem_true = GetAllTrueConstant(elem_type_id); + const Constant* length_const = type_manager_.FindConstantById(shadow_type->inst_.Word(3)); + if (!length_const) return constant_true_id_; + uint32_t length = length_const->GetValueUint32(); + auto new_inst = std::make_unique(3 + length, spv::OpConstantComposite); + std::vector words = {shadow_type_id, id}; + for (uint32_t i = 0; i < length; i++) words.push_back(elem_true); + new_inst->Fill(words); + type_manager_.AddConstant(std::move(new_inst), *shadow_type); + all_true_constants_[shadow_type_id] = id; + return id; + } + + if (shadow_type->spv_type_ == SpvType::kStruct) { + std::vector words = {shadow_type_id, id}; + for (uint32_t i = 2; i < shadow_type->inst_.Length(); i++) { + words.push_back(GetAllTrueConstant(shadow_type->inst_.Word(i))); + } + auto new_inst = std::make_unique(static_cast(words.size() + 1), spv::OpConstantComposite); + new_inst->Fill(words); + type_manager_.AddConstant(std::move(new_inst), *shadow_type); + all_true_constants_[shadow_type_id] = id; + return id; + } + + return constant_true_id_; +} + +// Componentwise AND of two shadow values of the same type. +// For bool/vector, emits a single OpLogicalAnd. For struct/array, recursively +// extracts members, ANDs them, and reconstructs. +uint32_t PoisonPass::ComponentwiseAnd(BasicBlock& block, InstructionIt* inst_it, uint32_t a_id, uint32_t b_id, + uint32_t shadow_type_id) { + if (a_id == b_id) return a_id; + + const Type* shadow_type = type_manager_.FindTypeById(shadow_type_id); + assert(shadow_type); + + if (shadow_type->spv_type_ == SpvType::kBool || shadow_type->spv_type_ == SpvType::kVector || + shadow_type->spv_type_ == SpvType::kVectorIdEXT) { + uint32_t result_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpLogicalAnd, {shadow_type_id, result_id, a_id, b_id}, inst_it); + return result_id; + } + + if (shadow_type->spv_type_ == SpvType::kArray) { + uint32_t elem_type_id = shadow_type->inst_.Word(2); + const Constant* length_const = type_manager_.FindConstantById(shadow_type->inst_.Word(3)); + assert(length_const); + uint32_t length = length_const->GetValueUint32(); + uint32_t result_id = module_.TakeNextId(); + std::vector words = {shadow_type_id, result_id}; + for (uint32_t i = 0; i < length; i++) { + uint32_t a_elem = module_.TakeNextId(); + block.CreateInstruction(spv::OpCompositeExtract, {elem_type_id, a_elem, a_id, i}, inst_it); + uint32_t b_elem = module_.TakeNextId(); + block.CreateInstruction(spv::OpCompositeExtract, {elem_type_id, b_elem, b_id, i}, inst_it); + words.push_back(ComponentwiseAnd(block, inst_it, a_elem, b_elem, elem_type_id)); + } + block.CreateInstruction(spv::OpCompositeConstruct, words, inst_it); + return result_id; + } + + if (shadow_type->spv_type_ == SpvType::kStruct) { + uint32_t result_id = module_.TakeNextId(); + std::vector words = {shadow_type_id, result_id}; + for (uint32_t i = 2; i < shadow_type->inst_.Length(); i++) { + uint32_t member_type_id = shadow_type->inst_.Word(i); + uint32_t a_member = module_.TakeNextId(); + block.CreateInstruction(spv::OpCompositeExtract, {member_type_id, a_member, a_id, i - 2}, inst_it); + uint32_t b_member = module_.TakeNextId(); + block.CreateInstruction(spv::OpCompositeExtract, {member_type_id, b_member, b_id, i - 2}, inst_it); + words.push_back(ComponentwiseAnd(block, inst_it, a_member, b_member, member_type_id)); + } + block.CreateInstruction(spv::OpCompositeConstruct, words, inst_it); + return result_id; + } + + assert(false); + return a_id; +} + +// Convert a shadow value from one shadow type to another by reducing to scalar +// then broadcasting. Both functions are no-ops when already at the target shape +// (ReduceShadowToScalar returns immediately for bool, BroadcastShadow returns +// immediately when target is bool), so this handles all cases uniformly. +uint32_t PoisonPass::EnsureShadowType(BasicBlock& block, InstructionIt* inst_it, uint32_t shadow_id, uint32_t from_type_id, + uint32_t to_type_id) { + if (from_type_id == to_type_id) return shadow_id; + uint32_t scalar = ReduceShadowToScalar(block, inst_it, shadow_id, from_type_id); + return BroadcastShadow(block, inst_it, scalar, to_type_id); +} + +void PoisonPass::EmitPoisonError(BasicBlock& block, InstructionIt* inst_it, uint32_t shadow_id, const Instruction& trigger_inst, + uint32_t error_sub_code) { + const uint32_t function_def = GetLinkFunction(link_function_id_, kOfflineFunction); + + const Type& bool_type = type_manager_.GetTypeBool(); + const uint32_t void_type = type_manager_.GetTypeVoid().Id(); + + // shadow_id is true when clean, false when poison. Negate to get is_poison. + const uint32_t is_poison_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpLogicalNot, {bool_type.Id(), is_poison_id, shadow_id}, inst_it); + + const uint32_t inst_offset = trigger_inst.GetPositionOffset(); + const uint32_t inst_offset_id = type_manager_.CreateConstantUInt32(inst_offset).Id(); + const uint32_t opcode_id = type_manager_.CreateConstantUInt32(trigger_inst.Opcode()).Id(); + const uint32_t sub_code_id = type_manager_.CreateConstantUInt32(error_sub_code).Id(); + + const uint32_t function_result = module_.TakeNextId(); + block.CreateInstruction(spv::OpFunctionCall, + {void_type, function_result, function_def, is_poison_id, inst_offset_id, opcode_id, sub_code_id}, + inst_it); + + module_.need_log_error_ = true; +} + +// Main per-function instrumentation loop. Seeds shadow_pointer_map_ from shadow_var_map_, +// then iterates every instruction: +// - PropagateShadowPointer: forward-propagate shadow pointers through access chains / copy object +// - OpLoad/OpStore: bridge shadow variables and shadow values (may split the basic block) +// - Non-poison results: check UB triggers (branch, switch, return, function call cleanup) +// - Poison results: dispatch to specialized or generic shadow propagation +void PoisonPass::InstrumentFunction(Function& function) { + std::unordered_set func_param_ids; + for (const auto& pre_inst : function.pre_block_inst_) { + if (pre_inst->Opcode() == spv::OpFunctionParameter) { + func_param_ids.insert(pre_inst->ResultId()); + } + } + + shadow_pointer_map_.clear(); + for (auto& [var_id, info] : shadow_var_map_) { + shadow_pointer_map_[var_id] = {info.shadow_var_id, info.shadow_pointee_type_id, info.shadow_sc, var_id}; + } + + for (auto block_it = function.blocks_.begin(); block_it != function.blocks_.end(); ++block_it) { + BasicBlock& block = **block_it; + + for (auto inst_it = block.instructions_.begin(); inst_it != block.instructions_.end(); ++inst_it) { + const Instruction& inst = **inst_it; + const auto opcode = static_cast(inst.Opcode()); + const uint32_t result_id = inst.ResultId(); + + // Propagate shadow pointers forward through pointer-deriving instructions + // (OpAccessChain, OpPtrAccessChain, OpCopyObject) so that + // InstrumentLoad/InstrumentStore can look up the shadow pointer directly. + PropagateShadowPointer(block, &inst_it, inst); + + // Load/store bridge shadow variables/pointers and shadow values, + // run regardless of IsPotentiallyPoison, and may split the basic block. + if (opcode == spv::OpLoad) { + if (InstrumentLoad(function, block, block_it, &inst_it, inst)) { + ++block_it; + ++block_it; + break; + } + continue; + } + if (opcode == spv::OpStore) { + if (InstrumentStore(function, block, block_it, &inst_it, inst, func_param_ids)) { + ++block_it; + ++block_it; + break; + } + continue; + } + if (result_id == 0 || !IsPotentiallyPoison(result_id)) { + InstrumentNonPoisonResult(block, &inst_it, inst); + continue; + } + InstrumentPoisonResult(function, block, &inst_it, inst); + } + } +} + +// After a function call, mark shadow variables for any pointer arguments as clean. +// The callee may have written to out/inout parameters, so we conservatively assume +// they are now initialized. +void PoisonPass::MarkCallArgsClean(BasicBlock& block, InstructionIt* inst_it, const Instruction& call_inst) { + for (uint32_t w = 4; w < call_inst.Length(); w++) { + if (const ShadowPtrInfo* spi = FindShadowPointer(call_inst.Word(w))) { + auto sit = shadow_var_map_.find(spi->var_id); + if (sit != shadow_var_map_.end()) { + const ShadowVarInfo& si = sit->second; + uint32_t all_true = GetAllTrueConstant(si.shadow_pointee_type_id); + block.CreateInstruction(spv::OpStore, {si.shadow_var_id, all_true}, inst_it); + } + } + } +} + +// Handles OpLoad: two cases depending on whether the pointer itself or the loaded value is poison. +// 1. Poison pointer dereference (BDA/variable pointers): the pointer operand is derived from a +// poison value. Emit an error and wrap the load in a conditional to prevent GPU crash. +// Returns true (caller must skip 2 blocks for the injected if/else). +// 2. Tracked variable load: the pointer is a tracked local variable's shadow pointer. +// Load the shadow value and register it for downstream propagation. +bool PoisonPass::InstrumentLoad(Function& function, BasicBlock& block, BasicBlockIt block_it, InstructionIt* inst_it, + const Instruction& inst) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + const uint32_t result_id = inst.ResultId(); + uint32_t ptr_id = inst.Operand(0); + + // UB trigger: dereferencing a poison pointer (e.g. BDA from uninit uint64, variable pointers). + // Must skip the actual load to avoid GPU crash from garbage pointer. + if (IsPotentiallyPoison(ptr_id)) { + uint32_t ptr_shadow = GetShadowValue(ptr_id); + uint32_t ptr_shadow_type = GetShadowType(ptr_id); + uint32_t scalar_shadow = EnsureShadowType(block, inst_it, ptr_shadow, ptr_shadow_type, bool_type_id); + if (scalar_shadow != constant_true_id_) { + EmitPoisonError(block, inst_it, scalar_shadow, inst, glsl::kErrorSubCode_PoisonValue_PoisonPointerDereference); + instrumentations_count_++; + + // Wrap the load in a conditional: execute only if pointer is clean. + // InjectFunctionPre moves the load to a "valid" block, creates a phi + // with null for the "invalid" (skipped) path, and moves remaining + // instructions to a merge block. + InjectConditionalData ic_data = InjectFunctionPre(function, block_it, *inst_it); + ic_data.function_result_id = scalar_shadow; + InjectFunctionPost(block, ic_data); + return true; // caller must skip 2 blocks and break + } + return false; + } + + const ShadowPtrInfo* sp = FindShadowPointer(ptr_id); + if (!sp || !IsPotentiallyPoison(result_id)) return false; + uint32_t shadow_ptr_id = sp->shadow_ptr_id; + uint32_t shadow_load_type_id = sp->shadow_pointee_type_id; + + uint32_t loaded_shadow_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpLoad, {shadow_load_type_id, loaded_shadow_id, shadow_ptr_id}, inst_it); + + SetShadowValue(result_id, loaded_shadow_id, shadow_load_type_id); + return false; +} + +// Handles OpStore: three cases. +// 1. Poison pointer dereference: the pointer operand is itself poison (BDA/variable pointers). +// Emit an error and wrap the store in a conditional. Returns true. +// 2. External/parameter store of poison: the pointer is NOT a tracked local variable but the +// stored value is poison. Emit a UB error (external store or store to out/inout param). +// 3. Tracked variable store: update the shadow variable to reflect the stored value's shadow. +bool PoisonPass::InstrumentStore(Function& function, BasicBlock& block, BasicBlockIt block_it, InstructionIt* inst_it, + const Instruction& inst, const std::unordered_set& func_param_ids) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + uint32_t ptr_id = inst.Operand(0); + uint32_t stored_value_id = inst.Operand(1); + + // UB trigger: storing through a poison pointer (e.g. BDA from uninit uint64, variable pointers). + // Must skip the actual store to avoid GPU crash from garbage pointer. + if (IsPotentiallyPoison(ptr_id)) { + uint32_t ptr_shadow = GetShadowValue(ptr_id); + uint32_t ptr_shadow_type = GetShadowType(ptr_id); + uint32_t scalar_shadow = EnsureShadowType(block, inst_it, ptr_shadow, ptr_shadow_type, bool_type_id); + if (scalar_shadow != constant_true_id_) { + EmitPoisonError(block, inst_it, scalar_shadow, inst, glsl::kErrorSubCode_PoisonValue_PoisonPointerDereference); + instrumentations_count_++; + + InjectConditionalData ic_data = InjectFunctionPre(function, block_it, *inst_it); + ic_data.function_result_id = scalar_shadow; + InjectFunctionPost(block, ic_data); + return true; // caller must skip 2 blocks and break + } + return false; + } + + const ShadowPtrInfo* sp = FindShadowPointer(ptr_id); + + if (!sp) { + // Not storing to a tracked variable - check for external store of poison + if (IsPotentiallyPoison(stored_value_id)) { + uint32_t sub_code = 0; + const Instruction* ptr_inst = function.FindInstruction(ptr_id); + const Type* ptr_type = ptr_inst ? type_manager_.FindTypeById(ptr_inst->TypeId()) : nullptr; + if (ptr_type && ptr_type->spv_type_ == SpvType::kPointer) { + auto sc = static_cast(ptr_type->inst_.Word(2)); + if (!IsLocalStorageClass(sc)) { + sub_code = glsl::kErrorSubCode_PoisonValue_UninitializedVariable; + } + } + if (sub_code == 0) { + uint32_t base_ptr = ptr_id; + if (ptr_inst && (ptr_inst->Opcode() == spv::OpAccessChain || ptr_inst->Opcode() == spv::OpInBoundsAccessChain || + ptr_inst->Opcode() == spv::OpPtrAccessChain)) { + base_ptr = ptr_inst->Operand(0); + } + if (func_param_ids.count(base_ptr)) { + sub_code = glsl::kErrorSubCode_PoisonValue_StoreToFunctionParam; + } + } + if (sub_code != 0) { + uint32_t value_shadow = GetShadowValue(stored_value_id); + uint32_t value_shadow_type = GetShadowType(stored_value_id); + uint32_t scalar_shadow = EnsureShadowType(block, inst_it, value_shadow, value_shadow_type, bool_type_id); + if (scalar_shadow != constant_true_id_) { + EmitPoisonError(block, inst_it, scalar_shadow, inst, sub_code); + instrumentations_count_++; + } + } + } + return false; + } + + // Storing to a tracked variable - update its shadow + uint32_t value_shadow = GetShadowValue(stored_value_id); + uint32_t value_shadow_type = GetShadowType(stored_value_id); + uint32_t converted_shadow = EnsureShadowType(block, inst_it, value_shadow, value_shadow_type, sp->shadow_pointee_type_id); + block.CreateInstruction(spv::OpStore, {sp->shadow_ptr_id, converted_shadow}, inst_it); + return false; +} + +// Handles instructions whose result is NOT potentially poison (or that have no result), +// but whose operands might be. These are UB triggers where poison flows into control flow +// or escapes the function: +// OpBranchConditional - poison condition replaced with safe constant after error +// OpSwitch - poison selector replaced with zero after error +// OpReturnValue - error emitted for poison return +// OpFunctionCall - post-call cleanup (mark out/inout args clean via MarkCallArgsClean) +void PoisonPass::InstrumentNonPoisonResult(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + const auto opcode = static_cast(inst.Opcode()); + + if (opcode == spv::OpBranchConditional) { + uint32_t cond_id = inst.Operand(0); + if (IsPotentiallyPoison(cond_id)) { + uint32_t cond_shadow = GetShadowValue(cond_id); + uint32_t cond_shadow_type = GetShadowType(cond_id); + uint32_t scalar_shadow = EnsureShadowType(block, inst_it, cond_shadow, cond_shadow_type, bool_type_id); + if (scalar_shadow != constant_true_id_) { + auto insert_it = *inst_it; + bool before_merge = false; + if (insert_it != block.instructions_.begin()) { + auto prev = std::prev(insert_it); + auto prev_op = static_cast((*prev)->Opcode()); + if (prev_op == spv::OpSelectionMerge || prev_op == spv::OpLoopMerge) { + insert_it = prev; + before_merge = true; + } + } + EmitPoisonError(block, &insert_it, scalar_shadow, inst); + instrumentations_count_++; + + // Poison branch conditions are replaced with safe constants after error reporting, because + // compilers may optimize away unreachable code + uint32_t safe_cond_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpSelect, {bool_type_id, safe_cond_id, scalar_shadow, cond_id, constant_false_id_}, + &insert_it); + + *inst_it = before_merge ? std::next(insert_it) : insert_it; + (**inst_it)->UpdateWord(1, safe_cond_id); + } + } + } else if (opcode == spv::OpSwitch) { + uint32_t selector_id = inst.Operand(0); + if (IsPotentiallyPoison(selector_id)) { + uint32_t selector_shadow = GetShadowValue(selector_id); + uint32_t selector_shadow_type = GetShadowType(selector_id); + uint32_t scalar_shadow = EnsureShadowType(block, inst_it, selector_shadow, selector_shadow_type, bool_type_id); + if (scalar_shadow != constant_true_id_) { + auto insert_it = *inst_it; + bool before_merge = false; + if (insert_it != block.instructions_.begin()) { + auto prev = std::prev(insert_it); + auto prev_op = static_cast((*prev)->Opcode()); + if (prev_op == spv::OpSelectionMerge || prev_op == spv::OpLoopMerge) { + insert_it = prev; + before_merge = true; + } + } + EmitPoisonError(block, &insert_it, scalar_shadow, inst); + instrumentations_count_++; + + const uint32_t zero_id = type_manager_.GetConstantZeroUint32().Id(); + uint32_t safe_selector_id = module_.TakeNextId(); + const Type& selector_type = type_manager_.GetTypeInt(32, false); + block.CreateInstruction(spv::OpSelect, {selector_type.Id(), safe_selector_id, scalar_shadow, selector_id, zero_id}, + &insert_it); + + *inst_it = before_merge ? std::next(insert_it) : insert_it; + (**inst_it)->UpdateWord(1, safe_selector_id); + } + } + } else if (opcode == spv::OpReturnValue) { + uint32_t value_id = inst.Operand(0); + if (IsPotentiallyPoison(value_id)) { + uint32_t value_shadow = GetShadowValue(value_id); + uint32_t value_shadow_type = GetShadowType(value_id); + uint32_t scalar_shadow = EnsureShadowType(block, inst_it, value_shadow, value_shadow_type, bool_type_id); + if (scalar_shadow != constant_true_id_) { + EmitPoisonError(block, inst_it, scalar_shadow, inst); + instrumentations_count_++; + } + } + } else if (opcode == spv::OpFunctionCall) { + MarkCallArgsClean(block, inst_it, inst); + } +} + +void PoisonPass::InstrumentPoisonResult(Function& function, BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + const auto opcode = static_cast(inst.Opcode()); + const uint32_t result_id = inst.ResultId(); + + // Cooperative matrix instructions are opaque; treat as clean + if (opcode == spv::OpCooperativeMatrixLoadKHR || opcode == spv::OpCooperativeMatrixMulAddKHR || + opcode == spv::OpCooperativeMatrixLengthKHR) { + SetShadowValue(result_id, constant_true_id_, bool_type_id); + return; + } + + // UB triggers - result-producing error-reporting instructions + if (opcode == spv::OpFunctionCall) { + InstrumentFunctionCall(block, inst_it, inst); + return; + } + if (opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain || opcode == spv::OpPtrAccessChain) { + InstrumentAccessChain(block, inst_it, inst); + return; + } + + // Pass-through - per-component shadow data movement + if (opcode == spv::OpCompositeExtract) { + InstrumentCompositeExtract(block, inst_it, inst); + return; + } + if (opcode == spv::OpCompositeInsert) { + InstrumentCompositeInsert(block, inst_it, inst); + return; + } + if (opcode == spv::OpCompositeConstruct) { + InstrumentCompositeConstruct(block, inst_it, inst); + return; + } + if (opcode == spv::OpVectorExtractDynamic) { + InstrumentVectorExtractDynamic(function, block, inst_it, inst); + return; + } + if (opcode == spv::OpVectorInsertDynamic) { + InstrumentVectorInsertDynamic(block, inst_it, inst); + return; + } + if (opcode == spv::OpVectorShuffle) { + InstrumentVectorShuffle(function, block, inst_it, inst); + return; + } + if (opcode == spv::OpSelect) { + InstrumentSelect(block, inst_it, inst); + return; + } + if (opcode == spv::OpPhi) { + InstrumentPhi(block, inst_it, inst); + return; + } + if (opcode == spv::OpCopyObject) { + uint32_t src_id = inst.Operand(0); + SetShadowValue(result_id, GetShadowValue(src_id), GetShadowType(src_id)); + return; + } + if (opcode == spv::OpCopyLogical) { + uint32_t src_id = inst.Operand(0); + uint32_t src_shadow = GetShadowValue(src_id); + uint32_t src_shadow_type = GetShadowType(src_id); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + if (src_shadow_type == result_shadow_type_id) { + SetShadowValue(result_id, src_shadow, src_shadow_type); + } else { + uint32_t result_shadow_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpCopyLogical, {result_shadow_type_id, result_shadow_id, src_shadow}, inst_it); + SetShadowValue(result_id, result_shadow_id, result_shadow_type_id); + } + return; + } + if (opcode == spv::OpCompositeConstructReplicateEXT) { + uint32_t value_id = inst.Operand(0); + uint32_t value_shadow = GetShadowValue(value_id); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + uint32_t result_shadow_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpCompositeConstructReplicateEXT, {result_shadow_type_id, result_shadow_id, value_shadow}, + inst_it); + SetShadowValue(result_id, result_shadow_id, result_shadow_type_id); + return; + } + + // Propagation - generic: AND of all poison operand shadows + { + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + uint32_t combined_shadow = 0; + + inst.ForEachIdOperand( + [&](uint32_t id) { + if (id == result_id || id == inst.TypeId()) return; + if (!IsPotentiallyPoison(id)) return; + + uint32_t operand_shadow = GetShadowValue(id); + uint32_t operand_shadow_type = GetShadowType(id); + uint32_t converted = EnsureShadowType(block, inst_it, operand_shadow, operand_shadow_type, result_shadow_type_id); + + if (combined_shadow == 0) { + combined_shadow = converted; + } else { + combined_shadow = ComponentwiseAnd(block, inst_it, combined_shadow, converted, result_shadow_type_id); + } + }, + /*include_ambiguous=*/false); + + if (combined_shadow == 0) { + combined_shadow = GetAllTrueConstant(result_shadow_type_id); + } + SetShadowValue(result_id, combined_shadow, result_shadow_type_id); + } +} + +// ============================================================================= +// UB triggers - result-producing error-reporting instructions +// ============================================================================= + +// Conservative cross-function poison propagation: the call result's shadow is the AND of +// all argument shadows (both value and pointer args), reduced to scalar then broadcast +// to match the result type. After the call, pointer args are marked clean (MarkCallArgsClean). +void PoisonPass::InstrumentFunctionCall(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + const uint32_t result_id = inst.ResultId(); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + uint32_t combined_shadow = 0; + + for (uint32_t w = 4; w < inst.Length(); w++) { + uint32_t arg_id = inst.Word(w); + uint32_t arg_scalar_shadow = 0; + + if (IsPotentiallyPoison(arg_id)) { + uint32_t s = GetShadowValue(arg_id); + arg_scalar_shadow = EnsureShadowType(block, inst_it, s, GetShadowType(arg_id), bool_type_id); + } else if (const ShadowPtrInfo* spi = FindShadowPointer(arg_id)) { + auto sit = shadow_var_map_.find(spi->var_id); + if (sit != shadow_var_map_.end()) { + const ShadowVarInfo& si = sit->second; + uint32_t loaded = module_.TakeNextId(); + block.CreateInstruction(spv::OpLoad, {si.shadow_pointee_type_id, loaded, si.shadow_var_id}, inst_it); + arg_scalar_shadow = EnsureShadowType(block, inst_it, loaded, si.shadow_pointee_type_id, bool_type_id); + } + } + + if (arg_scalar_shadow == 0) continue; + if (combined_shadow == 0) { + combined_shadow = arg_scalar_shadow; + } else { + uint32_t new_combined = module_.TakeNextId(); + block.CreateInstruction(spv::OpLogicalAnd, {bool_type_id, new_combined, combined_shadow, arg_scalar_shadow}, inst_it); + combined_shadow = new_combined; + } + } + + if (combined_shadow == 0) { + combined_shadow = GetAllTrueConstant(result_shadow_type_id); + } else { + combined_shadow = EnsureShadowType(block, inst_it, combined_shadow, bool_type_id, result_shadow_type_id); + } + SetShadowValue(result_id, combined_shadow, result_shadow_type_id); + MarkCallArgsClean(block, inst_it, inst); +} + +// A poison access chain index is UB. Emits an error for the first poison index, +// and combines all poison index shadows into a single scalar for the result. +void PoisonPass::InstrumentAccessChain(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + const uint32_t result_id = inst.ResultId(); + const uint32_t first_index_word = 4; + + uint32_t combined = 0; + bool error_emitted = false; + for (uint32_t w = first_index_word; w < inst.Length(); w++) { + uint32_t idx_id = inst.Word(w); + if (!IsPotentiallyPoison(idx_id)) continue; + uint32_t idx_shadow = GetShadowValue(idx_id); + uint32_t idx_shadow_type = GetShadowType(idx_id); + uint32_t scalar_shadow = EnsureShadowType(block, inst_it, idx_shadow, idx_shadow_type, bool_type_id); + + if (!error_emitted && scalar_shadow != constant_true_id_) { + EmitPoisonError(block, inst_it, scalar_shadow, inst); + instrumentations_count_++; + error_emitted = true; + } + + if (combined == 0) { + combined = scalar_shadow; + } else { + uint32_t new_combined = module_.TakeNextId(); + block.CreateInstruction(spv::OpLogicalAnd, {bool_type_id, new_combined, combined, scalar_shadow}, inst_it); + combined = new_combined; + } + } + if (combined != 0) { + SetShadowValue(result_id, combined, bool_type_id); + } +} + +// ============================================================================= +// Pass-through - per-component shadow data movement +// ============================================================================= + +void PoisonPass::InstrumentCompositeExtract(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t result_id = inst.ResultId(); + uint32_t composite_id = inst.Operand(0); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + uint32_t composite_shadow = GetShadowValue(composite_id); + uint32_t composite_shadow_type = GetShadowType(composite_id); + const Type* cst = type_manager_.FindTypeById(composite_shadow_type); + (void)cst; + + assert(cst); + assert(cst->spv_type_ != SpvType::kBool && cst->spv_type_ != SpvType::kInt && cst->spv_type_ != SpvType::kFloat); + uint32_t extracted_shadow = module_.TakeNextId(); + std::vector words = {result_shadow_type_id, extracted_shadow, composite_shadow}; + for (uint32_t w = 4; w < inst.Length(); w++) { + words.push_back(inst.Word(w)); + } + block.CreateInstruction(spv::OpCompositeExtract, words, inst_it); + SetShadowValue(result_id, extracted_shadow, result_shadow_type_id); +} + +// Shadow OpCompositeInsert: walks the shadow type through the index chain to find the +// element's shadow type, converts the inserted object's shadow to match, then emits +// a shadow OpCompositeInsert to splice it into the composite shadow. +void PoisonPass::InstrumentCompositeInsert(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t result_id = inst.ResultId(); + uint32_t object_id = inst.Operand(0); + uint32_t composite_id = inst.Operand(1); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + + uint32_t composite_shadow = GetShadowValue(composite_id); + uint32_t composite_shadow_type = GetShadowType(composite_id); + composite_shadow = EnsureShadowType(block, inst_it, composite_shadow, composite_shadow_type, result_shadow_type_id); + + uint32_t object_shadow = GetShadowValue(object_id); + uint32_t object_shadow_type = GetShadowType(object_id); + const Type* result_shadow = type_manager_.FindTypeById(result_shadow_type_id); + assert(result_shadow); + const Type* elem_shadow_type = result_shadow; + for (uint32_t w = 5; w < inst.Length(); w++) { + uint32_t index = inst.Word(w); + switch (elem_shadow_type->spv_type_) { + case SpvType::kVector: + case SpvType::kVectorIdEXT: + case SpvType::kArray: + case SpvType::kRuntimeArray: + elem_shadow_type = type_manager_.FindTypeById(elem_shadow_type->inst_.Word(2)); + break; + case SpvType::kStruct: + assert(index + 2 < elem_shadow_type->inst_.Length()); + elem_shadow_type = type_manager_.FindTypeById(elem_shadow_type->inst_.Word(2 + index)); + break; + default: + assert(false); + break; + } + assert(elem_shadow_type); + } + object_shadow = EnsureShadowType(block, inst_it, object_shadow, object_shadow_type, elem_shadow_type->Id()); + + uint32_t result_shadow_id = module_.TakeNextId(); + std::vector words = {result_shadow_type_id, result_shadow_id, object_shadow, composite_shadow}; + for (uint32_t w = 5; w < inst.Length(); w++) { + words.push_back(inst.Word(w)); + } + block.CreateInstruction(spv::OpCompositeInsert, words, inst_it); + SetShadowValue(result_id, result_shadow_id, result_shadow_type_id); +} + +void PoisonPass::InstrumentCompositeConstruct(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t result_id = inst.ResultId(); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + uint32_t result_shadow_id = module_.TakeNextId(); + std::vector words = {result_shadow_type_id, result_shadow_id}; + for (uint32_t w = 3; w < inst.Length(); w++) { + words.push_back(GetShadowValue(inst.Word(w))); + } + block.CreateInstruction(spv::OpCompositeConstruct, words, inst_it); + SetShadowValue(result_id, result_shadow_id, result_shadow_type_id); +} + +void PoisonPass::InstrumentVectorExtractDynamic(Function& function, BasicBlock& block, InstructionIt* inst_it, + const Instruction& inst) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + const uint32_t result_id = inst.ResultId(); + uint32_t vec_id = inst.Operand(0); + uint32_t idx_id = inst.Operand(1); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + + uint32_t vec_shadow = GetShadowValue(vec_id); + uint32_t vec_shadow_type = GetShadowType(vec_id); + + const Instruction* vec_def = function.FindInstruction(vec_id); + if (vec_def) { + uint32_t expected = GetOrCreateShadowType(vec_def->TypeId()); + vec_shadow = EnsureShadowType(block, inst_it, vec_shadow, vec_shadow_type, expected); + vec_shadow_type = GetOrCreateShadowType(vec_def->TypeId()); + } + + uint32_t extracted_shadow = module_.TakeNextId(); + block.CreateInstruction(spv::OpVectorExtractDynamic, {result_shadow_type_id, extracted_shadow, vec_shadow, idx_id}, inst_it); + + if (IsPotentiallyPoison(idx_id)) { + uint32_t idx_shadow = GetShadowValue(idx_id); + uint32_t idx_shadow_type = GetShadowType(idx_id); + uint32_t scalar_idx_shadow = EnsureShadowType(block, inst_it, idx_shadow, idx_shadow_type, bool_type_id); + uint32_t combined = module_.TakeNextId(); + block.CreateInstruction(spv::OpLogicalAnd, {bool_type_id, combined, extracted_shadow, scalar_idx_shadow}, inst_it); + SetShadowValue(result_id, combined, bool_type_id); + } else { + SetShadowValue(result_id, extracted_shadow, result_shadow_type_id); + } +} + +void PoisonPass::InstrumentVectorInsertDynamic(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + const uint32_t result_id = inst.ResultId(); + uint32_t vec_id = inst.Operand(0); + uint32_t component_id = inst.Operand(1); + uint32_t idx_id = inst.Operand(2); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + + uint32_t vec_shadow = GetShadowValue(vec_id); + uint32_t vec_shadow_type = GetShadowType(vec_id); + vec_shadow = EnsureShadowType(block, inst_it, vec_shadow, vec_shadow_type, result_shadow_type_id); + + uint32_t comp_shadow = GetShadowValue(component_id); + + uint32_t inserted_shadow = module_.TakeNextId(); + block.CreateInstruction(spv::OpVectorInsertDynamic, {result_shadow_type_id, inserted_shadow, vec_shadow, comp_shadow, idx_id}, + inst_it); + + if (IsPotentiallyPoison(idx_id)) { + uint32_t idx_shadow = GetShadowValue(idx_id); + uint32_t idx_shadow_type = GetShadowType(idx_id); + uint32_t scalar_idx_shadow = EnsureShadowType(block, inst_it, idx_shadow, idx_shadow_type, bool_type_id); + uint32_t broadcast = BroadcastShadow(block, inst_it, scalar_idx_shadow, result_shadow_type_id); + inserted_shadow = ComponentwiseAnd(block, inst_it, inserted_shadow, broadcast, result_shadow_type_id); + } + SetShadowValue(result_id, inserted_shadow, result_shadow_type_id); +} + +void PoisonPass::InstrumentVectorShuffle(Function& function, BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t result_id = inst.ResultId(); + uint32_t vec1_id = inst.Operand(0); + uint32_t vec2_id = inst.Operand(1); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + + uint32_t vec1_shadow = GetShadowValue(vec1_id); + uint32_t vec1_shadow_type = GetShadowType(vec1_id); + uint32_t vec2_shadow = GetShadowValue(vec2_id); + uint32_t vec2_shadow_type = GetShadowType(vec2_id); + + const Instruction* vec1_def = function.FindInstruction(vec1_id); + if (vec1_def) { + uint32_t expected = GetOrCreateShadowType(vec1_def->TypeId()); + vec1_shadow = EnsureShadowType(block, inst_it, vec1_shadow, vec1_shadow_type, expected); + } + const Instruction* vec2_def = function.FindInstruction(vec2_id); + if (vec2_def) { + uint32_t expected = GetOrCreateShadowType(vec2_def->TypeId()); + vec2_shadow = EnsureShadowType(block, inst_it, vec2_shadow, vec2_shadow_type, expected); + } + + uint32_t shuffled_shadow = module_.TakeNextId(); + std::vector words = {result_shadow_type_id, shuffled_shadow, vec1_shadow, vec2_shadow}; + for (uint32_t w = 5; w < inst.Length(); w++) { + words.push_back(inst.Word(w)); + } + block.CreateInstruction(spv::OpVectorShuffle, words, inst_it); + SetShadowValue(result_id, shuffled_shadow, result_shadow_type_id); +} + +// Shadow for OpSelect: select between the two operand shadows using the original condition, +// then AND with the condition's own shadow (broadcast to match the result type). This ensures +// a poison condition contaminates all components of the result without lossy scalar reduction. +void PoisonPass::InstrumentSelect(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t bool_type_id = type_manager_.GetTypeBool().Id(); + const uint32_t result_id = inst.ResultId(); + uint32_t cond_id = inst.Operand(0); + uint32_t a_id = inst.Operand(1); + uint32_t b_id = inst.Operand(2); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + + uint32_t cond_shadow = GetShadowValue(cond_id); + uint32_t cond_shadow_type = GetShadowType(cond_id); + uint32_t scalar_cond_shadow = EnsureShadowType(block, inst_it, cond_shadow, cond_shadow_type, bool_type_id); + + uint32_t a_shadow = GetShadowValue(a_id); + uint32_t a_shadow_type = GetShadowType(a_id); + a_shadow = EnsureShadowType(block, inst_it, a_shadow, a_shadow_type, result_shadow_type_id); + + uint32_t b_shadow = GetShadowValue(b_id); + uint32_t b_shadow_type = GetShadowType(b_id); + b_shadow = EnsureShadowType(block, inst_it, b_shadow, b_shadow_type, result_shadow_type_id); + + uint32_t selected_shadow = module_.TakeNextId(); + block.CreateInstruction(spv::OpSelect, {result_shadow_type_id, selected_shadow, cond_id, a_shadow, b_shadow}, inst_it); + + uint32_t cond_broadcast = BroadcastShadow(block, inst_it, scalar_cond_shadow, result_shadow_type_id); + uint32_t result_shadow = ComponentwiseAnd(block, inst_it, cond_broadcast, selected_shadow, result_shadow_type_id); + SetShadowValue(result_id, result_shadow, result_shadow_type_id); +} + +// Shadow OpPhi mirrors the original: each incoming value gets its shadow (using +// expected_shadow_type_id to get a type-matched constant for untracked operands, +// avoiding type mismatches since all phi operands must have the same type). +void PoisonPass::InstrumentPhi(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst) { + const uint32_t result_id = inst.ResultId(); + uint32_t result_shadow_type_id = GetOrCreateShadowType(inst.TypeId()); + uint32_t phi_shadow = module_.TakeNextId(); + std::vector phi_words = {result_shadow_type_id, phi_shadow}; + const uint32_t num_pairs = (inst.Length() - 3) / 2; + for (uint32_t i = 0; i < num_pairs; i++) { + uint32_t value_id = inst.Word(3 + i * 2); + uint32_t block_id = inst.Word(4 + i * 2); + phi_words.push_back(GetShadowValue(value_id, result_shadow_type_id)); + phi_words.push_back(block_id); + } + block.CreateInstruction(spv::OpPhi, phi_words, inst_it); + SetShadowValue(result_id, phi_shadow, result_shadow_type_id); +} + +// ============================================================================= +// Main entry point +// ============================================================================= + +bool PoisonPass::Instrument() { + for (Function& function : module_.functions_) { + if (!function.called_from_target_ && function.id_ != module_.target_entry_point_id_) { + continue; + } + + // Phase 1: Static analysis + FindUninitializedVariables(function); + if (uninitialized_var_ids_.empty()) continue; + + ComputePotentiallyPoisonSet(function); + if (potentially_poison_ids_.empty()) { + uninitialized_var_ids_.clear(); + continue; + } + + CreateShadowVariables(function); + InstrumentFunction(function); + } + + return instrumentations_count_ != 0; +} + +} // namespace spirv +} // namespace gpuav diff --git a/layers/gpuav/spirv/poison_pass.h b/layers/gpuav/spirv/poison_pass.h new file mode 100644 index 00000000000..3dcc4c75f7c --- /dev/null +++ b/layers/gpuav/spirv/poison_pass.h @@ -0,0 +1,126 @@ +/* Copyright (c) 2026 LunarG, Inc. + * Copyright (c) 2026 The Khronos Group Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Poison Pass - models poison value propagation and detects UB from their use at runtime. + +#pragma once + +#include "pass.h" +#include "gpuav/shaders/gpuav_error_codes.h" +#include +#include +#include + +namespace gpuav { +namespace spirv { + +class Module; +struct Function; + +class PoisonPass : public Pass { + public: + PoisonPass(Module& module); + const char* Name() const final { return "PoisonPass"; } + bool Instrument() final; + void PrintDebugInfo() const final; + + private: + // Static analysis to limit which variables are tracked + void FindUninitializedVariables(Function& function); + void ComputePotentiallyPoisonSet(Function& function); + + // Shadow types and variables + uint32_t GetOrCreateShadowType(uint32_t type_id); + void CreateShadowVariables(Function& function); + + // Instrumentation + void InstrumentFunction(Function& function); + bool InstrumentLoad(Function& function, BasicBlock& block, BasicBlockIt block_it, InstructionIt* inst_it, + const Instruction& inst); + bool InstrumentStore(Function& function, BasicBlock& block, BasicBlockIt block_it, InstructionIt* inst_it, + const Instruction& inst, const std::unordered_set& func_param_ids); + void InstrumentNonPoisonResult(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentPoisonResult(Function& function, BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + + // Pass-through - per-component shadow data movement + void InstrumentCompositeExtract(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentCompositeInsert(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentCompositeConstruct(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentVectorExtractDynamic(Function& function, BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentVectorInsertDynamic(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentVectorShuffle(Function& function, BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentSelect(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentPhi(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + + // UB triggers - result-producing error-reporting instructions + void InstrumentFunctionCall(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void InstrumentAccessChain(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + void MarkCallArgsClean(BasicBlock& block, InstructionIt* inst_it, const Instruction& call_inst); + void EmitPoisonError(BasicBlock& block, InstructionIt* inst_it, uint32_t shadow_id, const Instruction& trigger_inst, + uint32_t error_sub_code = glsl::kErrorSubCode_PoisonValue_UninitializedVariable); + + // Shadow value tracking (per-component: shadow type mirrors original type structure) + uint32_t GetShadowValue(uint32_t id, uint32_t expected_shadow_type_id = 0); + uint32_t GetShadowType(uint32_t id); + void SetShadowValue(uint32_t value_id, uint32_t shadow_id, uint32_t shadow_type_id); + uint32_t EnsureShadowType(BasicBlock& block, InstructionIt* inst_it, uint32_t shadow_id, uint32_t from_type_id, + uint32_t to_type_id); + uint32_t GetAllTrueConstant(uint32_t shadow_type_id); + + bool IsPotentiallyPoison(uint32_t id) const { return potentially_poison_ids_.count(id) != 0; } + uint32_t BroadcastShadow(BasicBlock& block, InstructionIt* inst_it, uint32_t scalar_shadow_id, uint32_t shadow_type_id); + uint32_t ReduceShadowToScalar(BasicBlock& block, InstructionIt* inst_it, uint32_t shadow_id, uint32_t shadow_type_id); + uint32_t ComponentwiseAnd(BasicBlock& block, InstructionIt* inst_it, uint32_t a_id, uint32_t b_id, uint32_t shadow_type_id); + + // Shadow pointer tracking: maps an original pointer id to its shadow pointer id. + // Propagated forward through OpAccessChain, OpPtrAccessChain, OpCopyObject. + struct ShadowPtrInfo { + uint32_t shadow_ptr_id; + uint32_t shadow_pointee_type_id; + spv::StorageClass shadow_sc; + uint32_t var_id; // base tracked variable this pointer derives from + }; + void PropagateShadowPointer(BasicBlock& block, InstructionIt* inst_it, const Instruction& inst); + const ShadowPtrInfo* FindShadowPointer(uint32_t ptr_id) const; + static bool IsLocalStorageClass(spv::StorageClass sc) { + return sc == spv::StorageClassFunction || sc == spv::StorageClassPrivate || sc == spv::StorageClassWorkgroup; + } + + struct ShadowVarInfo { + uint32_t shadow_var_id; + uint32_t shadow_pointee_type_id; + spv::StorageClass shadow_sc; + }; + + std::unordered_set uninitialized_var_ids_; + std::unordered_set all_local_var_ids_; + std::unordered_set contaminated_var_ids_; + std::unordered_set potentially_poison_ids_; + std::unordered_map shadow_type_cache_; + std::unordered_map shadow_var_map_; + std::unordered_map shadow_value_map_; + std::unordered_map shadow_value_type_map_; + std::unordered_map all_true_constants_; + std::unordered_map shadow_pointer_map_; + + uint32_t constant_true_id_ = 0; + uint32_t constant_false_id_ = 0; + + uint32_t link_function_id_ = 0; +}; + +} // namespace spirv +} // namespace gpuav diff --git a/layers/gpuav/spirv/type_manager.cpp b/layers/gpuav/spirv/type_manager.cpp index 075e5b929c9..cddfd46052c 100644 --- a/layers/gpuav/spirv/type_manager.cpp +++ b/layers/gpuav/spirv/type_manager.cpp @@ -336,6 +336,23 @@ const Type& TypeManager::GetTypeVector(const Type& component_type, uint32_t comp return AddType(std::move(new_inst), SpvType::kVector); } +const Type& TypeManager::GetTypeVectorIdEXT(const Type& component_type, const Constant& component_count) { + const uint32_t count = component_count.GetValueUint32(); + for (const auto& [id, type] : id_to_type_) { + if (type->spv_type_ != SpvType::kVectorIdEXT) continue; + if (type->meta_.vector.component_count != count) continue; + const Type* existing_component = FindTypeById(type->inst_.Word(2)); + if (existing_component && (*existing_component == component_type)) { + return *type; + } + } + + const uint32_t type_id = module_.TakeNextId(); + auto new_inst = std::make_unique(4, spv::OpTypeVectorIdEXT); + new_inst->Fill({type_id, component_type.Id(), component_count.Id()}); + return AddType(std::move(new_inst), SpvType::kVectorIdEXT); +} + const Type& TypeManager::GetTypeMatrix(const Type& column_type, uint32_t column_count) { for (const auto type : matrix_types_) { if (type->meta_.matrix.component_count != column_count) { diff --git a/layers/gpuav/spirv/type_manager.h b/layers/gpuav/spirv/type_manager.h index 73340b3e842..29cecdcf673 100644 --- a/layers/gpuav/spirv/type_manager.h +++ b/layers/gpuav/spirv/type_manager.h @@ -159,6 +159,7 @@ class TypeManager { const Type& GetTypeArray(const Type& element_type, const Constant& length, bool get_explicit_layout = true); const Type& GetTypeRuntimeArray(const Type& element_type, bool get_explicit_layout = true); const Type& GetTypeVector(const Type& component_type, uint32_t component_count); + const Type& GetTypeVectorIdEXT(const Type& component_type, const Constant& component_count); const Type& GetTypeMatrix(const Type& column_type, uint32_t column_count); const Type& GetTypeSampledImage(const Type& image_type); const Type& GetTypePointer(spv::StorageClass storage_class, const Type& pointer_type, bool get_explicit_layout = true); diff --git a/layers/layer_options.cpp b/layers/layer_options.cpp index 031021fc765..6a1e936746c 100644 --- a/layers/layer_options.cpp +++ b/layers/layer_options.cpp @@ -205,6 +205,7 @@ const char* VK_LAYER_GPUAV_MESH_SHADING = "gpuav_mesh_shading"; const char* VK_LAYER_GPUAV_POST_PROCESS_DESCRIPTOR_INDEXING = "gpuav_post_process_descriptor_indexing"; const char* VK_LAYER_GPUAV_VERTEX_ATTRIBUTE_FETCH_OOB = "gpuav_vertex_attribute_fetch_oob"; const char* VK_LAYER_GPUAV_SHADER_SANITIZER = "gpuav_shader_sanitizer"; +const char* VK_LAYER_GPUAV_POISON_VALUE = "gpuav_poison_value"; const char* VK_LAYER_GPUAV_SHARED_MEMORY_DATA_RACE = "gpuav_shared_memory_data_race"; const char* VK_LAYER_GPUAV_MAX_INDICES_COUNT = "gpuav_max_indices_count"; const char* VK_LAYER_GPUAV_SELECT_INSTRUMENTED_SHADERS = "gpuav_select_instrumented_shaders"; @@ -1053,6 +1054,11 @@ void ProcessConfigAndEnvSettings(ConfigAndEnvSettings* settings_data) { gpuav_settings.shader_instrumentation.sanitizer); } + if (vkuHasLayerSetting(layer_setting_set, VK_LAYER_GPUAV_POISON_VALUE)) { + vkuGetLayerSettingValue(layer_setting_set, VK_LAYER_GPUAV_POISON_VALUE, + gpuav_settings.shader_instrumentation.poison_value); + } + if (vkuHasLayerSetting(layer_setting_set, VK_LAYER_GPUAV_SHARED_MEMORY_DATA_RACE)) { vkuGetLayerSettingValue(layer_setting_set, VK_LAYER_GPUAV_SHARED_MEMORY_DATA_RACE, gpuav_settings.shader_instrumentation.shared_memory_data_race); diff --git a/layers/layer_options_validation.h b/layers/layer_options_validation.h index 0a6b717d0b6..ae1f60ad0f8 100644 --- a/layers/layer_options_validation.h +++ b/layers/layer_options_validation.h @@ -68,6 +68,7 @@ static void ValidateLayerSettingsProvided(const VkLayerSettingsCreateInfoEXT &la else if (strcmp(VK_LAYER_GPUAV_INDIRECT_TRACE_RAYS_BUFFERS, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPUAV_MAX_INDICES_COUNT, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_UINT32_EXT; } else if (strcmp(VK_LAYER_GPUAV_MESH_SHADING, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } + else if (strcmp(VK_LAYER_GPUAV_POISON_VALUE, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPUAV_POST_PROCESS_DESCRIPTOR_INDEXING, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPUAV_RAY_TRACING_BUFFERS_CONSISTENCY, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPUAV_SAFE_MODE, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } diff --git a/layers/state_tracker/shader_instruction.cpp b/layers/state_tracker/shader_instruction.cpp index be287264e57..c415a3058a7 100644 --- a/layers/state_tracker/shader_instruction.cpp +++ b/layers/state_tracker/shader_instruction.cpp @@ -316,6 +316,33 @@ void Instruction::ReplaceResultId(uint32_t new_result_id) { UpdateDebugInfo(); } +void Instruction::ForEachIdOperand(const std::function& callback, bool include_ambiguous) const { + const uint32_t length = Length(); + const uint32_t num_defined = static_cast(operand_info_.types.size()); + uint32_t type_index = 0; + for (uint32_t word_index = operand_index_; word_index < length; word_index++, type_index++) { + if (type_index < num_defined) { + OperandKind kind = operand_info_.types[type_index]; + if (kind == OperandKind::Id) { + callback(words_[word_index]); + } else if (kind == OperandKind::Composite && include_ambiguous) { + // Composite covers OpPhi (id,label pairs) and OpGroupMemberDecorate + // (id,literal pairs). Visiting all composite words is conservative - + // callers may see labels or literals alongside the IDs. + callback(words_[word_index]); + } + } else { + // Beyond the grammar's defined operands. For Id-variadic instructions + // (e.g. OpFunctionCall, OpCompositeConstruct) all remaining words are IDs. + // For BitEnum tails (ImageOperands, MemoryAccess, etc.) remaining words + // are a mix of IDs and literals whose types depend on the mask bits. + if (include_ambiguous || num_defined == 0 || operand_info_.types.back() != OperandKind::BitEnum) { + callback(words_[word_index]); + } + } + } +} + void Instruction::ReplaceOperandId(uint32_t old_word, uint32_t new_word) { const uint32_t length = Length(); uint32_t type_index = 0; diff --git a/layers/state_tracker/shader_instruction.h b/layers/state_tracker/shader_instruction.h index 2c5813a1089..91aa0c3a32c 100644 --- a/layers/state_tracker/shader_instruction.h +++ b/layers/state_tracker/shader_instruction.h @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -107,6 +108,11 @@ class Instruction { // Increments Length() as well void AppendWord(uint32_t word); void ReplaceResultId(uint32_t new_result_id); + // Calls |callback| with the value of each operand word that is an ID (not a literal). + // When |include_ambiguous| is true (default), composite words (which may include literals) + // and words beyond the grammar's defined operands (which may be BitEnum parameter literals) + // are visited. When false, only words that are unambiguously IDs are visited. + void ForEachIdOperand(const std::function& callback, bool include_ambiguous = true) const; // searchs all operands to replace ID if found void ReplaceOperandId(uint32_t old_word, uint32_t new_word); void ReplaceLinkedId(vvl::unordered_map& id_swap_map); diff --git a/layers/vk_layer_settings.txt b/layers/vk_layer_settings.txt index 1e5ec16aa3d..83dd1dd5919 100644 --- a/layers/vk_layer_settings.txt +++ b/layers/vk_layer_settings.txt @@ -135,6 +135,11 @@ khronos_validation.gpuav_max_indices_count = 8192 # Enable shader instrumentation on VK_EXT_mesh_shader khronos_validation.gpuav_mesh_shading = true +# Poison value (uninitialized variable detection) +# ===================== +# Instrument shaders to detect uses of uninitialized values. +khronos_validation.gpuav_poison_value = true + # Post process descriptor indexing # ===================== # Track which descriptor indexes were used in shader to run normal validation afterwards diff --git a/layers/vulkan/generated/gpuav_offline_spirv.cpp b/layers/vulkan/generated/gpuav_offline_spirv.cpp index 42cd289adc4..fe053a2239e 100644 --- a/layers/vulkan/generated/gpuav_offline_spirv.cpp +++ b/layers/vulkan/generated/gpuav_offline_spirv.cpp @@ -565,6 +565,39 @@ [[maybe_unused]] const uint32_t instrumentation_mesh_shading_comp_function_0_offset = 305; [[maybe_unused]] const uint32_t instrumentation_mesh_shading_comp_function_1_offset = 379; +[[maybe_unused]] const uint32_t instrumentation_poison_value_comp_size = 288; +[[maybe_unused]] const uint32_t instrumentation_poison_value_comp[288] = { + 0x07230203, 0x00010300, 0x0008000b, 0x0000001a, 0x00000000, 0x00020011, 0x00000001, 0x00020011, 0x00000005, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x00030003, 0x00000002, + 0x000001c2, 0x00070004, 0x415f4c47, 0x675f4252, 0x735f7570, 0x65646168, 0x6e695f72, 0x00343674, 0x00070004, 0x455f4c47, + 0x625f5458, 0x65666675, 0x65725f72, 0x65726566, 0x0065636e, 0x00080004, 0x455f4c47, 0x625f5458, 0x65666675, 0x65725f72, + 0x65726566, 0x3265636e, 0x00000000, 0x00090004, 0x455f4c47, 0x625f5458, 0x65666675, 0x65725f72, 0x65726566, 0x5f65636e, + 0x63657675, 0x00000032, 0x00080004, 0x455f4c47, 0x735f5458, 0x616c6163, 0x6c625f72, 0x5f6b636f, 0x6f79616c, 0x00007475, + 0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576, + 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x000a0005, 0x0000000a, + 0x74736e69, 0x696f705f, 0x5f6e6f73, 0x6f706572, 0x62287472, 0x31753b31, 0x3b31753b, 0x003b3175, 0x00050005, 0x00000006, + 0x705f7369, 0x6f73696f, 0x0000006e, 0x00050005, 0x00000007, 0x74736e69, 0x66666f5f, 0x00746573, 0x00060005, 0x00000008, + 0x67697274, 0x5f726567, 0x6f63706f, 0x00006564, 0x00060005, 0x00000009, 0x6f727265, 0x75735f72, 0x6f635f62, 0x00006564, + 0x00060005, 0x0000000e, 0x6f727245, 0x79615072, 0x64616f6c, 0x00000000, 0x00060006, 0x0000000e, 0x00000000, 0x74736e69, + 0x66666f5f, 0x00746573, 0x00090006, 0x0000000e, 0x00000001, 0x64616873, 0x655f7265, 0x726f7272, 0x636e655f, 0x6e69646f, + 0x00000067, 0x00060006, 0x0000000e, 0x00000002, 0x61726170, 0x6574656d, 0x00305f72, 0x00060006, 0x0000000e, 0x00000003, + 0x61726170, 0x6574656d, 0x00315f72, 0x00060006, 0x0000000e, 0x00000004, 0x61726170, 0x6574656d, 0x00325f72, 0x00060005, + 0x00000010, 0x6f727265, 0x61705f72, 0x616f6c79, 0x00000064, 0x00090005, 0x00000011, 0x63657053, 0x736e6f43, 0x746e6174, + 0x6b6e694c, 0x64616853, 0x64497265, 0x00000000, 0x00090047, 0x0000000a, 0x00000029, 0x74736e69, 0x696f705f, 0x5f6e6f73, + 0x6f706572, 0x00007472, 0x00000000, 0x00040047, 0x00000011, 0x00000001, 0x00000000, 0x00020014, 0x00000002, 0x00040015, + 0x00000003, 0x00000020, 0x00000000, 0x00020013, 0x00000004, 0x00070021, 0x00000005, 0x00000004, 0x00000002, 0x00000003, + 0x00000003, 0x00000003, 0x0007001e, 0x0000000e, 0x00000003, 0x00000003, 0x00000003, 0x00000003, 0x00000003, 0x00040020, + 0x0000000f, 0x00000006, 0x0000000e, 0x0004003b, 0x0000000f, 0x00000010, 0x00000006, 0x00040032, 0x00000003, 0x00000011, + 0x0dead001, 0x0004002b, 0x00000003, 0x00000012, 0x10000000, 0x00060034, 0x00000003, 0x00000013, 0x000000c5, 0x00000011, + 0x00000012, 0x00040015, 0x00000014, 0x00000020, 0x00000001, 0x0004002b, 0x00000014, 0x00000015, 0x00000012, 0x0004002b, + 0x00000003, 0x00000018, 0x00000000, 0x00050036, 0x00000004, 0x0000000a, 0x00000000, 0x00000005, 0x00030037, 0x00000002, + 0x00000006, 0x00030037, 0x00000003, 0x00000007, 0x00030037, 0x00000003, 0x00000008, 0x00030037, 0x00000003, 0x00000009, + 0x000200f8, 0x0000000b, 0x000300f7, 0x0000000d, 0x00000000, 0x000400fa, 0x00000006, 0x0000000c, 0x0000000d, 0x000200f8, + 0x0000000c, 0x000500c4, 0x00000003, 0x00000016, 0x00000009, 0x00000015, 0x000500c5, 0x00000003, 0x00000017, 0x00000013, + 0x00000016, 0x00080050, 0x0000000e, 0x00000019, 0x00000007, 0x00000017, 0x00000008, 0x00000018, 0x00000018, 0x0003003e, + 0x00000010, 0x00000019, 0x000200f9, 0x0000000d, 0x000200f8, 0x0000000d, 0x000100fd, 0x00010038}; +[[maybe_unused]] const uint32_t instrumentation_poison_value_comp_function_0_offset = 233; + [[maybe_unused]] const uint32_t instrumentation_post_process_descriptor_index_comp_size = 456; [[maybe_unused]] const uint32_t instrumentation_post_process_descriptor_index_comp[456] = { 0x07230203, 0x00010300, 0x0008000b, 0x00000034, 0x00000000, 0x00020011, 0x00000001, 0x00020011, 0x00000005, 0x00020011, diff --git a/layers/vulkan/generated/gpuav_offline_spirv.h b/layers/vulkan/generated/gpuav_offline_spirv.h index d5499540cee..d0b30efec9d 100644 --- a/layers/vulkan/generated/gpuav_offline_spirv.h +++ b/layers/vulkan/generated/gpuav_offline_spirv.h @@ -62,6 +62,11 @@ extern const uint32_t instrumentation_mesh_shading_comp[]; extern const uint32_t instrumentation_mesh_shading_comp_function_0_offset; extern const uint32_t instrumentation_mesh_shading_comp_function_1_offset; +extern const uint32_t instrumentation_poison_value_comp_size; +extern const uint32_t instrumentation_poison_value_comp[]; +// These offset match the function in the order they are declared in the GLSL source +extern const uint32_t instrumentation_poison_value_comp_function_0_offset; + extern const uint32_t instrumentation_post_process_descriptor_index_comp_size; extern const uint32_t instrumentation_post_process_descriptor_index_comp[]; // These offset match the function in the order they are declared in the GLSL source diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index adab9c5363a..afe58dbd9b9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -124,6 +124,8 @@ target_sources(vk_layer_validation_tests PRIVATE unit/gpu_av_shader_sanitizer_positive.cpp unit/gpu_av_shared_memory_data_race.cpp unit/gpu_av_shared_memory_data_race_positive.cpp + unit/gpu_av_poison_value.cpp + unit/gpu_av_poison_value_positive.cpp unit/gpu_av_spirv.cpp unit/gpu_av_spirv_positive.cpp unit/gpu_av_ray_query.cpp diff --git a/tests/unit/gpu_av_descriptor_class_general_buffer.cpp b/tests/unit/gpu_av_descriptor_class_general_buffer.cpp index a46ff502723..83409a2a6e1 100644 --- a/tests/unit/gpu_av_descriptor_class_general_buffer.cpp +++ b/tests/unit/gpu_av_descriptor_class_general_buffer.cpp @@ -2035,7 +2035,7 @@ TEST_F(NegativeGpuAVDescriptorClassGeneralBuffer, StructCopyGLSL) { }; void main() { - Bar new_bar; + Bar new_bar = Bar(0u, 0u, uint[2](0u, 0u)); b = new_bar; } )glsl"; @@ -2058,7 +2058,7 @@ TEST_F(NegativeGpuAVDescriptorClassGeneralBuffer, StructCopyGLSL2) { }; void main() { - Bar new_bar; + Bar new_bar = Bar(vec4(0.0)); b = new_bar; } )glsl"; @@ -2085,7 +2085,7 @@ TEST_F(NegativeGpuAVDescriptorClassGeneralBuffer, StructCopyGLSL3) { }; void main() { - Bar new_bar; + Bar new_bar = Bar(0u, Bar2(vec4(0.0))); b = new_bar; } )glsl"; diff --git a/tests/unit/gpu_av_descriptor_class_general_buffer_positive.cpp b/tests/unit/gpu_av_descriptor_class_general_buffer_positive.cpp index 5c7a66db842..16ad990e184 100644 --- a/tests/unit/gpu_av_descriptor_class_general_buffer_positive.cpp +++ b/tests/unit/gpu_av_descriptor_class_general_buffer_positive.cpp @@ -984,7 +984,7 @@ TEST_F(PositiveGpuAVDescriptorClassGeneralBuffer, StructCopyGLSL) { }; void main() { - Bar new_bar; + Bar new_bar = Bar(0u, 0u, uint[2](0u, 0u)); b = new_bar; } )glsl"; @@ -1005,7 +1005,7 @@ TEST_F(PositiveGpuAVDescriptorClassGeneralBuffer, StructCopyGLSL2) { }; void main() { - Bar new_bar; + Bar new_bar = Bar(vec4(0.0)); b = new_bar; } )glsl"; @@ -1033,7 +1033,7 @@ TEST_F(PositiveGpuAVDescriptorClassGeneralBuffer, StructCopyGLSL3) { }; void main() { - Bar new_bar; + Bar new_bar = Bar(0u, Bar2(vec4(0.0))); b = new_bar; } )glsl"; diff --git a/tests/unit/gpu_av_poison_value.cpp b/tests/unit/gpu_av_poison_value.cpp new file mode 100644 index 00000000000..7e753a51ede --- /dev/null +++ b/tests/unit/gpu_av_poison_value.cpp @@ -0,0 +1,1746 @@ +/* Copyright (c) 2026 The Khronos Group Inc. + * Copyright (c) 2026 LunarG, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +#include "../framework/layer_validation_tests.h" +#include "../framework/pipeline_helper.h" +#include "../framework/shader_helper.h" + +class NegativeGpuAVPoisonValue : public GpuAVTest { + protected: + void InitPoisonValue(); + void SimpleComputeTest(const char* shader, const char* expected_error, uint32_t error_count = 1, + SpvSourceType source_type = SPV_SOURCE_GLSL, spv_target_env spv_env = SPV_ENV_VULKAN_1_1); +}; + +void NegativeGpuAVPoisonValue::InitPoisonValue() { + SetTargetApiVersion(VK_API_VERSION_1_1); + RETURN_IF_SKIP(InitGpuAvFramework()); + RETURN_IF_SKIP(InitState()); +} + +void NegativeGpuAVPoisonValue::SimpleComputeTest(const char* shader, const char* expected_error, uint32_t error_count, + SpvSourceType source_type, spv_target_env spv_env) { + RETURN_IF_SKIP(InitPoisonValue()); + + CreateComputePipelineHelper pipe(*this); + pipe.dsl_bindings_[0] = {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr}; + pipe.cs_ = VkShaderObj(*m_device, shader, VK_SHADER_STAGE_COMPUTE_BIT, spv_env, source_type); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, kHostVisibleMemProps); + void* in_ptr = in_buffer.Memory().Map(); + memset(in_ptr, 0, 256); + + pipe.descriptor_set_.WriteDescriptorBufferInfo(0, in_buffer, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + pipe.descriptor_set_.UpdateDescriptorSets(); + + m_command_buffer.Begin(); + vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe); + vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe.pipeline_layout_, 0, 1, + &pipe.descriptor_set_.set_, 0, nullptr); + vk::CmdDispatch(m_command_buffer, 1, 1, 1); + m_command_buffer.End(); + + m_errorMonitor->SetDesiredError(expected_error, error_count); + m_default_queue->SubmitAndWait(m_command_buffer); + m_errorMonitor->VerifyFound(); +} + +TEST_F(NegativeGpuAVPoisonValue, BranchOnPoison) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + bool x; + if (x) { + output_val = 1; + } else { + output_val = 0; + } + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-BranchOnPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, SelectPoisonSideSelected) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint cond; + uint output_val; + }; + void main() { + uint x; + uint y = 42; + uint result = (cond == 0) ? x : y; + output_val = result; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPropagateArith) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + uint y = x + 1; + output_val = y; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonAccessChainIndex) { + // Hand-written SPIR-V: OpAccessChain with a poison index. + // The resulting pointer is never dereferenced, so only the access chain + // error fires (each thread can only report one error). + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %arr_type ArrayStride 4 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_42 = OpConstant %uint 42 + %uint_16 = OpConstant %uint 16 + %arr_type = OpTypeArray %uint %uint_16 + %SSBO = OpTypeStruct %arr_type + %ptr_ssbo = OpTypePointer StorageBuffer %SSBO + %ptr_ssbo_u = OpTypePointer StorageBuffer %uint + %ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %idx_var = OpVariable %ptr_func_u Function + %idx = OpLoad %uint %idx_var + %ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 %idx + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-AddressFromPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonExternalStore) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + output_val = x; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonVectorSwizzle) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + vec4 v; + float f = v.x; + output_val = uint(f); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonMultipleOperands) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint a; + uint b; + output_val = a + b; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PrivateScopeUninitialized) { + // Global variable in GLSL becomes Private storage class in SPIR-V + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint private_var; + void main() { + output_val = private_var; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, ContaminatedVariable) { + // Poison flows through a store into an initialized variable + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + uint y = 0u; + y = x; + output_val = y; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +// --- A1: Composite types --- + +TEST_F(NegativeGpuAVPoisonValue, PoisonVectorStore) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + vec4 output_val; + }; + void main() { + vec4 v; + output_val = v; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonStructMember) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct S { + uint a; + float b; + }; + void main() { + S s; + output_val = s.a; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonMatrixElement) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + float output_val; + }; + void main() { + mat4 m; + output_val = m[0][0]; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonArrayElement) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint arr[4]; + output_val = arr[0]; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +// --- A2: Pass-through instructions --- + +TEST_F(NegativeGpuAVPoisonValue, PoisonCompositeConstruct) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + vec4 output_val; + }; + void main() { + float a; + float b; + float c; + float d; + output_val = vec4(a, b, c, d); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonCompositeInsert) { + // Inserting a poison scalar into an initialized vector + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + vec4 output_val; + }; + void main() { + float poison_val; + vec4 v = vec4(1.0, 2.0, 3.0, 4.0); + v.x = poison_val; + output_val = v; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonVectorShuffleMixed) { + // Shuffle components from one uninitialized and one initialized vector + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + vec4 output_val; + }; + void main() { + vec4 poison_vec; + vec4 clean_vec = vec4(1.0, 2.0, 3.0, 4.0); + output_val = vec4(poison_vec.xy, clean_vec.zw); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPhi) { + // Poison flows through one branch of if/else, merges via phi + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint cond; + uint output_val; + }; + void main() { + uint x; + uint result; + if (cond == 0) { + result = x; + } else { + result = 42u; + } + output_val = result; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPhiScalarAsm) { + // Hand-written SPIR-V with an actual OpPhi. + // One branch loads from an uninitialized Function variable (poison), + // the other uses a constant (clean). The OpPhi merges them, and the + // result is stored to the SSBO -> should trigger an error when the + // poison path is taken (cond==0). + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpMemberDecorate %SSBO 1 Offset 4 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_42 = OpConstant %uint 42 + %bool = OpTypeBool + %SSBO = OpTypeStruct %uint %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %uninit_var = OpVariable %ptr_func_u Function + %cond_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + %cond_val = OpLoad %uint %cond_ptr + %is_zero = OpIEqual %bool %cond_val %uint_0 + OpSelectionMerge %merge None + OpBranchConditional %is_zero %true_br %false_br + %true_br = OpLabel + %poison_val = OpLoad %uint %uninit_var + OpBranch %merge + %false_br = OpLabel + OpBranch %merge + %merge = OpLabel + %result = OpPhi %uint %poison_val %true_br %uint_42 %false_br + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %result + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPhiVectorAsm) { + // OpPhi with vec4 type. One branch carries a poison vector (loaded from + // an uninitialized Function variable), the other a clean constant. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpMemberDecorate %SSBO 1 Offset 16 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %v4uint = OpTypeVector %uint 4 + %clean_vec = OpConstantComposite %v4uint %uint_1 %uint_1 %uint_1 %uint_1 + %bool = OpTypeBool + %SSBO = OpTypeStruct %uint %v4uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_ssbo_v = OpTypePointer StorageBuffer %v4uint +%ptr_func_v = OpTypePointer Function %v4uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %uninit_var = OpVariable %ptr_func_v Function + %cond_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + %cond_val = OpLoad %uint %cond_ptr + %is_zero = OpIEqual %bool %cond_val %uint_0 + OpSelectionMerge %merge None + OpBranchConditional %is_zero %true_br %false_br + %true_br = OpLabel + %poison_vec = OpLoad %v4uint %uninit_var + OpBranch %merge + %false_br = OpLabel + OpBranch %merge + %merge = OpLabel + %result = OpPhi %v4uint %poison_vec %true_br %clean_vec %false_br + %out_ptr = OpAccessChain %ptr_ssbo_v %ssbo_var %uint_1 + OpStore %out_ptr %result + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPhiBothPoisonAsm) { + // OpPhi where BOTH incoming values are poison (from two different + // uninitialized Function variables). Should trigger regardless of + // which branch is taken. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpMemberDecorate %SSBO 1 Offset 4 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %bool = OpTypeBool + %SSBO = OpTypeStruct %uint %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %uninit_a = OpVariable %ptr_func_u Function + %uninit_b = OpVariable %ptr_func_u Function + %cond_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + %cond_val = OpLoad %uint %cond_ptr + %is_zero = OpIEqual %bool %cond_val %uint_0 + OpSelectionMerge %merge None + OpBranchConditional %is_zero %true_br %false_br + %true_br = OpLabel + %val_a = OpLoad %uint %uninit_a + OpBranch %merge + %false_br = OpLabel + %val_b = OpLoad %uint %uninit_b + OpBranch %merge + %merge = OpLabel + %result = OpPhi %uint %val_a %true_br %val_b %false_br + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_1 + OpStore %out_ptr %result + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +// --- A3: Propagation instructions --- + +TEST_F(NegativeGpuAVPoisonValue, PoisonPropagateFloat) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + float f; + float g = f * 2.0; + output_val = uint(g); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPropagateBitwise) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + uint y = x & 0xFFu; + output_val = y; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPropagateComparison) { + // Poison propagates through comparison, then used as branch condition + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + if (x > 5u) { + output_val = 1; + } else { + output_val = 0; + } + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-BranchOnPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPropagateConversion) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + int x; + float y = float(x); + output_val = uint(y); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPropagateDot) { + // Dot product reduces vec4 to float, exercises dimensionality reduction + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + vec4 v; + float d = dot(v, vec4(1.0)); + output_val = uint(d); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +// --- A4: UB trigger instructions --- + +TEST_F(NegativeGpuAVPoisonValue, PoisonSwitchSelector) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + switch (x) { + case 0: output_val = 0; break; + case 1: output_val = 1; break; + default: output_val = 2; break; + } + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-BranchOnPoison"); +} + +// --- A5: Chaining and complex flow --- + +TEST_F(NegativeGpuAVPoisonValue, PoisonMultiStepChain) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + uint y = x * 2u; + uint z = y + 1u; + uint w = z & 0xFFu; + output_val = w; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonThroughLoop) { + // Poison initial value accumulated through loop iterations + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint sum; + for (int i = 0; i < 4; i++) { + sum += uint(i); + } + output_val = sum; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonPartialVectorPoison) { + // Per-component: 3 clean + 1 poison, extract the poison component + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + float d; + vec4 v = vec4(1.0, 2.0, 3.0, d); + output_val = uint(v.w); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonNestedStruct) { + // Struct containing an array, all uninitialized + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct Inner { + uint data[4]; + }; + void main() { + Inner s; + output_val = s.data[0]; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonContaminatedChain) { + // Contamination + further arithmetic propagation + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + uint y = 0u; + y = x; + uint z = y + 1u; + output_val = z; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonStructOfStruct) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct Inner { uint x; float y; }; + struct Outer { Inner i; uint z; }; + void main() { + Outer o; + output_val = o.i.x; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonArrayOfStructs) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct S { uint x; float y; }; + void main() { + S arr[3]; + output_val = arr[1].x; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonStructWithVector) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct S { vec4 v; uint x; }; + void main() { + S s; + output_val = uint(s.v.x); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonStructWithMatrix) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct S { mat4 m; }; + void main() { + S s; + output_val = uint(s.m[0][0]); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonArrayOfArrays) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint arr[2][3]; + output_val = arr[0][0]; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonDeepNesting) { + // 3 levels: struct of struct of struct + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct L1 { uint val; }; + struct L2 { L1 inner; }; + struct L3 { L2 mid; }; + void main() { + L3 s; + output_val = s.mid.inner.val; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonLargeStruct) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct Big { + uint a; uint b; uint c; uint d; + uint e; uint f; uint g; uint h; + }; + void main() { + Big s; + output_val = s.h; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonDoubleVector) { + AddRequiredFeature(vkt::Feature::shaderFloat64); + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + dvec4 v; + output_val = uint(v.x); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonSpecConstantArray) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + layout(constant_id = 0) const int N = 4; + void main() { + uint arr[N]; + output_val = arr[0]; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonStructArrayOfVectors) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct S { vec3 positions[2]; }; + void main() { + S s; + output_val = uint(s.positions[0].x); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonThroughFunctionCall) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint identity(uint x) { return x; } + void main() { + uint x; + output_val = identity(x); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonVectorThroughFunctionCall) { + // Only one component is poison; the shadow reduction should still taint the result + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + float sum_vec(vec4 v) { return v.x + v.y + v.z + v.w; } + void main() { + float d; + vec4 v = vec4(1.0, 2.0, 3.0, d); + output_val = uint(sum_vec(v)); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonStructThroughFunctionCall) { + // Only one member is poison; shadow reduction should detect it + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct S { uint a; float b; }; + uint get_a(S s) { return s.a; } + void main() { + float uninit_f; + S s = S(42u, uninit_f); + output_val = get_a(s); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonMixedArgsFunctionCall) { + // Two args, only one is poison + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint add(uint a, uint b) { return a + b; } + void main() { + uint x; + uint y = 42u; + output_val = add(x, y); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonReturnFromFunction) { + // Function has its own uninit local and returns it + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint make_poison() { + uint x; + return x; + } + void main() { + output_val = make_poison(); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ReturnOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonFunctionParamOut) { + // Callee has uninit local and stores it to an out parameter + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void make_poison(out uint result) { + uint x; + result = x; + } + void main() { + uint val; + make_poison(val); + output_val = val; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-StoreToFunctionParam"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonFunctionParamInout) { + // Callee has uninit local and stores it to an inout parameter + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void replace_with_poison(inout uint val) { + uint x; + val = x; + } + void main() { + uint val = 42u; + replace_with_poison(val); + output_val = val; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-StoreToFunctionParam"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonFunctionParamConstIn) { + // const in passes by value; callee returns it, caller stores externally + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint identity(const in uint x) { return x; } + void main() { + uint x; + output_val = identity(x); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonNestedFunctionCall) { + // Poison flows through two levels of function calls + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint inner(uint x) { return x + 1u; } + uint outer(uint x) { return inner(x); } + void main() { + uint x; + output_val = outer(x); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonFunctionCallChain) { + // Result of one function call feeds into another + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint add_one(uint x) { return x + 1u; } + uint double_it(uint x) { return x * 2u; } + void main() { + uint x; + output_val = double_it(add_one(x)); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonMultipleOutParams) { + // Function writes poison to multiple out parameters + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void make_two_poisons(out uint a, out uint b) { + uint x; + a = x; + b = x; + } + void main() { + uint a, b; + make_two_poisons(a, b); + output_val = 0u; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-StoreToFunctionParam"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonOutParamPartialInit) { + // Function writes poison to one out param and clean to another; + // the poison one should be detected + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void partial_init(out uint good, out uint bad) { + uint x; + good = 42u; + bad = x; + } + void main() { + uint a, b; + partial_init(a, b); + output_val = 0u; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-StoreToFunctionParam"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonNestedOutParam) { + // Inner function writes poison to out param, outer passes it through + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void make_poison(out uint result) { + uint x; + result = x; + } + void wrapper(out uint result) { + make_poison(result); + } + void main() { + uint val; + wrapper(val); + output_val = val; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-StoreToFunctionParam"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonReturnThenStore) { + // Function returns poison, caller stores the result externally + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint get_poison() { + uint x; + return x; + } + uint relay() { + return get_poison(); + } + void main() { + output_val = relay(); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ReturnOfPoison"); +} + +// --- BDA and variable pointer poison --- + +TEST_F(NegativeGpuAVPoisonValue, PoisonBdaPointerStore) { + // Uninitialized uint64 converted to BDA pointer, then stored through. + // The pointer value is poison; dereferencing it is UB. + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::bufferDeviceAddress); + AddRequiredFeature(vkt::Feature::shaderInt64); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability Int64 + OpCapability PhysicalStorageBufferAddresses + OpExtension "SPV_KHR_physical_storage_buffer" + OpMemoryModel PhysicalStorageBuffer64 GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %ssbo_type Block + OpMemberDecorate %ssbo_type 0 Offset 0 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %func = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint64 = OpTypeInt 64 0 + %ssbo_type = OpTypeStruct %uint +%ptr_sb_struct = OpTypePointer StorageBuffer %ssbo_type + %ptr_sb_uint = OpTypePointer StorageBuffer %uint +%ptr_psb_uint = OpTypePointer PhysicalStorageBuffer %uint +%ptr_func_u64 = OpTypePointer Function %uint64 + %ssbo = OpVariable %ptr_sb_struct StorageBuffer + %idx_0 = OpConstant %uint 0 + %val_42u = OpConstant %uint 42 + %main = OpFunction %void None %func + %entry = OpLabel + %addr_var = OpVariable %ptr_func_u64 Function + %addr = OpLoad %uint64 %addr_var + %ptr = OpConvertUToPtr %ptr_psb_uint %addr + OpStore %ptr %val_42u Aligned 4 + %out_p = OpAccessChain %ptr_sb_uint %ssbo %idx_0 + OpStore %out_p %val_42u + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-PoisonPointerDereference", 1, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonBdaPointerLoad) { + // Uninitialized uint64 converted to BDA pointer, then loaded through. + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::bufferDeviceAddress); + AddRequiredFeature(vkt::Feature::shaderInt64); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability Int64 + OpCapability PhysicalStorageBufferAddresses + OpExtension "SPV_KHR_physical_storage_buffer" + OpMemoryModel PhysicalStorageBuffer64 GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %ssbo_type Block + OpMemberDecorate %ssbo_type 0 Offset 0 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %func = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint64 = OpTypeInt 64 0 + %ssbo_type = OpTypeStruct %uint +%ptr_sb_struct = OpTypePointer StorageBuffer %ssbo_type + %ptr_sb_uint = OpTypePointer StorageBuffer %uint +%ptr_psb_uint = OpTypePointer PhysicalStorageBuffer %uint +%ptr_func_u64 = OpTypePointer Function %uint64 + %ssbo = OpVariable %ptr_sb_struct StorageBuffer + %idx_0 = OpConstant %uint 0 + %main = OpFunction %void None %func + %entry = OpLabel + %addr_var = OpVariable %ptr_func_u64 Function + %addr = OpLoad %uint64 %addr_var + %ptr = OpConvertUToPtr %ptr_psb_uint %addr + %val = OpLoad %uint %ptr Aligned 4 + %out_p = OpAccessChain %ptr_sb_uint %ssbo %idx_0 + OpStore %out_p %val + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-PoisonPointerDereference", 1, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(NegativeGpuAVPoisonValue, ChainedAccessChainLoadAsm) { + // Chained access chains (OpAccessChain of OpAccessChain) to an + // uninitialized variable. The poison pass should still detect the load + // as coming from a tracked variable despite the indirection. + const char* cs_chained = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_3 = OpConstant %uint 3 + %arr_type = OpTypeArray %uint %uint_3 + %arr2_type = OpTypeArray %arr_type %uint_3 + %SSBO = OpTypeStruct %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_a2 = OpTypePointer Function %arr2_type +%ptr_func_a = OpTypePointer Function %arr_type +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %arr_var = OpVariable %ptr_func_a2 Function + %inner_ptr = OpAccessChain %ptr_func_a %arr_var %uint_0 + %outer_ptr = OpAccessChain %ptr_func_u %inner_ptr %uint_0 + %val = OpLoad %uint %outer_ptr + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %val + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_chained, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, CopyObjectPointerLoadAsm) { + // OpCopyObject on a pointer to an uninitialized variable. + // TraceToVariable must walk through OpCopyObject to find the base variable. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %SSBO = OpTypeStruct %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %local = OpVariable %ptr_func_u Function + %copy = OpCopyObject %ptr_func_u %local + %val = OpLoad %uint %copy + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %val + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, CopyObjectBetweenAccessChainsAsm) { + // OpCopyObject between two levels of access chain into an uninitialized array. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_3 = OpConstant %uint 3 + %arr_type = OpTypeArray %uint %uint_3 + %SSBO = OpTypeStruct %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_a = OpTypePointer Function %arr_type +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %arr_var = OpVariable %ptr_func_a Function + %ac_arr = OpAccessChain %ptr_func_u %arr_var %uint_0 + %copy = OpCopyObject %ptr_func_u %ac_arr + %val = OpLoad %uint %copy + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %val + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, PtrAccessChainPoisonIndexAsm) { + // OpPtrAccessChain with an uninitialized Element index. The poison pass + // should detect the poison index and report AddressFromPoison. + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_KHR_VARIABLE_POINTERS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::variablePointersStorageBuffer); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability VariablePointersStorageBuffer + OpExtension "SPV_KHR_variable_pointers" + OpExtension "SPV_KHR_storage_buffer_storage_class" + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %ssbo_type Block + OpMemberDecorate %ssbo_type 0 Offset 0 + OpDecorate %arr_type ArrayStride 4 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + OpDecorate %ptr_sb_uint ArrayStride 4 + %void = OpTypeVoid + %func = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_42 = OpConstant %uint 42 + %uint_4 = OpConstant %uint 4 + %arr_type = OpTypeArray %uint %uint_4 + %ssbo_type = OpTypeStruct %arr_type +%ptr_sb_struct = OpTypePointer StorageBuffer %ssbo_type + %ptr_sb_arr = OpTypePointer StorageBuffer %arr_type + %ptr_sb_uint = OpTypePointer StorageBuffer %uint +%ptr_func_uint = OpTypePointer Function %uint + %ssbo = OpVariable %ptr_sb_struct StorageBuffer + %main = OpFunction %void None %func + %entry = OpLabel + %local_idx = OpVariable %ptr_func_uint Function + %poison_idx = OpLoad %uint %local_idx + %arr_ptr = OpAccessChain %ptr_sb_arr %ssbo %uint_0 + %first_p = OpAccessChain %ptr_sb_uint %arr_ptr %uint_0 + %elem_ptr = OpPtrAccessChain %ptr_sb_uint %first_p %poison_idx + %out_p = OpAccessChain %ptr_sb_uint %arr_ptr %uint_0 + OpStore %out_p %uint_42 + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-AddressFromPoison", 1, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(NegativeGpuAVPoisonValue, PtrAccessChainStructArrayPoisonStoreAsm) { + // Store a poison value through a StorageBuffer pointer derived via + // OpPtrAccessChain on a struct array. The Element index offsets to a + // struct element, then OpAccessChain descends into a struct member. + // The poison value (from an uninitialized local) stored through this + // pointer chain should trigger ExternalStoreOfPoison. + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_KHR_VARIABLE_POINTERS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::variablePointersStorageBuffer); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability VariablePointersStorageBuffer + OpExtension "SPV_KHR_variable_pointers" + OpExtension "SPV_KHR_storage_buffer_storage_class" + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo_var + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpMemberDecorate %Elem_S 0 Offset 0 + OpMemberDecorate %Elem_S 1 Offset 4 + OpDecorate %arr_type ArrayStride 8 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + OpDecorate %ptr_sb_S ArrayStride 8 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %uint_4 = OpConstant %uint 4 + %Elem_S = OpTypeStruct %uint %uint + %arr_type = OpTypeArray %Elem_S %uint_4 + %SSBO = OpTypeStruct %arr_type + %ptr_func_u = OpTypePointer Function %uint +%ptr_sb_SSBO = OpTypePointer StorageBuffer %SSBO + %ptr_sb_arr = OpTypePointer StorageBuffer %arr_type + %ptr_sb_S = OpTypePointer StorageBuffer %Elem_S + %ptr_sb_u = OpTypePointer StorageBuffer %uint + %ssbo_var = OpVariable %ptr_sb_SSBO StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %local_var = OpVariable %ptr_func_u Function + %poison_val = OpLoad %uint %local_var + %arr_ptr = OpAccessChain %ptr_sb_arr %ssbo_var %uint_0 + %elem0_p = OpAccessChain %ptr_sb_S %arr_ptr %uint_0 + %elem1_p = OpPtrAccessChain %ptr_sb_S %elem0_p %uint_1 + %member_p = OpAccessChain %ptr_sb_u %elem1_p %uint_0 + OpStore %member_p %poison_val + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(NegativeGpuAVPoisonValue, PartialInitStillPoisonAsm) { + // Only element 0 of an array is written via access chain; element 1 + // remains uninitialized. Loading element 1 should detect poison. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %uint_42 = OpConstant %uint 42 + %uint_4 = OpConstant %uint 4 + %arr_type = OpTypeArray %uint %uint_4 + %SSBO = OpTypeStruct %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_arr = OpTypePointer Function %arr_type +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %arr_var = OpVariable %ptr_func_arr Function + %elem_ptr0 = OpAccessChain %ptr_func_u %arr_var %uint_0 + OpStore %elem_ptr0 %uint_42 + %elem_ptr1 = OpAccessChain %ptr_func_u %arr_var %uint_1 + %val = OpLoad %uint %elem_ptr1 + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %val + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, LoadBeforeStoreNotInitAsm) { + // OpLoad from the variable before OpStore in the entry block. + // The load-before-store means the variable is NOT treated as initialized. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_42 = OpConstant %uint 42 + %SSBO = OpTypeStruct %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %local = OpVariable %ptr_func_u Function + %poison = OpLoad %uint %local + OpStore %local %uint_42 + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %poison + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, CrossBlockPropagationAsm) { + // Poison value loaded in the entry block, then used in a different block + // (via direct SSA reference, not through memory). Verifies that the static + // analysis and runtime shadow tracking work across basic block boundaries. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %bool = OpTypeBool + %SSBO = OpTypeStruct %uint + %ptr_ssbo = OpTypePointer StorageBuffer %SSBO + %ptr_ssbo_u = OpTypePointer StorageBuffer %uint + %ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %local = OpVariable %ptr_func_u Function + %poison = OpLoad %uint %local + OpBranch %next + %next = OpLabel + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %poison + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonLongVector) { + AddRequiredExtensions(VK_EXT_SHADER_LONG_VECTOR_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::longVector); + const char* cs_source = R"glsl( + #version 450 + #extension GL_EXT_long_vector : enable + layout(set=0, binding=0) buffer SSBO { + vector output_val; + }; + void main() { + vector v; + output_val = v; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonVariablePointerSelect) { + // OpSelect on SSBO pointers with a poison condition (uninitialized bool). + // The resulting pointer is poison; dereferencing it is UB. + AddRequiredExtensions(VK_KHR_VARIABLE_POINTERS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::variablePointersStorageBuffer); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability VariablePointersStorageBuffer + OpExtension "SPV_KHR_variable_pointers" + OpExtension "SPV_KHR_storage_buffer_storage_class" + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %ssbo_type Block + OpMemberDecorate %ssbo_type 0 Offset 0 + OpMemberDecorate %ssbo_type 1 Offset 4 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %func = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %bool = OpTypeBool + %ssbo_type = OpTypeStruct %uint %uint +%ptr_sb_struct = OpTypePointer StorageBuffer %ssbo_type + %ptr_sb_uint = OpTypePointer StorageBuffer %uint +%ptr_func_bool = OpTypePointer Function %bool + %ssbo = OpVariable %ptr_sb_struct StorageBuffer + %idx_0 = OpConstant %uint 0 + %idx_1 = OpConstant %uint 1 + %val_42u = OpConstant %uint 42 + %main = OpFunction %void None %func + %entry = OpLabel + %cond_var = OpVariable %ptr_func_bool Function + %cond = OpLoad %bool %cond_var + %ptr_a = OpAccessChain %ptr_sb_uint %ssbo %idx_0 + %ptr_b = OpAccessChain %ptr_sb_uint %ssbo %idx_1 + %chosen = OpSelect %ptr_sb_uint %cond %ptr_a %ptr_b + OpStore %chosen %val_42u + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-PoisonPointerDereference", 1, SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonSelectConditionScalar) { + // OpSelect where the condition is poison (uninitialized bool), scalar result. + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + bool cond; + uint result = cond ? 1u : 2u; + output_val = result; + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonSelectConditionVector) { + // OpSelect where the condition is poison (uninitialized bvec4), vector result. + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + vec4 output_val; + }; + void main() { + bvec4 cond; + vec4 a = vec4(1.0); + vec4 b = vec4(2.0); + output_val = mix(a, b, cond); + } + )glsl"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison"); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonCopyLogicalExtractPoisonMemberAsm) { + // Construct a struct with one poison member (x) and one clean member (y). + // OpCopyLogical to a different struct type, extract the POISON member, + // and store it externally. + SetTargetApiVersion(VK_API_VERSION_1_2); + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_42 = OpConstant %uint 42 + %StructA = OpTypeStruct %uint %uint + %StructB = OpTypeStruct %uint %uint + %SSBO = OpTypeStruct %uint + %ptr_sb_SSBO = OpTypePointer StorageBuffer %SSBO + %ptr_sb_u = OpTypePointer StorageBuffer %uint + %ptr_func_u = OpTypePointer Function %uint + %ssbo = OpVariable %ptr_sb_SSBO StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %poison_var = OpVariable %ptr_func_u Function + %poison_x = OpLoad %uint %poison_var + %src = OpCompositeConstruct %StructA %poison_x %uint_42 + %dst = OpCopyLogical %StructB %src + %poison_m0 = OpCompositeExtract %uint %dst 0 + %out_ptr = OpAccessChain %ptr_sb_u %ssbo %uint_0 + OpStore %out_ptr %poison_m0 + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonCompositeConstructReplicateAsm) { + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_EXT_SHADER_REPLICATED_COMPOSITES_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::shaderReplicatedComposites); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability ReplicatedCompositesEXT + OpExtension "SPV_EXT_replicated_composites" + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %ssbo_type Block + OpMemberDecorate %ssbo_type 0 Offset 0 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %func = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %v4uint = OpTypeVector %uint 4 + %uint_0 = OpConstant %uint 0 + %ssbo_type = OpTypeStruct %v4uint +%ptr_sb_struct = OpTypePointer StorageBuffer %ssbo_type +%ptr_sb_v4uint = OpTypePointer StorageBuffer %v4uint +%ptr_func_uint = OpTypePointer Function %uint + %ssbo = OpVariable %ptr_sb_struct StorageBuffer + %main = OpFunction %void None %func + %entry = OpLabel + %uninit_p = OpVariable %ptr_func_uint Function + %poison_u = OpLoad %uint %uninit_p + %result = OpCompositeConstructReplicateEXT %v4uint %poison_u + %out_ptr = OpAccessChain %ptr_sb_v4uint %ssbo %uint_0 + OpStore %out_ptr %result + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(NegativeGpuAVPoisonValue, PoisonSelectConditionStructAsm) { + // OpSelect (SPIR-V 1.4+) where the condition is poison, struct result. + SetTargetApiVersion(VK_API_VERSION_1_2); + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo_var + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpMemberDecorate %SSBO 1 Offset 4 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %bool = OpTypeBool + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %uint_10 = OpConstant %uint 10 + %uint_20 = OpConstant %uint 20 + %my_struct = OpTypeStruct %uint %uint + %SSBO = OpTypeStruct %uint %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_bool = OpTypePointer Function %bool + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %cond_var = OpVariable %ptr_func_bool Function + %cond = OpLoad %bool %cond_var + %val_a = OpCompositeConstruct %my_struct %uint_10 %uint_10 + %val_b = OpCompositeConstruct %my_struct %uint_20 %uint_20 + %selected = OpSelect %my_struct %cond %val_a %val_b + %member_0 = OpCompositeExtract %uint %selected 0 + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %member_0 + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, "SPIRV-PoisonValue-ExternalStoreOfPoison", 1, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} diff --git a/tests/unit/gpu_av_poison_value_positive.cpp b/tests/unit/gpu_av_poison_value_positive.cpp new file mode 100644 index 00000000000..6f2fd44efdd --- /dev/null +++ b/tests/unit/gpu_av_poison_value_positive.cpp @@ -0,0 +1,1031 @@ +/* Copyright (c) 2026 The Khronos Group Inc. + * Copyright (c) 2026 Valve Corporation + * Copyright (c) 2026 LunarG, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +#include "../framework/layer_validation_tests.h" +#include "../framework/pipeline_helper.h" +#include "../framework/shader_helper.h" +#include "../framework/cooperative_matrix_helper.h" + +class PositiveGpuAVPoisonValue : public GpuAVTest { + protected: + void InitPoisonValue(); + void SimpleComputeTest(const char* shader, SpvSourceType source_type = SPV_SOURCE_GLSL, + spv_target_env spv_env = SPV_ENV_VULKAN_1_1); +}; + +void PositiveGpuAVPoisonValue::InitPoisonValue() { + SetTargetApiVersion(VK_API_VERSION_1_1); + RETURN_IF_SKIP(InitGpuAvFramework()); + RETURN_IF_SKIP(InitState()); +} + +void PositiveGpuAVPoisonValue::SimpleComputeTest(const char* shader, SpvSourceType source_type, spv_target_env spv_env) { + RETURN_IF_SKIP(InitPoisonValue()); + + CreateComputePipelineHelper pipe(*this); + pipe.dsl_bindings_[0] = {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr}; + pipe.cs_ = VkShaderObj(*m_device, shader, VK_SHADER_STAGE_COMPUTE_BIT, spv_env, source_type); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, kHostVisibleMemProps); + void* in_ptr = in_buffer.Memory().Map(); + memset(in_ptr, 0, 256); + + pipe.descriptor_set_.WriteDescriptorBufferInfo(0, in_buffer, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + pipe.descriptor_set_.UpdateDescriptorSets(); + + m_command_buffer.Begin(); + vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe); + vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe.pipeline_layout_, 0, 1, + &pipe.descriptor_set_.set_, 0, nullptr); + vk::CmdDispatch(m_command_buffer, 1, 1, 1); + m_command_buffer.End(); + + m_default_queue->SubmitAndWait(m_command_buffer); +} + +TEST_F(PositiveGpuAVPoisonValue, InitializedVariable) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x = 42; + output_val = x; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, WriteBeforeRead) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint input_val; + uint output_val; + }; + void main() { + uint x; + x = input_val; + output_val = x; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, NoUninitializedVars) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint input_val; + uint output_val; + }; + void main() { + output_val = input_val + 1; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, InitializedWithZero) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x = 0; + if (x == 0) { + output_val = 1; + } + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, InitializedBranch) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + bool cond = true; + if (cond) { + output_val = 1; + } + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, ArrayInitialized) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint arr[4] = uint[4](1, 2, 3, 4); + output_val = arr[0]; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, ContaminatedWithClean) { + // Initialized variable overwritten with a clean value should not trigger + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint input_val; + uint output_val; + }; + void main() { + uint x = 0u; + uint y = 0u; + y = x; + output_val = y; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, VectorWriteBeforeRead) { + // Uninitialized vec4, fully written from buffer before read + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + vec4 input_val; + vec4 output_val; + }; + void main() { + vec4 v; + v = input_val; + output_val = v; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, PartialVectorCleanExtract) { + // Construct vec4 with 1 poison component, extract a clean component + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + float d; + vec4 v = vec4(1.0, 2.0, 3.0, d); + output_val = uint(v.x); + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, StructInitialized) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct S { + uint a; + float b; + }; + void main() { + S s = S(42u, 3.14); + output_val = s.a; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, LoopInitializes) { + // Variable uninitialized before loop, written on first iteration + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint x; + for (int i = 0; i < 1; i++) { + x = 42u; + } + output_val = x; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, PhiBothClean) { + // Uninitialized variable, written to clean values in both branches + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint cond; + uint output_val; + }; + void main() { + uint x; + if (cond == 0) { + x = 1u; + } else { + x = 2u; + } + output_val = x; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, PhiScalarBothCleanAsm) { + // Hand-written SPIR-V with an actual OpPhi. Both incoming values are + // clean constants, so the phi result should be clean. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpMemberDecorate %SSBO 1 Offset 4 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %uint_2 = OpConstant %uint 2 + %bool = OpTypeBool + %SSBO = OpTypeStruct %uint %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %cond_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + %cond_val = OpLoad %uint %cond_ptr + %is_zero = OpIEqual %bool %cond_val %uint_0 + OpSelectionMerge %merge None + OpBranchConditional %is_zero %true_br %false_br + %true_br = OpLabel + OpBranch %merge + %false_br = OpLabel + OpBranch %merge + %merge = OpLabel + %result = OpPhi %uint %uint_1 %true_br %uint_2 %false_br + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_1 + OpStore %out_ptr %result + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, SPV_SOURCE_ASM); +} + +TEST_F(PositiveGpuAVPoisonValue, PhiVectorBothCleanAsm) { + // Hand-written SPIR-V OpPhi with vec4 type. Both branches provide + // clean constant vectors. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpMemberDecorate %SSBO 1 Offset 16 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %uint_2 = OpConstant %uint 2 + %v4uint = OpTypeVector %uint 4 + %vec_a = OpConstantComposite %v4uint %uint_1 %uint_1 %uint_1 %uint_1 + %vec_b = OpConstantComposite %v4uint %uint_2 %uint_2 %uint_2 %uint_2 + %bool = OpTypeBool + %SSBO = OpTypeStruct %uint %v4uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_ssbo_v = OpTypePointer StorageBuffer %v4uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %cond_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + %cond_val = OpLoad %uint %cond_ptr + %is_zero = OpIEqual %bool %cond_val %uint_0 + OpSelectionMerge %merge None + OpBranchConditional %is_zero %true_br %false_br + %true_br = OpLabel + OpBranch %merge + %false_br = OpLabel + OpBranch %merge + %merge = OpLabel + %result = OpPhi %v4uint %vec_a %true_br %vec_b %false_br + %out_ptr = OpAccessChain %ptr_ssbo_v %ssbo_var %uint_1 + OpStore %out_ptr %result + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, SPV_SOURCE_ASM); +} + +TEST_F(PositiveGpuAVPoisonValue, NestedStructInitialized) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct Inner { uint x; float y; }; + struct Outer { Inner i; uint z; }; + void main() { + Inner i = Inner(42u, 3.14); + Outer o = Outer(i, 7u); + output_val = o.i.x; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, ArrayOfArraysInitialized) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + uint arr[2][3] = uint[2][3](uint[3](1,2,3), uint[3](4,5,6)); + output_val = arr[0][0]; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, MatrixInitialized) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void main() { + mat4 m = mat4(1.0); + output_val = uint(m[0][0]); + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, DeepNestingInitialized) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct L1 { uint val; }; + struct L2 { L1 inner; }; + struct L3 { L2 mid; }; + void main() { + L1 l1 = L1(42u); + L2 l2 = L2(l1); + L3 l3 = L3(l2); + output_val = l3.mid.inner.val; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, SpecConstantArrayInitialized) { + // Can't use aggregate initializer with spec-constant-sized arrays in GLSL, + // so initialize elements individually + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + layout(constant_id = 0) const int N = 4; + void main() { + uint arr[N]; + arr[0] = 1u; + arr[1] = 2u; + arr[2] = 3u; + arr[3] = 4u; + output_val = arr[0]; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanFunctionCall) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint identity(uint x) { return x; } + void main() { + uint x = 42u; + output_val = identity(x); + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanFunctionCallComposite) { + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + struct S { uint a; float b; }; + uint process(vec4 v, S s) { return uint(v.x) + s.a; } + void main() { + vec4 v = vec4(1.0, 2.0, 3.0, 4.0); + S s = S(10u, 3.14); + output_val = process(v, s); + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CooperativeMatrix) { + SetTargetApiVersion(VK_API_VERSION_1_3); + AddRequiredExtensions(VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME); + AddRequiredExtensions(VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::cooperativeMatrix); + AddRequiredFeature(vkt::Feature::vulkanMemoryModel); + AddRequiredFeature(vkt::Feature::shaderFloat16); + AddRequiredFeature(vkt::Feature::storageBuffer16BitAccess); + + RETURN_IF_SKIP(InitGpuAvFramework()); + RETURN_IF_SKIP(InitState()); + + CooperativeMatrixHelper helper(*this); + bool found = false; + for (const auto& prop : helper.coop_matrix_props) { + if (prop.scope == VK_SCOPE_SUBGROUP_KHR && prop.AType == VK_COMPONENT_TYPE_FLOAT16_KHR && prop.MSize == 16 && + prop.KSize == 16) { + found = true; + break; + } + } + if (!found) { + GTEST_SKIP() << "float16 16x16 coopmat property not found"; + } + + const char* cs_source = R"glsl( + #version 450 core + #pragma use_vulkan_memory_model + #extension GL_KHR_memory_scope_semantics : enable + #extension GL_KHR_cooperative_matrix : enable + #extension GL_EXT_shader_explicit_arithmetic_types : enable + layout(local_size_x = 64) in; + layout(set=0, binding=0) coherent buffer SSBO { float16_t x[]; } buf; + coopmat mat; + void main() { + coopMatLoad(mat, buf.x, 0, 16, gl_CooperativeMatrixLayoutRowMajor); + coopMatStore(mat, buf.x, 0, 16, gl_CooperativeMatrixLayoutRowMajor); + } + )glsl"; + + CreateComputePipelineHelper pipe(*this); + pipe.dsl_bindings_[0] = {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr}; + pipe.cs_ = VkShaderObj(*m_device, cs_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_3); + pipe.CreateComputePipeline(); + + vkt::Buffer buffer(*m_device, 1024, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, kHostVisibleMemProps); + void* ptr = buffer.Memory().Map(); + memset(ptr, 0, 1024); + + pipe.descriptor_set_.WriteDescriptorBufferInfo(0, buffer, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + pipe.descriptor_set_.UpdateDescriptorSets(); + + m_command_buffer.Begin(); + vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe); + vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe.pipeline_layout_, 0, 1, + &pipe.descriptor_set_.set_, 0, nullptr); + vk::CmdDispatch(m_command_buffer, 1, 1, 1); + m_command_buffer.End(); + + m_default_queue->SubmitAndWait(m_command_buffer); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanFunctionParamOut) { + // Callee writes an initialized value to an out parameter + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void make_clean(out uint result) { + result = 42u; + } + void main() { + uint val; + make_clean(val); + output_val = val; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanFunctionParamInout) { + // Callee writes an initialized value via inout parameter + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void add_one(inout uint val) { + val = val + 1u; + } + void main() { + uint val = 42u; + add_one(val); + output_val = val; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanFunctionParamConstIn) { + // const in passes by value with an initialized value + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint identity(const in uint x) { return x; } + void main() { + uint x = 42u; + output_val = identity(x); + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanNestedFunctionCall) { + // Clean value through two levels of function calls + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint inner(uint x) { return x + 1u; } + uint outer(uint x) { return inner(x); } + void main() { + uint x = 42u; + output_val = outer(x); + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanFunctionCallChain) { + // Result of one function feeds into another, all clean + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + uint add_one(uint x) { return x + 1u; } + uint double_it(uint x) { return x * 2u; } + void main() { + uint x = 10u; + output_val = double_it(add_one(x)); + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanMultipleOutParams) { + // Function writes clean values to multiple out parameters + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void init_pair(out uint a, out uint b) { + a = 1u; + b = 2u; + } + void main() { + uint a, b; + init_pair(a, b); + output_val = a + b; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, OutParamInitializesVariable) { + // Variable is uninitialized but out param initializes it before use + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void init_val(out uint result) { + result = 99u; + } + void main() { + uint val; + init_val(val); + output_val = val; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, CleanNestedOutParam) { + // Inner function writes clean to out param, outer passes it through + const char* cs_source = R"glsl( + #version 450 + layout(set=0, binding=0) buffer SSBO { + uint output_val; + }; + void make_clean(out uint result) { + result = 42u; + } + void wrapper(out uint result) { + make_clean(result); + } + void main() { + uint val; + wrapper(val); + output_val = val; + } + )glsl"; + SimpleComputeTest(cs_source); +} + +TEST_F(PositiveGpuAVPoisonValue, PtrAccessChainStructArrayInitializedStoreAsm) { + // Store an initialized value through a StorageBuffer pointer derived via + // OpPtrAccessChain on a struct. No poison error should fire. + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_KHR_VARIABLE_POINTERS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::variablePointersStorageBuffer); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability VariablePointersStorageBuffer + OpExtension "SPV_KHR_variable_pointers" + OpExtension "SPV_KHR_storage_buffer_storage_class" + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo_var + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpMemberDecorate %Elem_S 0 Offset 0 + OpMemberDecorate %Elem_S 1 Offset 4 + OpDecorate %arr_type ArrayStride 8 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + OpDecorate %ptr_sb_S ArrayStride 8 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %uint_42 = OpConstant %uint 42 + %uint_4 = OpConstant %uint 4 + %Elem_S = OpTypeStruct %uint %uint + %arr_type = OpTypeArray %Elem_S %uint_4 + %SSBO = OpTypeStruct %arr_type +%ptr_sb_SSBO = OpTypePointer StorageBuffer %SSBO + %ptr_sb_arr = OpTypePointer StorageBuffer %arr_type + %ptr_sb_S = OpTypePointer StorageBuffer %Elem_S + %ptr_sb_u = OpTypePointer StorageBuffer %uint + %ssbo_var = OpVariable %ptr_sb_SSBO StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %arr_ptr = OpAccessChain %ptr_sb_arr %ssbo_var %uint_0 + %elem0_p = OpAccessChain %ptr_sb_S %arr_ptr %uint_0 + %elem1_p = OpPtrAccessChain %ptr_sb_S %elem0_p %uint_1 + %member_p = OpAccessChain %ptr_sb_u %elem1_p %uint_0 + OpStore %member_p %uint_42 + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(PositiveGpuAVPoisonValue, BufferDeviceAddress) { + // BDA shader with initialized pointer — poison pass should not crash. + // NOTE: poison pointer dereference (e.g. OpConvertUToPtr from an + // uninitialized uint64) is a known unhandled case; see poison_pass.cpp. + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::bufferDeviceAddress); + AddRequiredFeature(vkt::Feature::shaderInt64); + + RETURN_IF_SKIP(InitGpuAvFramework()); + RETURN_IF_SKIP(InitState()); + + const char* cs_source = R"glsl( + #version 450 + #extension GL_EXT_buffer_reference : enable + #extension GL_EXT_buffer_reference2 : enable + layout(buffer_reference, std430, buffer_reference_align = 4) buffer BdaBuffer { + uint data; + }; + layout(set=0, binding=0) buffer SSBO { + BdaBuffer ptr; + }; + void main() { + uint val = 42u; + ptr.data = val; + } + )glsl"; + + CreateComputePipelineHelper pipe(*this); + pipe.dsl_bindings_[0] = {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr}; + pipe.cs_ = VkShaderObj(*m_device, cs_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2); + pipe.CreateComputePipeline(); + + vkt::Buffer bda_buffer(*m_device, 256, 0, vkt::device_address); + VkDeviceAddress bda_addr = bda_buffer.Address(); + + vkt::Buffer ssbo(*m_device, sizeof(VkDeviceAddress), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, kHostVisibleMemProps); + auto* ssbo_ptr = static_cast(ssbo.Memory().Map()); + *ssbo_ptr = bda_addr; + ssbo.Memory().Unmap(); + + pipe.descriptor_set_.WriteDescriptorBufferInfo(0, ssbo, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + pipe.descriptor_set_.UpdateDescriptorSets(); + + m_command_buffer.Begin(); + vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe); + vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe.pipeline_layout_, 0, 1, + &pipe.descriptor_set_.set_, 0, nullptr); + vk::CmdDispatch(m_command_buffer, 1, 1, 1); + m_command_buffer.End(); + + m_default_queue->SubmitAndWait(m_command_buffer); +} + +TEST_F(PositiveGpuAVPoisonValue, BufferDeviceAddressLocalVar) { + // BDA shader with a local variable that IS initialized — test that + // the poison pass correctly instruments around BDA loads/stores without + // false positives. + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::bufferDeviceAddress); + AddRequiredFeature(vkt::Feature::shaderInt64); + + RETURN_IF_SKIP(InitGpuAvFramework()); + RETURN_IF_SKIP(InitState()); + + const char* cs_source = R"glsl( + #version 450 + #extension GL_EXT_buffer_reference : enable + #extension GL_EXT_buffer_reference2 : enable + layout(buffer_reference, std430, buffer_reference_align = 4) buffer BdaBuffer { + uint data; + }; + layout(set=0, binding=0) buffer SSBO { + BdaBuffer ptr; + }; + void main() { + uint local_val = ptr.data; + local_val += 1u; + ptr.data = local_val; + } + )glsl"; + + CreateComputePipelineHelper pipe(*this); + pipe.dsl_bindings_[0] = {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr}; + pipe.cs_ = VkShaderObj(*m_device, cs_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2); + pipe.CreateComputePipeline(); + + vkt::Buffer bda_buffer(*m_device, 256, 0, vkt::device_address); + VkDeviceAddress bda_addr = bda_buffer.Address(); + + vkt::Buffer ssbo(*m_device, sizeof(VkDeviceAddress), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, kHostVisibleMemProps); + auto* ssbo_ptr = static_cast(ssbo.Memory().Map()); + *ssbo_ptr = bda_addr; + ssbo.Memory().Unmap(); + + pipe.descriptor_set_.WriteDescriptorBufferInfo(0, ssbo, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + pipe.descriptor_set_.UpdateDescriptorSets(); + + m_command_buffer.Begin(); + vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe); + vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipe.pipeline_layout_, 0, 1, + &pipe.descriptor_set_.set_, 0, nullptr); + vk::CmdDispatch(m_command_buffer, 1, 1, 1); + m_command_buffer.End(); + + m_default_queue->SubmitAndWait(m_command_buffer); +} + +TEST_F(PositiveGpuAVPoisonValue, EntryBlockStoreInitializesAsm) { + // OpVariable without initializer, followed by OpStore in the entry block + // before any load. The entry-block store should be treated as an initializer. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_42 = OpConstant %uint 42 + %uint_0 = OpConstant %uint 0 + %SSBO = OpTypeStruct %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %local = OpVariable %ptr_func_u Function + OpStore %local %uint_42 + %val = OpLoad %uint %local + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %val + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, SPV_SOURCE_ASM); +} + +TEST_F(PositiveGpuAVPoisonValue, EntryBlockStoreMultipleVarsAsm) { + // Two uninitialized variables, both stored to in the entry block before + // any load. Both should be treated as initialized. + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo_var DescriptorSet 0 + OpDecorate %ssbo_var Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_1 = OpConstant %uint 1 + %uint_2 = OpConstant %uint 2 + %SSBO = OpTypeStruct %uint +%ptr_ssbo = OpTypePointer StorageBuffer %SSBO +%ptr_ssbo_u = OpTypePointer StorageBuffer %uint +%ptr_func_u = OpTypePointer Function %uint + %ssbo_var = OpVariable %ptr_ssbo StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %var_a = OpVariable %ptr_func_u Function + %var_b = OpVariable %ptr_func_u Function + OpStore %var_a %uint_1 + OpStore %var_b %uint_2 + %val_a = OpLoad %uint %var_a + %val_b = OpLoad %uint %var_b + %sum = OpIAdd %uint %val_a %val_b + %out_ptr = OpAccessChain %ptr_ssbo_u %ssbo_var %uint_0 + OpStore %out_ptr %sum + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, SPV_SOURCE_ASM); +} + +TEST_F(PositiveGpuAVPoisonValue, CopyLogicalPreservesCleanMemberAsm) { + // Construct a struct with one poison member (x) and one clean member (y). + // OpCopyLogical to a different struct type with the same shape. + // Extract and store only the clean member — should NOT trigger an error. + // With lossy (generic) handling, the entire result would be treated as + // poison, causing a false positive. + SetTargetApiVersion(VK_API_VERSION_1_2); + const char* cs_source = R"asm( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %SSBO Block + OpMemberDecorate %SSBO 0 Offset 0 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %uint_0 = OpConstant %uint 0 + %uint_42 = OpConstant %uint 42 + %StructA = OpTypeStruct %uint %uint + %StructB = OpTypeStruct %uint %uint + %SSBO = OpTypeStruct %uint + %ptr_sb_SSBO = OpTypePointer StorageBuffer %SSBO + %ptr_sb_u = OpTypePointer StorageBuffer %uint + %ptr_func_u = OpTypePointer Function %uint + %ssbo = OpVariable %ptr_sb_SSBO StorageBuffer + %main = OpFunction %void None %void_fn + %entry = OpLabel + %poison_var = OpVariable %ptr_func_u Function + %poison_x = OpLoad %uint %poison_var + %src = OpCompositeConstruct %StructA %poison_x %uint_42 + %dst = OpCopyLogical %StructB %src + %clean_y = OpCompositeExtract %uint %dst 1 + %out_ptr = OpAccessChain %ptr_sb_u %ssbo %uint_0 + OpStore %out_ptr %clean_y + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(PositiveGpuAVPoisonValue, CompositeConstructReplicateInitializedAsm) { + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_EXT_SHADER_REPLICATED_COMPOSITES_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::shaderReplicatedComposites); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability ReplicatedCompositesEXT + OpExtension "SPV_EXT_replicated_composites" + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %ssbo_type Block + OpMemberDecorate %ssbo_type 0 Offset 0 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %func = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %v4uint = OpTypeVector %uint 4 + %uint_0 = OpConstant %uint 0 + %uint_42 = OpConstant %uint 42 + %ssbo_type = OpTypeStruct %v4uint +%ptr_sb_struct = OpTypePointer StorageBuffer %ssbo_type +%ptr_sb_v4uint = OpTypePointer StorageBuffer %v4uint + %ssbo = OpVariable %ptr_sb_struct StorageBuffer + %main = OpFunction %void None %func + %entry = OpLabel + %result = OpCompositeConstructReplicateEXT %v4uint %uint_42 + %out_ptr = OpAccessChain %ptr_sb_v4uint %ssbo %uint_0 + OpStore %out_ptr %result + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, SPV_SOURCE_ASM, SPV_ENV_VULKAN_1_2); +} + +TEST_F(PositiveGpuAVPoisonValue, VariablePointersStorageBuffer) { + // Shader using VariablePointersStorageBuffer capability (OpSelect on + // SSBO pointers). Verifies the poison pass doesn't crash on pointer- + // typed values flowing through OpSelect. + // NOTE: poison pointer dereference via variable pointers is a known + // unhandled case; see poison_pass.cpp. + AddRequiredExtensions(VK_KHR_VARIABLE_POINTERS_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::variablePointersStorageBuffer); + const char* cs_source = R"asm( + OpCapability Shader + OpCapability VariablePointersStorageBuffer + OpExtension "SPV_KHR_variable_pointers" + OpExtension "SPV_KHR_storage_buffer_storage_class" + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %ssbo_type Block + OpMemberDecorate %ssbo_type 0 Offset 0 + OpMemberDecorate %ssbo_type 1 Offset 4 + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %func = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %bool = OpTypeBool + %ssbo_type = OpTypeStruct %uint %uint +%ptr_ssbo_struct = OpTypePointer StorageBuffer %ssbo_type + %ptr_ssbo_uint = OpTypePointer StorageBuffer %uint + %ssbo = OpVariable %ptr_ssbo_struct StorageBuffer + %idx_0 = OpConstant %uint 0 + %idx_1 = OpConstant %uint 1 + %val_42u = OpConstant %uint 42 + %true = OpConstantTrue %bool + %main = OpFunction %void None %func + %entry = OpLabel + %ptr_a = OpAccessChain %ptr_ssbo_uint %ssbo %idx_0 + %ptr_b = OpAccessChain %ptr_ssbo_uint %ssbo %idx_1 + %chosen = OpSelect %ptr_ssbo_uint %true %ptr_a %ptr_b + OpStore %chosen %val_42u + OpReturn + OpFunctionEnd + )asm"; + SimpleComputeTest(cs_source, SPV_SOURCE_ASM); +}