KhronosGroup / KhronosGroup/SPIRV-Tools

[SPIRV-OPT] ASan heap-use-after-free in `MergeReturnPass` with `--merge-return`

Open
#6,711 0 comments 0 reactions 0 assignees View on GitHub
component:optimization
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.