src/hook_toggle.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file hook_toggle.cpp | ||
| 3 | * @brief This TU implements Hook::enable and Hook::disable over the shared toggle publication order. | ||
| 4 | */ | ||
| 5 | |||
| 6 | #include "DetourModKit/hook.hpp" | ||
| 7 | |||
| 8 | #include "internal/diagnostics_population.hpp" | ||
| 9 | #include "internal/hook_backend.hpp" | ||
| 10 | #include "internal/hook_backend_visit.hpp" | ||
| 11 | #include "internal/hook_emission.hpp" | ||
| 12 | #include "internal/hook_ledger.hpp" | ||
| 13 | #include "internal/hook_patch_witness.hpp" | ||
| 14 | #include "internal/hook_publication.hpp" | ||
| 15 | |||
| 16 | #include "DetourModKit/logger.hpp" | ||
| 17 | |||
| 18 | #include <atomic> | ||
| 19 | #include <cstddef> | ||
| 20 | #include <cstdint> | ||
| 21 | #include <memory> | ||
| 22 | #include <mutex> | ||
| 23 | #include <optional> | ||
| 24 | #include <string> | ||
| 25 | #include <utility> | ||
| 26 | |||
| 27 | namespace DetourModKit | ||
| 28 | { | ||
| 29 | namespace hook | ||
| 30 | { | ||
| 31 | namespace | ||
| 32 | { | ||
| 33 | using DetourModKit::detail::apply_backend; | ||
| 34 | using DetourModKit::detail::backend_value_or; | ||
| 35 | using DetourModKit::detail::emit_lifecycle; | ||
| 36 | using DetourModKit::detail::enable_patch_is_confirmed; | ||
| 37 | using DetourModKit::detail::inline_trampoline; | ||
| 38 | using DetourModKit::detail::LifecycleSnapshot; | ||
| 39 | using DetourModKit::detail::PatchWitness; | ||
| 40 | using DetourModKit::detail::snapshot_lifecycle; | ||
| 41 | using DetourModKit::detail::try_backend_disable; | ||
| 42 | using DetourModKit::detail::try_backend_enable; | ||
| 43 | using DetourModKit::detail::witness_description; | ||
| 44 | using DetourModKit::detail::witness_of; | ||
| 45 | using DetourModKit::detail::witness_permits_write; | ||
| 46 | |||
| 47 | enum class ToggleWarningKind : std::uint8_t | ||
| 48 | { | ||
| 49 | None, | ||
| 50 | EnableRefused, | ||
| 51 | EnableReconciled, | ||
| 52 | DisableRefused, | ||
| 53 | DisableReconciled | ||
| 54 | }; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * @brief Defers one warning until later-declared lock guards release. | ||
| 58 | * @details Declare it before the call-gate lock and target slot. Its destructor runs after both guards | ||
| 59 | * release. | ||
| 60 | */ | ||
| 61 | class DeferredToggleWarning | ||
| 62 | { | ||
| 63 | public: | ||
| 64 | 7397 | DeferredToggleWarning() = default; | |
| 65 | |||
| 66 | 7397 | ~DeferredToggleWarning() noexcept | |
| 67 | ✗ | { | |
| 68 |
5/6✓ Branch 2 → 3 taken 7387 times.
✓ Branch 2 → 4 taken 5 times.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 2 → 11 taken 3 times.
✓ Branch 2 → 15 taken 1 time.
✗ Branch 2 → 18 not taken.
|
7397 | switch (m_kind) |
| 69 | { | ||
| 70 | 7387 | case ToggleWarningKind::None: | |
| 71 | 7387 | return; | |
| 72 | 5 | case ToggleWarningKind::EnableRefused: | |
| 73 | 5 | (void)log().try_log( | |
| 74 | LogLevel::Warning, | ||
| 75 | "hook: '{}' at 0x{:0{}X} refused enable: {}.", | ||
| 76 | 5 | m_name, | |
| 77 | 5 | m_target, | |
| 78 | 5 | sizeof(std::uintptr_t) * 2, | |
| 79 | 5 | witness_description(m_witness) | |
| 80 | ); | ||
| 81 | 5 | return; | |
| 82 | 1 | case ToggleWarningKind::EnableReconciled: | |
| 83 | 1 | (void)log().try_log( | |
| 84 | LogLevel::Warning, | ||
| 85 | "hook: '{}' at 0x{:0{}X} has original bytes under an active state. This enable retries " | ||
| 86 | "the arm.", | ||
| 87 | 1 | m_name, | |
| 88 | 1 | m_target, | |
| 89 | 1 | sizeof(std::uintptr_t) * 2 | |
| 90 | ); | ||
| 91 | 1 | return; | |
| 92 | 3 | case ToggleWarningKind::DisableRefused: | |
| 93 | 3 | (void)log().try_log( | |
| 94 | LogLevel::Warning, | ||
| 95 | "hook: '{}' at 0x{:0{}X} refused disable: {}.", | ||
| 96 | 3 | m_name, | |
| 97 | 3 | m_target, | |
| 98 | 3 | sizeof(std::uintptr_t) * 2, | |
| 99 | 3 | witness_description(m_witness) | |
| 100 | ); | ||
| 101 | 3 | return; | |
| 102 | 1 | case ToggleWarningKind::DisableReconciled: | |
| 103 | 1 | (void)log().try_log( | |
| 104 | LogLevel::Warning, | ||
| 105 | "hook: '{}' at 0x{:0{}X} has owned bytes under a disabled state. This disable retries " | ||
| 106 | "the restore.", | ||
| 107 | 1 | m_name, | |
| 108 | 1 | m_target, | |
| 109 | 1 | sizeof(std::uintptr_t) * 2 | |
| 110 | ); | ||
| 111 | 1 | return; | |
| 112 | } | ||
| 113 |
1/2✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 7397 times.
|
7397 | } |
| 114 | |||
| 115 | DeferredToggleWarning(const DeferredToggleWarning &) = delete; | ||
| 116 | DeferredToggleWarning &operator=(const DeferredToggleWarning &) = delete; | ||
| 117 | DeferredToggleWarning(DeferredToggleWarning &&) = delete; | ||
| 118 | DeferredToggleWarning &operator=(DeferredToggleWarning &&) = delete; | ||
| 119 | |||
| 120 | /// Stores one warning and contains any name-copy failure. | ||
| 121 | void | ||
| 122 | 10 | arm(ToggleWarningKind kind, | |
| 123 | const std::string &name, | ||
| 124 | std::uintptr_t target, | ||
| 125 | PatchWitness witness = PatchWitness::Indeterminate) noexcept | ||
| 126 | { | ||
| 127 | 10 | m_kind = kind; | |
| 128 | 10 | m_target = target; | |
| 129 | 10 | m_witness = witness; | |
| 130 | try | ||
| 131 | { | ||
| 132 |
1/2✓ Branch 2 → 3 taken 10 times.
✗ Branch 2 → 4 not taken.
|
10 | m_name = name; |
| 133 | } | ||
| 134 | ✗ | catch (...) | |
| 135 | { | ||
| 136 | ✗ | } | |
| 137 | 10 | } | |
| 138 | |||
| 139 | private: | ||
| 140 | ToggleWarningKind m_kind{ToggleWarningKind::None}; | ||
| 141 | std::string m_name; | ||
| 142 | std::uintptr_t m_target{0}; | ||
| 143 | PatchWitness m_witness{PatchWitness::Indeterminate}; | ||
| 144 | }; | ||
| 145 | |||
| 146 | /** | ||
| 147 | * @class TargetSlot | ||
| 148 | * @brief Holds a target's ledger write slot across a toggle that can alter its bytes. | ||
| 149 | * @details The slot blocks every same-target install while held, so it MUST be released before the | ||
| 150 | * caller runs user code or takes the loader lock. | ||
| 151 | */ | ||
| 152 | class TargetSlot | ||
| 153 | { | ||
| 154 | public: | ||
| 155 | 7397 | TargetSlot(std::uintptr_t target, std::uint64_t id) noexcept | |
| 156 | 7397 | : m_target(target), m_id(id), | |
| 157 | 7397 | m_newer(DetourModKit::detail::HookLedger::instance().acquire_target_slot(target, id)) | |
| 158 | { | ||
| 159 | 7397 | } | |
| 160 | |||
| 161 | 7397 | ~TargetSlot() noexcept { release(); } | |
| 162 | |||
| 163 | TargetSlot(const TargetSlot &) = delete; | ||
| 164 | TargetSlot &operator=(const TargetSlot &) = delete; | ||
| 165 | TargetSlot(TargetSlot &&) = delete; | ||
| 166 | TargetSlot &operator=(TargetSlot &&) = delete; | ||
| 167 | |||
| 168 | 14744 | void release() noexcept | |
| 169 | { | ||
| 170 |
2/2✓ Branch 3 → 4 taken 7397 times.
✓ Branch 3 → 6 taken 7347 times.
|
14744 | if (!std::exchange(m_released, true)) |
| 171 | { | ||
| 172 | 7397 | DetourModKit::detail::HookLedger::instance().release_target_slot(m_target, m_id); | |
| 173 | } | ||
| 174 | 14744 | } | |
| 175 | |||
| 176 | /// Reports whether this hook is the newest live layer with authority to write target bytes. | ||
| 177 | 7397 | [[nodiscard]] bool is_top_layer() const noexcept { return m_newer == 0; } | |
| 178 | |||
| 179 | private: | ||
| 180 | std::uintptr_t m_target; | ||
| 181 | std::uint64_t m_id; | ||
| 182 | std::size_t m_newer; | ||
| 183 | bool m_released{false}; | ||
| 184 | }; | ||
| 185 | |||
| 186 | /** | ||
| 187 | * @brief Writes the published state, its population unit, and the gate callable for one outcome. | ||
| 188 | * @details Runs under the call gate that serialized the transition. An armed inline hook publishes its | ||
| 189 | * trampoline, while a mid hook gate stays null. A disarm clears the callable, so a later call() | ||
| 190 | * returns the inactive default. | ||
| 191 | */ | ||
| 192 | 7349 | template <class ImplT, class GateT> void write_toggle_state(ImplT &impl, GateT &gate, bool armed) noexcept | |
| 193 | { | ||
| 194 |
2/2✓ Branch 2 → 3 taken 3801 times.
✓ Branch 2 → 7 taken 3548 times.
|
7349 | if (armed) |
| 195 | { | ||
| 196 | 3801 | impl.status.store(HookState::Active, std::memory_order_release); | |
| 197 | 3801 | DetourModKit::detail::hook_population::record_enabled(); | |
| 198 | 3801 | gate.callable = inline_trampoline(impl.backend); | |
| 199 | } | ||
| 200 | else | ||
| 201 | { | ||
| 202 | 3548 | impl.status.store(HookState::Disabled, std::memory_order_release); | |
| 203 | 3548 | DetourModKit::detail::hook_population::record_disabled(); | |
| 204 | 3548 | gate.callable = nullptr; | |
| 205 | } | ||
| 206 | 7349 | } | |
| 207 | |||
| 208 | /** | ||
| 209 | * @brief Rewrites a published state when attributable target bytes contradict it. | ||
| 210 | * @details The rewrite updates population, gate callable, and backend flag before the caller retries. | ||
| 211 | * The caller performs the retry under the same locks. | ||
| 212 | * @note It emits no event because the caller still owns the call gate and target slot. | ||
| 213 | */ | ||
| 214 | template <class ImplT, class GateT> | ||
| 215 | 2 | void reconcile_published_state(ImplT &impl, GateT &gate, bool armed) noexcept | |
| 216 | { | ||
| 217 | 2 | write_toggle_state(impl, gate, armed); | |
| 218 | ( | ||
| 219 | void | ||
| 220 | 4 | )apply_backend(impl.backend, [armed](auto &backend) noexcept { backend.reconcile_enabled(armed); }); | |
| 221 | 2 | } | |
| 222 | |||
| 223 | /** | ||
| 224 | * @brief Publishes one enable or disable outcome in the single toggle order. | ||
| 225 | * @details The identity snapshot, slot release, unlock, and emission follow the state write | ||
| 226 | * (HookLifecycleName.*). The caller must read no hook state after this call because a subscriber | ||
| 227 | * can destroy the hook before emission ends. | ||
| 228 | */ | ||
| 229 | template <class ImplT, class GateT> | ||
| 230 | 7347 | void publish_toggle( | |
| 231 | ImplT &impl, | ||
| 232 | GateT &gate, | ||
| 233 | TargetSlot &slot, | ||
| 234 | std::unique_lock<std::recursive_mutex> &guard, | ||
| 235 | bool armed | ||
| 236 | ) noexcept | ||
| 237 | { | ||
| 238 | 7347 | write_toggle_state(impl, gate, armed); | |
| 239 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 240 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 14 taken 7345 times.
|
7347 | if (auto *probe = DetourModKit::detail::g_hook_toggle_publication_probe) |
| 241 | { | ||
| 242 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 1 time.
|
2 | const HookState expected = armed ? HookState::Active : HookState::Disabled; |
| 243 |
3/4✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 10 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
|
2 | const bool callable_matches = (gate.callable != nullptr) == (armed && impl.is_inline); |
| 244 | 2 | probe( | |
| 245 | armed, | ||
| 246 | 2 | guard.owns_lock(), | |
| 247 | 2 | impl.status.load(std::memory_order_relaxed) == expected, | |
| 248 | callable_matches | ||
| 249 | ); | ||
| 250 | } | ||
| 251 | #endif | ||
| 252 | 7347 | const LifecycleSnapshot snapshot = snapshot_lifecycle(impl.name, impl.ledger_id, impl.is_inline); | |
| 253 | 7347 | slot.release(); | |
| 254 | 7347 | guard.unlock(); | |
| 255 |
2/2✓ Branch 17 → 18 taken 3800 times.
✓ Branch 17 → 19 taken 3547 times.
|
14694 | emit_lifecycle( |
| 256 | snapshot.name, | ||
| 257 | 7347 | snapshot.ledger_id, | |
| 258 | 7347 | snapshot.kind, | |
| 259 | armed ? diagnostics::HookTransition::Enabled : diagnostics::HookTransition::Disabled | ||
| 260 | ); | ||
| 261 | 7347 | } | |
| 262 | } // namespace | ||
| 263 | |||
| 264 | 3828 | Result<void> Hook::enable() noexcept | |
| 265 | { | ||
| 266 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 9 taken 3827 times.
|
3828 | if (std::optional<Error> vetoed = refuse_on_loader_lock("hook::enable")) |
| 267 | { | ||
| 268 | 1 | return std::unexpected(*vetoed); | |
| 269 | } | ||
| 270 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 271 | 3827 | note_loader_veto_passed(DetourModKit::detail::HookLoaderEntry::Enable); | |
| 272 | #endif | ||
| 273 |
2/2✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 15 taken 3826 times.
|
3827 | if (!m_impl) |
| 274 | { | ||
| 275 | 1 | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"}); | |
| 276 | } | ||
| 277 | // A live handle always has a gate. The null check fails closed on the broken invariant. | ||
| 278 | 3826 | const std::shared_ptr<CallGate> gate = m_gate.load(std::memory_order_acquire); | |
| 279 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 21 taken 3826 times.
|
3826 | if (!gate) |
| 280 | { | ||
| 281 | ✗ | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"}); | |
| 282 | } | ||
| 283 | 3826 | DeferredToggleWarning deferred_warning; | |
| 284 | 3826 | std::unique_lock<std::recursive_mutex> guard = acquire_call_lock(gate.get()); | |
| 285 |
1/2✗ Branch 25 → 26 not taken.
✓ Branch 25 → 29 taken 3826 times.
|
3826 | if (!guard.owns_lock()) |
| 286 | { | ||
| 287 | ✗ | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"}); | |
| 288 | } | ||
| 289 | 7652 | if (!backend_value_or( | |
| 290 | 3826 | m_impl->backend, | |
| 291 | false, | ||
| 292 |
1/2✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 3826 times.
|
7652 | [](auto &backend) noexcept { return static_cast<bool>(backend); } |
| 293 | )) | ||
| 294 | { | ||
| 295 | ✗ | return std::unexpected(Error{ErrorCode::BackendFailed, "hook::enable"}); | |
| 296 | } | ||
| 297 | |||
| 298 | // Only the newest live layer can write target bytes. The slot makes the {decide, patch} pair atomic | ||
| 299 | // against a concurrent same-target install. Claim it before the state CAS so a refusal changes nothing. | ||
| 300 | 3826 | TargetSlot slot(m_impl->target, m_impl->ledger_id); | |
| 301 |
2/2✓ Branch 39 → 40 taken 5 times.
✓ Branch 39 → 44 taken 3821 times.
|
3826 | if (!slot.is_top_layer()) |
| 302 | { | ||
| 303 | 5 | return std::unexpected(Error{ErrorCode::LayerConflict, "hook::enable", m_impl->target}); | |
| 304 | } | ||
| 305 | |||
| 306 | // Classify before the transition claim. The backend emits its jmp over the current bytes. Foreign or | ||
| 307 | // unreadable bytes must refuse here while the hook remains as the caller left it. | ||
| 308 | 3821 | const PatchWitness before = witness_of(m_impl->backend); | |
| 309 |
2/2✓ Branch 47 → 48 taken 5 times.
✓ Branch 47 → 55 taken 3816 times.
|
3821 | if (!witness_permits_write(before)) |
| 310 | { | ||
| 311 | 5 | deferred_warning.arm(ToggleWarningKind::EnableRefused, m_impl->name, m_impl->target, before); | |
| 312 | 5 | return std::unexpected(Error{ErrorCode::EnableFailed, "hook::enable", m_impl->target}); | |
| 313 | } | ||
| 314 | |||
| 315 | 3816 | HookState expected = HookState::Disabled; | |
| 316 |
2/2✓ Branch 57 → 58 taken 11 times.
✓ Branch 57 → 72 taken 3805 times.
|
3816 | if (!m_impl->status.compare_exchange_strong(expected, HookState::Enabling, std::memory_order_acq_rel)) |
| 317 | { | ||
| 318 |
1/2✗ Branch 58 → 59 not taken.
✓ Branch 58 → 62 taken 11 times.
|
11 | if (expected != HookState::Active) |
| 319 | { | ||
| 320 | ✗ | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"}); | |
| 321 | } | ||
| 322 | // Active answers the request only over this hook's own patch. Here `before` is Original or | ||
| 323 | // OwnedPatch, because every other class refused above. | ||
| 324 |
2/2✓ Branch 62 → 63 taken 10 times.
✓ Branch 62 → 64 taken 1 time.
|
11 | if (before == PatchWitness::OwnedPatch) |
| 325 | { | ||
| 326 | 10 | return {}; | |
| 327 | } | ||
| 328 | // The bytes prove the target is unpatched. Rewrite the stale claim, then arm through the ordinary | ||
| 329 | // path so byte authority and the population tally both stay exact. | ||
| 330 | 1 | deferred_warning.arm(ToggleWarningKind::EnableReconciled, m_impl->name, m_impl->target); | |
| 331 | 1 | reconcile_published_state(*m_impl, *gate, false); | |
| 332 | 1 | m_impl->status.store(HookState::Enabling, std::memory_order_release); | |
| 333 | } | ||
| 334 | // Create leaves the target unpatched, so this is the first operation that can make the detour reachable. | ||
| 335 | 3806 | const bool backend_enabled = backend_value_or( | |
| 336 | 3806 | m_impl->backend, | |
| 337 | false, | ||
| 338 | 7612 | [](auto &backend) noexcept { return try_backend_enable(backend); } | |
| 339 | ); | ||
| 340 | 3806 | const bool patch_confirmed = backend_value_or( | |
| 341 | 3806 | m_impl->backend, | |
| 342 | false, | ||
| 343 | 7612 | [](auto &backend) noexcept { return enable_patch_is_confirmed(backend); } | |
| 344 | ); | ||
| 345 |
4/4✓ Branch 76 → 77 taken 3795 times.
✓ Branch 76 → 82 taken 11 times.
✓ Branch 77 → 78 taken 3785 times.
✓ Branch 77 → 82 taken 10 times.
|
3806 | if (backend_enabled && patch_confirmed) |
| 346 | { | ||
| 347 | 3785 | publish_toggle(*m_impl, *gate, slot, guard, true); | |
| 348 | 3785 | return {}; | |
| 349 | } | ||
| 350 | // A backend error says nothing about the target. The patch commits inside the thread trap transaction, so | ||
| 351 | // an error can sit over a fully armed target. Witness the bytes before publication. | ||
| 352 |
2/2✓ Branch 82 → 83 taken 11 times.
✓ Branch 82 → 108 taken 10 times.
|
21 | if (!backend_enabled) |
| 353 | { | ||
| 354 | const bool mutation_committed = | ||
| 355 | 22 | backend_value_or(m_impl->backend, false, [](auto &backend) noexcept { return backend.enabled(); }); | |
| 356 | 11 | const PatchWitness after_failure = witness_of(m_impl->backend); | |
| 357 |
3/4✓ Branch 87 → 88 taken 9 times.
✓ Branch 87 → 99 taken 2 times.
✓ Branch 88 → 89 taken 9 times.
✗ Branch 88 → 99 not taken.
|
11 | if (mutation_committed && after_failure != PatchWitness::Original) |
| 358 | { | ||
| 359 | // The mutation committed, so Original is the only witness that proves the hook disarmed. Retain | ||
| 360 | // the conservative Active state and report that safe disarm lacks confirmation. | ||
| 361 | 9 | const std::uintptr_t armed_target = m_impl->target; | |
| 362 | 9 | publish_toggle(*m_impl, *gate, slot, guard, true); | |
| 363 | 9 | const ErrorCode code = | |
| 364 |
2/2✓ Branch 93 → 94 taken 7 times.
✓ Branch 93 → 95 taken 2 times.
|
9 | after_failure == PatchWitness::OwnedPatch ? ErrorCode::BackendFailed : ErrorCode::DisableFailed; |
| 365 | 9 | return std::unexpected(Error{code, "hook::enable", armed_target}); | |
| 366 | } | ||
| 367 | // The backend committed no mutation, or the target already returned to Original. This hook is disarmed. | ||
| 368 |
1/2✓ Branch 99 → 100 taken 2 times.
✗ Branch 99 → 103 not taken.
|
2 | if (after_failure == PatchWitness::Original) |
| 369 | { | ||
| 370 | ( | ||
| 371 | void | ||
| 372 | 4 | )apply_backend(m_impl->backend, [](auto &backend) noexcept { backend.reconcile_enabled(false); }); | |
| 373 | } | ||
| 374 | 2 | m_impl->status.store(HookState::Disabled, std::memory_order_release); | |
| 375 | 2 | return std::unexpected(Error{ErrorCode::EnableFailed, "hook::enable"}); | |
| 376 | } | ||
| 377 | |||
| 378 | // The backend reported success but the bytes are not this hook's patch. Publish Disabled only after a | ||
| 379 | // compensation disable leaves the prologue at its original bytes. The rollback receives the same | ||
| 380 | // classification as any other toggle. A third party that took the window can otherwise lose its bytes to | ||
| 381 | // the unconditional restore. Refusal therefore falls through to the Active publication below. | ||
| 382 |
2/2✓ Branch 111 → 112 taken 8 times.
✓ Branch 111 → 124 taken 2 times.
|
20 | if (const PatchWitness rollback_before = witness_of(m_impl->backend); |
| 383 | 10 | witness_permits_write(rollback_before)) | |
| 384 | { | ||
| 385 | 8 | (void)backend_value_or( | |
| 386 | 8 | m_impl->backend, | |
| 387 | false, | ||
| 388 | 16 | [](auto &backend) noexcept { return try_backend_disable(backend); } | |
| 389 | ); | ||
| 390 |
2/2✓ Branch 116 → 117 taken 4 times.
✓ Branch 116 → 124 taken 4 times.
|
8 | if (witness_of(m_impl->backend) == PatchWitness::Original) |
| 391 | { | ||
| 392 | ( | ||
| 393 | void | ||
| 394 | 8 | )apply_backend(m_impl->backend, [](auto &backend) noexcept { backend.reconcile_enabled(false); }); | |
| 395 | 4 | m_impl->status.store(HookState::Disabled, std::memory_order_release); | |
| 396 | 4 | return std::unexpected(Error{ErrorCode::EnableFailed, "hook::enable"}); | |
| 397 | } | ||
| 398 | } | ||
| 399 | |||
| 400 | // A completed restore can be followed by a newer or uncertain owner. Retain backend reachability so | ||
| 401 | // is_enabled() and a later disable retry agree with the conservative Active state. | ||
| 402 | 12 | (void)apply_backend(m_impl->backend, [](auto &backend) noexcept { backend.reconcile_enabled(true); }); | |
| 403 | 6 | publish_toggle(*m_impl, *gate, slot, guard, true); | |
| 404 | 6 | return std::unexpected(Error{ErrorCode::DisableFailed, "hook::enable"}); | |
| 405 | 3826 | } | |
| 406 | |||
| 407 | 3573 | Result<void> Hook::disable() noexcept | |
| 408 | { | ||
| 409 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 9 taken 3572 times.
|
3573 | if (std::optional<Error> vetoed = refuse_on_loader_lock("hook::disable")) |
| 410 | { | ||
| 411 | 1 | return std::unexpected(*vetoed); | |
| 412 | } | ||
| 413 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 414 | 3572 | note_loader_veto_passed(DetourModKit::detail::HookLoaderEntry::Disable); | |
| 415 | #endif | ||
| 416 |
2/2✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 15 taken 3571 times.
|
3572 | if (!m_impl) |
| 417 | { | ||
| 418 | 1 | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"}); | |
| 419 | } | ||
| 420 | 3571 | const std::shared_ptr<CallGate> gate = m_gate.load(std::memory_order_acquire); | |
| 421 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 21 taken 3571 times.
|
3571 | if (!gate) |
| 422 | { | ||
| 423 | ✗ | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"}); | |
| 424 | } | ||
| 425 | 3571 | DeferredToggleWarning deferred_warning; | |
| 426 | 3571 | std::unique_lock<std::recursive_mutex> guard = acquire_call_lock(gate.get()); | |
| 427 |
1/2✗ Branch 25 → 26 not taken.
✓ Branch 25 → 29 taken 3571 times.
|
3571 | if (!guard.owns_lock()) |
| 428 | { | ||
| 429 | ✗ | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"}); | |
| 430 | } | ||
| 431 | 7142 | if (!backend_value_or( | |
| 432 | 3571 | m_impl->backend, | |
| 433 | false, | ||
| 434 |
1/2✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 3571 times.
|
7142 | [](auto &backend) noexcept { return static_cast<bool>(backend); } |
| 435 | )) | ||
| 436 | { | ||
| 437 | ✗ | return std::unexpected(Error{ErrorCode::BackendFailed, "hook::disable"}); | |
| 438 | } | ||
| 439 | |||
| 440 | // Only the newest live layer can write target bytes. A restore of this hook's saved prologue below a newer | ||
| 441 | // layer clobbers it. Refuse without any mutation. | ||
| 442 | 3571 | TargetSlot slot(m_impl->target, m_impl->ledger_id); | |
| 443 |
2/2✓ Branch 39 → 40 taken 4 times.
✓ Branch 39 → 44 taken 3567 times.
|
3571 | if (!slot.is_top_layer()) |
| 444 | { | ||
| 445 | 4 | return std::unexpected(Error{ErrorCode::LayerConflict, "hook::disable", m_impl->target}); | |
| 446 | } | ||
| 447 | |||
| 448 | // Classify before the transition claim (see enable()). The unconditional prologue restore clobbers a | ||
| 449 | // foreign writer's bytes. Refuse and leave the hook Active. | ||
| 450 | 3567 | const PatchWitness before = witness_of(m_impl->backend); | |
| 451 |
2/2✓ Branch 47 → 48 taken 3 times.
✓ Branch 47 → 55 taken 3564 times.
|
3567 | if (!witness_permits_write(before)) |
| 452 | { | ||
| 453 | 3 | deferred_warning.arm(ToggleWarningKind::DisableRefused, m_impl->name, m_impl->target, before); | |
| 454 | 3 | return std::unexpected(Error{ErrorCode::DisableFailed, "hook::disable", m_impl->target}); | |
| 455 | } | ||
| 456 | |||
| 457 | 3564 | HookState expected = HookState::Active; | |
| 458 |
2/2✓ Branch 57 → 58 taken 13 times.
✓ Branch 57 → 72 taken 3551 times.
|
3564 | if (!m_impl->status.compare_exchange_strong(expected, HookState::Disabling, std::memory_order_acq_rel)) |
| 459 | { | ||
| 460 |
1/2✗ Branch 58 → 59 not taken.
✓ Branch 58 → 62 taken 13 times.
|
13 | if (expected != HookState::Disabled) |
| 461 | { | ||
| 462 | ✗ | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"}); | |
| 463 | } | ||
| 464 | // Disabled answers the request only over the saved prologue (see enable() for the polarity). | ||
| 465 |
2/2✓ Branch 62 → 63 taken 12 times.
✓ Branch 62 → 64 taken 1 time.
|
13 | if (before == PatchWitness::Original) |
| 466 | { | ||
| 467 | 12 | return {}; | |
| 468 | } | ||
| 469 | 1 | deferred_warning.arm(ToggleWarningKind::DisableReconciled, m_impl->name, m_impl->target); | |
| 470 | 1 | reconcile_published_state(*m_impl, *gate, true); | |
| 471 | 1 | m_impl->status.store(HookState::Disabling, std::memory_order_release); | |
| 472 | } | ||
| 473 | // Confirm the saved prologue is back before Disabled publication. The witness is taken whatever the | ||
| 474 | // backend returns. An error can sit over restored bytes. A success without byte corroboration must not | ||
| 475 | // publish Disabled. | ||
| 476 | 3552 | const bool backend_disabled = backend_value_or( | |
| 477 | 3552 | m_impl->backend, | |
| 478 | false, | ||
| 479 | 7104 | [](auto &backend) noexcept { return try_backend_disable(backend); } | |
| 480 | ); | ||
| 481 | 3552 | const PatchWitness after = witness_of(m_impl->backend); | |
| 482 |
2/2✓ Branch 76 → 77 taken 3547 times.
✓ Branch 76 → 88 taken 5 times.
|
3552 | if (after == PatchWitness::Original) |
| 483 | { | ||
| 484 | 7094 | (void)apply_backend(m_impl->backend, [](auto &backend) noexcept { backend.reconcile_enabled(false); }); | |
| 485 | 3547 | const std::uintptr_t target = m_impl->target; | |
| 486 | 3547 | publish_toggle(*m_impl, *gate, slot, guard, false); | |
| 487 |
2/2✓ Branch 83 → 84 taken 5 times.
✓ Branch 83 → 87 taken 3542 times.
|
3547 | if (!backend_disabled) |
| 488 | { | ||
| 489 | // The disarm took effect, but the backend reported a post-commit failure (its page-protection | ||
| 490 | // restore). Report it rather than swallow it. | ||
| 491 | 5 | return std::unexpected(Error{ErrorCode::BackendFailed, "hook::disable", target}); | |
| 492 | } | ||
| 493 | 3542 | return {}; | |
| 494 | } | ||
| 495 | // A completed restore can be followed by a newer or uncertain owner. Retain backend reachability so | ||
| 496 | // is_enabled() and a later disable retry agree with the conservative Active state. | ||
| 497 | 10 | (void)apply_backend(m_impl->backend, [](auto &backend) noexcept { backend.reconcile_enabled(true); }); | |
| 498 | 5 | m_impl->status.store(HookState::Active, std::memory_order_release); | |
| 499 | 5 | return std::unexpected(Error{ErrorCode::DisableFailed, "hook::disable"}); | |
| 500 | 3571 | } | |
| 501 | } // namespace hook | ||
| 502 | } // namespace DetourModKit | ||
| 503 |