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 | 22431 | if (m_bypass) | |
| 74 | { | ||
| 75 | 681 | log_bypass(); | |
| 76 | 681 | return; | |
| 77 | } | ||
| 78 | 21750 | if (m_text == nullptr) | |
| 79 | { | ||
| 80 | 21733 | return; | |
| 81 | } | ||
| 82 | 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 | 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 | 698 | m_name = name; | |
| 158 | } | ||
| 159 | ✗ | catch (...) | |
| 160 | { | ||
| 161 | ✗ | } | |
| 162 | 698 | } | |
| 163 | |||
| 164 | 681 | void log_bypass() const noexcept | |
| 165 | { | ||
| 166 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 22368 | if (auto *probe = DetourModKit::detail::g_hook_toggle_publication_probe) | |
| 358 | { | ||
| 359 | 3 | const HookState expected = armed ? HookState::Active : HookState::Disabled; | |
| 360 | 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 | 22368 | armed ? diagnostics::HookTransition::Enabled : diagnostics::HookTransition::Disabled; | |
| 371 | 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 | 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 | 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 | 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 | 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 | 5 | if (refusal == CoordinatorRefusal::LayerConflict) | |
| 421 | { | ||
| 422 | ✗ | return std::unexpected(Error{ErrorCode::LayerConflict, "hook::enable", impl.target}); | |
| 423 | } | ||
| 424 | 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 | 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 | 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 | 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 | 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 | 1361 | if (!impl.status.compare_exchange_strong(expected, HookState::Enabling, std::memory_order_acq_rel)) | |
| 523 | { | ||
| 524 | ✗ | return expected == HookState::Active | |
| 525 | ✗ | ? Result<void>{} | |
| 526 | ✗ | : std::unexpected(Error{ErrorCode::InvalidHookState, "hook::enable"}); | |
| 527 | } | ||
| 528 | 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 | ✗ | 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 | 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 | 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 | 16033 | if (impl.bypassed) | |
| 582 | { | ||
| 583 | 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 | 15378 | kept_slot != nullptr ? *kept_slot : acquired_slot.emplace(impl.target, impl.ledger_id); | |
| 599 | 15378 | if (slot.unavailable()) | |
| 600 | { | ||
| 601 | ✗ | return std::unexpected(Error{ErrorCode::LayerConflict, "hook::enable", impl.target}); | |
| 602 | } | ||
| 603 | 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 | 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 | 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 | 15371 | if (!impl.status.compare_exchange_strong(expected, HookState::Enabling, std::memory_order_acq_rel)) | |
| 632 | { | ||
| 633 | 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 | 28 | if (before == PatchWitness::OwnedPatch || | |
| 640 | 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 | 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 | 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 | ✗ | 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 | ✗ | 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 | 15356 | if (backend_enabled && patch_confirmed) | |
| 694 | { | ||
| 695 | // The backend made the record the newest layer, so the local order follows it. | ||
| 696 | 15331 | if (promotes) | |
| 697 | { | ||
| 698 | 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 | 25 | if (!backend_enabled) | |
| 709 | { | ||
| 710 | // A promoting arm meets LayerConflict only when it cannot chain to the layer on the target. | ||
| 711 | 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 | 15 | else if (refusal == CoordinatorRefusal::CallerInTrampoline || | |
| 721 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 5692 | if (backend_claimed(m_impl->backend)) | |
| 808 | { | ||
| 809 | 10 | HookState expected = HookState::Active; | |
| 810 | 10 | if (!m_impl->status.compare_exchange_strong(expected, HookState::Disabling, std::memory_order_acq_rel)) | |
| 811 | { | ||
| 812 | ✗ | return expected == HookState::Disabled | |
| 813 | ✗ | ? 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 | 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 | 5681 | if (m_impl->bypassed) | |
| 831 | { | ||
| 832 | 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 | 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 | 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 | 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 | 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 | 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 | 5011 | if (active && !witness_permits_write(before)) | |
| 884 | { | ||
| 885 | 22 | return bypass(before); | |
| 886 | } | ||
| 887 | 4989 | if (!held) | |
| 888 | { | ||
| 889 | // A restored hook needs no write, so a repeat disable succeeds. | ||
| 890 | 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 | 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 | 4986 | if (!m_impl->status.compare_exchange_strong(expected, HookState::Disabling, std::memory_order_acq_rel)) | |
| 913 | { | ||
| 914 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 |