fix(gc): file-GC snapshot UAF + MakeCowRoot error ordering in Apply - #474
fix(gc): file-GC snapshot UAF + MakeCowRoot error ordering in Apply#474thweetkomputer wants to merge 2 commits into
Conversation
Apply() called cow_meta_.compression_->SampleAndBuildDictionaryIfNeeded before CHECK_KV_ERR(err). MakeCowRoot leaves cow_meta_.compression_ null on its error return paths (it only populates the CoW meta on success or the NotFound branch), so a root-load failure crashed on the null deref instead of propagating the error. Move the check ahead of the dereference.
TriggerFileGC / TriggerLocalFileGC call BuildRetainedFiles, which captures MappingSnapshot::Refs (snapshot_array) whose tbl_ident_ points into the partition's RootMeta entry, then release their per-call root Handle. GC then yields across ExecuteLocalGC. During those yields another task can drive the RootMeta cache over its limit and EvictIfNeeded picks this now-unpinned (ref_cnt_==0) entry as an LRU victim, erasing it and freeing entry->tbl_id_. snapshot_array keeps the MappingSnapshot alive, so at TriggerFileGC exit ~MappingSnapshot -> FreeMappingSnapshot dereferences the dangling tbl_ident_ (heap-use-after-free). Hold a root Handle for the whole GC so the entry stays off the LRU until the snapshot arrays are destroyed. When the entry is absent (NotFound, e.g. after a Drop cleared the manifest) there are no snapshots to dangle, so the handle is a no-op and GC still runs with empty retained sets to purge orphaned files and rmdir the partition directory. Adds a RootMetaMgr::ForceEvictForTest test hook (debug-only) and a "GcForceEvictRoot" fault point so the regression test drives the eviction/GC-yield interleaving deterministically; it aborts with ASAN heap-use-after-free in FreeMappingSnapshot on unfixed code.
WalkthroughThis PR adds a debug-only ChangesRoot-Meta Pinning and Eviction Test Hook
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant WriteTask
participant IndexManager
participant RootMetaMgr
participant FailPoint
WriteTask->>IndexManager: FindRoot(tbl_ident_)
IndexManager-->>WriteTask: RootMeta ref (pinned)
WriteTask->>FailPoint: check GcForceEvictRoot
FailPoint->>RootMetaMgr: ForceEvictForTest(tbl_id)
RootMetaMgr-->>FailPoint: eviction result (blocked while pinned)
WriteTask->>WriteTask: BuildRetainedFiles(snapshots)
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/storage/root_meta.cpp (1)
456-469: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider extracting the shared eviction tail to keep the test hook faithful to production.
Lines 456-469 duplicate the
EvictRootForCache→Dequeue→used_bytes_adjust →entries_.erasesequence fromEvictIfNeeded(lines 419-434). Since the whole point of this hook is to "mimic the LRU victim path", extracting a smallEvictVictim(Entry*)helper used by both would prevent the two paths from silently drifting if the production eviction sequence gains a step.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/storage/root_meta.cpp` around lines 456 - 469, The test hook in the root metadata eviction path duplicates the same victim removal sequence already used by EvictIfNeeded, so it can drift from production behavior. Extract the shared eviction tail into a small helper such as EvictVictim(Entry*) that performs EvictRootForCache, Dequeue, the used_bytes_ adjustment, and entries_.erase, then call it from both EvictIfNeeded and the hook to keep the LRU victim path consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/storage/root_meta.cpp`:
- Around line 456-469: The test hook in the root metadata eviction path
duplicates the same victim removal sequence already used by EvictIfNeeded, so it
can drift from production behavior. Extract the shared eviction tail into a
small helper such as EvictVictim(Entry*) that performs EvictRootForCache,
Dequeue, the used_bytes_ adjustment, and entries_.erase, then call it from both
EvictIfNeeded and the hook to keep the LRU victim path consistent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 87a5e098-8ff3-434e-82e9-338efaa8e545
📒 Files selected for processing (5)
include/storage/root_meta.hsrc/storage/root_meta.cppsrc/tasks/batch_write_task.cppsrc/tasks/write_task.cpptests/gc.cpp
Two related durability/lifecycle fixes.
1. File-GC use-after-free (audit #11)
TriggerFileGC/TriggerLocalFileGCbuildsnapshot_array—MappingSnapshot::Refs whosetbl_ident_points into the partition'sRootMetaentry (&entry->tbl_id_) — then release the per-call rootHandlethatBuildRetainedFilesheld. GC then yields acrossExecuteLocalGC. During those yields another task can push the RootMeta cache over its limit;EvictIfNeededpicks this now-unpinned (ref_cnt_==0) entry as an LRU victim and erases it, freeingentry->tbl_id_.snapshot_arraykeeps theMappingSnapshotalive, so atTriggerFileGCexit~MappingSnapshot → FreeMappingSnapshotdereferences the danglingtbl_ident_:Fix: hold a root
Handlefor the entire GC so the entry stays off the LRU (unevictable) until the snapshot arrays are destroyed. OnNotFound(entry absent, e.g. after a Drop cleared the manifest) there are no snapshots to dangle — the handle is a no-op and GC still runs with empty retained sets to purge orphaned files and rmdir the partition directory (verified by the existing drop / least_unflushed cleanup tests).Regression test:
gc snapshot refs survive root meta eviction([gc][local][uaf]). The eviction/GC-yield interleaving is a narrow race, so a debug-onlyForceEvictForTesthook +GcForceEvictRootfault point drive it deterministically — exactly the eviction the LRU would perform under pressure. Aborts with ASAN heap-use-after-free on unfixed code; passes with the fix (the pin makes the forced eviction a no-op). Both hooks compile out in release (NDEBUG).2. MakeCowRoot error ordering in Apply (audit #14)
BatchWriteTask::Applydereferencedcow_meta_.compression_before checking theMakeCowRooterror.MakeCowRoot's error branch (page_manager.cppreturn err;) leavescow_meta_.compression_null, so an error crashed on the null deref instead of surfacing. MovedCHECK_KV_ERR(err)before the compression use.Test
gc,batch_write,persist,segment_compact,standbysuites pass (non-ASAN).gc [local]suite passes under ASAN, including the new UAF regression test.Summary by CodeRabbit
Bug Fixes
Tests