Bug Report: NULL Dereference and Function Fallthrough in libFuzzer Fork Mode
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Overview
A critical regression exists in the libFuzzer runtime provided with recent LLVM/Clang 22 snapshots. When running in fork mode (`-fork=N`), the parent fuzzer process can encounter a `SEGV` due to a NULL pointer dereference. This is caused by a machine code generation defect in `fuzzer::GlobalEnv::RunOneMergeJob` where a code path lacks a termination instruction, leading to an illegal fallthrough into the next function in memory.
## Environment
- **Compiler:** Clang 22 Snapshot (e.g., `libclang-rt-22-dev_22~++20251026052801`)
- **Library:** `libclang_rt.fuzzer-x86_64.a`
- **Object File:** `FuzzerFork.cpp.o`
- **Platform:** Linux x86_64
## Root Cause Analysis
The issue resides in the pre-compiled `RunOneMergeJob` function. The compiler uses information from an `assert` to perform aggressive optimization on a code path handling small input files, but fails to emit a valid return/jump instruction for that branch.
### 1. Source Context (`FuzzerFork.cpp`)
In `RunOneMergeJob`, feature files are read and processed:
```cpp
auto FeatureBytes = FileToVector(FeatureFile, 0, false);
assert((FeatureBytes.size() % sizeof(uint32_t)) == 0);
std::vector NewFeatures(FeatureBytes.size() / sizeof(uint32_t));
memcpy(NewFeatures.data(), FeatureBytes.data(), FeatureBytes.size());
```
### 2. Defective Machine Code (`RunOneMergeJob`)
The compiler generates a specialized branch for `size <= 3`. Based on the `assert`, it concludes that if `size <= 3`, the only valid value is `0`. It then hardcodes the destination to NULL (`xor edi, edi`) but omits the function termination:
```assembly
.text._ZN6fuzzer9GlobalEnv14RunOneMergeJobEPNS_7FuzzJobE:0000000000006296 xor edi, edi ; dest = NULL (NewFeatures.data())
.text._ZN6fuzzer9GlobalEnv14RunOneMergeJobEPNS_7FuzzJobE:0000000000006298 mov rdx, r14 ; n = size
.text._ZN6fuzzer9GlobalEnv14RunOneMergeJobEPNS_7FuzzJobE:000000000000629B call memcpy ; Call interceptor
; --- MISSING RET/JMP ---
; Function ends here without a return instruction.
```
### 3. The Crash (`secondsSinceProcessStartUp`)
When `memcpy` returns (which it does safely if `size == 0`), execution falls through directly into the first instruction of the next function in memory, `fuzzer::GlobalEnv::secondsSinceProcessStartUp`. This function expects a valid `this` pointer in `rdi`, but `rdi` is `0` from the previous `xor`.
```assembly
.text._ZNK6fuzzer9GlobalEnv26secondsSinceProcessStartUpEv:00000000000062A0 push rbp
...
.text._ZNK6fuzzer9GlobalEnv26secondsSinceProcessStartUpEv:00000000000062A6 mov rbx, rdi ; rbx = 0
.text._ZNK6fuzzer9GlobalEnv26secondsSinceProcessStartUpEv:00000000000062A9 call system_clock::now
.text:00000000007AC48E 48 2B 83 78 01 00 00 sub rax, [rbx+178h] ; CRASH: [0 + 0x178]
```
## Trigger Mechanism
The crash is triggered by an **empty feature file (size 0)** on disk. Such files can be left behind if a libFuzzer child process is killed (e.g., via SIGKILL or OOM) after creating a file but before writing to it. While the source code intends to handle size 0 (via `memcpy(NULL, src, 0)`), the missing `ret` instruction in the binary makes this case fatal.
## Reproduction Steps
1. Compile any fuzzer using the buggy Clang 22 runtime.
2. Run in fork mode: `./fuzzer -fork=16 corpus_dir`.
3. Simulate a child failure by creating an empty feature file in the temporary directory used for merging:
`touch /tmp/libFuzzerTemp.../Features/some_input_name`
4. The parent process will crash with a `SEGV` on address `0x178` when it attempts to merge that job.
## Recommended Fix
Ensure that the compiler-generated paths for small/zero-size buffers in `RunOneMergeJob` are properly terminated with a return to the caller or a jump to the loop continuation point.
Contributor guide
Assessment
This issue has not been assessed yet.