fix(recompiler): guard narrow-type scanning bounds

Check module headers, instruction lengths and required operands before
scanning narrow types.

Cover malformed and byte-swapped modules while retaining the native
SPIR-V 1.3 conversion matrix.

Tested: Linux/llvmpipe CTest three times with SPIRV-Tools on and off,
cold-cache Vulkan regressions, and ASan/UBSan in all optimization modes.
This commit is contained in:
Nikhil Panchal 2026-10-08 00:42:11 +00:00 committed by Adrià Franch
parent 43074a805e
commit 39e8e70caf
2 changed files with 81 additions and 28 deletions

View File

@ -7,12 +7,14 @@
#include <exception>
#include <set>
#include <string>
#include <utility>
#include <vector>
namespace {
int failures = 0;
constexpr std::uint32_t Vulkan11 = 0x00401000u;
constexpr std::uint32_t Spirv11 = 0x00010100u;
constexpr std::uint32_t Spirv13 = 0x00010300u;
void check(bool condition, const std::string& message) {
@ -55,7 +57,8 @@ std::string moduleSource(const NarrowType& type, bool vector, ArithmeticSupport
text += "OpCapability DotProductInput4x8BitKHR\nOpExtension \"SPV_KHR_integer_dot_product\"\n";
}
text += type.width == 8 ? "OpExtension \"SPV_KHR_8bit_storage\"\n" : "OpExtension \"SPV_KHR_16bit_storage\"\n";
text += R"(OpMemoryModel Logical GLSL450
text += R"(OpExtension "SPV_KHR_storage_buffer_storage_class"
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
OpDecorate %block Block
@ -123,6 +126,14 @@ std::set<std::uint32_t> capabilities(const std::vector<std::uint32_t>& words) {
return result;
}
std::vector<std::uint32_t> swapByteOrder(std::vector<std::uint32_t> words) {
for (auto& word : words) {
word = (word >> 24u) | ((word & 0x00ff0000u) >> 8u) |
((word & 0x0000ff00u) << 8u) | (word << 24u);
}
return words;
}
void testNarrowConversions(bool optimizationEnabled) {
spvtools::SpirvTools validator(SPV_ENV_VULKAN_1_1);
for (const auto& type : Types) {
@ -130,43 +141,50 @@ void testNarrowConversions(bool optimizationEnabled) {
for (const auto support : {ArithmeticSupport::StorageOnly, ArithmeticSupport::Explicit, ArithmeticSupport::ImpliedInt8}) {
if (support == ArithmeticSupport::ImpliedInt8 && (type.width != 8 || !vector)) continue;
const bool arithmetic = support != ArithmeticSupport::StorageOnly;
const std::string label = std::string(type.name) + (vector ? " vector" : " scalar") +
const std::string name = std::string(type.name) + (vector ? " vector" : " scalar") +
(support == ArithmeticSupport::ImpliedInt8 ? " implied Int8" : (arithmetic ? " arithmetic" : " storage only"));
const auto input = assemble(moduleSource(type, vector, support));
if (!validator.Validate(input)) {
check(false, label + ": invalid test input");
continue;
}
try {
const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false);
if (!validator.Validate(output)) {
check(false, label + ": optimizer returned invalid SPIR-V");
auto native = assemble(moduleSource(type, vector, support));
for (const auto& [version, swapped] : {std::pair{Spirv13, false}, std::pair{Spirv11, false}, std::pair{Spirv11, true}}) {
native[1] = version;
const auto input = swapped ? swapByteOrder(native) : native;
const auto label = name + (version == Spirv13 ? " SPIR-V 1.3" : " SPIR-V 1.1") + (swapped ? " swapped" : " native");
if (!validator.Validate(input)) {
check(false, label + ": invalid test input");
continue;
}
check(capabilities(output) == capabilities(input), label + ": optimizer changed device capabilities");
check(opcodeCount(output, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores");
if (!optimizationEnabled) {
check(output == input, label + ": none mode changed the shader");
} else {
check(opcodeCount(output, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic");
if (arithmetic && !type.floating) {
const auto conversion = type.signedInteger ? spv::OpSConvert : spv::OpUConvert;
check(opcodeCount(output, conversion) == 0, label + ": safe constant folding was disabled");
try {
const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false);
if (!validator.Validate(output)) {
check(false, label + ": optimizer returned invalid SPIR-V");
continue;
}
const auto normalized = output.front() == spv::MagicNumber ? output : swapByteOrder(output);
check(capabilities(normalized) == capabilities(native), label + ": optimizer changed device capabilities");
check(opcodeCount(normalized, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores");
if (!optimizationEnabled) {
check(output == input, label + ": none mode changed the shader");
} else {
check(opcodeCount(normalized, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic");
if (arithmetic && !type.floating) {
const auto conversion = type.signedInteger ? spv::OpSConvert : spv::OpUConvert;
check(opcodeCount(normalized, conversion) == 0, label + ": safe constant folding was disabled");
}
}
} catch (const std::exception& error) {
check(false, label + ": " + error.what());
}
} catch (const std::exception& error) {
check(false, label + ": " + error.what());
}
}
}
}
}
void expectRejected(const std::vector<std::uint32_t>& words, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, const char* reason) {
void expectRejected(const std::vector<std::uint32_t>& words, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, const char* reason, const char* diagnostic = nullptr) {
try {
static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(words, vulkanVersion, spirvVersion, false));
check(false, std::string("accepted ") + reason);
} catch (const std::exception&) {
} catch (const std::exception& error) {
if (diagnostic != nullptr) check(std::string(error.what()).starts_with(diagnostic), std::string(reason) + ": unexpected rejection: " + error.what());
}
}
@ -178,9 +196,32 @@ void testInvalidInputIsRejected() {
auto invalid = moduleSource(Types.front(), false, ArithmeticSupport::StorageOnly);
invalid.insert(invalid.find("%main = OpFunction"), "%illegal = OpConstantNull %small\n");
expectRejected(assemble(invalid), Vulkan11, Spirv13, "an illegal storage-only narrow constant even when unused");
const auto rejectMalformed = [](const std::vector<std::uint32_t>& words, const char* reason) {
auto native = words;
if (native.size() > 1) native[1] = Spirv11;
constexpr auto diagnostic = "SPIR-V validation before optimization failed:";
expectRejected(native, Vulkan11, Spirv13, reason, diagnostic);
expectRejected(swapByteOrder(native), Vulkan11, Spirv13, (std::string(reason) + " (swapped)").c_str(), diagnostic);
};
for (std::size_t length = 0; length < 5; ++length) {
rejectMalformed(std::vector<std::uint32_t>(valid.begin(), valid.begin() + length), "an incomplete module header");
}
auto malformed = valid;
malformed[0] = 0;
rejectMalformed(malformed, "an invalid module magic");
malformed = valid;
malformed[5] = spv::OpCapability;
expectRejected(malformed, Vulkan11, Spirv13, "a zero-length SPIR-V instruction");
rejectMalformed(malformed, "a zero-length SPIR-V instruction");
malformed[5] = (0xffffu << spv::WordCountShift) | spv::OpCapability;
rejectMalformed(malformed, "an instruction extending beyond the module");
for (const auto& instruction : {
std::vector<std::uint32_t>{(1u << spv::WordCountShift) | spv::OpCapability},
std::vector<std::uint32_t>{(3u << spv::WordCountShift) | spv::OpTypeInt, 1u, 8u},
std::vector<std::uint32_t>{(2u << spv::WordCountShift) | spv::OpTypeFloat, 1u}}) {
malformed.assign(valid.begin(), valid.begin() + 5);
malformed.insert(malformed.end(), instruction.begin(), instruction.end());
rejectMalformed(malformed, "truncated capability or type operands");
}
}
}

View File

@ -11,6 +11,7 @@ namespace ShaderRecompiler {
namespace {
bool HasLimitedUseTypes(std::span<const std::uint32_t> spirv) {
if (spirv.size() < 5) throw std::runtime_error("SPIRV-Tools: incomplete module header");
bool int8 = false, int16 = false, float16 = false;
bool int8Capability = false, int16Capability = false, float16Capability = false;
const bool swapped = spirv[0] != spv::MagicNumber;
@ -20,8 +21,18 @@ bool HasLimitedUseTypes(std::span<const std::uint32_t> spirv) {
((word << 8u) & 0x00ff0000u) | (word << 24u)
: word;
};
for (std::size_t offset = 5; offset < spirv.size(); offset += wordAt(offset) >> 16u) {
switch (wordAt(offset) & 0xffffu) {
if (wordAt(0) != spv::MagicNumber) throw std::runtime_error("SPIRV-Tools: invalid module magic");
for (std::size_t offset = 5; offset < spirv.size();) {
const auto instruction = wordAt(offset);
const auto wordCount = instruction >> spv::WordCountShift;
const auto opcode = instruction & spv::OpCodeMask;
if (wordCount == 0 || wordCount > spirv.size() - offset ||
(opcode == spv::OpCapability && wordCount < 2) ||
(opcode == spv::OpTypeInt && wordCount < 4) ||
(opcode == spv::OpTypeFloat && wordCount < 3)) {
throw std::runtime_error("SPIRV-Tools: malformed instruction while scanning narrow types");
}
switch (opcode) {
case spv::OpCapability:
int8Capability |= wordAt(offset + 1) == spv::CapabilityInt8 ||
wordAt(offset + 1) == spv::CapabilityDotProductInput4x8Bit;
@ -33,11 +44,12 @@ bool HasLimitedUseTypes(std::span<const std::uint32_t> spirv) {
int16 |= wordAt(offset + 2) == 16;
break;
case spv::OpTypeFloat:
float16 |= (wordAt(offset) >> 16u) == 3 && wordAt(offset + 2) == 16;
float16 |= wordCount == 3 && wordAt(offset + 2) == 16;
break;
case spv::OpFunction:
return (int8 && !int8Capability) || (int16 && !int16Capability) || (float16 && !float16Capability);
}
offset += wordCount;
}
return (int8 && !int8Capability) || (int16 && !int16Capability) || (float16 && !float16Capability);
}