KhronosGroup / KhronosGroup/glslang

Use of uninitialized stack memory in GLSL swizzle parser

Open
#4,281 1 comment 0 reactions 1 assignee Claimed by @arcady-lunarg View on GitHub
bug GLSL/ESSL
Dominant language
C++
Stars
3.6k
Forks
989
Avg merge
1d 2h
Merged PRs (30d)
31

Description

I found a use of uninitialized stack memory in the GLSL swizzle parser. This is not related to the deprecated HLSL front-end.

The issue is in `TParseContextBase::parseSwizzleSelector()` in `glslang/MachineIndependent/ParseContextBase.cpp`.

`fieldSet` is a local stack array:

```c++
enum {
exyzw,
ergba,
estpq,
} fieldSet[MaxSwizzleSelectors];
```

Legal swizzle characters initialize `fieldSet[i]`, but the `default` case for an invalid character only reports an error:

```c++
default:
error(loc, "unknown swizzle selection", compString.c_str(), "");
break;
```

The later consistency check reads `fieldSet[i]`:

```c++
if (i > 0 && fieldSet[i] != fieldSet[i-1]) {
error(loc, "vector swizzle selectors not from the same set", compString.c_str(), "");
selector.resize(i);
break;
}
```

For example, with `in_v.xmy`, `x` initializes `fieldSet[0]`, `m` takes the `default` path and leaves `fieldSet[1]` uninitialized, and `y` pushes the second valid selector entry.

The second loop is indexed by `selector.size()`, so it reaches `i == 1` and reads the uninitialized `fieldSet[1]`.

Affected versions:

- first affected commit found: `c142c889` from 2017-01-13;
- first affected public tag found: `vulkan-1.1-rc1` from 2017-10-04;
- first affected stable release tag found: `5.0` from 2018-03-06;
- still affected: `16.3.0` / `vulkan-sdk-1.4.350.0` from 2026-05-01 and current source `605c7f6` from 2026-04-23;
- older public tags `2.1` through `3.0` do not appear affected by this specific uninitialized read.

Memcheck reports:

```text
Conditional jump or move depends on uninitialised value(s)
at glslang::TParseContextBase::parseSwizzleSelector(...) (ParseContextBase.cpp:612)
Uninitialised value was created by a stack allocation
at glslang::TParseContextBase::parseSwizzleSelector(...) (ParseContextBase.cpp:530)
```

The test shader is:

```glsl
#version 450
layout(location = 0) in vec4 in_v;
layout(location = 0) out vec4 out_color;

void main() {
float a = length(in_v.xmy);
out_color = vec4(a, 0.0, 0.0, 1.0);
}
```

I would classify this as low severity, with potential security relevance for applications that compile or validate untrusted GLSL through the glslang library API.

The normal standalone command line path rejects the malformed shader and does not emit SPIR-V.

The security-relevant scenario is a library caller that uses the public parse/codegen API on attacker-controlled GLSL and passes `EShMsgOnlyPreprocessor` / `GLSLANG_MSG_ONLY_PREPROCESSOR_BIT` during parsing.

In that mode, `TParseContextBase::error()` returns at the top of the function, so it neither increments `numErrors` nor calls `currentScanner->setEndOfInput()`, and the malformed swizzle can be accepted and code generation can continue.

I verified the following in Docker:

- normal CLI compilation rejects the shader and reports that SPIR-V is not generated;
- the public C API path with `GLSLANG_MSG_ONLY_PREPROCESSOR_BIT` parses and links the same malformed shader;
- glslang generates non-empty SPIR-V for `length(in_v.xmy)` and for a branch condition using `length(in_v.xmy)`;
- the generated SPIR-V passes `spirv-val --target-env vulkan1.2`;
- Memcheck reports the uninitialized conditional in `parseSwizzleSelector()`.

A realistic attacker-controlled input would be a malformed GLSL shader supplied to a service, tool, engine, or pipeline that compiles untrusted shaders using glslang as a library and uses this public parse/codegen path.

The demonstrated impact is acceptance of malformed shader input and generation of valid SPIR-V with unintended shader semantics, including branch/discard behavior based on the malformed swizzle expression.

I am not claiming code execution, invalid writes, memory disclosure, or impact on the normal standalone CLI compile path.

My suggested fix is to avoid reading `fieldSet[i]` for a character that did not produce a valid selector entry. Initializing the whole array would silence the uninitialized read but would still let a malformed swizzle like `.xmy` be silently accepted as if it were `.xy`, so it only fixes the UB and not the underlying semantic bug.

A cleaner fix may be to only record/check field-set state for accepted selector entries (e.g. index `fieldSet` by `selector.size()` at write time so the two loops stay in sync), or to stop swizzle processing after an invalid selector character.

I have a small Docker PoC that builds glslang, runs the C API reproduction, validates the generated SPIR-V, and shows the Memcheck report. I can attach it if useful.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.