Skip to content

fix: resolve concurrency bugs, improve safety, and add SSE2/sanitizer support#24

Merged
tkhquang merged 4 commits intomainfrom
fix/pre-release-bugs-3
Mar 22, 2026
Merged

fix: resolve concurrency bugs, improve safety, and add SSE2/sanitizer support#24
tkhquang merged 4 commits intomainfrom
fix/pre-release-bugs-3

Conversation

@tkhquang
Copy link
Copy Markdown
Owner

@tkhquang tkhquang commented Mar 22, 2026

Summary by CodeRabbit

  • New Features

    • Project version bumped to 2.0.0
    • SSE2-accelerated pattern scanning for patterns ≥16 bytes
    • New CMake option to enable AddressSanitizer/UndefinedBehaviorSanitizer
    • Opt-out short-name aliases via build flag
  • Documentation

    • Scanner docs updated for SSE2 behavior
    • Added "Enabling Sanitizers" and stronger shutdown guidance
  • Improvements

    • Stricter compiler warnings and tightened noexcept/teardown semantics
    • Improved thread-safety and logger shutdown/reconfiguration
  • Tests

    • Added tests for async logger, logger concurrency, scanner SSE2, and hook-manager shutdown cases

@tkhquang tkhquang self-assigned this Mar 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

Bumps project version to 2.0.0; adds an opt-in sanitizers CMake flag; introduces SSE2-accelerated AOB scanning; and applies broad concurrency, noexcept, and API signature changes across logger, async-logger, config, hook, input, and memory subsystems, with related tests and CMake tweaks.

Changes

Cohort / File(s) Summary
Project & Docs
CMakeLists.txt, README.md
Project version → 2.0.0; added -Wpedantic; new DMK_ENABLE_SANITIZERS option and README instructions to enable ASan/UBSan; documented SSE2-accelerated AOB scanner for patterns ≥16 bytes.
Public Header Aliases
include/DetourModKit.hpp
Short-name type aliases (e.g., DMKLogger, DMKHookManager, ...) are now defined only when DMK_NO_SHORT_NAMES is not set; DMK_Shutdown() doc warns against calling from DllMain.
Logger (API & Impl)
include/DetourModKit/logger.hpp, src/logger.cpp, tests/test_logger.cpp
Swapped static string state for atomic shared_ptr<const StaticConfig>; log() now takes std::string_view; destructors marked noexcept; added shutdown_internal(); timestamp formatting uses strftime; added concurrency and timestamp tests.
Async Logger & StringPool
include/DetourModKit/async_logger.hpp, src/async_logger.cpp, tests/test_async_logger.cpp
release_slotreturn_slot_locked(PoolSlot*, Block*); centralized slot return logic; removed cache-line alignment on atomic head; strengthened noexcept on destructors; LogMessage move uses memcpy for inline buffer; writer flush notification guarded by mutex; added move/flush unit tests.
Configuration System
src/config.cpp
ConfigItemBase made non-copyable/non-movable; added take_deferred_apply() to defer setter invocation; setters applied after releasing config mutex to avoid invoking callbacks under lock.
Hook Manager
include/DetourModKit/hook_manager.hpp, src/hook_manager.cpp
Added HookError::ShutdownInProgress; Hook move ops deleted; destructor noexcept; shutdown now disables hooks and clears m_hooks; creation functions produce deferred log entries emitted after lock release and return ShutdownInProgress when shutdown started.
Input Subsystem
include/DetourModKit/input.hpp, src/input.cpp
Added std::atomic<InputPoller*> active_poller_; InputPoller destructor noexcept; strengthened acquire/release memory ordering for running/active flags; is_binding_active() reads active_poller_ atomically to avoid locking; adjusted shutdown ordering.
Memory & Scanner
src/memory.cpp, src/scanner.cpp, tests/test_scanner.cpp
Changed shard-index shift (>> 58>> 48); ActiveReaderGuard constructed earlier; added compile-time DMK_HAS_SSE2 path using 16-byte SIMD compares for pattern matching and corresponding SSE2 tests.
Build & Tests
tests/CMakeLists.txt, tests/*
Test discovery uses file(GLOB ... CONFIGURE_DEPENDS) for reconfigure-on-change; added/updated tests for async-logger, logger concurrency/timestamp, hook-manager shutdown behavior, and scanner SSE2 cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: concurrency bug fixes, safety improvements (noexcept specifications, stricter exception guarantees), and new features (SSE2 pattern matching acceleration, sanitizer support). It covers the primary objectives without being overly detailed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@tkhquang tkhquang force-pushed the fix/pre-release-bugs-3 branch from 562838e to 85a57b6 Compare March 22, 2026 10:51
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
README.md (1)

10-10: Qualify the SSE2 claim as build-dependent.

src/scanner.cpp only enables the vectorized path when the target/compiler exposes SSE2, so this sentence overstates the guarantee for non-SSE2 x86 builds. “On SSE2-capable builds” would match the implementation.

💬 Suggested wording
-* **AOB Scanner:** Find array-of-bytes (signatures) in memory with wildcard support and SSE2-accelerated pattern verification for patterns >= 16 bytes. Supports `|` offset markers for targeting a specific instruction within a wider pattern (e.g., `"48 8B 88 B8 00 00 00 | 48 89 4C 24 68"` sets the offset to byte 7) and nth-occurrence matching (1-based) for patterns that hit multiple locations. Includes RIP-relative instruction resolution for extracting absolute addresses from x86-64 code.
+* **AOB Scanner:** Find array-of-bytes (signatures) in memory with wildcard support and SSE2-accelerated pattern verification on SSE2-capable builds for patterns >= 16 bytes. Supports `|` offset markers for targeting a specific instruction within a wider pattern (e.g., `"48 8B 88 B8 00 00 00 | 48 89 4C 24 68"` sets the offset to byte 7) and nth-occurrence matching (1-based) for patterns that hit multiple locations. Includes RIP-relative instruction resolution for extracting absolute addresses from x86-64 code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 10, Update the README description of "AOB Scanner" to
qualify the SSE2 acceleration as build-dependent by changing the claim that it
"SSE2-accelerated pattern verification" to something like "SSE2-accelerated
pattern verification on SSE2-capable builds" or "when compiled with SSE2
support", so the wording matches the implementation in src/scanner.cpp that only
enables the vectorized path when SSE2 is available; keep the rest of the
sentence (wildcard support, `|` offset markers, nth-occurrence matching,
RIP-relative resolution) unchanged.
src/hook_manager.cpp (1)

122-232: Deferred logging pattern avoids holding mutex during logging.

The refactoring to collect log entries in a vector and emit them after releasing the lock is a good concurrency improvement. This prevents potential deadlocks if the logger itself needs synchronization.

One observation: the DeferredLog struct is defined identically in both create_inline_hook and create_mid_hook. Consider moving it to class scope or an anonymous namespace to avoid duplication.

♻️ Optional: Extract DeferredLog to reduce duplication
+namespace {
+    struct DeferredLog {
+        std::string msg;
+        LogLevel level;
+    };
+}
+
 std::expected<std::string, HookError> HookManager::create_inline_hook(...)
 {
-    struct DeferredLog
-    {
-        std::string msg;
-        LogLevel level;
-    };
     // ... rest of implementation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hook_manager.cpp` around lines 122 - 232, The DeferredLog struct is
duplicated in create_inline_hook and create_mid_hook; extract it to a shared
scope (e.g., move struct DeferredLog into the class definition of HookManager or
into an anonymous namespace above the methods) and update both functions to use
that single definition (referencing DeferredLog, create_inline_hook, and
create_mid_hook) to remove duplication and keep the deferred-logging behavior
unchanged.
tests/test_logger.cpp (1)

942-965: Timestamp format verification is reasonable but fragile.

The test correctly verifies the log message appears and checks for expected date format structure. However, the offset-based checks (Lines 962-963) assume the timestamp always starts exactly at pos, which depends on other content not appearing before the timestamp.

Consider a more robust approach using a regex or at least finding the closing bracket position:

💡 Suggested improvement
// Instead of fixed offsets, verify the pattern more robustly
auto bracket_end = content.find(']', pos);
if (bracket_end != std::string::npos) {
    std::string timestamp = content.substr(pos + 1, bracket_end - pos - 1);
    // Verify format: YYYY-MM-DD HH:MM:SS (19 chars minimum)
    EXPECT_GE(timestamp.size(), 19u);
    EXPECT_EQ(timestamp[4], '-');
    EXPECT_EQ(timestamp[7], '-');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_logger.cpp` around lines 942 - 965, The timestamp verification in
TEST_F(LoggerTest, TimestampFormat_StrftimeOutput) is fragile because it uses
fixed offsets from content.find("[20"); update the test to locate the closing
bracket (']') after the opening '[' (found at pos), extract the timestamp
substring between them, assert its minimum length (e.g., >= 19), and then check
the expected separators at timestamp[4] and timestamp[7] (and other positions as
needed) instead of using fixed offsets from pos; this change keeps the checks
resilient to any preceding log content while still using the same
Logger::get_instance(), logger.info call, and test_log_file_ reading flow.
🤖 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/config.cpp`:
- Around line 397-405: The race occurs because the new CallbackConfigItem is
published (getRegisteredConfigItems().push_back) before invoking
setter(default_value), allowing Config::load() to see the item and later be
overwritten; fix by creating the CallbackConfigItem (e.g.,
make_unique<CallbackConfigItem<int>>(...)) but do NOT push it into
getRegisteredConfigItems() until after you've invoked setter(default_value) on
the new item (or alternatively set and check an initialization flag that load()
respects). Concretely: construct the unique_ptr locally, call
setter(default_value) while the item is still unpublished, then acquire the
mutex and push_back the unique_ptr into getRegisteredConfigItems(); apply the
same pattern to the other overloads (the blocks around lines 412-420, 427-435,
442-450, 459-467) and/or implement a load-initialized flag checked by
Config::load().
- Around line 262-268: The lambda returned by take_deferred_apply currently
captures this, allowing the callback to observe mutated current_value or
dangling storage; instead, make the callback self-contained by capturing setter
and current_value by value (e.g., copy setter and current_value into the lambda)
so the closure invokes the copied setter(copied_current_value) without
referencing this or any members and thus is safe across later load() calls or
registry changes.

In `@src/logger.cpp`:
- Around line 69-75: The lifecycle flag shutdown_called_ is not sufficient for
mutual exclusion between configure()/reconfigure() and shutdown(); serialize
configure()/reconfigure() and shutdown() with a single lifecycle mutex so they
cannot interleave (prevent configure clearing shutdown_called_ then entering
reconfigure() while shutdown_internal() is running). Modify configure(),
reconfigure(), and shutdown()/shutdown_internal() to acquire the same lifecycle
lock (e.g., add/std::mutex lifecycle_mutex or reuse an existing lifecycle lock)
around the state mutation and stream open/close operations (the
shutdown_called_.store(false) in configure(), the check/CAS and
shutdown_internal() path in shutdown(), and the body of reconfigure()) so
open/close of the file stream and updates to prefix/file_name/timestamp_fmt are
performed under that lock.
- Around line 89-95: The fast-path in configure() incorrectly returns when
log_prefix_, log_file_name_, and timestamp_format_ match, ignoring whether the
underlying stream is closed; update the early-return condition to also verify
the stream is usable (e.g., check the file stream/member like
log_stream_.is_open() or an equivalent "open/healthy" flag) so that after
shutdown_internal() or a prior open failure configure() will attempt to reopen
via open() instead of skipping; adjust the condition in configure() that
compares log_prefix_, log_file_name_, and timestamp_format_ to include this
stream-state check.
- Around line 289-295: The fixed-size char buf[128] causes valid timestamps to
be rejected when strftime needs more space; change the timestamp formatting path
(the code using timestamp_format_ and timeinfo_struct) to allocate a dynamic
buffer and retry until std::strftime returns a non-zero length (e.g., start with
128 bytes and double on failure) or use an std::ostringstream/std::put_time
fallback to produce the formatted string without a hard cap, then return the
resulting std::string; ensure you preserve the existing error branch only if an
actual formatting failure occurs after reasonable retries.

In `@src/memory.cpp`:
- Around line 1004-1007: The current unconditional construction of
MemoryUtilsCacheInternal::ActiveReaderGuard increments s_activeReaders before
checking s_cacheInitialized, which causes fallback calls to block shutdown;
modify DetourModKit::Memory::is_readable() and
DetourModKit::Memory::is_writable() to perform the cheap hot-path checks first
(e.g., if s_shardCount == 0 or !s_cacheInitialized) and only construct/enter
ActiveReaderGuard after those checks pass, so the reader count is incremented
only when the cache/shards are actually in use.

---

Nitpick comments:
In `@README.md`:
- Line 10: Update the README description of "AOB Scanner" to qualify the SSE2
acceleration as build-dependent by changing the claim that it "SSE2-accelerated
pattern verification" to something like "SSE2-accelerated pattern verification
on SSE2-capable builds" or "when compiled with SSE2 support", so the wording
matches the implementation in src/scanner.cpp that only enables the vectorized
path when SSE2 is available; keep the rest of the sentence (wildcard support,
`|` offset markers, nth-occurrence matching, RIP-relative resolution) unchanged.

In `@src/hook_manager.cpp`:
- Around line 122-232: The DeferredLog struct is duplicated in
create_inline_hook and create_mid_hook; extract it to a shared scope (e.g., move
struct DeferredLog into the class definition of HookManager or into an anonymous
namespace above the methods) and update both functions to use that single
definition (referencing DeferredLog, create_inline_hook, and create_mid_hook) to
remove duplication and keep the deferred-logging behavior unchanged.

In `@tests/test_logger.cpp`:
- Around line 942-965: The timestamp verification in TEST_F(LoggerTest,
TimestampFormat_StrftimeOutput) is fragile because it uses fixed offsets from
content.find("[20"); update the test to locate the closing bracket (']') after
the opening '[' (found at pos), extract the timestamp substring between them,
assert its minimum length (e.g., >= 19), and then check the expected separators
at timestamp[4] and timestamp[7] (and other positions as needed) instead of
using fixed offsets from pos; this change keeps the checks resilient to any
preceding log content while still using the same Logger::get_instance(),
logger.info call, and test_log_file_ reading flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 525d8e04-c5cb-49f8-b88d-2becd821de7f

📥 Commits

Reviewing files that changed from the base of the PR and between e933316 and 85a57b6.

📒 Files selected for processing (18)
  • CMakeLists.txt
  • README.md
  • include/DetourModKit.hpp
  • include/DetourModKit/async_logger.hpp
  • include/DetourModKit/hook_manager.hpp
  • include/DetourModKit/input.hpp
  • include/DetourModKit/logger.hpp
  • src/async_logger.cpp
  • src/config.cpp
  • src/hook_manager.cpp
  • src/input.cpp
  • src/logger.cpp
  • src/memory.cpp
  • src/scanner.cpp
  • tests/CMakeLists.txt
  • tests/test_async_logger.cpp
  • tests/test_logger.cpp
  • tests/test_scanner.cpp

Comment thread src/config.cpp
Comment thread src/config.cpp Outdated
Comment thread src/logger.cpp
Comment thread src/logger.cpp Outdated
Comment thread src/logger.cpp
Comment thread src/memory.cpp
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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/hook_manager.cpp`:
- Around line 186-227: The success path is not exception-safe: expensive
allocations and side-effects (std::format/push_back, assigning
*original_trampoline) happen before committing the managed hook to m_hooks, so a
thrown exception can leave a live hook with dangling state or misreported
UnknownError; to fix, build and reserve the DeferredLog vector and all formatted
messages (including status_message and auto-enable warning) before
creating/moving the InlineHook into m_hooks, only call m_hooks.emplace(...) once
all formatting succeeds, and assign *original_trampoline after emplace returns
successfully (use sh_inline_hook_ptr and InlineHook construction locally, then
move into m_hooks); apply the same changes for the inline path and the similar
block around lines 331-371 so that logs are prepared and trampoline assignment
happens post-commit.
- Around line 131-160: After acquiring the lock in create_inline_hook (and
similarly in create_mid_hook) check
m_shutdown_called.load(std::memory_order_acquire) and return an appropriate
std::unexpected(HookError::ShutdownInProgress) with a deferred log if shutdown
has begun; place this guard immediately after the
std::unique_lock<std::shared_mutex> lock(m_hooks_mutex) so no hook creation
proceeds while shutdown is in progress, and add the same check in the other
analogous block(s) that create hooks to prevent recreation after shutdown drains
m_hooks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a0d3151-0b82-46ea-a58e-c5e476bda3e2

📥 Commits

Reviewing files that changed from the base of the PR and between 85a57b6 and 895f3cd.

📒 Files selected for processing (5)
  • README.md
  • src/config.cpp
  • src/hook_manager.cpp
  • src/logger.cpp
  • tests/test_logger.cpp
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • tests/test_logger.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/logger.cpp

Comment thread src/hook_manager.cpp
Comment thread src/hook_manager.cpp
Repository owner deleted a comment from coderabbitai Bot Mar 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/hook_manager.cpp (1)

198-203: Minor log message formatting inconsistency.

The disabled case produces "Successfully created (created disabled) inline hook..." with a double space and redundant "created" wording.

💡 Suggested fix for clearer log messages
-            std::string status_message = (initial_status == HookStatus::Active) ? "and enabled" : " (created disabled)";
+            const char* status_message = (initial_status == HookStatus::Active) ? "enabled" : "disabled";
             std::vector<DeferredLog> logs;
             logs.reserve(2);
-            logs.push_back({std::format("HookManager: Successfully created {} inline hook '{}' targeting {}.",
+            logs.push_back({std::format("HookManager: Successfully created inline hook '{}' targeting {} ({}).",
-                                        status_message, name, DetourModKit::Format::format_address(target_address)),
+                                        name, DetourModKit::Format::format_address(target_address), status_message),
                             LogLevel::Info});

Also applies to Line 343 for mid hooks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hook_manager.cpp` around lines 198 - 203, The log message builds
status_message with a leading space and redundant "created" for the disabled
case; update the assignment of status_message (used when pushing DeferredLog in
HookManager) so the active case is "enabled" and the disabled case is
"(disabled)" or "disabled" without leading/trailing extra spaces, and ensure the
std::format call in the DeferredLog push_back concatenates with a single space
only once; apply the same change to the equivalent mid-hook path where
status_message is constructed for mid hooks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/hook_manager.cpp`:
- Around line 198-203: The log message builds status_message with a leading
space and redundant "created" for the disabled case; update the assignment of
status_message (used when pushing DeferredLog in HookManager) so the active case
is "enabled" and the disabled case is "(disabled)" or "disabled" without
leading/trailing extra spaces, and ensure the std::format call in the
DeferredLog push_back concatenates with a single space only once; apply the same
change to the equivalent mid-hook path where status_message is constructed for
mid hooks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d9c05fe5-fba4-4a11-a11e-9caa3e1dd12e

📥 Commits

Reviewing files that changed from the base of the PR and between f2019d7 and 1d5f363.

📒 Files selected for processing (3)
  • include/DetourModKit/hook_manager.hpp
  • src/hook_manager.cpp
  • tests/test_hook_manager.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • include/DetourModKit/hook_manager.hpp

@tkhquang tkhquang merged commit 6030274 into main Mar 22, 2026
2 checks passed
@tkhquang tkhquang deleted the fix/pre-release-bugs-3 branch March 23, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant