GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.6% 209 / 0 / 221
Functions: 97.4% 37 / 0 / 38
Branches: 83.7% 72 / 0 / 86

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