KhronosGroup / KhronosGroup/SPIRV-Tools
[SPIRV-OPT] ASan heap-use-after-free in `MergeReturnPass` with `--merge-return`
- Dominant language
- C++
- Stars
- 1.4k
- Forks
- 709
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 28
Description
## Description
Running `spirv-opt --merge-return` on the attached SPIR-V module triggers an AddressSanitizer heap-use-after-free in `MergeReturnPass`.
The issue appears to be in `MergeReturnPass` updating/reanalyzing def-use information after erasing an instruction. On my machine, a plain
Debug build exits successfully, but an ASan build catches the UAF deterministically.
## SPIRV-Tools version
`SPIRV-Tools v2026.2 v2026.2.rc2-22-g2acb87f8`
## Input
Attach:
[groupshared-direct-O0-g.spv.txt](https://github.com/user-attachments/files/28114410/groupshared-direct-O0-g.spv.txt)
The input was generated from Slang with:
```bash
slangc tests/metal/groupshared-threadlocal-same-parameter.slang \
-target spirv -stage compute -entry computeMain \
-emit-spirv-directly -O0 -g \
-o groupshared-direct-O0-g.spv
```
The module includes NonSemantic.Shader.DebugInfo.100, but no CreatePropagateLineInfoPass API driver is needed to reproduce this. The normal
spirv-opt CLI reproduces with --merge-return.
## Build SPIRV-Tools with ASan
From the SPIRV-Tools source directory:
```
$ python3 utils/git-sync-deps
$ cmake . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
$ cmake --build build --target spirv-opt
```
## Repro command
```
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 \
build-asan/tools/spirv-opt \
--merge-return \
groupshared-direct-O0-g.spv \
-o out.spv
```
`--skip-validation` is not required.
## Observed result
ASan reports a heap-use-after-free:
```
ERROR: AddressSanitizer: heap-use-after-free
READ of size 4
#0 spvtools::opt::Instruction::unique_id() const
#1 spvtools::opt::analysis::UserEntryLess::operator()
#5 spvtools::opt::analysis::DefUseManager::EraseUseRecordsOfOperandIds
#6 spvtools::opt::analysis::DefUseManager::ClearInst
#7 spvtools::opt::analysis::DefUseManager::AnalyzeInstDef
#8 spvtools::opt::analysis::DefUseManager::AnalyzeInstDefUse
#10 spvtools::opt::MergeReturnPass::BranchToBlock
#11 spvtools::opt::MergeReturnPass::ProcessStructuredBlock
#12 spvtools::opt::MergeReturnPass::ProcessStructured
#20 spvtools::opt::MergeReturnPass::Process
#23 spvtools::Optimizer::Run
#24 main tools/opt/opt.cpp
```
The freed allocation is from:
spvtools::opt::InstructionList::iterator::Erase()
spvtools::opt::MergeReturnPass::CreateSingleCaseSwitch()
spvtools::opt::MergeReturnPass::AddSingleCaseSwitchAroundFunction()
spvtools::opt::MergeReturnPass::ProcessStructured()
## Expected result
`spirv-opt --merge-return` should complete without use-after-free or stale def-use records.
Contributor guide
Research direction
Start with MergeReturnPass::CreateSingleCaseSwitch, AddSingleCaseSwitchAroundFunction, BranchToBlock, and the DefUseManager calls shown in the ASan stack. Reproduce with the provided SPIR-V input and ASan spirv-opt build using --merge-return. Done means the command completes without a heap-use-after-free or stale def-use records.
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
- Mostly clear
- Newbie friendliness
- 64/100