Skip to content

Security/Bug Audit Reports… #1315

@dibyx

Description

@dibyx

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]().
// 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.
// 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, 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.
// 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.
// 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.
// 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions