Security/Bug Audit Reports…
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
Crafted SSTable parsing memory corruption via Varints + Uninitialized Data An attacker can supply a crafted
.sstfile with heavily nested or manipulated varints. The lack of strict bounds checking coupled withtable/format.ccleaving 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.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. Becausedb/log_reader.ccmay mask "EOF" incorrectly as simply an incomplete physical record (e.g. padding tokBlockSize), an attacker can cause large unbounded memory allocation bursts asscratchbuffers are appended without upper limits, crashing the process via OOM.Time-of-Check to Time-of-Use (TOCTOU) Local File Manipulation
env_posix.ccutilizesaccess()to checkFileExists. On a system where multiple processes have access to the underlying storage directory, an attacker can exploit the race condition window betweenaccess()andopen()/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
- High Priority: Fix memory leaks and uninitialized buffers in decompression routines (
table/format.cc). - High Priority: Add strict length bounds in varint decoder fallback loops (
util/coding.cc). - High Priority: Add max size caps to logical records during recovery (
db/log_reader.cc). - Medium Priority: Replace
access()with standardopen()+ENOENTchecks for existence to eliminate TOCTOU (util/env_posix.cc). - 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_Uncompressorport::Zstd_Uncompressfails (returns false),table/format.ccproperly deletes the allocatedubuf, but returns aStatus::Corruptionindicating that the block is corrupt. However, if an attacker provides a crafted compressed block whereulengthis large, but uncompression fails midway,ubufpoints to uninitialized heap data before being deleted. More crucially, in certain paths, memory could be leaked if the method aborts unexpectedly, though the explicitdelete[] ubuf;handles the main failure branch. However,cppcheckwarns thatubufis allocated but not initialized. IfSnappyreads 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
.sstfile with a valid block length but a corrupted snappy payload. Would be caught by MSan (MemorySanitizer) if partial reads occur. - RECOMMENDATION: Initialize
ubufexplicitly or use zero-initializationnew char[ulength]().
// 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
GetVarint32PtrFallbackiterates 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 loopfor (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 returnsnullptr, 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
0x80or higher. - RECOMMENDATION: Enforce that the 5th byte does not contain bits beyond the 32-bit boundary.
// 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<const char*>(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,ReadRecordappends fragments to astd::string* scratchbuffer:scratch->append(fragment.data(), fragment.size());. If an attacker provides a corrupted.logfile that consists of thousands ofkFirstTypeandkMiddleTyperecords without a terminatingkLastTyperecord,scratchwill grow indefinitely until the process is killed by the OS OOM killer. - REPRODUCTION: Create a
WriteBatchlog file with an artificially huge sequence ofkMiddleTyperecords 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
scratchbuffer exceeds it.
// 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, aCompactionState::Output outstruct is allocated on the stack.out.number,out.smallest.Clear(), andout.largest.Clear()are set, butout.file_sizeis not initialized before it is pushed tocompact->outputs.push_back(out). Whilefile_sizeis later updated inFinishCompactionOutputFile, if an error occurs and the compaction is aborted or processed early,file_sizecould be read as uninitialized stack garbage, potentially corrupting the manifestAddFilecall. - REPRODUCTION: Force an I/O error during the creation of a compaction output file (
TableFileNamecreation) and observe the cleanup/manifest sequence. - RECOMMENDATION: Initialize
file_sizeexplicitly when creating the output struct.
// 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::FileExistsuses::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 timeFileExistsreturns true and the time the file is actually opened by LevelDB. - REPRODUCTION: Run a rapid symlink replacement loop on the
CURRENTfile while LevelDB is attempting toOpen()the database. - RECOMMENDATION: Avoid
access(). Just attempt toopen()the file, and handleENOENTorEACCESgracefully. IfFileExistsis strictly needed, usestat()to at least verify file type, thoughopen()is the only safe way.
// 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, trustinggetenv()for file paths could lead to privilege escalation. - RECOMMENDATION: Ensure LevelDB is not used in setuid contexts, or sanitize the environment variables before use.
Source: google/leveldb