diff --git a/BUILD.gn b/BUILD.gn index 1e9449dd7ad..07b83b05893 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -217,6 +217,7 @@ vvl_sources = [ "layers/gpuav/instrumentation/register_validation.h", "layers/gpuav/instrumentation/sanitizer.cpp", "layers/gpuav/instrumentation/shared_memory_data_race.cpp", + "layers/gpuav/instrumentation/array_oob.cpp", "layers/gpuav/instrumentation/trace_ray.cpp", "layers/gpuav/instrumentation/vertex_attribute_fetch_oob.cpp", "layers/gpuav/resources/gpuav_vulkan_objects.cpp", @@ -264,6 +265,8 @@ vvl_sources = [ "layers/gpuav/spirv/sanitizer_pass.h", "layers/gpuav/spirv/shared_memory_data_race_pass.h", "layers/gpuav/spirv/shared_memory_data_race_pass.cpp", + "layers/gpuav/spirv/array_oob_pass.cpp", + "layers/gpuav/spirv/array_oob_pass.h", "layers/gpuav/spirv/debug_descriptor_pass.cpp", "layers/gpuav/spirv/debug_descriptor_pass.h", "layers/gpuav/spirv/debug_printf_pass.cpp", diff --git a/layers/CMakeLists.txt b/layers/CMakeLists.txt index 4a8ca7d78cd..c16384eb93e 100644 --- a/layers/CMakeLists.txt +++ b/layers/CMakeLists.txt @@ -335,6 +335,7 @@ target_sources(vvl PRIVATE gpuav/instrumentation/gpuav_shader_instrumentor.h gpuav/instrumentation/gpuav_instrumentation.h gpuav/instrumentation/gpuav_instrumentation.cpp + gpuav/instrumentation/array_oob.cpp gpuav/instrumentation/buffer_device_address.cpp gpuav/instrumentation/descriptor_checks.h gpuav/instrumentation/descriptor_checks.cpp diff --git a/layers/VkLayer_khronos_validation.json.in b/layers/VkLayer_khronos_validation.json.in index db94ac20677..3b770b89f64 100644 --- a/layers/VkLayer_khronos_validation.json.in +++ b/layers/VkLayer_khronos_validation.json.in @@ -933,6 +933,21 @@ ] } }, + { + "key": "gpuav_array_oob", + "label": "Array Out of Bounds Detection", + "description": "Detects out-of-bounds accesses on arrays and vectors in Workgroup, Private, and Function storage during GPU-AV shader instrumentation.", + "type": "BOOL", + "view": "DEBUG", + "default": true, + "dependence": { + "mode": "ALL", + "settings": [ + { "key": "gpuav_enable", "value": true }, + { "key": "gpuav_shader_instrumentation", "value": true } + ] + } + }, { "key": "gpuav_max_indices_count", "label": "Maximum Indices", diff --git a/layers/error_message/spirv_logging.cpp b/layers/error_message/spirv_logging.cpp index eb2f1bdc94e..162903ecab6 100644 --- a/layers/error_message/spirv_logging.cpp +++ b/layers/error_message/spirv_logging.cpp @@ -486,6 +486,20 @@ void FindShaderSource(std::ostringstream& ss, const std::vector& instr } } +uint32_t GetOpcodeAtOffset(const std::vector& instructions, uint32_t instruction_position_offset) { + uint32_t offset = kModuleStartingOffset; + while (offset < instructions.size()) { + const uint32_t instruction = instructions[offset]; + if (offset >= instruction_position_offset) { + const uint32_t opcode = Opcode(instruction); + return opcode; + } + const uint32_t length = Length(instruction); + offset += length; + } + return (uint32_t)spv::OpMax; +} + void FindGlobalName(std::ostringstream& ss, const std::vector& instructions, uint32_t find_opcode, uint32_t find_id) { uint32_t shader_debug_info_set_id = 0; uint32_t offset = kModuleStartingOffset; diff --git a/layers/error_message/spirv_logging.h b/layers/error_message/spirv_logging.h index a201c26cec1..3fb0afe337c 100644 --- a/layers/error_message/spirv_logging.h +++ b/layers/error_message/spirv_logging.h @@ -42,6 +42,9 @@ void FindGlobalName(std::ostringstream& ss, const std::vector& instruc // Will try to get the OpStruct from a BDA access void FindOpStructFromBDA(std::ostringstream& ss, const std::vector& instructions, uint32_t instruction_position_offset); +// Returns the SPIR-V opcode at the given word offset in the instruction stream +uint32_t GetOpcodeAtOffset(const std::vector& instructions, uint32_t instruction_position_offset); + // These are used where we can't use normal spirv::Instructions. // The main spot is post-processisng error message in GPU-AV, the time it takes to interchange back from a vector to a // vector is too high to do mid-frame. Most things just need these simple helpers diff --git a/layers/gpuav/core/gpuav_record.cpp b/layers/gpuav/core/gpuav_record.cpp index 0a619529bba..7b4c05f0c2e 100644 --- a/layers/gpuav/core/gpuav_record.cpp +++ b/layers/gpuav/core/gpuav_record.cpp @@ -166,6 +166,7 @@ void Validator::PreCallRecordBeginCommandBuffer(VkCommandBuffer commandBuffer, c RegisterMeshShadingValidation(*this, gpuav_cb_state); RegisterRayQueryValidation(*this, gpuav_cb_state); RegisterSharedMemoryDataRaceValidation(*this, gpuav_cb_state); + RegisterArrayOobValidation(*this, gpuav_cb_state); RegisterSanitizer(*this, gpuav_cb_state); RegisterTraceRayValidation(*this, gpuav_cb_state); debug_printf::RegisterDebugPrintf(*this, gpuav_cb_state); diff --git a/layers/gpuav/core/gpuav_settings.cpp b/layers/gpuav/core/gpuav_settings.cpp index 5545ee725c3..2411f7678d3 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.trace_ray || 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.sanitizer || shader_instrumentation.shared_memory_data_race || + shader_instrumentation.array_oob; } bool GpuAVSettings::IsSpirvModified() const { return IsShaderInstrumentationEnabled() || debug_printf_enabled || debug_descriptor_enabled; @@ -50,6 +51,7 @@ void GpuAVSettings::DisableShaderInstrumentationAndOptions() { shader_instrumentation.vertex_attribute_fetch_oob = false; shader_instrumentation.sanitizer = false; shader_instrumentation.shared_memory_data_race = false; + shader_instrumentation.array_oob = false; // Because of this setting, cannot really have an "enabled" parameter to pass to this method select_instrumented_shaders = false; } @@ -134,6 +136,7 @@ void GpuAVSettings::TracyLogSettings() const { VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(vertex_attribute_fetch_oob); VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(sanitizer); VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(shared_memory_data_race); + VVL_TRACY_PRINT_INSTRUMENTATION_SETTING(array_oob); VVL_TRACY_PRINT_GPUAV_SETTING(debug_printf_only); VVL_TRACY_PRINT_GPUAV_SETTING(debug_printf_enabled); VVL_TRACY_PRINT_GPUAV_SETTING(debug_printf_to_stdout); diff --git a/layers/gpuav/core/gpuav_settings.h b/layers/gpuav/core/gpuav_settings.h index d12fd61db33..a886b576374 100644 --- a/layers/gpuav/core/gpuav_settings.h +++ b/layers/gpuav/core/gpuav_settings.h @@ -68,6 +68,7 @@ struct GpuAVSettings { bool vertex_attribute_fetch_oob = true; bool sanitizer = true; bool shared_memory_data_race = true; + bool array_oob = true; } shader_instrumentation; bool IsShaderInstrumentationEnabled() const; diff --git a/layers/gpuav/instrumentation/array_oob.cpp b/layers/gpuav/instrumentation/array_oob.cpp new file mode 100644 index 00000000000..5045dc1465f --- /dev/null +++ b/layers/gpuav/instrumentation/array_oob.cpp @@ -0,0 +1,86 @@ +/* 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 "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" +#include "error_message/spirv_logging.h" + +namespace gpuav { + +void RegisterArrayOobValidation(Validator& gpuav, CommandBufferSubState& cb) { + if (!gpuav.gpuav_settings.shader_instrumentation.array_oob) { + 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_ArrayOob) { + return error_found; + } + error_found = true; + + const uint32_t index = error_record[kInst_LogError_ParameterOffset_0]; + const uint32_t encoded_bound = error_record[kInst_LogError_ParameterOffset_1]; + const uint32_t variable_id = error_record[kInst_LogError_ParameterOffset_2]; + + const uint32_t bound = encoded_bound & 0x00FFFFFFu; + const uint32_t access_type = (encoded_bound >> 24) & 0x3u; + const uint32_t dim_index = (encoded_bound >> 26) & 0x1Fu; + + const uint32_t instruction_position_offset = error_record[kHeader_StageInstructionIdOffset] & kInstructionId_Mask; + + std::ostringstream strm; + strm << "Variable \""; + if (instrumented_shader) { + ::spirv::FindGlobalName(strm, instrumented_shader->original_spirv, (uint32_t)spv::OpVariable, variable_id); + } else { + strm << "[error, original SPIR-V not found]"; + } + strm << "\" "; + if (access_type == 1) { + strm << "vector component"; + } else { + strm << "array index"; + if (dim_index > 0) { + strm << " (dimension " << dim_index << ")"; + } + } + strm << " " << index << " is >= " << ((access_type == 1) ? "vector size " : "array size ") << bound << "."; + + uint32_t opcode = (uint32_t)spv::OpMax; + if (instrumented_shader) { + opcode = ::spirv::GetOpcodeAtOffset(instrumented_shader->original_spirv, instruction_position_offset); + } + strm << GetSpirvSpecLink(opcode); + + out_vuid_msg = std::string("SPIRV-ArrayOob-") + string_SpvOpcode(opcode); + + out_error_msg += strm.str(); + return error_found; + }; + + return inst_error_logger; + }); +} + +} // namespace gpuav diff --git a/layers/gpuav/instrumentation/gpuav_shader_instrumentor.cpp b/layers/gpuav/instrumentation/gpuav_shader_instrumentor.cpp index 399dfc0147a..24ef58d2e23 100644 --- a/layers/gpuav/instrumentation/gpuav_shader_instrumentor.cpp +++ b/layers/gpuav/instrumentation/gpuav_shader_instrumentor.cpp @@ -51,6 +51,7 @@ #include "gpuav/spirv/ray_query_pass.h" #include "gpuav/spirv/trace_ray_pass.h" #include "gpuav/spirv/shared_memory_data_race_pass.h" +#include "gpuav/spirv/array_oob_pass.h" #include "gpuav/spirv/mesh_shading_pass.h" #include "gpuav/spirv/debug_printf_pass.h" #include "gpuav/spirv/debug_descriptor_pass.h" @@ -1677,6 +1678,13 @@ bool GpuShaderInstrumentor::InstrumentShader(const vvl::span& in modified |= pass.Run(); } + // Array OOB must run before shared-memory race instrumentation so guarded stores/loads are not + // treated as races on OOB paths, and so cooperative-matrix + OOB cases get consistent errors. + if (gpuav_settings.shader_instrumentation.array_oob) { + spirv::ArrayOobPass pass(module); + modified |= pass.Run(); + } + if (gpuav_settings.shader_instrumentation.shared_memory_data_race) { spirv::SharedMemoryDataRacePass pass(module); modified |= pass.Run(); @@ -1981,4 +1989,11 @@ std::string GpuShaderInstrumentor::GenerateDebugInfoMessage(VkCommandBuffer comm return ss.str(); } +std::string GetSpirvSpecLink(const uint32_t opcode) { + // Currently the Working Group decided to not provide "real" VUIDs as it would become duplicating the SPIR-V spec + // So these are not "UNASSIGNED", but instead are "SPIRV" VUs because we can point to the instruction in the SPIR-V spec + // (https://gitlab.khronos.org/vulkan/vulkan/-/merge_requests/7853) + return "\nSee more at https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#" + std::string(string_SpvOpcode(opcode)); +} + } // namespace gpuav diff --git a/layers/gpuav/instrumentation/gpuav_shader_instrumentor.h b/layers/gpuav/instrumentation/gpuav_shader_instrumentor.h index 41923c3e8f8..006a3b1b84b 100644 --- a/layers/gpuav/instrumentation/gpuav_shader_instrumentor.h +++ b/layers/gpuav/instrumentation/gpuav_shader_instrumentor.h @@ -258,4 +258,6 @@ class GpuShaderInstrumentor : public vvl::DeviceProxy { vvl::unordered_set selected_instrumented_shaders; }; +std::string GetSpirvSpecLink(const uint32_t opcode); + } // namespace gpuav diff --git a/layers/gpuav/instrumentation/register_validation.h b/layers/gpuav/instrumentation/register_validation.h index 8dd0025173a..d739d179b9c 100644 --- a/layers/gpuav/instrumentation/register_validation.h +++ b/layers/gpuav/instrumentation/register_validation.h @@ -25,6 +25,7 @@ void RegisterMeshShadingValidation(Validator& gpuav, CommandBufferSubState& cb); void RegisterSanitizer(Validator& gpuav, CommandBufferSubState& cb); void RegisterVertexAttributeFetchOobValidation(Validator& gpuav, CommandBufferSubState& cb); void RegisterSharedMemoryDataRaceValidation(Validator& gpuav, CommandBufferSubState& cb); +void RegisterArrayOobValidation(Validator& gpuav, CommandBufferSubState& cb); void RegisterTraceRayValidation(Validator& gpuav, CommandBufferSubState& cb); } // namespace gpuav diff --git a/layers/gpuav/instrumentation/sanitizer.cpp b/layers/gpuav/instrumentation/sanitizer.cpp index d7f5789fa62..043e31ccdda 100644 --- a/layers/gpuav/instrumentation/sanitizer.cpp +++ b/layers/gpuav/instrumentation/sanitizer.cpp @@ -22,13 +22,6 @@ namespace gpuav { -static std::string GetSpirvSpecLink(const uint32_t opcode) { - // Currently the Working Group decided to not provide "real" VUIDs as it would become duplicating the SPIR-V spec - // So these are not "UNASSIGNED", but instead are "SPIRV" VUs because we can point to the instruction in the SPIR-V spec - // (https://gitlab.khronos.org/vulkan/vulkan/-/merge_requests/7853) - return "\nSee more at https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#" + std::string(string_SpvOpcode(opcode)); -} - void RegisterSanitizer(Validator& gpuav, CommandBufferSubState& cb) { if (!gpuav.gpuav_settings.shader_instrumentation.sanitizer) { return; diff --git a/layers/gpuav/shaders/gpuav_error_codes.h b/layers/gpuav/shaders/gpuav_error_codes.h index d8fafeb5da1..d3b8c59e867 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_SharedMemoryDataRace = 15; const int kErrorGroup_TraceRay = 16; +const int kErrorGroup_ArrayOob = 17; // We just take ExecutionModel and normalize it so we only use 5 bits to store it const int kExecutionModel_Vertex = 0; diff --git a/layers/gpuav/shaders/instrumentation/array_oob.comp b/layers/gpuav/shaders/instrumentation/array_oob.comp new file mode 100644 index 00000000000..a46078d1989 --- /dev/null +++ b/layers/gpuav/shaders/instrumentation/array_oob.comp @@ -0,0 +1,35 @@ +// 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 +// +// 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" + +bool inst_array_oob(const uint index, const uint encoded_bound, const uint inst_offset, const uint variable_id) { + const uint bound = encoded_bound & 0x00FFFFFFu; + if (index >= bound) { + error_payload = ErrorPayload( + inst_offset, + SpecConstantLinkShaderId | (kErrorGroup_ArrayOob << kErrorGroup_Shift), + index, + encoded_bound, + variable_id); + return false; + } + return true; +} diff --git a/layers/gpuav/spirv/CMakeLists.txt b/layers/gpuav/spirv/CMakeLists.txt index d56c50976cb..a9b304c65b9 100644 --- a/layers/gpuav/spirv/CMakeLists.txt +++ b/layers/gpuav/spirv/CMakeLists.txt @@ -17,6 +17,8 @@ add_library(gpu_av_spirv STATIC) target_sources(gpu_av_spirv PRIVATE # Passes + array_oob_pass.h + array_oob_pass.cpp descriptor_indexing_oob_pass.h descriptor_indexing_oob_pass.cpp descriptor_class_general_buffer_pass.h diff --git a/layers/gpuav/spirv/array_oob_pass.cpp b/layers/gpuav/spirv/array_oob_pass.cpp new file mode 100644 index 00000000000..040434b6e45 --- /dev/null +++ b/layers/gpuav/spirv/array_oob_pass.cpp @@ -0,0 +1,256 @@ +/* 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 "array_oob_pass.h" +#include "containers/container_utils.h" +#include "module.h" +#include +#include +#include +#include + +#include "generated/gpuav_offline_spirv.h" +#include "type_manager.h" + +namespace gpuav { +namespace spirv { + +const static OfflineModule kOfflineModule = {instrumentation_array_oob_comp, instrumentation_array_oob_comp_size, + UseErrorPayloadVariable}; + +const static OfflineFunction kOfflineFunction = {"inst_array_oob", instrumentation_array_oob_comp_function_0_offset}; + +static bool IsTrackedStorageClass(spv::StorageClass sc) { + return sc == spv::StorageClassWorkgroup || sc == spv::StorageClassPrivate || sc == spv::StorageClassFunction; +} + +bool ArrayOobPass::IsTrackedPointerStorageClass(const Instruction* inst) const { + if (!inst) return false; + spv::StorageClass sc = inst->StorageClass(); // OpVariable + if (sc == spv::StorageClassMax) { + const Type* ptr_type = type_manager_.FindTypeById(inst->TypeId()); + if (ptr_type && ptr_type->spv_type_ == SpvType::kPointer) { + sc = ptr_type->inst_.StorageClass(); + } + } + return IsTrackedStorageClass(sc); +} + +ArrayOobPass::ArrayOobPass(Module& module) : Pass(module, kOfflineModule) {} + +void ArrayOobPass::EmitBoundsChecks(BasicBlock& block, InstructionIt* inst_it, Instruction& target_inst, + const InstructionMeta& meta) { + const uint32_t function_def = GetLinkFunction(link_function_id_, kOfflineFunction); + const uint32_t bool_type = type_manager_.GetTypeBool().Id(); + const uint32_t inst_position_id = type_manager_.GetConstantUInt32(meta.inst_position_offset).Id(); + const uint32_t variable_id_const = type_manager_.GetConstantUInt32(meta.variable_id).Id(); + + // Operand ids still on the access chain / vector op (before any UpdateWord). `check.index_id` may be a CastToUint32 + // rewriting for inst_array_oob; OpSelect must use the original integer type the consumer instruction expects. + std::vector orig_index_ids; + orig_index_ids.reserve(meta.checks.size()); + for (const auto& check : meta.checks) { + orig_index_ids.push_back(target_inst.Word(check.index_spirv_word)); + } + + for (size_t ci = 0; ci < meta.checks.size(); ++ci) { + const auto& check = meta.checks[ci]; + const uint32_t orig_index_id = orig_index_ids[ci]; + const uint32_t encoded_bound = check.bound | (check.access_type << 24) | (check.dim_index << 26); + const uint32_t bound_id = type_manager_.GetConstantUInt32(encoded_bound).Id(); + const uint32_t ok_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpFunctionCall, + {bool_type, ok_id, function_def, check.index_id, bound_id, inst_position_id, variable_id_const}, + inst_it); + module_.need_log_error_ = true; + + if (module_.settings_.safe_mode) { + const Instruction* orig_def = block.function_->FindInstruction(orig_index_id); + const Constant* orig_const = type_manager_.FindConstantById(orig_index_id); + const Type* select_type = nullptr; + if (orig_def) { + select_type = type_manager_.FindTypeById(orig_def->TypeId()); + } else if (orig_const) { + select_type = &orig_const->type_; + } + assert(select_type); + const uint32_t select_type_id = select_type->Id(); + const uint32_t zero_id = type_manager_.CreateConstantScalar(0, *select_type).Id(); + // inst_array_oob returns true iff index is in bounds. Force index to 0 when OOB (no CFG split). + const uint32_t safe_id = module_.TakeNextId(); + block.CreateInstruction(spv::OpSelect, {select_type_id, safe_id, ok_id, orig_index_id, zero_id}, inst_it); + target_inst.UpdateWord(check.index_spirv_word, safe_id); + } + } +} + +bool ArrayOobPass::RequiresInstrumentation(const Function& function, BasicBlock& block, InstructionIt& inst_it, + const Instruction& inst, InstructionMeta& meta) { + const spv::Op opcode = (spv::Op)inst.Opcode(); + + if (opcode == spv::OpVectorExtractDynamic || opcode == spv::OpVectorInsertDynamic) { + const uint32_t vector_id = inst.Operand(0); + const Instruction* vector_inst = function.FindInstruction(vector_id); + // OpVectorExtract/InsertDynamic operate on values, not pointers, so the vector operand + // will be an OpLoad from the variable. We trace through the OpLoad to find the source + // pointer. Even though the OOB occurs on the loaded value, we still report it because + // the vector's bounds are determined by the variable's declaration and the index is + // checked before the access. + if (!vector_inst || vector_inst->Opcode() != spv::OpLoad) { + return false; + } + + const uint32_t ptr_id = vector_inst->Operand(0); + + const Variable* variable = type_manager_.FindVariableById(ptr_id); + const Instruction* base_inst = function.FindInstruction(ptr_id); + while (base_inst && base_inst->IsNonPtrAccessChain()) { + const uint32_t base_id = base_inst->Operand(0); + variable = type_manager_.FindVariableById(base_id); + if (variable) { + break; + } + base_inst = function.FindInstruction(base_id); + } + const Instruction* leaf_inst = variable ? &variable->inst_ : base_inst; + if (!leaf_inst || !IsTrackedPointerStorageClass(leaf_inst)) { + return false; + } + + const Type* vector_type = type_manager_.FindTypeById(vector_inst->TypeId()); + if (!vector_type || !vector_type->inst_.IsVector()) { + return false; + } + + uint32_t index_id = (opcode == spv::OpVectorExtractDynamic) ? inst.Operand(1) : inst.Operand(2); + index_id = CastToUint32(index_id, block, &inst_it); + + meta.inst_position_offset = inst.GetPositionOffset(); + meta.variable_id = leaf_inst->ResultId(); + // OpVectorExtractDynamic: Index is word 4. OpVectorInsertDynamic: Index is word 5 (Component is word 4). + const uint32_t index_word = (opcode == spv::OpVectorExtractDynamic) ? 4u : 5u; + meta.checks.push_back({index_id, vector_type->meta_.vector.component_count, 1, 0, index_word}); + return true; + } else if (!inst.IsNonPtrAccessChain()) { + return false; + } + + const uint32_t base_id = inst.Operand(0); + const Variable* variable = type_manager_.FindVariableById(base_id); + const Instruction* chain_inst = function.FindInstruction(base_id); + while (chain_inst && chain_inst->IsNonPtrAccessChain()) { + const uint32_t chain_base_id = chain_inst->Operand(0); + variable = type_manager_.FindVariableById(chain_base_id); + if (variable) { + break; + } + chain_inst = function.FindInstruction(chain_base_id); + } + const Instruction* leaf_inst = variable ? &variable->inst_ : chain_inst; + if (!leaf_inst || !IsTrackedPointerStorageClass(leaf_inst)) { + return false; + } + meta.variable_id = leaf_inst->ResultId(); + meta.inst_position_offset = inst.GetPositionOffset(); + + const Instruction* base_inst = function.FindInstruction(base_id); + const Type* base_ptr_type = base_inst ? type_manager_.FindTypeById(base_inst->TypeId()) : nullptr; + if (!base_ptr_type) { + assert(leaf_inst->ResultId() == base_id); + base_ptr_type = type_manager_.FindTypeById(leaf_inst->TypeId()); + } + assert(base_ptr_type); + const Type* pointee_type = type_manager_.FindChildType(*base_ptr_type, 0); + + uint32_t dim_index = 0; + for (uint32_t i = 4; i < inst.Length(); ++i) { + uint32_t idx_id = inst.Word(i); + + switch (pointee_type->spv_type_) { + case SpvType::kArray: + case SpvType::kVector: + case SpvType::kVectorIdEXT: { + idx_id = CastToUint32(idx_id, block, &inst_it); + const bool is_array = (pointee_type->spv_type_ == SpvType::kArray); + const uint32_t bound = is_array ? pointee_type->meta_.array.length : pointee_type->meta_.vector.component_count; + const uint32_t access_type = is_array ? 0 : 1; + meta.checks.push_back({idx_id, bound, access_type, dim_index, i}); + dim_index++; + pointee_type = type_manager_.FindChildType(*pointee_type, 0); + } break; + case SpvType::kStruct: { + auto idx_c = type_manager_.FindConstantById(idx_id); + uint32_t member_idx = idx_c->GetValueUint32(); + pointee_type = type_manager_.FindChildType(*pointee_type, member_idx); + } break; + case SpvType::kMatrix: { + // TODO check matrix column indices + pointee_type = type_manager_.FindChildType(*pointee_type, 0); + } break; + default: + pointee_type = type_manager_.FindChildType(*pointee_type, 0); + break; + } + } + + return !meta.checks.empty(); +} + +bool ArrayOobPass::Instrument() { + for (Function& function : module_.functions_) { + if (!function.called_from_target_) { + continue; + } + + for (auto block_it = function.blocks_.begin(); block_it != function.blocks_.end(); ++block_it) { + BasicBlock& current_block = **block_it; + + cf_.Update(current_block); + if (debug_disable_loops_ && cf_.in_loop) { + continue; + } + + if (current_block.IsLoopHeader()) { + continue; + } + + auto& block_instructions = current_block.instructions_; + for (auto inst_it = block_instructions.begin(); inst_it != block_instructions.end(); ++inst_it) { + Instruction& inst = *inst_it->get(); + + InstructionMeta meta; + if (!RequiresInstrumentation(function, current_block, inst_it, inst, meta)) { + continue; + } + + if (MaxInstrumentationsCountReached()) { + continue; + } + instrumentations_count_++; + + EmitBoundsChecks(current_block, &inst_it, inst, meta); + } + } + } + + return instrumentations_count_ != 0; +} + +void ArrayOobPass::PrintDebugInfo() const { + std::cout << "ArrayOobPass instrumentation count: " << instrumentations_count_ << '\n'; +} + +} // namespace spirv +} // namespace gpuav diff --git a/layers/gpuav/spirv/array_oob_pass.h b/layers/gpuav/spirv/array_oob_pass.h new file mode 100644 index 00000000000..8d5f2793d8a --- /dev/null +++ b/layers/gpuav/spirv/array_oob_pass.h @@ -0,0 +1,61 @@ +/* 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. + */ +#pragma once + +#include +#include +#include +#include "pass.h" + +namespace gpuav { +namespace spirv { + +// Bound checks for array/vector accesses to variables in storage classes whose size is +// statically determined by the SPIR-V type (Workgroup, Private, Function), unlike SSBO/UBO +// where the descriptor binding controls the actual memory bound. +class ArrayOobPass : public Pass { + public: + ArrayOobPass(Module& module); + const char* Name() const final { return "ArrayOobPass"; } + bool Instrument() final; + void PrintDebugInfo() const final; + + private: + struct IndexCheck { + uint32_t index_id; + uint32_t bound; + uint32_t access_type; // 0 = array, 1 = vector, 2 = matrix + uint32_t dim_index; // which dimension in a multi-dimensional access + // Word index in the target instruction (OpAccessChain / vector dynamic op) holding index_id. + uint32_t index_spirv_word; + }; + + struct InstructionMeta { + uint32_t inst_position_offset = 0; + uint32_t variable_id = 0; + std::vector checks; + }; + + bool RequiresInstrumentation(const Function& function, BasicBlock& block, InstructionIt& inst_it, const Instruction& inst, + InstructionMeta& meta); + // Unsafe: emit inst_array_oob only. Safe: emit inst_array_oob then OpSelect (in-bounds ? index : 0) and patch the target. + void EmitBoundsChecks(BasicBlock& block, InstructionIt* inst_it, Instruction& target_inst, const InstructionMeta& meta); + bool IsTrackedPointerStorageClass(const Instruction* inst) const; + + uint32_t link_function_id_ = 0; +}; + +} // namespace spirv +} // namespace gpuav diff --git a/layers/layer_options.cpp b/layers/layer_options.cpp index 6c21bcb7732..b29d46aa26e 100644 --- a/layers/layer_options.cpp +++ b/layers/layer_options.cpp @@ -207,6 +207,7 @@ const char* VK_LAYER_GPUAV_POST_PROCESS_DESCRIPTOR_INDEXING = "gpuav_post_proces 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_SHARED_MEMORY_DATA_RACE = "gpuav_shared_memory_data_race"; +const char* VK_LAYER_GPUAV_ARRAY_OOB = "gpuav_array_oob"; const char* VK_LAYER_GPUAV_MAX_INDICES_COUNT = "gpuav_max_indices_count"; const char* VK_LAYER_GPUAV_SELECT_INSTRUMENTED_SHADERS = "gpuav_select_instrumented_shaders"; const char* VK_LAYER_GPUAV_SHADERS_TO_INSTRUMENT = "gpuav_shaders_to_instrument"; @@ -1072,6 +1073,11 @@ void ProcessConfigAndEnvSettings(ConfigAndEnvSettings* settings_data) { gpuav_settings.shader_instrumentation.shared_memory_data_race); } + if (vkuHasLayerSetting(layer_setting_set, VK_LAYER_GPUAV_ARRAY_OOB)) { + vkuGetLayerSettingValue(layer_setting_set, VK_LAYER_GPUAV_ARRAY_OOB, + gpuav_settings.shader_instrumentation.array_oob); + } + if (vkuHasLayerSetting(layer_setting_set, VK_LAYER_GPUAV_MAX_INDICES_COUNT)) { vkuGetLayerSettingValue(layer_setting_set, VK_LAYER_GPUAV_MAX_INDICES_COUNT, gpuav_settings.invalid_index_command); // Hard limit is set by the fact we have kActionId_Mask used... which is also arbitrary diff --git a/layers/layer_options_validation.h b/layers/layer_options_validation.h index 13c0ef88253..51d0f84a834 100644 --- a/layers/layer_options_validation.h +++ b/layers/layer_options_validation.h @@ -57,6 +57,7 @@ static void ValidateLayerSettingsProvided(const VkLayerSettingsCreateInfoEXT &la else if (strcmp(VK_LAYER_GPU_DUMP_DEVICE_GENERATED_COMMANDS, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPU_DUMP_TO_STDOUT, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPUAV_ACCELERATION_STRUCTURES_BUILDS, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } + else if (strcmp(VK_LAYER_GPUAV_ARRAY_OOB, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPUAV_BUFFER_ADDRESS_OOB, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPUAV_BUFFER_COPIES, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } else if (strcmp(VK_LAYER_GPUAV_BUFFERS_VALIDATION, name) == 0) { required_type = VK_LAYER_SETTING_TYPE_BOOL32_EXT; } diff --git a/layers/vk_layer_settings.txt b/layers/vk_layer_settings.txt index 087a0f051cb..2591715b3dc 100644 --- a/layers/vk_layer_settings.txt +++ b/layers/vk_layer_settings.txt @@ -80,6 +80,11 @@ khronos_validation.gpu_dump_to_stdout = false # Validate acceleration structures references in TLAS build commands. khronos_validation.gpuav_acceleration_structures_builds = true +# Array Out of Bounds Detection +# ===================== +# Detects out-of-bounds accesses on arrays and vectors in Workgroup, Private, and Function storage during GPU-AV shader instrumentation. +khronos_validation.gpuav_array_oob = true + # Out of bounds buffer device addresses # ===================== # Check for invalid access using buffer device address diff --git a/layers/vulkan/generated/gpuav_offline_spirv.cpp b/layers/vulkan/generated/gpuav_offline_spirv.cpp index 3833bbe9782..7fc745e2050 100644 --- a/layers/vulkan/generated/gpuav_offline_spirv.cpp +++ b/layers/vulkan/generated/gpuav_offline_spirv.cpp @@ -25,6 +25,39 @@ // To view SPIR-V, copy contents of an array and paste in https://www.khronos.org/spir/visualizer/ +[[maybe_unused]] const uint32_t instrumentation_array_oob_comp_size = 281; +[[maybe_unused]] const uint32_t instrumentation_array_oob_comp[281] = { + 0x07230203, 0x00010300, 0x0008000b, 0x0000001d, 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, 0x00090005, 0x00000009, + 0x74736e69, 0x7272615f, 0x6f5f7961, 0x7528626f, 0x31753b31, 0x3b31753b, 0x003b3175, 0x00040005, 0x00000005, 0x65646e69, + 0x00000078, 0x00060005, 0x00000006, 0x6f636e65, 0x5f646564, 0x6e756f62, 0x00000064, 0x00050005, 0x00000007, 0x74736e69, + 0x66666f5f, 0x00746573, 0x00050005, 0x00000008, 0x69726176, 0x656c6261, 0x0064695f, 0x00060005, 0x00000013, 0x6f727245, + 0x79615072, 0x64616f6c, 0x00000000, 0x00060006, 0x00000013, 0x00000000, 0x74736e69, 0x66666f5f, 0x00746573, 0x00090006, + 0x00000013, 0x00000001, 0x64616873, 0x655f7265, 0x726f7272, 0x636e655f, 0x6e69646f, 0x00000067, 0x00060006, 0x00000013, + 0x00000002, 0x61726170, 0x6574656d, 0x00305f72, 0x00060006, 0x00000013, 0x00000003, 0x61726170, 0x6574656d, 0x00315f72, + 0x00060006, 0x00000013, 0x00000004, 0x61726170, 0x6574656d, 0x00325f72, 0x00060005, 0x00000015, 0x6f727265, 0x61705f72, + 0x616f6c79, 0x00000064, 0x00090005, 0x00000016, 0x63657053, 0x736e6f43, 0x746e6174, 0x6b6e694c, 0x64616853, 0x64497265, + 0x00000000, 0x00080047, 0x00000009, 0x00000029, 0x74736e69, 0x7272615f, 0x6f5f7961, 0x0000626f, 0x00000000, 0x00040047, + 0x00000016, 0x00000001, 0x00000000, 0x00040015, 0x00000002, 0x00000020, 0x00000000, 0x00020014, 0x00000003, 0x00070021, + 0x00000004, 0x00000003, 0x00000002, 0x00000002, 0x00000002, 0x00000002, 0x0004002b, 0x00000002, 0x0000000d, 0x00ffffff, + 0x0007001e, 0x00000013, 0x00000002, 0x00000002, 0x00000002, 0x00000002, 0x00000002, 0x00040020, 0x00000014, 0x00000006, + 0x00000013, 0x0004003b, 0x00000014, 0x00000015, 0x00000006, 0x00040032, 0x00000002, 0x00000016, 0x0dead001, 0x0004002b, + 0x00000002, 0x00000017, 0x11000000, 0x00060034, 0x00000002, 0x00000018, 0x000000c5, 0x00000016, 0x00000017, 0x0003002a, + 0x00000003, 0x0000001a, 0x00030029, 0x00000003, 0x0000001c, 0x00050036, 0x00000003, 0x00000009, 0x00000000, 0x00000004, + 0x00030037, 0x00000002, 0x00000005, 0x00030037, 0x00000002, 0x00000006, 0x00030037, 0x00000002, 0x00000007, 0x00030037, + 0x00000002, 0x00000008, 0x000200f8, 0x0000000a, 0x000500c7, 0x00000002, 0x0000000e, 0x00000006, 0x0000000d, 0x000500ae, + 0x00000003, 0x00000010, 0x00000005, 0x0000000e, 0x000300f7, 0x00000012, 0x00000000, 0x000400fa, 0x00000010, 0x00000011, + 0x00000012, 0x000200f8, 0x00000011, 0x00080050, 0x00000013, 0x00000019, 0x00000007, 0x00000018, 0x00000005, 0x00000006, + 0x00000008, 0x0003003e, 0x00000015, 0x00000019, 0x000200fe, 0x0000001a, 0x000200f8, 0x00000012, 0x000200fe, 0x0000001c, + 0x00010038}; +[[maybe_unused]] const uint32_t instrumentation_array_oob_comp_function_0_offset = 225; + [[maybe_unused]] const uint32_t instrumentation_buffer_device_address_comp_size = 1086; [[maybe_unused]] const uint32_t instrumentation_buffer_device_address_comp[1086] = { 0x07230203, 0x00010300, 0x0008000b, 0x00000097, 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 22a7f6c3c78..65b59c7001d 100644 --- a/layers/vulkan/generated/gpuav_offline_spirv.h +++ b/layers/vulkan/generated/gpuav_offline_spirv.h @@ -27,6 +27,11 @@ // We have found having spirv code defined the header can lead to MSVC not recognizing changes +extern const uint32_t instrumentation_array_oob_comp_size; +extern const uint32_t instrumentation_array_oob_comp[]; +// These offset match the function in the order they are declared in the GLSL source +extern const uint32_t instrumentation_array_oob_comp_function_0_offset; + extern const uint32_t instrumentation_buffer_device_address_comp_size; extern const uint32_t instrumentation_buffer_device_address_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 c85b430675f..35ec7919aba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -90,6 +90,8 @@ target_sources(vk_layer_validation_tests PRIVATE unit/geometry_tessellation.cpp unit/geometry_tessellation_positive.cpp unit/gpu_av.cpp + unit/gpu_av_array_oob.cpp + unit/gpu_av_array_oob_positive.cpp unit/gpu_av_buffer_device_address.cpp unit/gpu_av_buffer_device_address_positive.cpp unit/gpu_av_cooperative_vector.cpp diff --git a/tests/framework/layer_validation_tests.h b/tests/framework/layer_validation_tests.h index c1cc710b56e..1800aabc5a7 100644 --- a/tests/framework/layer_validation_tests.h +++ b/tests/framework/layer_validation_tests.h @@ -328,6 +328,11 @@ class GpuAVSharedMemoryDataRaceTest : public GpuAVTest { void InitSharedMemoryDataRace(uint32_t message_limit = 1); }; +class GpuAVArrayOobTest : public GpuAVTest { + public: + void InitArrayOob(); +}; + class DebugPrintfTests : public VkLayerTest { public: void InitDebugPrintfFramework(void *p_next = nullptr, bool reserve_slot = false); diff --git a/tests/unit/gpu_av_array_oob.cpp b/tests/unit/gpu_av_array_oob.cpp new file mode 100644 index 00000000000..f2096d392f9 --- /dev/null +++ b/tests/unit/gpu_av_array_oob.cpp @@ -0,0 +1,555 @@ +/* + * 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 NegativeGpuAVArrayOob : public GpuAVArrayOobTest { + protected: + void TestHelper(const char* source, uint32_t expected_index, uint32_t expected_bound, + const char* vuid = "SPIRV-ArrayOob-OpAccessChain", SpvSourceType source_type = SPV_SOURCE_GLSL, + VkSpecializationInfo* spec_info = nullptr, const char* expected_variable_name = nullptr); +}; + +void NegativeGpuAVArrayOob::TestHelper(const char* shader_source, uint32_t expected_index, uint32_t expected_bound, + const char* vuid, SpvSourceType source_type, VkSpecializationInfo* spec_info, + const char* expected_variable_name) { + RETURN_IF_SKIP(InitArrayOob()); + + 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_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2, source_type, spec_info); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + uint32_t* buf_ptr = (uint32_t*)in_buffer.Memory().Map(); + memset((void*)buf_ptr, 0, 32); + in_buffer.Memory().Unmap(); + + 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(); + + std::string regex; + if (expected_variable_name) { + regex = std::string("\"") + expected_variable_name + "\".*"; + } + regex += std::to_string(expected_index) + " is >= .* " + std::to_string(expected_bound); + m_errorMonitor->SetDesiredErrorRegex(vuid, regex.c_str()); + m_default_queue->SubmitAndWait(m_command_buffer); + m_errorMonitor->VerifyFound(); +} + +TEST_F(NegativeGpuAVArrayOob, Simple1DArray) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { + uint idx; + uint data[]; + }; + shared uint arr[4]; + void main() { + uint i = idx + data[0] + 4; + arr[i] = 0; + } + )glsl"; + + TestHelper(shader_source, 4, 4); +} + +TEST_F(NegativeGpuAVArrayOob, Array2DInner) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[2][4]; + void main() { + uint i = ssbo.data[0] + 4; + arr[0][i] = 0; + } + )glsl"; + + TestHelper(shader_source, 4, 4); +} + +TEST_F(NegativeGpuAVArrayOob, Array2DOuter) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[2][4]; + void main() { + uint i = ssbo.data[0] + 2; + arr[i][0] = 0; + } + )glsl"; + + TestHelper(shader_source, 2, 2); +} + +// Outer index is a constant in bounds, inner is dynamic and OOB. Spot check that the inner +// check fires correctly when the outer is a constant. +TEST_F(NegativeGpuAVArrayOob, Array2DConstantOuterDynamicInner) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[2][4]; + void main() { + uint i = ssbo.data[0] + 4; + arr[1][i] = 0; + } + )glsl"; + + TestHelper(shader_source, 4, 4); +} + +// Outer is dynamic and OOB, inner is a constant in bounds. Spot check that the outer check +// fires correctly when the inner is a constant. +TEST_F(NegativeGpuAVArrayOob, Array2DDynamicOuterConstantInner) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[2][4]; + void main() { + uint i = ssbo.data[0] + 2; + arr[i][3] = 0; + } + )glsl"; + + TestHelper(shader_source, 2, 2); +} + +TEST_F(NegativeGpuAVArrayOob, StructWithArray) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + struct S { uint a; uint b[4]; }; + shared S s; + void main() { + uint i = ssbo.data[0] + 4; + s.b[i] = 0; + } + )glsl"; + + TestHelper(shader_source, 4, 4); +} + +TEST_F(NegativeGpuAVArrayOob, ArrayOfStructs) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + struct S { uint x; }; + shared S s[2]; + void main() { + uint i = ssbo.data[0] + 2; + s[i].x = 0; + } + )glsl"; + + TestHelper(shader_source, 2, 2); +} + +TEST_F(NegativeGpuAVArrayOob, VectorExtractDynamic) { + const char* shader_source = R"spirv( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo %v + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %runtime_arr ArrayStride 4 + OpMemberDecorate %ssbo_struct 0 Offset 0 + OpDecorate %ssbo_struct Block + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 + %uint_0 = OpConstant %uint 0 + %uint_4 = OpConstant %uint 4 + %float_1 = OpConstant %float 1 +%runtime_arr = OpTypeRuntimeArray %uint +%ssbo_struct = OpTypeStruct %runtime_arr + %ssbo_ptr = OpTypePointer StorageBuffer %ssbo_struct + %elem_ptr = OpTypePointer StorageBuffer %uint + %ssbo = OpVariable %ssbo_ptr StorageBuffer + %wg_ptr = OpTypePointer Workgroup %v4float + %v = OpVariable %wg_ptr Workgroup + %main = OpFunction %void None %void_fn + %entry = OpLabel + %init_vec = OpCompositeConstruct %v4float %float_1 %float_1 %float_1 %float_1 + OpStore %v %init_vec + %chain = OpAccessChain %elem_ptr %ssbo %uint_0 %uint_0 + %idx_val = OpLoad %uint %chain + %oob_idx = OpIAdd %uint %idx_val %uint_4 + %loaded_v = OpLoad %v4float %v + %extracted = OpVectorExtractDynamic %float %loaded_v %oob_idx + %cast = OpBitcast %uint %extracted + OpStore %chain %cast + OpReturn + OpFunctionEnd + )spirv"; + + TestHelper(shader_source, 4, 4, "SPIRV-ArrayOob-OpVectorExtractDynamic", SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVArrayOob, VectorInsertDynamic) { + const char* shader_source = R"spirv( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo %v + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %runtime_arr ArrayStride 4 + OpMemberDecorate %ssbo_struct 0 Offset 0 + OpDecorate %ssbo_struct Block + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 + %uint_0 = OpConstant %uint 0 + %uint_4 = OpConstant %uint 4 + %float_1 = OpConstant %float 1 + %float_2 = OpConstant %float 2 +%runtime_arr = OpTypeRuntimeArray %uint +%ssbo_struct = OpTypeStruct %runtime_arr + %ssbo_ptr = OpTypePointer StorageBuffer %ssbo_struct + %elem_ptr = OpTypePointer StorageBuffer %uint + %ssbo = OpVariable %ssbo_ptr StorageBuffer + %wg_ptr = OpTypePointer Workgroup %v4float + %v = OpVariable %wg_ptr Workgroup + %main = OpFunction %void None %void_fn + %entry = OpLabel + %init_vec = OpCompositeConstruct %v4float %float_1 %float_1 %float_1 %float_1 + OpStore %v %init_vec + %chain = OpAccessChain %elem_ptr %ssbo %uint_0 %uint_0 + %idx_val = OpLoad %uint %chain + %oob_idx = OpIAdd %uint %idx_val %uint_4 + %loaded_v = OpLoad %v4float %v + %inserted = OpVectorInsertDynamic %v4float %loaded_v %float_2 %oob_idx + OpStore %v %inserted + %cast = OpBitcast %uint %float_2 + OpStore %chain %cast + OpReturn + OpFunctionEnd + )spirv"; + + TestHelper(shader_source, 4, 4, "SPIRV-ArrayOob-OpVectorInsertDynamic", SPV_SOURCE_ASM); +} + +TEST_F(NegativeGpuAVArrayOob, LongVectorOob) { + AddRequiredFeature(vkt::Feature::longVector); + AddRequiredExtensions(VK_EXT_SHADER_LONG_VECTOR_EXTENSION_NAME); + + const char* shader_source = R"glsl( + #version 450 + #extension GL_EXT_long_vector : enable + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + layout(constant_id = 0) const uint N = 5; + shared vector v; + void main() { + uint i = ssbo.data[0] + 5; + v[i] = 0; + } + )glsl"; + + TestHelper(shader_source, 5, 5); +} + +TEST_F(NegativeGpuAVArrayOob, SlangNestedStruct) { + RETURN_IF_SKIP(InitArrayOob()); + RETURN_IF_SKIP(CheckSlangSupport()); + + const char* shader_source = R"slang( + RWStructuredBuffer outputBuffer; + + [numthreads(1, 1, 1)] + void main(uint3 groupThreadID : SV_GroupThreadID) + { + struct A { + uint32_t b; + uint32_t c; + }; + struct S { + uint32_t x; + uint32_t y; + A z[2]; + }; + + static groupshared S temp; + uint i = outputBuffer[0] + 2; + temp.z[i].b = 0; + outputBuffer[0] = temp.z[0].b; + } + )slang"; + + 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_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2, SPV_SOURCE_SLANG); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + uint32_t* buf_ptr = (uint32_t*)in_buffer.Memory().Map(); + buf_ptr[0] = 0; + in_buffer.Memory().Unmap(); + + 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->SetDesiredErrorRegex("SPIRV-ArrayOob-OpAccessChain", "2 is >= array size 2"); + m_default_queue->SubmitAndWait(m_command_buffer); + m_errorMonitor->VerifyFound(); +} + +TEST_F(NegativeGpuAVArrayOob, SpecConstantArraySize) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + layout(constant_id = 0) const uint SIZE = 4; + shared uint arr[SIZE]; + void main() { + arr[3] = 1; + ssbo.data[0] = arr[3]; + } + )glsl"; + + uint32_t spec_value = 3; + VkSpecializationMapEntry entry = {0, 0, sizeof(uint32_t)}; + VkSpecializationInfo spec_info = {1, &entry, sizeof(uint32_t), &spec_value}; + TestHelper(shader_source, 3, 3, "SPIRV-ArrayOob-OpAccessChain", SPV_SOURCE_GLSL, &spec_info); +} + +TEST_F(NegativeGpuAVArrayOob, MaxSharedMemorySize) { + SetTargetApiVersion(VK_API_VERSION_1_2); + RETURN_IF_SKIP(InitGpuAvFramework( + {{OBJECT_LAYER_NAME, "gpuav_shared_memory_data_race", VK_LAYER_SETTING_TYPE_BOOL32_EXT, 1, &kVkFalse}}, false)); + RETURN_IF_SKIP(InitState()); + + const uint32_t max_shared_memory_size = m_device->Physical().limits_.maxComputeSharedMemorySize; + const uint32_t array_size = max_shared_memory_size / sizeof(uint32_t); + + std::ostringstream cs; + cs << R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[)glsl"; + cs << array_size; + cs << R"glsl(]; + void main() { + uint i = ssbo.data[0] + )glsl"; + cs << array_size; + cs << R"glsl(; + arr[i] = 0; + } + )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.str().c_str(), VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + uint32_t* buf_ptr = (uint32_t*)in_buffer.Memory().Map(); + memset((void*)buf_ptr, 0, 32); + in_buffer.Memory().Unmap(); + + 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(); + + const std::string regex = std::to_string(array_size) + " is >= array size " + std::to_string(array_size); + m_errorMonitor->SetDesiredErrorRegex("SPIRV-ArrayOob-OpAccessChain", regex.c_str()); + m_default_queue->SubmitAndWait(m_command_buffer); + m_errorMonitor->VerifyFound(); +} + +TEST_F(NegativeGpuAVArrayOob, MeshShader) { + SetTargetApiVersion(VK_API_VERSION_1_2); + AddRequiredExtensions(VK_EXT_MESH_SHADER_EXTENSION_NAME); + AddRequiredFeature(vkt::Feature::meshShader); + AddRequiredFeature(vkt::Feature::taskShader); + RETURN_IF_SKIP(InitGpuAvFramework()); + RETURN_IF_SKIP(InitState()); + InitRenderTarget(); + + const char* mesh_source = R"glsl( + #version 460 + #extension GL_EXT_mesh_shader : require + layout(local_size_x = 1) in; + layout(max_vertices = 3, max_primitives = 1) out; + layout(triangles) out; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[4]; + void main() { + SetMeshOutputsEXT(0, 0); + uint i = ssbo.data[0] + 4; + arr[i] = 0; + } + )glsl"; + + VkShaderObj ms(*m_device, mesh_source, VK_SHADER_STAGE_MESH_BIT_EXT, SPV_ENV_VULKAN_1_2); + VkShaderObj fs(*m_device, kFragmentMinimalGlsl, VK_SHADER_STAGE_FRAGMENT_BIT, SPV_ENV_VULKAN_1_2); + + OneOffDescriptorSet descriptor_set(m_device, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, nullptr}}); + vkt::PipelineLayout pipeline_layout(*m_device, {&descriptor_set.layout_}); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + uint32_t* buf_ptr = (uint32_t*)in_buffer.Memory().Map(); + buf_ptr[0] = 0; + in_buffer.Memory().Unmap(); + + descriptor_set.WriteDescriptorBufferInfo(0, in_buffer, 0, VK_WHOLE_SIZE, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + descriptor_set.UpdateDescriptorSets(); + + CreatePipelineHelper pipe(*this); + pipe.gp_ci_.layout = pipeline_layout; + pipe.shader_stages_ = {ms.GetStageCreateInfo(), fs.GetStageCreateInfo()}; + pipe.CreateGraphicsPipeline(); + + m_command_buffer.Begin(); + m_command_buffer.BeginRenderPass(m_renderPassBeginInfo); + vk::CmdBindDescriptorSets(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor_set.set_, 0, + nullptr); + vk::CmdBindPipeline(m_command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe); + vk::CmdDrawMeshTasksEXT(m_command_buffer, 1, 1, 1); + m_command_buffer.EndRenderPass(); + m_command_buffer.End(); + + m_errorMonitor->SetDesiredErrorRegex("SPIRV-ArrayOob-OpAccessChain", "4 is >= array size 4"); + m_default_queue->SubmitAndWait(m_command_buffer); + m_errorMonitor->VerifyFound(); +} + +// Function-scope local array. Dynamic indexing keeps glslang/spirv-opt from promoting it +// to SSA, so it stays as a Function-storage OpVariable. +TEST_F(NegativeGpuAVArrayOob, FunctionStorageArrayOob) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + void main() { + uint arr[4]; + arr[0] = 1; + arr[1] = 2; + arr[2] = 3; + arr[3] = 4; + uint i = ssbo.data[0] + 4; + arr[i] = 99; + ssbo.data[0] = arr[ssbo.data[0] & 3u]; + } + )glsl"; + + TestHelper(shader_source, 4, 4); +} + +// File-scope global array, which glslang lowers to Private storage. +TEST_F(NegativeGpuAVArrayOob, PrivateStorageArrayOob) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + uint privateArr[4]; + void main() { + privateArr[0] = 1; + privateArr[1] = 2; + privateArr[2] = 3; + privateArr[3] = 4; + uint i = ssbo.data[0] + 4; + privateArr[i] = 99; + ssbo.data[0] = privateArr[ssbo.data[0] & 3u]; + } + )glsl"; + + TestHelper(shader_source, 4, 4); +} + +// Verify the host logger prints the variable name in the error message for each tracked +// storage class. +TEST_F(NegativeGpuAVArrayOob, VariableNameInMessageWorkgroup) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint workgroupArr[4]; + void main() { + workgroupArr[ssbo.data[0] + 4] = 0; + } + )glsl"; + + TestHelper(shader_source, 4, 4, "SPIRV-ArrayOob-OpAccessChain", SPV_SOURCE_GLSL, nullptr, "workgroupArr"); +} + +TEST_F(NegativeGpuAVArrayOob, VariableNameInMessagePrivate) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + uint privateArr[4]; + void main() { + privateArr[ssbo.data[0] + 4] = 0; + } + )glsl"; + + TestHelper(shader_source, 4, 4, "SPIRV-ArrayOob-OpAccessChain", SPV_SOURCE_GLSL, nullptr, "privateArr"); +} + +TEST_F(NegativeGpuAVArrayOob, VariableNameInMessageFunction) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + void main() { + uint functionArr[4]; + functionArr[0] = 1; + functionArr[1] = 2; + functionArr[2] = 3; + functionArr[3] = 4; + functionArr[ssbo.data[0] + 4] = 99; + ssbo.data[0] = functionArr[ssbo.data[0] & 3u]; + } + )glsl"; + + TestHelper(shader_source, 4, 4, "SPIRV-ArrayOob-OpAccessChain", SPV_SOURCE_GLSL, nullptr, "functionArr"); +} diff --git a/tests/unit/gpu_av_array_oob_positive.cpp b/tests/unit/gpu_av_array_oob_positive.cpp new file mode 100644 index 00000000000..d5fff29e1cf --- /dev/null +++ b/tests/unit/gpu_av_array_oob_positive.cpp @@ -0,0 +1,469 @@ +/* + * 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" + +void GpuAVArrayOobTest::InitArrayOob() { + SetTargetApiVersion(VK_API_VERSION_1_2); + RETURN_IF_SKIP(InitGpuAvFramework()); + RETURN_IF_SKIP(InitState()); +} + +class PositiveGpuAVArrayOob : public GpuAVArrayOobTest { + protected: + void TestHelper(const char* source); +}; + +void PositiveGpuAVArrayOob::TestHelper(const char* shader_source) { + RETURN_IF_SKIP(InitArrayOob()); + + 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_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + 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(PositiveGpuAVArrayOob, Simple1DInBounds) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 4) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[4]; + void main() { + arr[gl_LocalInvocationIndex % 4] = gl_LocalInvocationIndex; + barrier(); + ssbo.data[gl_LocalInvocationIndex] = arr[gl_LocalInvocationIndex % 4]; + } + )glsl"; + + TestHelper(shader_source); +} + +// Safe mode uses OpSelect with the access chain's original index type; this uses a signed 32-bit index (CastToUint32 feeds +// inst_array_oob, but the chain keeps OpTypeInt signed). +TEST_F(PositiveGpuAVArrayOob, WorkgroupArraySignedIndexFromSsboInBounds) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { + int idx; + uint data[]; + } ssbo; + shared uint arr[4]; + void main() { + arr[ssbo.idx] = 7u; + ssbo.data[0] = arr[0]; + } + )glsl"; + + TestHelper(shader_source); +} + +TEST_F(PositiveGpuAVArrayOob, Array2DInBounds) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[2][4]; + void main() { + arr[0][3] = 1; + arr[1][0] = 2; + ssbo.data[0] = arr[0][3] + arr[1][0]; + } + )glsl"; + + TestHelper(shader_source); +} + +TEST_F(PositiveGpuAVArrayOob, StructArrayInBounds) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + struct S { uint x; }; + shared S s[4]; + void main() { + s[0].x = 1; + s[3].x = 2; + ssbo.data[0] = s[0].x + s[3].x; + } + )glsl"; + + TestHelper(shader_source); +} + +TEST_F(PositiveGpuAVArrayOob, BoundaryIndex) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[4]; + void main() { + arr[3] = 42; + ssbo.data[0] = arr[3]; + } + )glsl"; + + TestHelper(shader_source); +} + +TEST_F(PositiveGpuAVArrayOob, VectorExtractInBounds) { + const char* shader_source = R"spirv( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo %v + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %runtime_arr ArrayStride 4 + OpMemberDecorate %ssbo_struct 0 Offset 0 + OpDecorate %ssbo_struct Block + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 + %uint_0 = OpConstant %uint 0 + %uint_3 = OpConstant %uint 3 + %float_1 = OpConstant %float 1 +%runtime_arr = OpTypeRuntimeArray %uint +%ssbo_struct = OpTypeStruct %runtime_arr + %ssbo_ptr = OpTypePointer StorageBuffer %ssbo_struct + %elem_ptr = OpTypePointer StorageBuffer %uint + %ssbo = OpVariable %ssbo_ptr StorageBuffer + %wg_ptr = OpTypePointer Workgroup %v4float + %v = OpVariable %wg_ptr Workgroup + %main = OpFunction %void None %void_fn + %entry = OpLabel + %init_vec = OpCompositeConstruct %v4float %float_1 %float_1 %float_1 %float_1 + OpStore %v %init_vec + %loaded_v = OpLoad %v4float %v + %extracted = OpVectorExtractDynamic %float %loaded_v %uint_3 + %cast = OpBitcast %uint %extracted + %chain = OpAccessChain %elem_ptr %ssbo %uint_0 %uint_0 + OpStore %chain %cast + OpReturn + OpFunctionEnd + )spirv"; + + RETURN_IF_SKIP(InitArrayOob()); + + 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_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2, SPV_SOURCE_ASM); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + 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(PositiveGpuAVArrayOob, LongVectorInBounds) { + AddRequiredFeature(vkt::Feature::longVector); + AddRequiredExtensions(VK_EXT_SHADER_LONG_VECTOR_EXTENSION_NAME); + + const char* shader_source = R"glsl( + #version 450 + #extension GL_EXT_long_vector : enable + layout(local_size_x = 5) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + layout(constant_id = 0) const uint N = 5; + shared vector v; + void main() { + v[gl_LocalInvocationIndex % N] = gl_LocalInvocationIndex; + barrier(); + ssbo.data[gl_LocalInvocationIndex] = v[gl_LocalInvocationIndex % N]; + } + )glsl"; + + TestHelper(shader_source); +} + +TEST_F(PositiveGpuAVArrayOob, SlangNestedStruct) { + RETURN_IF_SKIP(InitArrayOob()); + RETURN_IF_SKIP(CheckSlangSupport()); + + const char* shader_source = R"slang( + RWStructuredBuffer outputBuffer; + + [numthreads(2, 1, 1)] + void main(uint3 groupThreadID : SV_GroupThreadID) + { + struct A { + uint32_t b; + uint32_t c; + }; + struct S { + uint32_t x; + uint32_t y; + A z[2]; + }; + + static groupshared S temp; + temp.z[groupThreadID.x].b = groupThreadID.x; + outputBuffer[groupThreadID.x] = temp.z[groupThreadID.x].b; + } + )slang"; + + 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_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2, SPV_SOURCE_SLANG); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + 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); +} + +// Mixed constant + dynamic indexing on a 2D array, both in bounds. +TEST_F(PositiveGpuAVArrayOob, MixedConstantDynamicArray2D) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uint arr[2][4]; + void main() { + uint i = ssbo.data[0] % 4; + uint j = ssbo.data[0] % 2; + arr[1][i] = 1; + arr[j][2] = 2; + ssbo.data[0] = arr[1][i] + arr[j][2]; + } + )glsl"; + + TestHelper(shader_source); +} + +// Vector accessed by OpAccessChain with a constant index (the GLSL .x writes an access chain +// with a literal 0 index). +TEST_F(PositiveGpuAVArrayOob, VectorAccessChainConstantIndex) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uvec4 v; + void main() { + v.x = 1; + v.w = 4; + ssbo.data[0] = v.x + v.w; + } + )glsl"; + + TestHelper(shader_source); +} + +// OpCompositeExtract is not matched by the pass; this just confirms a pattern that lowers +// to OpCompositeExtract doesn't break anything. +TEST_F(PositiveGpuAVArrayOob, CompositeExtractConstantIndex) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + shared uvec4 v; + void main() { + v = uvec4(1, 2, 3, 4); + uvec4 loaded = v; + ssbo.data[0] = loaded.x + loaded.z; + } + )glsl"; + + TestHelper(shader_source); +} + +// OpVectorExtractDynamic with a literal constant index, in bounds. +TEST_F(PositiveGpuAVArrayOob, VectorExtractDynamicConstantIndex) { + const char* shader_source = R"spirv( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo %v + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %runtime_arr ArrayStride 4 + OpMemberDecorate %ssbo_struct 0 Offset 0 + OpDecorate %ssbo_struct Block + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 + %uint_0 = OpConstant %uint 0 + %uint_2 = OpConstant %uint 2 + %float_1 = OpConstant %float 1 +%runtime_arr = OpTypeRuntimeArray %uint +%ssbo_struct = OpTypeStruct %runtime_arr + %ssbo_ptr = OpTypePointer StorageBuffer %ssbo_struct + %elem_ptr = OpTypePointer StorageBuffer %uint + %ssbo = OpVariable %ssbo_ptr StorageBuffer + %wg_ptr = OpTypePointer Workgroup %v4float + %v = OpVariable %wg_ptr Workgroup + %main = OpFunction %void None %void_fn + %entry = OpLabel + %init_vec = OpCompositeConstruct %v4float %float_1 %float_1 %float_1 %float_1 + OpStore %v %init_vec + %loaded_v = OpLoad %v4float %v + %extracted = OpVectorExtractDynamic %float %loaded_v %uint_2 + %cast = OpBitcast %uint %extracted + %chain = OpAccessChain %elem_ptr %ssbo %uint_0 %uint_0 + OpStore %chain %cast + OpReturn + OpFunctionEnd + )spirv"; + + RETURN_IF_SKIP(InitArrayOob()); + + 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_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2, SPV_SOURCE_ASM); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + 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); +} + +// Function-scope local array, dynamic index always in bounds. +TEST_F(PositiveGpuAVArrayOob, FunctionStorageArrayInBounds) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + void main() { + uint arr[4]; + uint i = ssbo.data[0] & 3u; + arr[i] = 42; + ssbo.data[0] = arr[i]; + } + )glsl"; + + TestHelper(shader_source); +} + +// File-scope global array, which glslang lowers to Private storage. Dynamic index in bounds. +TEST_F(PositiveGpuAVArrayOob, PrivateStorageArrayInBounds) { + const char* shader_source = R"glsl( + #version 450 + layout(local_size_x = 1) in; + layout(set = 0, binding = 0) buffer StorageBuffer { uint data[]; } ssbo; + uint privateArr[4]; + void main() { + uint i = ssbo.data[0] & 3u; + privateArr[i] = 42; + ssbo.data[0] = privateArr[i]; + } + )glsl"; + + TestHelper(shader_source); +} + +TEST_F(PositiveGpuAVArrayOob, VectorInsertInBounds) { + const char* shader_source = R"spirv( + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %main "main" %ssbo %v + OpExecutionMode %main LocalSize 1 1 1 + OpDecorate %runtime_arr ArrayStride 4 + OpMemberDecorate %ssbo_struct 0 Offset 0 + OpDecorate %ssbo_struct Block + OpDecorate %ssbo DescriptorSet 0 + OpDecorate %ssbo Binding 0 + %void = OpTypeVoid + %void_fn = OpTypeFunction %void + %uint = OpTypeInt 32 0 + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 + %uint_0 = OpConstant %uint 0 + %uint_3 = OpConstant %uint 3 + %float_1 = OpConstant %float 1 + %float_2 = OpConstant %float 2 +%runtime_arr = OpTypeRuntimeArray %uint +%ssbo_struct = OpTypeStruct %runtime_arr + %ssbo_ptr = OpTypePointer StorageBuffer %ssbo_struct + %elem_ptr = OpTypePointer StorageBuffer %uint + %ssbo = OpVariable %ssbo_ptr StorageBuffer + %wg_ptr = OpTypePointer Workgroup %v4float + %v = OpVariable %wg_ptr Workgroup + %main = OpFunction %void None %void_fn + %entry = OpLabel + %init_vec = OpCompositeConstruct %v4float %float_1 %float_1 %float_1 %float_1 + OpStore %v %init_vec + %loaded_v = OpLoad %v4float %v + %inserted = OpVectorInsertDynamic %v4float %loaded_v %float_2 %uint_3 + OpStore %v %inserted + %chain = OpAccessChain %elem_ptr %ssbo %uint_0 %uint_0 + %cast = OpBitcast %uint %float_2 + OpStore %chain %cast + OpReturn + OpFunctionEnd + )spirv"; + + RETURN_IF_SKIP(InitArrayOob()); + + 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_source, VK_SHADER_STAGE_COMPUTE_BIT, SPV_ENV_VULKAN_1_2, SPV_SOURCE_ASM); + pipe.CreateComputePipeline(); + + vkt::Buffer in_buffer(*m_device, 32, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + 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); +}