llvm / llvm/llvm-project

[clang][HLSL] ParseHLSLSemantic returns a dangling StringRef for cleaned semantic spellings

Open
#194,223 2 comments 0 reactions 0 assignees View on GitHub
clang:frontend code-quality confirmed HLSL
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

I reproduced a `stack-use-after-return` on clean upstream LLVM/Clang head.

Tested revision: d593279c0b2891f0b0c8af3f70a1a0383b4ad1b5

### Why this is a bug

[`ParsedSemantic`](https://github.com/llvm/llvm-project/blob/d593279c0b2891f0b0c8af3f70a1a0383b4ad1b5/clang/include/clang/Parse/Parser.h#L5219-L5223) stores the semantic name as a borrowed `StringRef`:

```cpp
struct ParsedSemantic {
StringRef Name = "";
unsigned Index = 0;
bool Explicit = false;
};
```

The vulnerable trace is:

1. [`ParseHLSLSemantic()`](https://github.com/llvm/llvm-project/blob/d593279c0b2891f0b0c8af3f70a1a0383b4ad1b5/clang/lib/Parse/ParseHLSL.cpp#L129-L139): allocates a local `SmallString<256> Buffer`, calls `PP.getSpelling(Tok, Buffer)`, and slices `SemanticName` from the returned spelling.
2. [`Preprocessor::getSpelling(...)` fast path](https://github.com/llvm/llvm-project/blob/d593279c0b2891f0b0c8af3f70a1a0383b4ad1b5/clang/lib/Lex/Preprocessor.cpp#L517-L521): only returns stable identifier storage when `!Tok.hasUCN()` and `Tok.getIdentifierInfo()` succeeds. [`Preprocessor::getSpelling(...)` cleaning path](https://github.com/llvm/llvm-project/blob/d593279c0b2891f0b0c8af3f70a1a0383b4ad1b5/clang/lib/Lex/Preprocessor.cpp#L524-L530): if `Tok.needsCleaning()` is true, it resizes the caller-provided buffer and returns `StringRef(Ptr, Len)` into that buffer.
3. [`ParseHLSLSemantic()` return](https://github.com/llvm/llvm-project/blob/d593279c0b2891f0b0c8af3f70a1a0383b4ad1b5/clang/lib/Parse/ParseHLSL.cpp#L152): returns `{SemanticName, Index, Explicit}`, so `ParsedSemantic::Name` may point into that local `Buffer`.
4. When `ParseHLSLSemantic()` returns, the local `Buffer` dies.
5. [`ParseHLSLAnnotations()`](https://github.com/llvm/llvm-project/blob/d593279c0b2891f0b0c8af3f70a1a0383b4ad1b5/clang/lib/Parse/ParseHLSL.cpp#L323-L330): later calls `PP.getIdentifierInfo(Semantic.Name)`, so if step 2 took the scratch-buffer path, this dereferences a stale `StringRef`.

This is reachable with an HLSL semantic identifier that combines:
- a non-basic UCN, to bypass the identifier-table fast path
- a line continuation, to force cleaning into the caller scratch buffer

### Reproducer

Build an ASan `clang` with no inlining:

```bash
cmake -S llvm -B build -G Ninja \
-DLLVM_ENABLE_PROJECTS=clang \
-DLLVM_TARGETS_TO_BUILD=X86 \
-DLLVM_USE_SANITIZER=Address \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_C_FLAGS_DEBUG='-O0 -fno-inline' \
-DCMAKE_CXX_FLAGS_DEBUG='-O0 -fno-inline' \
-DLLVM_INCLUDE_TESTS=OFF \
-DLLVM_INCLUDE_BENCHMARKS=OFF \
-DLLVM_INCLUDE_EXAMPLES=OFF \
-DLLVM_INCLUDE_DOCS=OFF

cmake --build build --target clang -j8
```

Create the testcase:

- `TEX\` followed by a newline forces line splicing and leaves the token in the `needsCleaning()` path.
- `\u00E9` introduces a UCN inside the semantic identifier, so `Tok.hasUCN()` is true and the stable identifier fast path is skipped.
- After preprocessing, the semantic is effectively `TEXCérd0`, but the path used to produce it goes through caller-owned scratch storage.

```bash
cat > /tmp/hlsl_semantic_ucn_linecont.hlsl <<'EOF'
void f(float x : TEX\
C\u00E9rd0) {}
EOF
```

Run:

```bash
ASAN_OPTIONS=detect_stack_use_after_return=1:detect_leaks=0:halt_on_error=1 \
./build/bin/clang-23 \
-cc1 -triple dxil-pc-shadermodel6.3-library \
-x hlsl -fsyntax-only /tmp/hlsl_semantic_ucn_linecont.hlsl
```

### Observed result

ASan reports `stack-use-after-return`.

Representative frames from the clean-head run:

```text
ERROR: AddressSanitizer: stack-use-after-return
#13 clang::Preprocessor::getIdentifierInfo(llvm::StringRef) const
#14 clang::Parser::ParseHLSLAnnotations(...)
...
#0 clang::Parser::ParseHLSLSemantic()
...
This frame has 6 object(s):
[32, 312) 'Buffer' <== Memory access at offset 56 is inside this variable
```

Contributor guide

Open the contributing guide

Research direction

Start in clang/lib/Parse/ParseHLSL.cpp at ParseHLSLSemantic() and ParseHLSLAnnotations(), then inspect ParsedSemantic in clang/include/clang/Parse/Parser.h and the getSpelling paths in clang/lib/Lex/Preprocessor.cpp. Reproduce with the supplied HLSL input under ASan and verify that cleaned semantic names remain valid when annotations later call getIdentifierInfo().

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.