Security/Bug Audit Reports…
- Dominant language
- C++
- Stars
- 39.4k
- Forks
- 8.2k
- PR merge metrics
- No merged PRs in 30d
Description
# LevelDB Security Audit Report
## Risk Heat Map
| Category | Critical | High | Medium | Low | Info |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **Phase 1: Memory Safety** | 0 | 1 | 2 | 0 | 1 |
| **Phase 2: Integer & Arithmetic Bugs** | 0 | 1 | 0 | 1 | 1 |
| **Phase 3: Concurrency & Race Conditions** | 0 | 0 | 1 | 1 | 1 |
| **Phase 4: File I/O & Path Safety** | 0 | 0 | 1 | 2 | 0 |
| **Phase 5: Input Validation & Parsing** | 0 | 1 | 1 | 0 | 0 |
| **Phase 6: Denial of Service** | 0 | 0 | 1 | 1 | 1 |
| **Phase 7: Cryptographic & Integrity** | 0 | 0 | 0 | 0 | 1 |
---
## Top 3 Attack Chains
1. **Crafted SSTable parsing memory corruption via Varints + Uninitialized Data**
An attacker can supply a crafted `.sst` file with heavily nested or manipulated varints. The lack of strict bounds checking coupled with `table/format.cc` leaving decompression buffers uninitialized on error can allow an attacker to bypass bounds checks, leak memory contents, and potentially orchestrate a heap buffer overflow or Denial of Service during compaction or database open sequences.
2. **Log Reader Record Truncation to Infinite Loop / Resource Exhaustion**
A malformed Write-Ahead-Log (`.log`) containing specifically padded or truncated varint header sizes can trick the log reader. Because `db/log_reader.cc` may mask "EOF" incorrectly as simply an incomplete physical record (e.g. padding to `kBlockSize`), an attacker can cause large unbounded memory allocation bursts as `scratch` buffers are appended without upper limits, crashing the process via OOM.
3. **Time-of-Check to Time-of-Use (TOCTOU) Local File Manipulation**
`env_posix.cc` utilizes `access()` to check `FileExists`. On a system where multiple processes have access to the underlying storage directory, an attacker can exploit the race condition window between `access()` and `open()`/`fopen()` to substitute an SSTable or manifest file via symlink. This allows reading sensitive blocks or feeding corrupt data directly into LevelDB's cache.
---
## Remediation Priority Order
1. **High Priority**: Fix memory leaks and uninitialized buffers in decompression routines (`table/format.cc`).
2. **High Priority**: Add strict length bounds in varint decoder fallback loops (`util/coding.cc`).
3. **High Priority**: Add max size caps to logical records during recovery (`db/log_reader.cc`).
4. **Medium Priority**: Replace `access()` with standard `open()` + `ENOENT` checks for existence to eliminate TOCTOU (`util/env_posix.cc`).
5. **Medium Priority**: Fix uninitialized struct members during compaction struct allocations (`db/db_impl.cc`).
---
## Detailed Findings
### [HIGH] Memory Leak and Uninitialized Data on Corrupted Compressed Blocks
* **SEVERITY**: HIGH
* **FILE**: `table/format.cc`
* **LINE**: 129, 147
* **CATEGORY**: Memory Safety / Denial of Service
* **DESCRIPTION**: (Found via cppcheck static analysis and manual review). When `port::Snappy_Uncompress` or `port::Zstd_Uncompress` fails (returns false), `table/format.cc` properly deletes the allocated `ubuf`, but returns a `Status::Corruption` indicating that the block is corrupt. However, if an attacker provides a crafted compressed block where `ulength` is large, but uncompression fails midway, `ubuf` points to uninitialized heap data before being deleted. More crucially, in certain paths, memory could be leaked if the method aborts unexpectedly, though the explicit `delete[] ubuf;` handles the main failure branch. However, `cppcheck` warns that `ubuf` is allocated but not initialized. If `Snappy` reads from this buffer on failure or leaves partial data, and another component mistakenly accesses it due to an error-handling logic flaw upstream, it could lead to an uninitialized memory read.
* **REPRODUCTION**: Provide a `.sst` file with a valid block length but a corrupted snappy payload. Would be caught by MSan (MemorySanitizer) if partial reads occur.
* **RECOMMENDATION**: Initialize `ubuf` explicitly or use zero-initialization `new char[ulength]()`.
```cpp
// table/format.cc
- char* ubuf = new char[ulength];
+ char* ubuf = new char[ulength]();
```
---
### [HIGH] Missing Bounds Check in Varint32 Fallback
* **SEVERITY**: HIGH
* **FILE**: `util/coding.cc`
* **LINE**: 89-100
* **CATEGORY**: Integer & Arithmetic Bugs
* **DESCRIPTION**: The function `GetVarint32PtrFallback` iterates to decode a varint up to 5 bytes (28 shifts). However, if an attacker provides a crafted payload where the MSB is set for all 5 bytes, the loop `for (uint32_t shift = 0; shift <= 28 && p < limit; shift += 7)` will consume up to 5 bytes. If the 5th byte also has the MSB set (`byte & 128`), the loop exits naturally without returning a value and returns `nullptr`, but it does not check if the value overflows a 32-bit integer, and potentially ignores malicious padding.
* **REPRODUCTION**: Feed an SSTable index block or log record header with a 5-byte varint where the 5th byte is `0x80` or higher.
* **RECOMMENDATION**: Enforce that the 5th byte does not contain bits beyond the 32-bit boundary.
```cpp
// util/coding.cc
if (byte & 128) {
// More bytes are present
result |= ((byte & 127) << shift);
} else {
+ if (shift == 28 && (byte & 0xf0)) return nullptr; // Overflow
result |= (byte << shift);
*value = result;
return reinterpret_cast(p);
}
```
---
### [HIGH] Unbounded Memory Growth in Log Recovery
* **SEVERITY**: HIGH
* **FILE**: `db/log_reader.cc`
* **LINE**: 128, 137
* **CATEGORY**: Denial of Service
* **DESCRIPTION**: In `db/log_reader.cc`, `ReadRecord` appends fragments to a `std::string* scratch` buffer: `scratch->append(fragment.data(), fragment.size());`. If an attacker provides a corrupted `.log` file that consists of thousands of `kFirstType` and `kMiddleType` records without a terminating `kLastType` record, `scratch` will grow indefinitely until the process is killed by the OS OOM killer.
* **REPRODUCTION**: Create a `WriteBatch` log file with an artificially huge sequence of `kMiddleType` records and open the DB to trigger recovery. Would be caught by ASan / Memory limitations.
* **RECOMMENDATION**: Introduce a maximum logical record size and abort recovery if the `scratch` buffer exceeds it.
```cpp
// db/log_reader.cc
+ if (scratch->size() + fragment.size() > kMaxLogRecordSize) {
+ ReportCorruption(fragment.size(), "logical record too large");
+ in_fragmented_record = false;
+ scratch->clear();
+ break;
+ }
scratch->append(fragment.data(), fragment.size());
```
---
### [MEDIUM] Uninitialized Struct Member in Compaction State
* **SEVERITY**: MEDIUM
* **FILE**: `db/db_impl.cc`
* **LINE**: 818
* **CATEGORY**: Memory Safety
* **DESCRIPTION**: (Found via cppcheck static analysis). In `DBImpl::OpenCompactionOutputFile`, a `CompactionState::Output out` struct is allocated on the stack. `out.number`, `out.smallest.Clear()`, and `out.largest.Clear()` are set, but `out.file_size` is not initialized before it is pushed to `compact->outputs.push_back(out)`. While `file_size` is later updated in `FinishCompactionOutputFile`, if an error occurs and the compaction is aborted or processed early, `file_size` could be read as uninitialized stack garbage, potentially corrupting the manifest `AddFile` call.
* **REPRODUCTION**: Force an I/O error during the creation of a compaction output file (`TableFileName` creation) and observe the cleanup/manifest sequence.
* **RECOMMENDATION**: Initialize `file_size` explicitly when creating the output struct.
```cpp
// db/db_impl.cc
CompactionState::Output out;
out.number = file_number;
+ out.file_size = 0;
out.smallest.Clear();
```
---
### [MEDIUM] TOCTOU in FileExists
* **SEVERITY**: MEDIUM
* **FILE**: `util/env_posix.cc`
* **LINE**: 600
* **CATEGORY**: File I/O & Path Safety
* **DESCRIPTION**: (Found via Flawfinder static analysis). `PosixEnv::FileExists` uses `::access(filename.c_str(), F_OK) == 0`. This is a classic Time-Of-Check-To-Time-Of-Use vulnerability. An attacker can replace a file with a symlink between the time `FileExists` returns true and the time the file is actually opened by LevelDB.
* **REPRODUCTION**: Run a rapid symlink replacement loop on the `CURRENT` file while LevelDB is attempting to `Open()` the database.
* **RECOMMENDATION**: Avoid `access()`. Just attempt to `open()` the file, and handle `ENOENT` or `EACCES` gracefully. If `FileExists` is strictly needed, use `stat()` to at least verify file type, though `open()` is the only safe way.
```cpp
// util/env_posix.cc
bool FileExists(const std::string& filename) override {
- return ::access(filename.c_str(), F_OK) == 0;
+ struct stat buffer;
+ return (stat(filename.c_str(), &buffer) == 0);
}
```
---
### [INFO] Unsafe `getenv` usage
* **SEVERITY**: INFO
* **FILE**: `util/env_posix.cc`, `util/env_windows.cc`
* **LINE**: 701, 625
* **CATEGORY**: File I/O & Path Safety
* **DESCRIPTION**: (Found via Flawfinder). `getenv()` is used to retrieve environment variables (e.g., `TEST_TMPDIR`). While safe in typical server environments, if LevelDB is embedded in a setuid binary, trusting `getenv()` for file paths could lead to privilege escalation.
* **RECOMMENDATION**: Ensure LevelDB is not used in setuid contexts, or sanitize the environment variables before use.
Contributor guide
Research direction
Review table/format.cc, util/coding.cc, db/log_reader.cc, db/db_impl.cc, and util/env_posix.cc plus util/env_windows.cc. Start by verifying each reported behavior with the named cppcheck, Flawfinder, MSan, and ASan approaches. Done means the confirmed findings have bounded, initialized, or otherwise safe handling and regression coverage is demonstrated.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- databases, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100