Missing Bounds Checks on Bitcode String Table Accesses
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
**Bug summary**
Multiple missing and integer-overflow-prone bounds checks in LLVM's bitcode reader allow a crafted `.bc` file to trigger a wild memory read. The primary crash path is in `parseGlobalVarRecord` (`BitcodeReader.cpp:4308`), which uses attacker-controlled values as a string table offset/length without validating them against the string table, computing `Strtab.data() + Record[14]` and reading from the resulting pointer. Additionally, the centralized bounds check in `readNameFromStrtab` and similar checks in other partition-name parsing paths use `offset + length > Strtab.size()`, which can overflow for large values.
A 56-byte PoC demonstrates a SEGV at attacker-influenced address `0x1000deadbeef`. This affects LLVM tools that process bitcode files (e.g., `llvm-dis`, `opt`, and LTO pipelines).
**PoC / reproduction**
- Entrypoint: `llvm-dis-fuzzer`
Reproduce via OSS-Fuzz (uses `reproduce.py`, which reads the PoC file above):
```bash
export OSS_FUZZ_DIR=~/oss-fuzz
python3 reproduce.py poc.bin
```
PoC contents:
This PoC is binary. Reconstruct it from base64:
```bash
python3 - <<'PY'
import base64
b64 = '''QkPA3iEMAAALAAAAC4IgEQMAAAACAAAAC4JgRwAGAAA7IEAAAAAAAAAAAAB473dX3xGEAgAAAAA='''
open('poc.bin', 'wb').write(base64.b64decode(b64))
print('wrote poc.bin')
PY
```
**ASAN backtrace**
```text
+ FUZZER=llvm-dis-fuzzer
+ shift
+ '[' '!' -v TESTCASE ']'
+ TESTCASE=/testcase
+ '[' '!' -f /testcase ']'
+ export RUN_FUZZER_MODE=interactive
+ RUN_FUZZER_MODE=interactive
+ export FUZZING_ENGINE=libfuzzer
+ FUZZING_ENGINE=libfuzzer
+ export SKIP_SEED_CORPUS=1
+ SKIP_SEED_CORPUS=1
+ run_fuzzer llvm-dis-fuzzer -runs=100 /testcase
vm.mmap_rnd_bits = 28
/out/llvm-dis-fuzzer -- -rss_limit_mb=2560 -timeout=25 -runs=100 /testcase < /dev/null
INFO: libFuzzer ignores flags that start with '--'
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2029432446
INFO: Loaded 1 modules (196366 inline 8-bit counters): 196366 [0x555557167b30, 0x555557197a3e),
INFO: Loaded 1 PC tables (196366 PCs): 196366 [0x555557197a40,0x555557496b20),
/out/llvm-dis-fuzzer: Running 1 inputs 100 time(s) each.
Running: /testcase
AddressSanitizer:DEADLYSIGNAL
=================================================================
[1m[31m==14==ERROR: AddressSanitizer: SEGV on unknown address 0x1000deadbeef (pc 0x555556400fb3 bp 0x7fffffffd330 sp 0x7fffffffd310 T0)
[1m[0m==14==The signal is caused by a READ memory access.
SCARINESS: 20 (wild-addr-read)
#0 0x555556400fb3 in hash_1to3_bytes /src/llvm-project/llvm/include/llvm/ADT/Hashing.h:183:15
#1 0x555556400fb3 in llvm::hashing::detail::hash_short(char const*, unsigned long, unsigned long) /src/llvm-project/llvm/include/llvm/ADT/Hashing.h:245:12
#2 0x5555568d4846 in bool llvm::DenseMapBase, llvm::detail::DenseSetPair>, llvm::StringRef, llvm::detail::DenseSetEmpty, llvm::DenseMapInfo, llvm::detail::DenseSetPair>::LookupBucketFor(llvm::StringRef const&, llvm::detail::DenseSetPair*&) /src/llvm-project/llvm/include/llvm/ADT/DenseMap.h:667:25
#3 0x5555568d4d0f in llvm::detail::DenseSetPair* llvm::DenseMapBase, llvm::detail::DenseSetPair>, llvm::StringRef, llvm::detail::DenseSetEmpty, llvm::DenseMapInfo, llvm::detail::DenseSetPair>::findBucketForInsertion(llvm::StringRef const&, llvm::detail::DenseSetPair*) /src/llvm-project/llvm/include/llvm/ADT/DenseMap.h
#4 0x555556e2cdc9 in lookupOrInsertIntoBucket /src/llvm-project/llvm/include/llvm/ADT/DenseMap.h:510:17
#5 0x555556e2cdc9 in try_emplace_impl /src/llvm-project/llvm/include/llvm/ADT/DenseMap.h:518:31
#6 0x555556e2cdc9 in try_emplace<> /src/llvm-project/llvm/include/llvm/ADT/DenseMap.h:265:12
#7 0x555556e2cdc9 in insert /src/llvm-project/llvm/include/llvm/ADT/DenseSet.h:203:19
#8 0x555556e2cdc9 in llvm::UniqueStringSaver::save(llvm::StringRef) /src/llvm-project/llvm/lib/Support/StringSaver.cpp:29:19
#9 0x555556762142 in llvm::GlobalValue::setPartition(llvm::StringRef) /src/llvm-project/llvm/lib/IR/Globals.cpp:237:35
#10 0x55555630c9bc in parseGlobalVarRecord /src/llvm-project/llvm/lib/Bitcode/Reader/BitcodeReader.cpp:4308:12
#11 0x55555630c9bc in (anonymous namespace)::BitcodeReader::parseModule(unsigned long, bool, llvm::ParserCallbacks) /src/llvm-project/llvm/lib/Bitcode/Reader/BitcodeReader.cpp:4872:23
#12 0x55555627bf4e in parseBitcodeInto /src/llvm-project/llvm/lib/Bitcode/Reader/BitcodeReader.cpp:4921:10
#13 0x55555627bf4e in llvm::BitcodeModule::getModuleImpl(llvm::LLVMContext&, bool, bool, bool, llvm::ParserCallbacks) /src/llvm-project/llvm/lib/Bitcode/Reader/BitcodeReader.cpp:8714:22
#14 0x55555628b13c in llvm::BitcodeModule::parseModule(llvm::LLVMContext&, llvm::ParserCallbacks) /src/llvm-project/llvm/lib/Bitcode/Reader/BitcodeReader.cpp:8900:10
#15 0x55555628b9b7 in llvm::parseBitcodeFile(llvm::MemoryBufferRef, llvm::LLVMContext&, llvm::ParserCallbacks) /src/llvm-project/llvm/lib/Bitcode/Reader/BitcodeReader.cpp:8912:14
#16 0x55555626e431 in LLVMFuzzerTestOneInput /src/llvm-project/llvm/tools/llvm-dis-fuzzer/llvm-dis-fuzzer.cpp:26:7
#17 0x55555610b84d in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:619:13
#18 0x5555560f5562 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:329:6
#19 0x5555560fb430 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:865:9
#20 0x555556127fc2 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
#21 0x7ffff7c1d082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d)
#22 0x5555560ee64d in _start (/out/llvm-dis-fuzzer+0xb9a64d)
DEDUP_TOKEN: hash_1to3_bytes--llvm::hashing::detail::hash_short(char const*, unsigned long, unsigned long)--bool llvm::DenseMapBase, llvm::detail::DenseSetPair>, llvm::StringRef, llvm::detail::DenseSetEmpty, llvm::DenseMapInfo, llvm::detail::DenseSetPair>::LookupBucketFor(llvm::StringRef const&, llvm::detail::DenseSetPair*&)
==14==Register values:
rax = 0x0000000000000000 rbx = 0x0000555556da4520 rcx = 0x0000000000000001 rdx = 0x00000000000018e8
rdi = 0x0000000000000002 rsi = 0x0000000000000002 rbp = 0x00007fffffffd330 rsp = 0x00007fffffffd310
r8 = 0x00005555575d2c00 r9 = 0x0000000000000001 r10 = 0x0000000000000000 r11 = 0x000000000000000e
r12 = 0x00007d8ff6de1980 r13 = 0x0000000000000040 r14 = 0x00001000deadbeef r15 = 0x0000000000000001
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /src/llvm-project/llvm/include/llvm/ADT/Hashing.h:183:15 in hash_1to3_bytes
==14==ABORTING
```
**Fix (diff)**
```diff
diff --git a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
index d6d28ca7b..fdba2031d 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -481,8 +481,10 @@ std::pair>
BitcodeReaderBase::readNameFromStrtab(ArrayRef Record) {
if (!UseStrtab)
return {"", Record};
+ if (!Strtab.data())
+ return {"", {}};
// Invalid reference. Let the caller complain about the record being empty.
- if (Record[0] + Record[1] > Strtab.size())
+ if (Record[0] > Strtab.size() || Record[1] > Strtab.size() - Record[0])
return {"", {}};
return {StringRef(Strtab.data() + Record[0], Record[1]), Record.slice(2)};
}
@@ -4304,7 +4306,8 @@ Error BitcodeReader::parseGlobalVarRecord(ArrayRef Record) {
inferDSOLocal(NewGV);
// Check whether we have enough values to read a partition name.
- if (Record.size() > 15)
+ if (Record.size() > 15 && Strtab.data() && Record[14] <= Strtab.size() &&
+ Record[15] <= Strtab.size() - Record[14])
NewGV->setPartition(StringRef(Strtab.data() + Record[14], Record[15]));
if (Record.size() > 16 && Record[16]) {
@@ -4488,7 +4491,7 @@ Error BitcodeReader::parseFunctionRecord(ArrayRef Record) {
// Check whether we have enough values to read a partition name. Also make
// sure Strtab has enough values.
if (Record.size() > 18 && Strtab.data() &&
- Record[17] + Record[18] <= Strtab.size()) {
+ Record[17] <= Strtab.size() && Record[18] <= Strtab.size() - Record[17]) {
Func->setPartition(StringRef(Strtab.data() + Record[17], Record[18]));
}
@@ -4590,7 +4593,8 @@ Error BitcodeReader::parseGlobalIndirectSymbolRecord(
// Check whether we have enough values to read a partition name.
if (OpNum + 1 < Record.size()) {
// Check Strtab has enough values for the partition.
- if (Record[OpNum] + Record[OpNum + 1] > Strtab.size())
+ if (Record[OpNum] > Strtab.size() ||
+ Record[OpNum + 1] > Strtab.size() - Record[OpNum])
return error("Malformed partition, too large.");
NewGA->setPartition(
StringRef(Strtab.data() + Record[OpNum], Record[OpNum + 1]));
```
Contributor guide
Assessment
This issue has not been assessed yet.