[clang][HLSL] ParseHLSLSemantic returns a dangling StringRef for cleaned semantic spellings
- 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
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