llvm / llvm/llvm-project

[IR] `llvm::InlineAsm::ConstraintInfo::Parse` reads past the end of the constraint string on malformed `@N` multi-letter constraints

Open
#223,956 0 comments 0 reactions 0 assignees View on GitHub
inline-asm llvm:ir
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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.