./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
365 / 423
86.3%
Branches:
184 / 238
77.3%
Functions:
63 / 66
95.5%

src/hook_toggle.cpp
Line Branch Exec Source
1 /**
2 * @file hook_toggle.cpp
3 * @brief This TU implements Hook::enable, Hook::disable, and the HookStack::install arm over the shared toggle
4 * publication order.
5 */
6
7 #include "DetourModKit/hook.hpp"
8
9 #include "internal/diagnostics_population.hpp"
10 #include "internal/hook_backend.hpp"
11 #include "internal/hook_backend_visit.hpp"
12 #include "internal/hook_coordinator.hpp"
13 #include "internal/hook_emission.hpp"
14 #include "internal/hook_ledger.hpp"
15 #include "internal/hook_patch_witness.hpp"
16 #include "internal/hook_publication.hpp"
17
18 #include "DetourModKit/logger.hpp"
19
20 #include <atomic>
21 #include <chrono>
22 #include <cstddef>
23 #include <cstdint>
24 #include <memory>
25 #include <mutex>
26 #include <optional>
27 #include <string>
28 #include <string_view>
29 #include <thread>
30 #include <tuple>
31 #include <utility>
32
33 namespace DetourModKit
34 {
35 namespace hook
36 {
37 namespace
38 {
39 using DetourModKit::detail::apply_backend;
40 using DetourModKit::detail::arm_witness_of;
41 using DetourModKit::detail::backend_claimed;
42 using DetourModKit::detail::backend_layer_chains_here;
43 using DetourModKit::detail::backend_value_or;
44 using DetourModKit::detail::CoordinatorRefusal;
45 using DetourModKit::detail::emit_lifecycle;
46 using DetourModKit::detail::enable_patch_is_confirmed;
47 using DetourModKit::detail::inline_trampoline;
48 using DetourModKit::detail::LifecycleSnapshot;
49 using DetourModKit::detail::PatchWitness;
50 using DetourModKit::detail::snapshot_lifecycle;
51 using DetourModKit::detail::switch_backend;
52 using DetourModKit::detail::try_backend_disable;
53 using DetourModKit::detail::try_backend_enable;
54 using DetourModKit::detail::witness_description;
55 using DetourModKit::detail::witness_of;
56 using DetourModKit::detail::witness_permits_write;
57 using DetourModKit::detail::witness_promotes_arm;
58
59 /**
60 * @brief Defers one toggle log line until later-declared lock guards release.
61 * @details Declare it before the call-gate lock and target slot. Its destructor runs after both guards
62 * release. HookToggleWarning.* pins the witness and bypass texts, HookLayerBypass.* pins the
63 * layer-count bypass text, and Lifecycle.RouteCopiesReportCoordinatorRefusal pins the coordinator
64 * warning texts.
65 */
66 class DeferredToggleLog
67 {
68 public:
69 22431 DeferredToggleLog() = default;
70
71 22431 ~DeferredToggleLog() noexcept
72 7 {
73
2/2
✓ Branch 2 → 3 taken 681 times.
✓ Branch 2 → 5 taken 21750 times.
22431 if (m_bypass)
74 {
75 681 log_bypass();
76 681 return;
77 }
78
2/2
✓ Branch 5 → 6 taken 21733 times.
✓ Branch 5 → 7 taken 17 times.
21750 if (m_text == nullptr)
79 {
80 21733 return;
81 }
82
2/2
✓ Branch 8 → 9 taken 10 times.
✓ Branch 8 → 12 taken 7 times.
17 if (!m_reason.empty())
83 {
84 10 (void)log().try_log(
85 LogLevel::Warning,
86 "hook: '{}' at 0x{:0{}X} {}: {}.",
87 10 m_name,
88 10 m_target,
89 20 sizeof(std::uintptr_t) * 2,
90 10 m_text,
91 10 m_reason
92 );
93 10 return;
94 }
95 7 (void)log().try_log(
96 LogLevel::Warning,
97 "hook: '{}' at 0x{:0{}X} {}.",
98 7 m_name,
99 7 m_target,
100 14 sizeof(std::uintptr_t) * 2,
101 7 m_text
102 );
103
2/2
✓ Branch 16 → 17 taken 7 times.
✓ Branch 16 → 18 taken 22424 times.
22431 }
104
105 DeferredToggleLog(const DeferredToggleLog &) = delete;
106 DeferredToggleLog &operator=(const DeferredToggleLog &) = delete;
107 DeferredToggleLog(DeferredToggleLog &&) = delete;
108 DeferredToggleLog &operator=(DeferredToggleLog &&) = delete;
109
110 /**
111 * @brief Stores one warning and contains any name-copy failure.
112 * @param text A string literal: only its pointer is stored.
113 * @param reason A static description that appends as ": <reason>", or empty.
114 */
115 void
116 17 arm(const char *text,
117 const std::string &name,
118 std::uintptr_t target,
119 std::string_view reason = {}) noexcept
120 {
121 17 m_text = text;
122 17 m_target = target;
123 17 m_reason = reason;
124 17 copy_name(name);
125 17 }
126
127 /**
128 * @brief Stores one Info line for a bypass under a newer layer.
129 * @param newer_layers The newer hooks of this kit, or zero when @p witness or a coordinator refusal
130 * proved the newer layer.
131 */
132 681 void arm_bypass(
133 const std::string &name,
134 std::uintptr_t target,
135 std::size_t newer_layers,
136 std::optional<PatchWitness> witness
137 ) noexcept
138 {
139 681 m_bypass = true;
140 681 m_target = target;
141 681 m_newer_layers = newer_layers;
142 681 m_witness = witness;
143 681 copy_name(name);
144 681 }
145
146 /// Moves the stored warning out, so this object logs nothing. Returns the text and the reason.
147 4231 [[nodiscard]] std::pair<const char *, std::string_view> detach() noexcept
148 {
149 4231 return {std::exchange(m_text, nullptr), m_reason};
150 }
151
152 private:
153 698 void copy_name(const std::string &name) noexcept
154 {
155 try
156 {
157
1/2
✓ Branch 2 → 3 taken 698 times.
✗ Branch 2 → 4 not taken.
698 m_name = name;
158 }
159 ✗ catch (...)
160 {
161 ✗ }
162 698 }
163
164 681 void log_bypass() const noexcept
165 {
166
2/2
✓ Branch 2 → 3 taken 658 times.
✓ Branch 2 → 6 taken 23 times.
681 if (m_newer_layers != 0)
167 {
168 658 (void)log().try_log(
169 LogLevel::Info,
170 "hook: '{}' at 0x{:0{}X} bypassed under {} newer layer(s).",
171 658 m_name,
172 658 m_target,
173 1316 sizeof(std::uintptr_t) * 2,
174 658 m_newer_layers
175 );
176 658 return;
177 }
178 23 (void)log().try_log(
179 LogLevel::Info,
180 "hook: '{}' at 0x{:0{}X} bypassed under a newer layer: {}.",
181 23 m_name,
182 23 m_target,
183 23 sizeof(std::uintptr_t) * 2,
184 23 m_witness
185
2/2
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 11 taken 1 time.
46 ? witness_description(*m_witness)
186 1 : DetourModKit::detail::coordinator_refusal_description(CoordinatorRefusal::LayerConflict)
187 );
188 }
189
190 const char *m_text{nullptr};
191 std::string_view m_reason;
192 std::string m_name;
193 std::uintptr_t m_target{0};
194 std::optional<PatchWitness> m_witness;
195 std::size_t m_newer_layers{0};
196 bool m_bypass{false};
197 };
198
199 /**
200 * @class TargetSlot
201 * @brief Holds a target's ledger write slot across a toggle that can alter its bytes.
202 * @details The slot blocks every same-target install while held, so it MUST be released before the
203 * caller runs user code or takes the loader lock. A coordinator hold nests inside the slot and
204 * releases first (`[B-109]`).
205 */
206 class TargetSlot
207 {
208 public:
209 /// Selects the slot that HookStack::install keeps from its reservation.
210 struct Kept
211 {
212 };
213
214 16829 TargetSlot(std::uintptr_t target, std::uint64_t id) noexcept
215 16829 : m_target(target), m_id(id),
216 16829 m_newer(DetourModKit::detail::HookLedger::instance().acquire_target_slot(target, id))
217 {
218 16829 }
219
220 /**
221 * @brief Adopts the slot of HookLedger::commit_hook_keep_slot, whose install holds the coordinator.
222 * @details That call proved the hook the top layer of this kit. The install releases the slot and
223 * emits the lifecycle event that @ref defer_emission records.
224 */
225 4231 TargetSlot(std::uintptr_t target, std::uint64_t id, Kept) noexcept
226 4231 : m_target(target), m_id(id), m_newer(0), m_released(true), m_kept(true)
227 {
228 4231 }
229
230 21060 ~TargetSlot() noexcept { release(); }
231
232 TargetSlot(const TargetSlot &) = delete;
233 TargetSlot &operator=(const TargetSlot &) = delete;
234 TargetSlot(TargetSlot &&) = delete;
235 TargetSlot &operator=(TargetSlot &&) = delete;
236
237 /**
238 * @brief Waits until this thread holds the backend coordinator, so the backend toggle acquires nested.
239 * @return False for a refusal that no retry changes. @ref coordinator_refusal names it.
240 */
241 20399 [[nodiscard]] bool hold_coordinator(std::string_view where, std::string_view name) noexcept
242 {
243
2/2
✓ Branch 2 → 3 taken 4231 times.
✓ Branch 2 → 4 taken 16168 times.
20399 if (m_kept)
244 {
245 4231 return true;
246 }
247 16168 m_coordinator.emplace(DetourModKit::detail::WaitUntilHeld{where, name});
248 16168 return static_cast<bool>(*m_coordinator);
249 }
250
251 /// Records @p transition for the install of a kept slot. Returns false, and records nothing, otherwise.
252 20997 [[nodiscard]] bool defer_emission(diagnostics::HookTransition transition) noexcept
253 {
254
2/2
✓ Branch 2 → 3 taken 4229 times.
✓ Branch 2 → 5 taken 16768 times.
20997 if (m_kept)
255 {
256 4229 m_deferred_transition = transition;
257 }
258 20997 return m_kept;
259 }
260
261 4231 [[nodiscard]] std::optional<diagnostics::HookTransition> deferred_transition() const noexcept
262 {
263 4231 return m_deferred_transition;
264 }
265
266 2 [[nodiscard]] DetourModKit::detail::CoordinatorHoldRefusal coordinator_refusal() const noexcept
267 {
268
1/2
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
2 return m_coordinator ? m_coordinator->refusal()
269 ✗ : DetourModKit::detail::CoordinatorHoldRefusal::None;
270 }
271
272 37828 void release() noexcept
273 {
274 37828 m_coordinator.reset();
275
2/2
✓ Branch 4 → 5 taken 16829 times.
✓ Branch 4 → 7 taken 20999 times.
37828 if (!std::exchange(m_released, true))
276 {
277 16829 DetourModKit::detail::HookLedger::instance().release_target_slot(m_target, m_id);
278 }
279 37828 }
280
281 /// Reports whether this hook is the newest live layer with authority to write target bytes.
282 5032 [[nodiscard]] bool is_top_layer() const noexcept { return m_newer == 0; }
283
284 /// Reports whether the ledger failed to decide a count. The layer order is then undecided.
285 27394 [[nodiscard]] bool unavailable() const noexcept
286 {
287 27394 return m_newer == DetourModKit::detail::HookLedger::SLOT_UNAVAILABLE;
288 }
289
290 /// Returns the newer hooks of this kit, or zero when the ledger failed to decide a count.
291
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6334 times.
6334 [[nodiscard]] std::size_t newer_layers() const noexcept { return unavailable() ? 0 : m_newer; }
292
293 private:
294 std::uintptr_t m_target;
295 std::uint64_t m_id;
296 std::size_t m_newer;
297 bool m_released{false};
298 bool m_kept{false};
299 std::optional<diagnostics::HookTransition> m_deferred_transition;
300 std::optional<DetourModKit::detail::BackendCoordinatorHold> m_coordinator;
301 };
302
303 /**
304 * @brief Writes the published state, its population unit, and the gate callable for one outcome.
305 * @details Runs under the call gate that serialized the transition. An armed inline hook publishes its
306 * trampoline, while a mid hook gate stays null. A disarm clears the callable, so a later call()
307 * returns the inactive default.
308 */
309 22375 template <class ImplT, class GateT> void write_toggle_state(ImplT &impl, GateT &gate, bool armed) noexcept
310 {
311
2/2
✓ Branch 2 → 3 taken 16710 times.
✓ Branch 2 → 7 taken 5665 times.
22375 if (armed)
312 {
313 16710 impl.status.store(HookState::Active, std::memory_order_release);
314 16710 DetourModKit::detail::hook_population::record_enabled();
315 16710 gate.callable = inline_trampoline(impl.backend);
316 }
317 else
318 {
319 5665 impl.status.store(HookState::Disabled, std::memory_order_release);
320 5665 DetourModKit::detail::hook_population::record_disabled();
321 5665 gate.callable = nullptr;
322 }
323 22375 }
324
325 /**
326 * @brief Rewrites a published state when attributable target bytes contradict it.
327 * @details The rewrite updates population, gate callable, and backend flag before the caller retries.
328 * The caller performs the retry under the same locks.
329 * @note It emits no event because the caller still owns the call gate and target slot.
330 */
331 template <class ImplT, class GateT>
332 7 void reconcile_published_state(ImplT &impl, GateT &gate, bool armed) noexcept
333 {
334 7 write_toggle_state(impl, gate, armed);
335 (
336 void
337 14 )apply_backend(impl.backend, [armed](auto &backend) noexcept { backend.reconcile_enabled(armed); });
338 7 }
339
340 /**
341 * @brief Publishes one enable or disable outcome in the single toggle order.
342 * @details The identity snapshot, slot release, unlock, and emission follow the state write
343 * (HookLifecycleName.*). The caller must read no hook state after this call because a subscriber
344 * can destroy the hook before emission ends. A kept slot records the event for its install.
345 */
346 template <class ImplT, class GateT>
347 22368 void publish_toggle(
348 ImplT &impl,
349 GateT &gate,
350 TargetSlot *slot,
351 std::unique_lock<std::recursive_mutex> &guard,
352 bool armed
353 ) noexcept
354 {
355 22368 write_toggle_state(impl, gate, armed);
356 #if defined(DMK_ENABLE_TEST_SEAMS)
357
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 14 taken 22365 times.
22368 if (auto *probe = DetourModKit::detail::g_hook_toggle_publication_probe)
358 {
359
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 1 time.
3 const HookState expected = armed ? HookState::Active : HookState::Disabled;
360
3/4
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 10 taken 1 time.
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 10 not taken.
3 const bool callable_matches = (gate.callable != nullptr) == (armed && impl.is_inline);
361 3 probe(
362 armed,
363 3 guard.owns_lock(),
364 3 impl.status.load(std::memory_order_relaxed) == expected,
365 callable_matches
366 );
367 }
368 #endif
369 22368 const diagnostics::HookTransition transition =
370
2/2
✓ Branch 14 → 15 taken 16708 times.
✓ Branch 14 → 16 taken 5660 times.
22368 armed ? diagnostics::HookTransition::Enabled : diagnostics::HookTransition::Disabled;
371
6/6
✓ Branch 17 → 18 taken 20997 times.
✓ Branch 17 → 21 taken 1371 times.
✓ Branch 19 → 20 taken 4229 times.
✓ Branch 19 → 21 taken 16768 times.
✓ Branch 22 → 23 taken 4229 times.
✓ Branch 22 → 25 taken 18139 times.
22368 if (slot != nullptr && slot->defer_emission(transition))
372 {
373 4229 guard.unlock();
374 4229 return;
375 }
376 18139 const LifecycleSnapshot snapshot = snapshot_lifecycle(impl.name, impl.ledger_id, impl.is_inline);
377
2/2
✓ Branch 26 → 27 taken 16768 times.
✓ Branch 26 → 28 taken 1371 times.
18139 if (slot != nullptr)
378 {
379 16768 slot->release();
380 }
381 18139 guard.unlock();
382 18139 emit_lifecycle(snapshot.name, snapshot.ledger_id, snapshot.kind, transition);
383 18139 }
384
385 /**
386 * @brief Settles an enable that the backend reported as failed.
387 * @note The caller holds the call-gate lock and the target slot. An Active outcome releases both through
388 * publish_toggle(), so the caller must read no hook state after the call.
389 */
390 template <class ImplT, class GateT>
391 15 [[nodiscard]] Result<void> settle_backend_enable_failure(
392 ImplT &impl,
393 GateT &gate,
394 TargetSlot &slot,
395 std::unique_lock<std::recursive_mutex> &guard,
396 CoordinatorRefusal refusal
397 ) noexcept
398 {
399 // A backend error says nothing about the target. The patch commits inside the thread trap transaction,
400 // so an error can sit over a fully armed target. Witness the bytes before publication.
401 const bool mutation_committed =
402 30 backend_value_or(impl.backend, false, [](auto &backend) noexcept { return backend.enabled(); });
403 15 const PatchWitness after_failure = witness_of(impl.backend);
404
3/4
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 13 taken 5 times.
✓ Branch 5 → 6 taken 10 times.
✗ Branch 5 → 13 not taken.
15 if (mutation_committed && after_failure != PatchWitness::Original)
405 {
406 // The mutation committed, so Original is the only witness that proves the hook disarmed. Retain
407 // the conservative Active state. The returned error reports that a safe disarm is unproven.
408 10 const std::uintptr_t armed_target = impl.target;
409 10 publish_toggle(impl, gate, &slot, guard, true);
410 10 const ErrorCode code =
411
2/2
✓ Branch 7 → 8 taken 8 times.
✓ Branch 7 → 9 taken 2 times.
10 after_failure == PatchWitness::OwnedPatch ? ErrorCode::BackendFailed : ErrorCode::DisableFailed;
412 10 return std::unexpected(Error{code, "hook::enable", armed_target});
413 }
414 // The backend committed no mutation, or the target already returned to Original. This hook is disarmed.
415
2/2
✓ Branch 13 → 14 taken 4 times.
✓ Branch 13 → 16 taken 1 time.
5 if (after_failure == PatchWitness::Original)
416 {
417 8 (void)apply_backend(impl.backend, [](auto &backend) noexcept { backend.reconcile_enabled(false); });
418 }
419 5 impl.status.store(HookState::Disabled, std::memory_order_release);
420
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 21 taken 5 times.
5 if (refusal == CoordinatorRefusal::LayerConflict)
421 {
422 ✗ return std::unexpected(Error{ErrorCode::LayerConflict, "hook::enable", impl.target});
423 }
424
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 25 taken 4 times.
5 if (refusal == CoordinatorRefusal::CallerInTrampoline)
425 {
426 1 return std::unexpected(Error{ErrorCode::ReentrantCallRejected, "hook::enable", impl.target});
427 }
428 4 return std::unexpected(Error{ErrorCode::EnableFailed, "hook::enable"});
429 }
430
431 /**
432 * @brief Settles a backend enable success that did not leave this hook's own patch armed.
433 * @note The caller holds the call-gate lock and the target slot. An Active outcome releases both through
434 * publish_toggle(), so the caller must read no hook state after the call.
435 */
436 template <class ImplT, class GateT>
437 10 [[nodiscard]] Result<void> settle_unconfirmed_enable(
438 ImplT &impl,
439 GateT &gate,
440 TargetSlot &slot,
441 std::unique_lock<std::recursive_mutex> &guard
442 ) noexcept
443 {
444 // Disabled publishes only after a rollback disable leaves the prologue at its original bytes. The
445 // rollback receives the same classification as any other toggle. A third party that took the window can
446 // otherwise lose its bytes to the unconditional restore. Refusal therefore falls through to the Active
447 // publication below.
448
2/2
✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 13 taken 2 times.
20 if (const PatchWitness rollback_before = witness_of(impl.backend);
449 10 witness_permits_write(rollback_before))
450 {
451 8 (void)backend_value_or(
452 8 impl.backend,
453 false,
454 16 [](auto &backend) noexcept { return try_backend_disable(backend); }
455 );
456
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 13 taken 4 times.
8 if (witness_of(impl.backend) == PatchWitness::Original)
457 {
458 (
459 void
460 8 )apply_backend(impl.backend, [](auto &backend) noexcept { backend.reconcile_enabled(false); });
461 4 impl.status.store(HookState::Disabled, std::memory_order_release);
462 4 return std::unexpected(Error{ErrorCode::EnableFailed, "hook::enable"});
463 }
464 }
465
466 // A newer or uncertain owner can follow a completed restore. Retain backend reachability so
467 // is_enabled() and a later disable retry agree with the conservative Active state.
468 12 (void)apply_backend(impl.backend, [](auto &backend) noexcept { backend.reconcile_enabled(true); });
469 6 publish_toggle(impl, gate, &slot, guard, true);
470 6 return std::unexpected(Error{ErrorCode::DisableFailed, "hook::enable"});
471 }
472
473 /**
474 * @brief Switches a Disabling hook off without a target write and publishes Disabled.
475 * @details The position stays in the target's chain, so every caller runs past the detour. A switch refusal
476 * restores Active and returns DisableFailed with nothing else changed. A successful switch arms
477 * the Info line in @p note, unless @p note is null.
478 * @note The caller holds the call-gate lock and, when @p slot is non-null, the target slot. A Disabled
479 * outcome releases both through publish_toggle(), so the caller must read no hook state after the
480 * call.
481 */
482 template <class ImplT, class GateT>
483 691 [[nodiscard]] Result<void> publish_bypass(
484 ImplT &impl,
485 GateT &gate,
486 TargetSlot *slot,
487 std::unique_lock<std::recursive_mutex> &guard,
488 DeferredToggleLog *note,
489 std::size_t newer_layers,
490 std::optional<PatchWitness> witness
491 ) noexcept
492 {
493
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 691 times.
691 if (!switch_backend(impl.backend, false))
494 {
495 ✗ impl.status.store(HookState::Active, std::memory_order_release);
496 ✗ return std::unexpected(Error{ErrorCode::DisableFailed, "hook::disable", impl.target});
497 }
498 691 impl.bypassed = true;
499
2/2
✓ Branch 8 → 9 taken 681 times.
✓ Branch 8 → 10 taken 10 times.
691 if (note != nullptr)
500 {
501 681 note->arm_bypass(impl.name, impl.target, newer_layers, witness);
502 }
503 691 publish_toggle(impl, gate, slot, guard, false);
504 691 return {};
505 }
506
507 /**
508 * @brief Switches a Disabled hook back on without a target write and publishes Active.
509 * @note The caller holds the call-gate lock, and no target slot unless @p kept_slot is non-null. An Active
510 * outcome releases the lock through publish_toggle(), so the caller must read no hook state after the
511 * call.
512 */
513 template <class ImplT, class GateT>
514 1361 [[nodiscard]] Result<void> publish_resume(
515 ImplT &impl,
516 GateT &gate,
517 TargetSlot *kept_slot,
518 std::unique_lock<std::recursive_mutex> &guard
519 ) noexcept
520 {
521 1361 HookState expected = HookState::Disabled;
522
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 10 taken 1361 times.
1361 if (!impl.status.compare_exchange_strong(expected, HookState::Enabling, std::memory_order_acq_rel))
523 {
524 ✗ return expected == HookState::Active
525
0/2
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
✗ ? Result<void>{}
526 ✗ : std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"});
527 }
528
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 16 taken 1361 times.
1361 if (!switch_backend(impl.backend, true))
529 {
530 ✗ impl.status.store(HookState::Disabled, std::memory_order_release);
531 ✗ return std::unexpected(Error{ErrorCode::EnableFailed, "hook::enable", impl.target});
532 }
533 1361 impl.bypassed = false;
534 1361 publish_toggle(impl, gate, kept_slot, guard, true);
535 1361 return {};
536 }
537
538 /// Takes the call gate again after a wait. A lock that throws fails closed, as the first acquisition does.
539 ✗ [[nodiscard]] bool relock_call_gate(std::unique_lock<std::recursive_mutex> &guard) noexcept
540 {
541 try
542 {
543
0/2
✗ Branch 2 → 3 not taken.
✗ Branch 2 → 5 not taken.
✗ guard.lock();
544 ✗ return true;
545 }
546 ✗ catch (...)
547 {
548 ✗ return false;
549 ✗ }
550 }
551
552 /**
553 * @brief Runs the arm of Hook::enable after its call-gate lock and backend check.
554 * @param kept_slot The kept slot of HookStack::install, or null to claim the target slot here.
555 * @details A thread inside the trampoline that the arm recaptures can need the call gate or the target slot
556 * to leave. The busy wait therefore holds no DMK lock (`[B-110]`). A kept slot refuses instead,
557 * because an install arm never recaptures (HookStackTest.InstallOverAnArmedLayerRunsNoIdlePass).
558 * @note An Active outcome releases the lock through publish_toggle(), so the caller must read no hook state
559 * after the call.
560 */
561 template <class ImplT, class GateT>
562 16739 [[nodiscard]] Result<void> arm_hook(
563 ImplT &impl,
564 GateT &gate,
565 std::unique_lock<std::recursive_mutex> &guard,
566 DeferredToggleLog &deferred_warning,
567 TargetSlot *kept_slot
568 ) noexcept
569 {
570 16739 bool busy_logged = false;
571
1/2
✓ Branch 120 → 121 taken 15378 times.
✗ Branch 120 → 122 not taken.
15378 for (;;)
572 {
573 // A claimed hook writes no target byte, so it toggles by one store with no ledger slot or byte
574 // witness.
575
2/2
✓ Branch 4 → 5 taken 706 times.
✓ Branch 4 → 6 taken 16033 times.
16739 if (backend_claimed(impl.backend))
576 {
577 16739 return publish_resume(impl, gate, kept_slot, guard);
578 }
579 // A bypassed hook keeps its position. Unless the bytes are back to Original, which no chain
580 // reaches, the resume needs no top-layer authority.
581
2/2
✓ Branch 6 → 7 taken 655 times.
✓ Branch 6 → 12 taken 15378 times.
16033 if (impl.bypassed)
582 {
583
1/2
✓ Branch 8 → 9 taken 655 times.
✗ Branch 8 → 10 not taken.
655 if (witness_of(impl.backend) != PatchWitness::Original)
584 {
585 655 return publish_resume(impl, gate, kept_slot, guard);
586 }
587 (
588 void
589 ✗ )apply_backend(impl.backend, [](auto &backend) noexcept { backend.reconcile_enabled(false); });
590 ✗ impl.bypassed = false;
591 }
592
593 // The slot makes the {decide, patch} pair atomic against a concurrent same-target install. Claim
594 // it before the state CAS so a refusal changes nothing. A ledger that cannot decide the order
595 // refuses.
596 15378 std::optional<TargetSlot> acquired_slot;
597 4231 TargetSlot &slot =
598
2/2
✓ Branch 12 → 13 taken 11147 times.
✓ Branch 12 → 14 taken 4231 times.
15378 kept_slot != nullptr ? *kept_slot : acquired_slot.emplace(impl.target, impl.ledger_id);
599
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 20 taken 15378 times.
15378 if (slot.unavailable())
600 {
601 ✗ return std::unexpected(Error{ErrorCode::LayerConflict, "hook::enable", impl.target});
602 }
603
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 30 taken 15377 times.
15378 if (!slot.hold_coordinator("hook::enable", impl.name))
604 {
605 1 deferred_warning.arm(
606 "refused enable",
607 1 impl.name,
608 impl.target,
609 DetourModKit::detail::coordinator_hold_refusal_description(slot.coordinator_refusal())
610 );
611 1 return std::unexpected(Error{ErrorCode::EnableFailed, "hook::enable", impl.target});
612 }
613
614 // Classify before the transition claim. The backend emits its jmp over the current bytes, or
615 // recaptures the patch of another layer (`[B-110]`). Foreign or unreadable bytes must refuse here
616 // while the hook remains as the caller left it.
617 15377 const PatchWitness before = arm_witness_of(impl.backend);
618 15377 const bool promotes = witness_promotes_arm(before);
619
6/6
✓ Branch 32 → 33 taken 15 times.
✓ Branch 32 → 36 taken 15362 times.
✓ Branch 34 → 35 taken 6 times.
✓ Branch 34 → 36 taken 9 times.
✓ Branch 37 → 38 taken 6 times.
✓ Branch 37 → 43 taken 15371 times.
15377 if (!promotes && !witness_permits_write(before))
620 {
621 6 deferred_warning.arm("refused enable", impl.name, impl.target, witness_description(before));
622 6 return std::unexpected(Error{ErrorCode::EnableFailed, "hook::enable", impl.target});
623 }
624 // An arm over this hook's own patch keeps its layer, so only the newest layer writes it.
625
4/6
✓ Branch 43 → 44 taken 9 times.
✓ Branch 43 → 47 taken 15362 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 9 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 52 taken 15371 times.
15371 if (!promotes && !slot.is_top_layer())
626 {
627 ✗ return std::unexpected(Error{ErrorCode::LayerConflict, "hook::enable", impl.target});
628 }
629
630 15371 HookState expected = HookState::Disabled;
631
2/2
✓ Branch 53 → 54 taken 20 times.
✓ Branch 53 → 74 taken 15351 times.
15371 if (!impl.status.compare_exchange_strong(expected, HookState::Enabling, std::memory_order_acq_rel))
632 {
633
1/2
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 58 taken 20 times.
20 if (expected != HookState::Active)
634 {
635 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"});
636 }
637 // Active answers the request over this hook's own patch, and under a newer layer whose chain
638 // still reaches this hook.
639
6/6
✓ Branch 58 → 59 taken 11 times.
✓ Branch 58 → 62 taken 9 times.
✓ Branch 59 → 60 taken 8 times.
✓ Branch 59 → 63 taken 3 times.
✓ Branch 64 → 65 taken 15 times.
✓ Branch 64 → 67 taken 5 times.
28 if (before == PatchWitness::OwnedPatch ||
640
2/2
✓ Branch 61 → 62 taken 6 times.
✓ Branch 61 → 63 taken 2 times.
8 (before == PatchWitness::LayeredOver && backend_layer_chains_here(impl.backend)))
641 {
642 15 return {};
643 }
644 // A restore outside DMK removed this hook from the chain. Rewrite the stale claim, then arm
645 // through the ordinary path so byte authority and the population tally both stay exact.
646 10 deferred_warning.arm(
647 before == PatchWitness::LayeredOver
648 ? "left the layer chain under an active state. This enable retries the arm"
649 : "has original bytes under an active state. This enable retries the arm",
650
2/2
✓ Branch 68 → 69 taken 2 times.
✓ Branch 68 → 70 taken 3 times.
5 impl.name,
651 impl.target
652 );
653 5 reconcile_published_state(impl, gate, false);
654 5 impl.status.store(HookState::Enabling, std::memory_order_release);
655 }
656 // Create leaves the target unpatched, so this is the first operation that can make the detour
657 // reachable.
658 15356 CoordinatorRefusal refusal = CoordinatorRefusal::None;
659 15356 const bool backend_enabled = backend_value_or(
660 15356 impl.backend,
661 false,
662 30712 [&refusal](auto &backend) noexcept { return try_backend_enable(backend, &refusal); }
663 );
664 // A thread runs the trampoline that the arm recaptures. It leaves by itself, so the arm waits with
665 // no DMK lock held and no count. Nothing changed yet.
666
3/6
✓ Branch 75 → 76 taken 15 times.
✓ Branch 75 → 94 taken 15341 times.
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 94 taken 15 times.
✗ Branch 77 → 78 not taken.
✗ Branch 77 → 94 not taken.
15356 if (!backend_enabled && refusal == CoordinatorRefusal::TrampolineBusy && kept_slot == nullptr)
667 {
668 ✗ impl.status.store(HookState::Disabled, std::memory_order_release);
669 ✗ acquired_slot.reset();
670 ✗ guard.unlock();
671
0/2
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 86 not taken.
✗ if (!std::exchange(busy_logged, true))
672 {
673 ✗ (void)log().try_log(
674 LogLevel::Warning,
675 "hook::enable: '{}' waits for a thread to leave its trampoline at 0x{:0{}X}.",
676 ✗ impl.name,
677 ✗ impl.target,
678 ✗ sizeof(std::uintptr_t) * 2
679 );
680 }
681 ✗ std::this_thread::sleep_for(std::chrono::milliseconds{1});
682
0/2
✗ Branch 89 → 90 not taken.
✗ Branch 89 → 93 not taken.
✗ if (!relock_call_gate(guard))
683 {
684 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"});
685 }
686 ✗ continue;
687 }
688 15356 const bool patch_confirmed = backend_value_or(
689 15356 impl.backend,
690 false,
691 30712 [](auto &backend) noexcept { return enable_patch_is_confirmed(backend); }
692 );
693
4/4
✓ Branch 95 → 96 taken 15341 times.
✓ Branch 95 → 105 taken 15 times.
✓ Branch 96 → 97 taken 15331 times.
✓ Branch 96 → 105 taken 10 times.
15356 if (backend_enabled && patch_confirmed)
694 {
695 // The backend made the record the newest layer, so the local order follows it.
696
1/2
✓ Branch 97 → 98 taken 15331 times.
✗ Branch 97 → 102 not taken.
15331 if (promotes)
697 {
698
1/2
✓ Branch 100 → 101 taken 15331 times.
✗ Branch 100 → 102 not taken.
30662 if (const std::uint64_t stamp = DetourModKit::detail::HookLedger::instance()
699 15331 .promote_to_newest(impl.target, impl.ledger_id);
700 stamp != 0)
701 {
702 15331 impl.layer_stamp = stamp;
703 }
704 }
705 15331 publish_toggle(impl, gate, &slot, guard, true);
706 15331 return {};
707 }
708
2/2
✓ Branch 105 → 106 taken 15 times.
✓ Branch 105 → 117 taken 10 times.
25 if (!backend_enabled)
709 {
710 // A promoting arm meets LayerConflict only when it cannot chain to the layer on the target.
711
2/4
✓ Branch 106 → 107 taken 15 times.
✗ Branch 106 → 111 not taken.
✗ Branch 107 → 108 not taken.
✓ Branch 107 → 111 taken 15 times.
15 if (promotes && refusal == CoordinatorRefusal::LayerConflict)
712 {
713 ✗ deferred_warning.arm(
714 "refused enable",
715 ✗ impl.name,
716 impl.target,
717 ✗ "the hook cannot chain to the layer on the target"
718 );
719 }
720
2/2
✓ Branch 111 → 112 taken 14 times.
✓ Branch 111 → 113 taken 1 time.
15 else if (refusal == CoordinatorRefusal::CallerInTrampoline ||
721
1/2
✗ Branch 112 → 113 not taken.
✓ Branch 112 → 115 taken 14 times.
14 refusal == CoordinatorRefusal::TrampolineBusy)
722 {
723 1 deferred_warning.arm(
724 "refused enable",
725 1 impl.name,
726 impl.target,
727 DetourModKit::detail::coordinator_refusal_description(refusal)
728 );
729 }
730 15 return settle_backend_enable_failure(impl, gate, slot, guard, refusal);
731 }
732 10 return settle_unconfirmed_enable(impl, gate, slot, guard);
733 }
734 }
735 } // namespace
736
737 12510 Result<void> Hook::enable() noexcept
738 {
739
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 9 taken 12509 times.
12510 if (std::optional<Error> vetoed = refuse_on_loader_lock("hook::enable"))
740 {
741 1 return std::unexpected(*vetoed);
742 }
743 #if defined(DMK_ENABLE_TEST_SEAMS)
744 12509 note_loader_veto_passed(DetourModKit::detail::HookLoaderEntry::Enable);
745 #endif
746
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 15 taken 12508 times.
12509 if (!m_impl)
747 {
748 1 return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"});
749 }
750 // A live handle always has a gate. The null check fails closed on the broken invariant.
751 12508 const std::shared_ptr<CallGate> gate = gate_slot().load(std::memory_order_acquire);
752
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 22 taken 12508 times.
12508 if (!gate)
753 {
754 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"});
755 }
756 12508 DeferredToggleLog deferred_warning;
757 12508 std::unique_lock<std::recursive_mutex> guard = acquire_call_lock(gate.get());
758
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 30 taken 12508 times.
12508 if (!guard.owns_lock())
759 {
760 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"});
761 }
762 25016 if (!backend_value_or(
763 12508 m_impl->backend,
764 false,
765
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 36 taken 12508 times.
25016 [](auto &backend) noexcept { return static_cast<bool>(backend); }
766 ))
767 {
768 ✗ return std::unexpected(Error{ErrorCode::BackendFailed, "hook::enable"});
769 }
770 12508 return arm_hook(*m_impl, *gate, guard, deferred_warning, nullptr);
771 12508 }
772
773 5694 Result<void> Hook::disable() noexcept
774 {
775
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 9 taken 5693 times.
5694 if (std::optional<Error> vetoed = refuse_on_loader_lock("hook::disable"))
776 {
777 1 return std::unexpected(*vetoed);
778 }
779 #if defined(DMK_ENABLE_TEST_SEAMS)
780 5693 note_loader_veto_passed(DetourModKit::detail::HookLoaderEntry::Disable);
781 #endif
782
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 15 taken 5692 times.
5693 if (!m_impl)
783 {
784 1 return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"});
785 }
786 5692 const std::shared_ptr<CallGate> gate = gate_slot().load(std::memory_order_acquire);
787
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 22 taken 5692 times.
5692 if (!gate)
788 {
789 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"});
790 }
791 5692 DeferredToggleLog deferred_warning;
792 5692 std::unique_lock<std::recursive_mutex> guard = acquire_call_lock(gate.get());
793
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 30 taken 5692 times.
5692 if (!guard.owns_lock())
794 {
795 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"});
796 }
797 11384 if (!backend_value_or(
798 5692 m_impl->backend,
799 false,
800
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 36 taken 5692 times.
11384 [](auto &backend) noexcept { return static_cast<bool>(backend); }
801 ))
802 {
803 ✗ return std::unexpected(Error{ErrorCode::BackendFailed, "hook::disable"});
804 }
805
806 // A claimed hook never restores. It switches off by one store with no ledger slot or byte witness.
807
2/2
✓ Branch 38 → 39 taken 10 times.
✓ Branch 38 → 54 taken 5682 times.
5692 if (backend_claimed(m_impl->backend))
808 {
809 10 HookState expected = HookState::Active;
810
1/2
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 48 taken 10 times.
10 if (!m_impl->status.compare_exchange_strong(expected, HookState::Disabling, std::memory_order_acq_rel))
811 {
812 ✗ return expected == HookState::Disabled
813
0/2
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ ? Result<void>{}
814 ✗ : std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"});
815 }
816 10 return publish_bypass(*m_impl, *gate, nullptr, guard, nullptr, 0, std::nullopt);
817 }
818
819 // Only a branch that can restore bytes takes the coordinator. A bypass writes no target byte.
820 5682 TargetSlot slot(m_impl->target, m_impl->ledger_id);
821 // The ledger failed to claim the slot, so the layer order is undecided and the toggle refuses unchanged.
822
5/6
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 62 taken 5681 times.
✓ Branch 60 → 61 taken 1 time.
✗ Branch 60 → 62 not taken.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 63 → 68 taken 5681 times.
5682 if (slot.unavailable() && !m_impl->bypassed)
823 {
824 1 return std::unexpected(Error{ErrorCode::LayerConflict, "hook::disable", m_impl->target});
825 }
826
827 // A bypassed hook is already Disabled. Once it is the top layer over its own patch again, the restore
828 // removes its position. Any other answer, a refused hold included, keeps the bypass, which is a complete
829 // disarm.
830
2/2
✓ Branch 69 → 70 taken 12 times.
✓ Branch 69 → 100 taken 5669 times.
5681 if (m_impl->bypassed)
831 {
832
8/8
✓ Branch 71 → 72 taken 10 times.
✓ Branch 71 → 81 taken 2 times.
✓ Branch 76 → 77 taken 9 times.
✓ Branch 76 → 81 taken 1 time.
✓ Branch 79 → 80 taken 8 times.
✓ Branch 79 → 81 taken 1 time.
✓ Branch 82 → 83 taken 8 times.
✓ Branch 82 → 99 taken 4 times.
21 if (slot.is_top_layer() && slot.hold_coordinator("hook::disable", m_impl->name) &&
833 9 witness_of(m_impl->backend) == PatchWitness::OwnedPatch)
834 {
835 8 (void)backend_value_or(
836 8 m_impl->backend,
837 false,
838 16 [](auto &backend) noexcept { return try_backend_disable(backend); }
839 );
840
2/2
✓ Branch 87 → 88 taken 7 times.
✓ Branch 87 → 92 taken 1 time.
8 if (witness_of(m_impl->backend) == PatchWitness::Original)
841 {
842 7 (void)apply_backend(
843 7 m_impl->backend,
844 14 [](auto &backend) noexcept { backend.reconcile_enabled(false); }
845 );
846 7 m_impl->bypassed = false;
847 }
848
1/2
✗ Branch 94 → 95 not taken.
✓ Branch 94 → 99 taken 1 time.
1 else if (!switch_backend(m_impl->backend, false))
849 {
850 ✗ return std::unexpected(Error{ErrorCode::DisableFailed, "hook::disable", m_impl->target});
851 }
852 }
853 12 return {};
854 }
855
856 // An Active layer under a newer one cannot restore its saved prologue without clobbering the newer layer.
857 // It bypasses instead.
858 5669 const bool active = m_impl->status.load(std::memory_order_acquire) == HookState::Active;
859 680 const auto bypass = [&](std::optional<PatchWitness> witness) noexcept -> Result<void>
860 {
861 680 HookState expected = HookState::Active;
862
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 680 times.
680 if (!m_impl->status.compare_exchange_strong(expected, HookState::Disabling, std::memory_order_acq_rel))
863 {
864 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"});
865 }
866 680 return publish_bypass(*m_impl, *gate, &slot, guard, &deferred_warning, slot.newer_layers(), witness);
867 5669 };
868
6/6
✓ Branch 102 → 103 taken 5654 times.
✓ Branch 102 → 106 taken 15 times.
✓ Branch 104 → 105 taken 658 times.
✓ Branch 104 → 106 taken 4996 times.
✓ Branch 107 → 108 taken 658 times.
✓ Branch 107 → 111 taken 5011 times.
5669 if (active && slot.newer_layers() > 0)
869 {
870 658 return bypass(std::nullopt);
871 }
872
873 // A Disabled hook that restored keeps the newest-layer rule.
874
1/2
✗ Branch 112 → 113 not taken.
✓ Branch 112 → 117 taken 5011 times.
5011 if (!slot.is_top_layer())
875 {
876 ✗ return std::unexpected(Error{ErrorCode::LayerConflict, "hook::disable", m_impl->target});
877 }
878
879 5011 const bool held = slot.hold_coordinator("hook::disable", m_impl->name);
880 5011 const PatchWitness before = witness_of(m_impl->backend);
881 // Bytes of another participant or foreign bytes also bypass. Under a refused hold the read is not
882 // settled, and a torn read can only turn a restore into this bypass.
883
6/6
✓ Branch 123 → 124 taken 4996 times.
✓ Branch 123 → 127 taken 15 times.
✓ Branch 125 → 126 taken 22 times.
✓ Branch 125 → 127 taken 4974 times.
✓ Branch 128 → 129 taken 22 times.
✓ Branch 128 → 132 taken 4989 times.
5011 if (active && !witness_permits_write(before))
884 {
885 22 return bypass(before);
886 }
887
2/2
✓ Branch 132 → 133 taken 2 times.
✓ Branch 132 → 145 taken 4987 times.
4989 if (!held)
888 {
889 // A restored hook needs no write, so a repeat disable succeeds.
890
3/4
✓ Branch 133 → 134 taken 1 time.
✓ Branch 133 → 136 taken 1 time.
✓ Branch 134 → 135 taken 1 time.
✗ Branch 134 → 136 not taken.
2 if (!active && before == PatchWitness::Original)
891 {
892 1 return {};
893 }
894 1 deferred_warning.arm(
895 "refused disable",
896 1 m_impl->name,
897 1 m_impl->target,
898 DetourModKit::detail::coordinator_hold_refusal_description(slot.coordinator_refusal())
899 );
900 1 return std::unexpected(Error{ErrorCode::DisableFailed, "hook::disable", m_impl->target});
901 }
902
903 // Classify before the transition claim (see enable()). The unconditional prologue restore clobbers a
904 // foreign writer's bytes. Refuse and leave the hook as it is.
905
2/2
✓ Branch 146 → 147 taken 1 time.
✓ Branch 146 → 155 taken 4986 times.
4987 if (!witness_permits_write(before))
906 {
907 1 deferred_warning.arm("refused disable", m_impl->name, m_impl->target, witness_description(before));
908 1 return std::unexpected(Error{ErrorCode::DisableFailed, "hook::disable", m_impl->target});
909 }
910
911 4986 HookState expected = HookState::Active;
912
2/2
✓ Branch 157 → 158 taken 13 times.
✓ Branch 157 → 173 taken 4973 times.
4986 if (!m_impl->status.compare_exchange_strong(expected, HookState::Disabling, std::memory_order_acq_rel))
913 {
914
1/2
✗ Branch 158 → 159 not taken.
✓ Branch 158 → 162 taken 13 times.
13 if (expected != HookState::Disabled)
915 {
916 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::disable"});
917 }
918 // Disabled answers the request only over the saved prologue (see enable() for the polarity).
919
2/2
✓ Branch 162 → 163 taken 11 times.
✓ Branch 162 → 164 taken 2 times.
13 if (before == PatchWitness::Original)
920 {
921 11 return {};
922 }
923 2 deferred_warning.arm(
924 "has owned bytes under a disabled state. This disable retries the restore",
925 2 m_impl->name,
926 2 m_impl->target
927 );
928 2 reconcile_published_state(*m_impl, *gate, true);
929 2 m_impl->status.store(HookState::Disabling, std::memory_order_release);
930 }
931 // Confirm the saved prologue is back before Disabled publication. The witness is taken whatever the
932 // backend returns. An error can sit over restored bytes. A success without byte corroboration must not
933 // publish Disabled.
934 4975 CoordinatorRefusal refusal = CoordinatorRefusal::None;
935 4975 const bool backend_disabled = backend_value_or(
936 4975 m_impl->backend,
937 false,
938 9950 [&refusal](auto &backend) noexcept { return try_backend_disable(backend, &refusal); }
939 );
940 4975 const PatchWitness after = witness_of(m_impl->backend);
941
2/2
✓ Branch 177 → 178 taken 4969 times.
✓ Branch 177 → 189 taken 6 times.
4975 if (after == PatchWitness::Original)
942 {
943 9938 (void)apply_backend(m_impl->backend, [](auto &backend) noexcept { backend.reconcile_enabled(false); });
944 4969 const std::uintptr_t target = m_impl->target;
945 4969 publish_toggle(*m_impl, *gate, &slot, guard, false);
946
2/2
✓ Branch 184 → 185 taken 5 times.
✓ Branch 184 → 188 taken 4964 times.
4969 if (!backend_disabled)
947 {
948 // The disarm took effect, but the backend reported a post-commit failure (its page-protection
949 // restore). Report it rather than swallow it.
950 5 return std::unexpected(Error{ErrorCode::BackendFailed, "hook::disable", target});
951 }
952 4964 return {};
953 }
954 // The coordinator refused before any write because another participant layered on top. The backend
955 // still reports this hook's committed patch, so the hook bypasses like a lower local layer.
956 const bool patch_committed =
957 12 backend_value_or(m_impl->backend, false, [](auto &backend) noexcept { return backend.enabled(); });
958
4/6
✓ Branch 191 → 192 taken 1 time.
✓ Branch 191 → 199 taken 5 times.
✓ Branch 192 → 193 taken 1 time.
✗ Branch 192 → 199 not taken.
✓ Branch 193 → 194 taken 1 time.
✗ Branch 193 → 199 not taken.
6 if (refusal == CoordinatorRefusal::LayerConflict && patch_committed && after == PatchWitness::OwnedPatch)
959 {
960 1 return publish_bypass(*m_impl, *gate, &slot, guard, &deferred_warning, 0, std::nullopt);
961 }
962 // A completed restore can be followed by a newer or uncertain owner. Retain backend reachability so
963 // is_enabled() and a later disable retry agree with the conservative Active state.
964 10 (void)apply_backend(m_impl->backend, [](auto &backend) noexcept { backend.reconcile_enabled(true); });
965 5 m_impl->status.store(HookState::Active, std::memory_order_release);
966
1/2
✗ Branch 203 → 204 not taken.
✓ Branch 203 → 208 taken 5 times.
5 if (refusal == CoordinatorRefusal::LayerConflict)
967 {
968 ✗ return std::unexpected(Error{ErrorCode::LayerConflict, "hook::disable", m_impl->target});
969 }
970 5 return std::unexpected(Error{ErrorCode::DisableFailed, "hook::disable"});
971 5692 }
972
973 4231 Result<void> detail::HookAccess::arm_installed(Hook &hook, DeferredArm &deferred) noexcept
974 {
975 4231 const std::shared_ptr<Hook::CallGate> gate = hook.gate_slot().load(std::memory_order_acquire);
976
3/6
✓ Branch 5 → 6 taken 4231 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 4231 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 4231 times.
4231 if (!hook.m_impl || !gate)
977 {
978 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"});
979 }
980 4231 DeferredToggleLog deferred_warning;
981 4231 std::unique_lock<std::recursive_mutex> guard = hook.acquire_call_lock(gate.get());
982
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 22 taken 4231 times.
4231 if (!guard.owns_lock())
983 {
984 ✗ return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"});
985 }
986 4231 TargetSlot slot(hook.m_impl->target, hook.m_impl->ledger_id, TargetSlot::Kept{});
987 4231 Result<void> armed = arm_hook(*hook.m_impl, *gate, guard, deferred_warning, &slot);
988 4231 deferred.transition = slot.deferred_transition();
989 4231 std::tie(deferred.warning, deferred.reason) = deferred_warning.detach();
990 4231 return armed;
991 4231 }
992
993 4231 void detail::HookAccess::log_deferred_arm(
994 const DeferredArm &deferred,
995 const std::string &name,
996 std::uintptr_t target
997 ) noexcept
998 {
999
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 7 taken 4231 times.
4231 if (deferred.warning != nullptr)
1000 {
1001 ✗ DeferredToggleLog line;
1002 ✗ line.arm(deferred.warning, name, target, deferred.reason);
1003 ✗ }
1004 4231 }
1005 } // namespace hook
1006 } // namespace DetourModKit
1007