fix: harden profiler, hook manager, and logger#69
Conversation
Address v3.1.0 audit findings: switch profiler seqlock to fetch_add(acq_rel), serialize HookManager destructor via mutator_gate with loader-lock module pinning, make logger shutdown leak vector append-only, enforce scanner prologue fallback hit cap, raise input cardinality mismatch to warning, scope scanner -O2 downgrade to Release only, and make ScopedProfile compile-time literal-only.
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 28 minutes and 9 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThe PR updates shutdown semantics and thread-safety documentation for HookManager, Logger, and Profiler while implementing targeted changes: HookManager gains a loader-lock-aware destructor fallback, Logger retains multiple leaked AsyncLogger instances in a vector, Profiler shifts to monotonic sequence-counter increments with Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application
participant HM as HookManager
participant Gate as m_mutator_gate
participant HMutex as m_hooks_mutex
participant OSL as OS / Loader Lock
rect rgba(255, 200, 100, 0.5)
Note over App,OSL: Normal Shutdown Path (No Loader Lock)
App->>HM: ~HookManager() destructor
HM->>HM: Check m_shutdown_called
alt Already shutdown
HM-->>App: Return early
else Shutdown in progress
HM->>Gate: Acquire exclusive lock
HM->>HM: Set m_shutdown_called = true
HM->>Gate: Release exclusive lock
HM->>HMutex: Acquire shared lock
Note over HM: Drain in-flight readers
HM->>HMutex: Release shared lock
HM->>HMutex: Acquire exclusive lock
HM->>HM: Clear hook maps
HM->>HMutex: Release exclusive lock
HM-->>App: Return safely
end
end
rect rgba(200, 150, 255, 0.5)
Note over App,OSL: Fallback Path (Loader Lock Detected)
App->>HM: ~HookManager() during DLL unload
HM->>OSL: Detect loader lock engaged
HM->>HM: Pin current module
HM->>HM: Set m_shutdown_called
HM->>HM: Leak hook/VMT maps to static vector
HM-->>App: Return without cleanup (deadlock avoidance)
Note over OSL: OS releases leaked memory at process exit
end
sequenceDiagram
participant User as User Code
participant SP as ScopedProfile
participant Prof as Profiler
participant Slot as Ring-Buffer Slot
rect rgba(100, 200, 100, 0.5)
Note over User,Slot: Old Protocol (Odd/Even with Loaded Seq)
User->>SP: Create ScopedProfile("func")
SP->>Prof: record_open(name)
Prof->>Slot: Load seq (relaxed)
Prof->>Slot: Store seq | 1 (odd)
Note over Prof,Slot: Thread race: another producer fetches new seq
User->>User: Do work
User->>SP: ~ScopedProfile()
SP->>Prof: record_close()
Prof->>Slot: Load old seq (now stale!)
Prof->>Slot: Store seq | 0 (even)
Note over Prof,Slot: Potential rollback if concurrent fetch_add occurred
end
rect rgba(100, 150, 255, 0.5)
Note over User,Slot: New Protocol (Monotonic fetch_add)
User->>SP: Create ScopedProfile("func")
SP->>Prof: record_open(name)
Prof->>Slot: fetch_add(1, acq_rel)
Note over Prof,Slot: Unconditional atomic increment, no rollback possible
User->>User: Do work
User->>SP: ~ScopedProfile()
SP->>Prof: record_close()
Prof->>Slot: fetch_add(1, release)
Note over Prof,Slot: Sequence always advances, races between producers safe
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes The PR contains intricate concurrency and lifetime-management changes (HookManager loader-lock fallback, Logger AsyncLogger retention vector, Profiler sequence protocol shift) that span multiple core subsystems, mixed with documentation and test additions requiring careful reasoning about thread-safety implications and shutdown semantics. Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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.
Actionable comments posted: 3
🧹 Nitpick comments (2)
tests/test_scanner.cpp (1)
2014-2069: Test correctly validates the uniqueness-ceiling guard.Seeding five identical trampoline-shaped sequences (
E9+ disp32 + 8-byte literal tail) into aPAGE_EXECUTE_READbuffer and assertingResolveError::NoMatchprecisely exercises thehits > kPrologueFallbackMaxHitsbranch inscan_candidates_hooked_prologue. The tail bytes (A5 B6 C7 D8 E9 FA 0B 1C) are distinctive enough to avoid cross-test aliasing.Minor nit (optional): this test rolls its own
VirtualAlloc/VirtualFreeinstead of reusing theExecBufferRAII helper a few tests above. If the intermediateASSERT_TRUE(VirtualProtect(...))at line 2046 fires,rawleaks until process exit. Not a correctness bug, just an RAII inconsistency with the surrounding tests; anExecBuffer-style guard (or a small scope-exit) would make the cleanup path uniform.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_scanner.cpp` around lines 2014 - 2069, The test ScannerCascade::PrologueFallbackRejectsAmbiguousTail uses manual VirtualAlloc/VirtualFree/VirtualProtect and can leak 'raw' if VirtualProtect ASSERT_TRUE fails; replace the manual allocation with the existing ExecBuffer RAII helper (or wrap raw in a scope-exit guard) so allocation and protection changes are always undone on failure, i.e., remove direct calls to VirtualAlloc/VirtualFree/VirtualProtect and use ExecBuffer (or a small RAII/protector) to allocate, set PAGE_EXECUTE_READ, and restore/free in its destructor to ensure deterministic cleanup.tests/test_hook_manager.cpp (1)
2436-2446: Optional: assert the ordering invariant explicitly.The comment states "it must not return while the reader holds shared_lock", but the test never checks that before flipping
reader_may_return. Adding a pre-release assertion onshutdown_returnedmakes the invariant a hard test failure rather than relying solely on thereader_observed_validindirect check. Purely optional.♻️ Proposed strengthener
std::this_thread::sleep_for(std::chrono::milliseconds(20)); + // Shutdown must still be blocked because the reader holds shared_lock. + EXPECT_FALSE(shutdown_returned.load(std::memory_order_acquire)); reader_may_return.store(true, std::memory_order_release);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_hook_manager.cpp` around lines 2436 - 2446, Add an explicit pre-release assertion to enforce the ordering invariant: before calling reader_may_return.store(true, std::memory_order_release) in the test, assert that shutdown_returned.load() is false (e.g., EXPECT_FALSE(shutdown_returned.load())); this makes the requirement "must not return while the reader holds shared_lock" a hard check instead of relying solely on reader_observed_valid, referencing the existing test variables reader_may_return and shutdown_returned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/logger.cpp`:
- Around line 211-216: The branch that only retains the detached AsyncLogger
when local_logger.use_count() == 1 is unsafe; instead, whenever
detail::is_loader_lock_held() is true and local_logger is non-null, move
local_logger into the static s_leaked_loggers to keep the object alive
regardless of use_count(). Update the block around local_logger and
detail::is_loader_lock_held() so it no longer checks use_count() before
emplacing std::move(local_logger) into s_leaked_loggers, ensuring
AsyncLogger::shutdown()'s detached writer thread cannot be destroyed while other
threads still hold temporaries.
In `@tests/test_profiler.cpp`:
- Around line 501-569: The test currently only validates JSON envelope from
export_chrome_json() and therefore cannot detect per-slot sequence rollbacks;
add a test-only way to observe ProfileSample::sequence (or equivalent slot
sequence counters) and assert each slot's sequence is strictly non-decreasing
across reads. Modify Profiler to expose a test-only accessor (e.g., a method
named snapshot_slot_sequences() or get_slot_sequence(size_t) guarded by `#ifdef`
TESTING or an `#if` defined unit-test build flag, or mark the test as a friend)
that atomically reads each slot's sequence value from the ring buffer, then
update ConcurrentRecord_WrapsBuffer_ReaderObservesNoSequenceRollback to
repeatedly call that accessor and verify for each slot that successive reads do
not decrease (store any violation into rollback_detected); keep existing writers
and stop logic but replace the JSON check with these direct sequence snapshots
so the test will fail if any slot counter rolls back.
- Around line 483-493: The header comment in profiler.hpp incorrectly claims
const char (&)[N] enforces string-literal/static-storage semantics; either
strengthen the API or correct the docs—do not change the tests. Update
profiler.hpp's documentation for ScopedProfile to state that the array-reference
constructor accepts any array (including local automatic arrays) and therefore
may produce a dangling pointer if given a non-static array, and recommend safe
alternatives (e.g., pass a compile-time literal constant, use a static const
char array, or use an API that accepts std::string_view with documented lifetime
requirements or a factory that copies the string). Reference the ScopedProfile
constructor and its array-reference overload in the comment so callers see the
exact symbol whose semantics were clarified.
---
Nitpick comments:
In `@tests/test_hook_manager.cpp`:
- Around line 2436-2446: Add an explicit pre-release assertion to enforce the
ordering invariant: before calling reader_may_return.store(true,
std::memory_order_release) in the test, assert that shutdown_returned.load() is
false (e.g., EXPECT_FALSE(shutdown_returned.load())); this makes the requirement
"must not return while the reader holds shared_lock" a hard check instead of
relying solely on reader_observed_valid, referencing the existing test variables
reader_may_return and shutdown_returned.
In `@tests/test_scanner.cpp`:
- Around line 2014-2069: The test
ScannerCascade::PrologueFallbackRejectsAmbiguousTail uses manual
VirtualAlloc/VirtualFree/VirtualProtect and can leak 'raw' if VirtualProtect
ASSERT_TRUE fails; replace the manual allocation with the existing ExecBuffer
RAII helper (or wrap raw in a scope-exit guard) so allocation and protection
changes are always undone on failure, i.e., remove direct calls to
VirtualAlloc/VirtualFree/VirtualProtect and use ExecBuffer (or a small
RAII/protector) to allocate, set PAGE_EXECUTE_READ, and restore/free in its
destructor to ensure deterministic cleanup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d307992c-1f84-4797-9a28-2a8d23791aa9
📒 Files selected for processing (15)
AGENTS.mdCMakeLists.txtREADME.mdinclude/DetourModKit.hppinclude/DetourModKit/async_logger.hppinclude/DetourModKit/config.hppinclude/DetourModKit/profiler.hppsrc/async_logger.cppsrc/hook_manager.cppsrc/input.cppsrc/logger.cppsrc/profiler.cpptests/test_hook_manager.cpptests/test_profiler.cpptests/test_scanner.cpp
- logger: emplace AsyncLogger into the leak vector unconditionally under loader lock; use_count() is not a reliable proxy for 'no other owners' when concurrent log() callers hold temporaries obtained before exchange() - profiler: add static_assert + guardrail comment forbidding load-then-store RMW on the seqlock sequence counter - profiler: correct docs to describe what const char (&)[N] actually guarantees (rejects pointer decay, accepts any array) - test_hook_manager: add pre-release EXPECT_FALSE(shutdown_returned) to catch premature-return regressions directly - test_scanner: replace leak-prone raw VirtualAlloc/Protect/Free with existing ExecBuffer RAII helper - test_profiler: rename the concurrent-wrap test to match what it verifies and add per-event dur range check for torn-sample detection - README + hot-reload guide: document Mod Bootstrap and StoppableWorker helpers added in #68
Summary
Addresses all v3.1.0 audit findings. Both release blockers and six strong recommendations are resolved with local fixes; no public API changes beyond the two appended enum values already scoped for v3.1.0.
Changes
fetch_add(1, acq_rel)on open and close, eliminating stale-publish races under concurrent wrap collisions.m_mutator_gatebefore clearing maps, drains in-flight readers, and pins the module while leaking maps (not Hook objects) when running under loader lock.s_leaked_loggersis now an append-only vector so repeated shutdown cycles in one process do not drop prior handles.count_pattern_hits_bounded; ambiguous candidates (hits > cap) are rejected withResolveError::NoMatch.update_binding_comboscardinality mismatch is now logged at Warning level (was Debug) so silent rejections are visible.-O2downgrade is scoped tosrc/scanner.cppunder$<$<CONFIG:Release>:-O2>only, restoring-O3on the rest of the tree.std::string::c_str()UB on export.Test plan
ConcurrentRecordAndExport_NoTornReadspasses under TSan.LateShutdown_DrainsReadersBeforeClearingMapspasses under ASan.PrologueFallbackRejectsAmbiguousTailpasses.static_assertchecks intest_profiler.cppconfirmScopedProfileliteral-only enforcement.Summary by CodeRabbit
New Features
DMK_PROFILE_SCOPEmacro names, ensuring captured profiler data references static memory only.Bug Fixes
Documentation
Chores