KhronosGroup / KhronosGroup/glslang
HLSL parser SEGV: hasUnsized() calls front() on empty TArraySizes in acceptParameterDeclaration
- Dominant language
- C++
- Stars
- 3.6k
- Forks
- 989
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 31
Description
## Summary
Parsing malformed HLSL that declares a geometry-shader-style `main` with a corrupted parameter list causes `glslang::HlslGrammar::acceptParameterDeclaration()` to call `TArraySizes::hasUnsized()` on a `TArraySizes` object whose internal size vector is empty. `hasUnsized()` → `getOuterSize()` → `frontSize()` then calls `std::vector::front()` on an empty container, producing an AddressSanitizer SEGV (null-page read). This is reachable through the public `TShader::parse()` API when compiling untrusted HLSL (`EShSourceHlsl` / `EShMsgReadHlsl`), not a fuzzer harness artifact.
### Reproduction
Create a directory with `poc.hlsl` (above) and `repro.cpp` (below), then run:
```bash
REPRO_DIR="$(pwd)/repro" # directory containing poc.hlsl and repro.cpp
docker run --rm -v "$REPRO_DIR":/repro gcr.io/oss-fuzz/glslang bash -c '
set -e
cd /src/glslang
rm -rf build-repro && mkdir build-repro && cd build-repro
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_OPT=OFF \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -DNDEBUG" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build . -j"$(nproc)" --target glslang glslang-default-resource-limits
clang++ -fsanitize=address -fno-omit-frame-pointer -g -std=c++17 -DNDEBUG \
-I/src/glslang -I/src/glslang/build-repro/include \
/repro/repro.cpp \
glslang/libglslang.a glslang/libglslang-default-resource-limits.a \
-lpthread -o /repro/repro
/repro/repro /repro/poc.hlsl
'
```
**Expected result:** process aborts with AddressSanitizer SEGV in `TArraySizes::frontSize()` (`arrays.h:112`), matching the fuzzer finding.
With `-DCMAKE_BUILD_TYPE=Debug` (no `-DNDEBUG`), the same input typically hits:
```text
arrays.h:111: Assertion `sizes != nullptr && sizes->size() > 0' failed.
```
poc.hlsl (base64)
```
c3RydWN0IEdTSW4gIHsgZmxvYXQ0IHBvcyA6IFNWX1BPU0lUSU9OOyB9OwpzdHJ1Y3QgR1NPdXQgeyBmbG9hdDQgcG9zIDogU1ZfUE9TSVRJT047IH07ClttYXh2ZXJ0ZXhjb3VudCgzKV0Kdm9pZCBtYWluKHRyaWFuZ2xlIEdTSW4gaW5wdXRbM2ksb111biB0IFRyaWFuZ2xlU3RyZWFtPEdTT3V0PiB0cmlzKSB7CiAgICBHU091dCBvOwogICAgZm9yIChpbnQgaSA9IDA7IHN0cnVjdCBHU0luICB7aSA8IDM7ICsraSkgewogICAgICAgIG8ucG9zID0gaW5wdXRbaV0ucG9zOwogICAgICAgIHRyaXMuQXBwZW5kKG8pOwogICAgfQogICAgdHJpcy5SIGZsb2F0NCBwb3Nlc3RhciA=
```
### Option B — Local build
```bash
git clone https://github.com/KhronosGroup/glslang.git
cd glslang
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_OPT=OFF \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -DNDEBUG" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build . -j"$(nproc)" --target glslang glslang-default-resource-limits
clang++ -fsanitize=address -fno-omit-frame-pointer -g -std=c++17 -DNDEBUG \
-I../glslang -Iinclude \
/path/to/repro.cpp \
glslang/libglslang.a glslang/libglslang-default-resource-limits.a \
-lpthread -o repro
./repro ./poc.hlsl
```
### `repro.cpp`
```cpp
#include
#include
#include
#include
#include "glslang/Public/ResourceLimits.h"
#include "glslang/Public/ShaderLang.h"
static std::string readFile(const char* path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
std::fprintf(stderr, "cannot open %s\n", path);
std::exit(1);
}
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
int main(int argc, char** argv) {
const char* path = (argc > 1) ? argv[1] : "poc.hlsl";
const std::string source = readFile(path);
const char* shader_string = source.data();
const int shader_length = static_cast(source.size());
glslang::InitializeProcess();
glslang::TShader shader(EShLangVertex);
shader.setEnvInput(glslang::EShSourceHlsl, EShLangVertex,
glslang::EShClientVulkan, 100);
shader.setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_0);
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_0);
shader.setStringsWithLengths(&shader_string, &shader_length, 1);
shader.setEntryPoint("main");
shader.setSourceEntryPoint("main");
const EShMessages messages = static_cast(
EShMsgDefault | EShMsgReadHlsl | EShMsgSpvRules | EShMsgVulkanRules);
(void)shader.parse(GetDefaultResources(), 100, false, messages);
glslang::FinalizeProcess();
return 0;
}
```
##ASAN CRASH:
```
==315==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (pc 0x55be8237733e bp 0x7ffda6f4f030 sp 0x7ffda6f4ef80 T0)
==315==The signal is caused by a READ memory access.
==315==Hint: address points to the zero page.
#0 0x55be8237733e in __gnu_cxx::__normal_iterator>>::__normal_iterator(glslang::TArraySize* const&) .../bits/stl_iterator.h:804:20
#1 0x55be8237733e in std::vector>::begin() .../bits/stl_vector.h:809:16
#2 0x55be8237733e in std::vector>::front() .../bits/stl_vector.h:1121:10
#3 0x55be8237733e in glslang::TSmallArrayVector::frontSize() const
/src/glslang/glslang/MachineIndependent/../Include/arrays.h:112:23
#4 0x55be8237733e in glslang::TArraySizes::getOuterSize() const
/src/glslang/glslang/MachineIndependent/../Include/arrays.h:259:45
#5 0x55be8237733e in glslang::TArraySizes::hasUnsized() const
/src/glslang/glslang/MachineIndependent/../Include/arrays.h:319:38
#6 0x55be8237733e in glslang::HlslGrammar::acceptParameterDeclaration(glslang::TFunction&)
/src/glslang/glslang/HLSL/hlslGrammar.cpp:2932:25
#7 0x55be8236667a in glslang::HlslGrammar::acceptFunctionParameters(glslang::TFunction&)
/src/glslang/glslang/HLSL/hlslGrammar.cpp:2837:19
#8 0x55be82362bcf in glslang::HlslGrammar::acceptDeclaration(TIntermNode*&)
/src/glslang/glslang/HLSL/hlslGrammar.cpp:420:18
#9 0x55be82361342 in glslang::HlslGrammar::acceptDeclarationList(TIntermNode*&)
/src/glslang/glslang/HLSL/hlslGrammar.cpp:166:15
#10 0x55be82361342 in glslang::HlslGrammar::acceptCompilationUnit()
/src/glslang/glslang/HLSL/hlslGrammar.cpp:131:11
#11 0x55be822aad11 in glslang::HlslParseContext::parseShaderStrings(glslang::TPpContext&, glslang::TInputScanner&, bool)
/src/glslang/glslang/HLSL/hlslParseHelper.cpp:132:18
#12 0x55be821bedf0 in (anonymous namespace)::DoFullParse::operator()(...)
/src/glslang/glslang/MachineIndependent/ShaderLang.cpp:1233:28
#13 0x55be821bedf0 in ProcessDeferred(...)
/src/glslang/glslang/MachineIndependent/ShaderLang.cpp:1008:20
#14 0x55be821bedf0 in CompileDeferred(...)
/src/glslang/glslang/MachineIndependent/ShaderLang.cpp:1321:12
#15 0x55be821c27cd in glslang::TShader::parse(TBuiltInResource const*, int, EProfile, bool, bool, EShMessages, glslang::TShader::Includer&)
/src/glslang/glslang/MachineIndependent/ShaderLang.cpp:1894:12
#16 0x55be821bb966 in glslang::TShader::parse(...) ShaderLang.h:680:16
#17 0x55be821bb7f5 in glslang::TShader::parse(...) ShaderLang.h:686:16
#18 0x55be821bae41 in main /repro/repro.cpp:40:18
#19 0x7f62f83e0082 in __libc_start_main
#20 0x55be820d368d in _start
```
`repro.cpp` mirrors the libFuzzer harness: it calls `TShader::parse()` on the PoC bytes with `EShSourceHlsl`, Vulkan 1.0 client, entry point `main`, and `EShMsgReadHlsl | EShMsgSpvRules | EShMsgVulkanRules`.
## Root cause (analysis)
During HLSL function parameter parsing, `acceptParameterDeclaration()` queries `type.arraySizes->hasUnsized()` without ensuring the `TArraySizes` object was populated. For this input, array size metadata ends up empty, so `hasUnsized()` → `getOuterSize()` → `frontSize()` dereferences `sizes->front()` on an empty vector.
**Suggested fix:** Guard `hasUnsized()` / `frontSize()` when `sizes` is null or empty (return `false` / skip unsized handling), and/or ensure `acceptParameterDeclaration()` only calls `hasUnsized()` after valid array dimensions are recorded. Add a regression test with `poc.hlsl` parsed via `TShader` + `EShMsgReadHlsl`.
Credit: Aisle Research (Ze Sheng, Dmitrijs Trizna, Luigino Camastra, Guido Vranken)
Contributor guide
Research direction
Start with glslang/HLSL/hlslGrammar.cpp at HlslGrammar::acceptParameterDeclaration() and inspect the TArraySizes implementation in Include/arrays.h. Run the supplied TShader::parse() reproducer with the HLSL PoC under AddressSanitizer, then add the suggested regression test. Done means malformed input reports a parse failure without an SEGV or assertion.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100