region_private.cpp 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. #include <array>
  2. #include <atomic>
  3. #include <cstddef>
  4. #include <cstdint>
  5. #include <cstdio>
  6. #include <intrin.h>
  7. #include <string_view>
  8. #include "../../../core/logging/log.h"
  9. #include "../../../core/settings/settings.h"
  10. #include "../../../state/activity/forced/activity_forced_destination.h"
  11. #include "../../hooking/detour.h"
  12. #include "internal.h"
  13. namespace sunrise::client::hooks::bootflow {
  14. namespace {
  15. /**
  16. * The bubble public-flag reader. The pattern is its whole body: a call to the state-byte getter,
  17. * then a cmovnz that turns the byte into a bool.
  18. */
  19. constexpr std::string_view kReaderSignatureText =
  20. "48 83 EC 28 E8 ? ? ? ? 48 8B C8 32 C0 48 85 C9 74 ? 80 39 00 BA 01 00 00 00 0F B6 C0 0F 45 "
  21. "C2 48 83 C4 28 C3";
  22. /** Compiled pattern bytes of the signature text above. */
  23. constexpr auto kReaderSignature =
  24. signature<signature_length(kReaderSignatureText)>(kReaderSignatureText);
  25. /**
  26. * The region transition starter. Anchored on its stack-cookie prologue and the read of the
  27. * manager's phase byte, which no other function pairs this way.
  28. */
  29. constexpr std::string_view kStarterSignatureText =
  30. "44 89 44 24 18 55 53 56 57 41 54 41 56 41 57 48 8D AC 24 ? ? ? ? 48 81 EC ? ? ? ? 48 8B 05 "
  31. "? ? ? ? 48 33 C4 48 89 85 ? ? ? ? 0F B6 81 09 02 00 00 4D 8B E1 FE C8 4C 63 F2 48 8B F1";
  32. /** Compiled pattern bytes of the signature text above. */
  33. constexpr auto kStarterSignature =
  34. signature<signature_length(kStarterSignatureText)>(kStarterSignatureText);
  35. /** `call rel32`, the encoding the starter uses to reach the reader. */
  36. constexpr std::byte kCallOpcode{0xE8};
  37. /** The call's displacement follows its opcode byte. */
  38. constexpr std::size_t kCallOperandOffset = 1;
  39. /** A near call is its opcode plus a signed 32-bit displacement. */
  40. constexpr std::size_t kCallLength = kCallOperandOffset + 4;
  41. /**
  42. * Bytes of the starter searched for that call. The body is shorter than this, and the search
  43. * needs one match, so a second hit fails the install instead of picking one.
  44. */
  45. constexpr std::size_t kStarterSearchBytes = 0x600;
  46. /** Lines allowed per run. Region transitions are rare, so this shows every one a boot makes. */
  47. constexpr unsigned kMaxReports = 8;
  48. /** Size of one line, set by its stage and slice-set fields. */
  49. constexpr std::size_t kLineCapacity = 96;
  50. using Reader = bool(__fastcall*)(std::uint32_t);
  51. hooking::detour::Handle g_handle{};
  52. std::atomic<Reader> g_original{nullptr};
  53. std::atomic<const std::byte*> g_returnSite{nullptr};
  54. std::atomic<unsigned> g_forced{0};
  55. /**
  56. * Finds the return address of the starter's own call to the reader.
  57. * A stray opcode byte inside another instruction can decode to the reader, so the whole window is
  58. * swept and an unclear result is rejected.
  59. * @return Address after the single matching call, or null when there is not exactly one.
  60. */
  61. [[nodiscard]] const std::byte* find_return_site(const std::byte* starter,
  62. const std::byte* reader) noexcept {
  63. const std::byte* found = nullptr;
  64. for (std::size_t offset = 0; offset + kCallLength <= kStarterSearchBytes; ++offset) {
  65. const std::byte* const site = starter + offset;
  66. if (*site != kCallOpcode) {
  67. continue;
  68. }
  69. const std::byte* const next = site + kCallLength;
  70. if (resolve_relative(site + kCallOperandOffset, next) != reader) {
  71. continue;
  72. }
  73. if (found != nullptr) {
  74. return nullptr;
  75. }
  76. found = next;
  77. }
  78. return found;
  79. }
  80. /**
  81. * Emits one decision event while the per-run budget lasts. Only a public bubble reaches here.
  82. * @param sliceSet Slice-set index whose bubble the reader called public.
  83. * @param forced True when the answer was replaced, false when the region stays public.
  84. */
  85. void report(std::uint32_t sliceSet, bool forced) noexcept {
  86. // One atomic claim per line, so a concurrent transition cannot reuse a budget slot.
  87. if (g_forced.fetch_add(1, std::memory_order_relaxed) >= kMaxReports) {
  88. return;
  89. }
  90. std::array<char, kLineCapacity> line{};
  91. const int written = std::snprintf(line.data(),
  92. line.size(),
  93. "ev=bootflow stage=region result=%s slice_set=%u",
  94. forced ? "forced" : "public",
  95. static_cast<unsigned>(sliceSet));
  96. if (written > 0) {
  97. core::log::write(core::log::Channel::client,
  98. core::log::Level::info,
  99. {line.data(), static_cast<std::size_t>(written)});
  100. }
  101. }
  102. /**
  103. * Reports a bubble as private, for the region transition's own call only.
  104. * A public region holds its slice-set switch until a public activity host connects. The answer
  105. * is public unless `client.region_private` is on, or a destination is forced.
  106. * @return False on the starter's call, otherwise the reader's own answer.
  107. */
  108. __declspec(noinline) bool __fastcall reader(std::uint32_t sliceSet) noexcept {
  109. const Reader original = g_original.load(std::memory_order_acquire);
  110. // The detour is live for a few instructions before install publishes its trampoline.
  111. if (original == nullptr) {
  112. return false;
  113. }
  114. if (!original(sliceSet)) {
  115. return false;
  116. }
  117. const auto* const caller = static_cast<const std::byte*>(_ReturnAddress());
  118. if (caller != g_returnSite.load(std::memory_order_acquire)) {
  119. return true;
  120. }
  121. const core::settings::Settings& settings = core::settings::get();
  122. const bool forced = settings.client.regionPrivate || state::activity::forced::override_active();
  123. report(sliceSet, forced);
  124. return !forced;
  125. }
  126. /** @param reason Key naming the step that failed. */
  127. void report_failure(const char* reason) noexcept {
  128. std::array<char, kLineCapacity> line{};
  129. const int written = std::snprintf(
  130. line.data(), line.size(), "ev=bootflow stage=region result=fail reason=%s", reason);
  131. if (written > 0) {
  132. core::log::write(core::log::Channel::client,
  133. core::log::Level::warn,
  134. {line.data(), static_cast<std::size_t>(written)});
  135. }
  136. }
  137. } // namespace
  138. /** Stages the private-region force. */
  139. StageResult stage_region_private(hooking::detour::Spec& spec) noexcept {
  140. if (g_handle.attached) {
  141. return StageResult::attached;
  142. }
  143. std::byte* const target = scan_main_image_unique(kReaderSignature, "slice_set_is_public");
  144. if (target == nullptr) {
  145. report_failure("reader");
  146. return StageResult::unavailable;
  147. }
  148. const std::byte* const starter =
  149. scan_main_image_unique(kStarterSignature, "region_start_transition");
  150. if (starter == nullptr) {
  151. report_failure("starter");
  152. return StageResult::unavailable;
  153. }
  154. const std::byte* const returnSite = find_return_site(starter, target);
  155. if (returnSite == nullptr) {
  156. report_failure("call_site");
  157. return StageResult::unavailable;
  158. }
  159. // Published before the detour attaches, so the first call already has its filter.
  160. g_returnSite.store(returnSite, std::memory_order_release);
  161. spec = hooking::detour::Spec{target, reinterpret_cast<void*>(&reader)};
  162. return StageResult::staged;
  163. }
  164. /** Takes the private-region force's attached handle, or a detached one. */
  165. void publish_region_private(const hooking::detour::Handle& handle) noexcept {
  166. if (!handle.attached) {
  167. report_failure("attach");
  168. return;
  169. }
  170. g_handle = handle;
  171. g_original.store(reinterpret_cast<Reader>(g_handle.original), std::memory_order_release);
  172. core::log::write(
  173. core::log::Channel::client, core::log::Level::info, "ev=bootflow stage=region result=ok");
  174. }
  175. /** Detaches the private-region force. */
  176. void uninstall_region_private() noexcept {
  177. if (g_handle.attached) {
  178. (void)hooking::detour::uninstall(g_handle);
  179. }
  180. g_original.store(nullptr, std::memory_order_release);
  181. g_returnSite.store(nullptr, std::memory_order_release);
  182. g_forced.store(0, std::memory_order_release);
  183. }
  184. } // namespace sunrise::client::hooks::bootflow