[IR] `llvm::InlineAsm::ConstraintInfo::Parse` reads past the end of the constraint string on malformed `@N` multi-letter constraints
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
Category: Crash on invalid
## Tested LLVM
- Arch Linux package `llvm 22.1.8-2` / `clang 22.1.8-2` (`llvm-config --version` -> `22.1.8`, `--build-mode` -> `Release`, `--assertion-mode` -> `OFF`).
- Verified unchanged on `llvm-project` git `main` at commit `f7cc8c8407551aabcd013555bafaafd62f0494fa` (2026-09-16): `llvm/lib/IR/InlineAsm.cpp` is byte-for-byte identical to 22.1.8.
## What's wrong
`InlineAsm::ConstraintInfo::Parse` in `llvm/lib/IR/InlineAsm.cpp` handles the `@` operand marker (used for flag-output constraints, e.g. `={@ccz}`) like this:
```cpp
} else if (*I == '@') {
// Multi-letter constraint
++I;
unsigned char C = static_cast(*I);
assert(isdigit(C) && "Expected a digit!");
int N = C - '0';
assert(N > 0 && "Found a zero letter constraint!");
++I;
pCodes->push_back(std::string(StringRef(I, N)));
I += N;
}
```
If the character after `@` is not a digit, `N = C - '0'` is garbage, and nothing but an `assert` stops it from being used as a byte count for a `StringRef`/`std::string` copy that isn't checked against the remaining length of the buffer. `assert` is compiled out under `NDEBUG`.
AddressSanitizer confirms this exactly: `READ of size 51`, on an `18-byte region`, with the invalid access starting `0 bytes after` it, aka offset 3 + 51 walks off the end of an allocation whose valid range starts at offset 3 and ends at offset 18, exactly as the arithmetic above predicts.
Two public entry points reach this with no validation of what follows `@`:
- `Verifier::verifyInlineAsmCall` (`llvm/lib/IR/Verifier.cpp:2803`, `for (const InlineAsm::ConstraintInfo &CI : IA->ParseConstraints())`), called from `llvm::verifyModule` for every inline-asm call. Anyone who builds an `InlineAsm` via the C++ API and validates later hits this.
- `InlineAsm::verify(FunctionType*, StringRef)`, called directly by `LLParser` while parsing textual IR. So `llvm-as` on a `.ll` file with this constraint also runs the same unbounded read.
## How to reproduce
### Minimal `.ll` file
```llvm
target triple = "x86_64-unknown-linux-gnu"
define i8 @test(i64 %a, i64 %b) {
entry:
%0 = call i8 asm "cmpq $2, $1", "=@ccz,{rax},{rbx}"(i64 %a, i64 %b)
ret i8 %0
}
```
Compile with `llvm-as repro.ll -o out.bc` with a release-built as.
### Minimal `c++` reproduction
```cpp
#include "llvm/IR/InlineAsm.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Verifier.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
int main() {
LLVMContext Ctx;
Module M("m", Ctx);
std::string Constraints = "=@ccz,{rax},{rbx}";
Type *I8Ty = Type::getInt8Ty(Ctx);
Type *I64Ty = Type::getInt64Ty(Ctx);
FunctionType *AsmFTy = FunctionType::get(I8Ty, {I64Ty, I64Ty}, false);
InlineAsm *IA = InlineAsm::get(AsmFTy, "cmpq $2, $1", Constraints, true);
Function *F = Function::Create(AsmFTy, Function::ExternalLinkage, "test", M);
BasicBlock *BB = BasicBlock::Create(Ctx, "entry", F);
IRBuilder<> B(BB);
Value *Call = B.CreateCall(AsmFTy, IA, {F->getArg(0), F->getArg(1)});
B.CreateRet(Call);
bool Broken = verifyModule(M, &errs());
errs() << "verifyModule broken = " << Broken << "\n";
return 0;
}
```
Or via `InlineAsm::verify`:
```cpp
#include "llvm/IR/InlineAsm.h"
#include "llvm/IR/DerivedTypes.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/Error.h"
using namespace llvm;
int main() {
LLVMContext Ctx;
std::string Constraints = "=@ccz,{rax},{rbx}";
Type *I8Ty = Type::getInt8Ty(Ctx);
Type *I64Ty = Type::getInt64Ty(Ctx);
FunctionType *AsmFTy = FunctionType::get(I8Ty, {I64Ty, I64Ty}, false);
Error Err = InlineAsm::verify(AsmFTy, Constraints);
errs() << (Err ? toString(std::move(Err)) : "no error") << "\n";
return 0;
}
```
Compiled via `clang++ $(llvm-config --cxxflags) poc.cpp -o poc $(llvm-config --ldflags --libs core)` with clang built in release mode.
## ASan error
ASan output
```
❯ clang++ $(llvm-config --cxxflags) -fsanitize=address -g poc.cpp -o poc_asan $(llvm-config --ldflags --libs core)
ASAN_OPTIONS="abort_on_error=0" ./poc_asan 2>&1 | tee asan_full.log
constraint string: "=@ccz,{rax},{rbx}"
verifyModule...
=================================================================
==991649==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b420cbe0e32 at pc 0x55cf570d2da7 bp 0x7fffb0df1cf0 sp 0x7fffb0df14b0
READ of size 51 at 0x7b420cbe0e32 thread T0
#0 0x55cf570d2da6 in memcpy (/tmp/llvm-stinky-bug-repro/poc_asan+0x16cda6) (BuildId: 88451cdf5cb722ccfe762f9b58e5bf791b74fb5f)
#1 0x7f1216e966be in copy /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/char_traits.h:432:33
#2 0x7f1216e966be in _S_copy /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.h:457:4
#3 0x7f1216e966be in _S_copy_chars /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.h:493:6
#4 0x7f1216e966be in void std::__cxx11::basic_string, std::allocator>::_M_construct(char const*, char const*, std::forward_iterator_tag) /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.tcc:253:2
#5 0x7f12163466a5 in basic_string /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.h:716:2
#6 0x7f12163466a5 in basic_string /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.h:192:9
#7 0x7f12163466a5 in basic_string /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.h:912:4
#8 0x7f12163466a5 in llvm::InlineAsm::ConstraintInfo::Parse(llvm::StringRef, std::vector>&) /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/InlineAsm.cpp:211:25
#9 0x7f12163454c9 in llvm::InlineAsm::ParseConstraints(llvm::StringRef) /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/InlineAsm.cpp:248:14
#10 0x7f121634502a in ParseConstraints /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/include/llvm/IR/InlineAsm.h:195:12
#11 0x7f121634502a in verifyInlineAsmCall /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/Verifier.cpp:2803:50
#12 0x7f1214d4140e in visitCallBase /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/Verifier.cpp:4131:5
#13 0x7f1214d3f73c in visitCallInst /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/Verifier.cpp:4273:3
#14 0x7f1214d36264 in delegateCallInst /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/include/llvm/IR/InstVisitor.h:298:5
#15 0x7f1214d36264 in visitCall /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/include/llvm/IR/Instruction.def:210:1
#16 0x7f1214d36264 in visit /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/include/llvm/IR/Instruction.def:210:1
#17 0x7f1214d36264 in visit /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/Verifier.cpp:718:26
#18 0x7f1214d36264 in visit, false, false> > /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/include/llvm/IR/InstVisitor.h:89:37
#19 0x7f1214d25bc0 in visit /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/include/llvm/IR/InstVisitor.h:104:5
#20 0x7f1214d25bc0 in visit, false, false> > /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/include/llvm/IR/InstVisitor.h:89:37
#21 0x7f1214d25bc0 in visit /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/include/llvm/IR/InstVisitor.h:100:5
#22 0x7f1214d25bc0 in verify /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/Verifier.cpp:435:5
#23 0x7f1214d2559f in llvm::verifyModule(llvm::Module const&, llvm::raw_ostream*, bool*) /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/Verifier.cpp:7769:18
#24 0x55cf57135467 in main /tmp/llvm-stinky-bug-repro/poc.cpp:42:17
#25 0x7f120fe27c8d in __libc_start_call_main /usr/src/debug/glibc/glibc/csu/../sysdeps/nptl/libc_start_call_main.h:59:16
#26 0x7f120fe27dca in __libc_start_main /usr/src/debug/glibc/glibc/csu/../csu/libc-start.c:372:3
#27 0x55cf56f953c4 in _start (/tmp/llvm-stinky-bug-repro/poc_asan+0x2f3c4) (BuildId: 88451cdf5cb722ccfe762f9b58e5bf791b74fb5f)
0x7b420cbe0e32 is located 0 bytes after 18-byte region [0x7b420cbe0e20,0x7b420cbe0e32)
allocated by thread T0 here:
#0 0x55cf57133101 in operator new(unsigned long) (/tmp/llvm-stinky-bug-repro/poc_asan+0x1cd101) (BuildId: 88451cdf5cb722ccfe762f9b58e5bf791b74fb5f)
#1 0x7f1217835769 in allocate /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/new_allocator.h:162:29
#2 0x7f1217835769 in allocate /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/alloc_traits.h:638:20
#3 0x7f1217835769 in _S_allocate /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.h:141:16
#4 0x7f1217835769 in _M_create /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.tcc:164:14
#5 0x7f1217835769 in _M_construct /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.tcc:291:12
#6 0x7f1217835769 in basic_string /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/16/../../../../include/c++/16/bits/basic_string.h:621:2
#7 0x7f1217835769 in llvm::InlineAsm::InlineAsm(llvm::FunctionType*, std::__cxx11::basic_string, std::allocator> const&, std::__cxx11::basic_string, std::allocator> const&, bool, bool, llvm::InlineAsm::AsmDialect, bool) /usr/src/debug/llvm/llvm-project-22.1.8.src/llvm/lib/IR/InlineAsm.cpp:34:29
SUMMARY: AddressSanitizer: heap-buffer-overflow (/tmp/llvm-stinky-bug-repro/poc_asan+0x16cda6) (BuildId: 88451cdf5cb722ccfe762f9b58e5bf791b74fb5f) in memcpy
Shadow bytes around the buggy address:
0x7b420cbe0b80: 00 00 00 fa fa fa 00 00 00 06 fa fa 00 00 00 06
0x7b420cbe0c00: fa fa 00 00 00 05 fa fa 00 00 00 fa fa fa 00 00
0x7b420cbe0c80: 00 fa fa fa 00 00 05 fa fa fa 00 00 00 00 fa fa
0x7b420cbe0d00: 00 00 06 fa fa fa 00 00 00 05 fa fa 00 00 01 fa
0x7b420cbe0d80: fa fa 00 00 00 00 fa fa 00 00 02 fa fa fa fd fd
=>0x7b420cbe0e00: fd fa fa fa 00 00[02]fa fa fa 00 00 05 fa fa fa
0x7b420cbe0e80: 00 00 00 00 fa fa 00 00 06 fa fa fa fa fa fa fa
0x7b420cbe0f00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7b420cbe0f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7b420cbe1000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7b420cbe1080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==991649==ABORTING
```
> Related: #25035 reports the same missing-bounds-check class in the adjacent `^` multi-letter-constraint handler.
Contributor guide
Research direction
Read llvm/lib/IR/InlineAsm.cpp at ConstraintInfo::Parse, then trace InlineAsm::verify and Verifier::verifyInlineAsmCall. Reproduce the issue with the supplied .ll file using llvm-as, preferably under AddressSanitizer. Done means malformed @N constraints are handled without reading past the constraint buffer through either public entry point.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100