investment_derived_rebuild.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #include "investment_derived_rebuild.h"
  2. #include <array>
  3. #include <atomic>
  4. #include <cstddef>
  5. #include <cstdint>
  6. #include <string_view>
  7. #include "../../../../core/logging/log.h"
  8. #include "../../../hooking/detour.h"
  9. #include "internal.h"
  10. namespace sunrise::client::hooks::network::investment {
  11. namespace {
  12. /**
  13. * The derived-state freshness verdict. Only a rebuild recomputes the expiry, so a character
  14. * cached before its replicated objects arrive never goes stale by itself.
  15. */
  16. constexpr std::string_view kFreshnessSignatureText =
  17. "48 89 5C 24 ? 57 48 83 EC ? 48 8B D9 E8 ? ? ? ? 48 8B F8 0F B6 40 08 84 C0 74 ? 48 8B 53 18 "
  18. "48 8B 4B 08 E8 ? ? ? ?";
  19. /** Compiled pattern bytes for the freshness verdict above. */
  20. constexpr auto kFreshnessSignature =
  21. signature<signature_length(kFreshnessSignatureText)>(kFreshnessSignatureText);
  22. /** The state-three family-four lookup call. A nonnull result proves the real object arrived. */
  23. constexpr std::string_view kFamily4CallSignatureText =
  24. "48 8D 4B 10 E8 ? ? ? ? 48 8D B8 28 07 00 00 48 83 3F 00";
  25. /** Compiled pattern bytes for the family-four lookup call above. */
  26. constexpr auto kFamily4CallSignature =
  27. signature<signature_length(kFamily4CallSignatureText)>(kFamily4CallSignatureText);
  28. /** Byte offset of the `E8` near call inside the matched family-four pattern. */
  29. constexpr std::size_t kFamily4CallOperandOffset = 5;
  30. /** An x64 near call has a 4-byte relative displacement. */
  31. constexpr std::size_t kNearCallOperandSize = 4;
  32. /** Detour handle slots, fixed so install and uninstall use the same pair. */
  33. constexpr std::size_t kFreshnessHandle = 0;
  34. constexpr std::size_t kFamily4Handle = 1;
  35. /** The freshness verdict the game reads as "rebuild required". */
  36. constexpr char kStale = 0;
  37. using Freshness = char(__fastcall*)(void*);
  38. using Family4Lookup = void*(__fastcall*)(std::uint64_t*);
  39. std::array<hooking::detour::Handle, 2> g_handles{};
  40. std::atomic<Freshness> g_originalFreshness{nullptr};
  41. std::atomic<Family4Lookup> g_originalFamily4Lookup{nullptr};
  42. std::atomic_bool g_rebuildArmed{false};
  43. std::atomic_bool g_reportedRebuild{false};
  44. std::atomic<void*> g_committedFamily4{nullptr};
  45. std::atomic_uint32_t g_pendingFamily4Publications{0};
  46. /** @return True while either primary rebuild detour is attached. */
  47. [[nodiscard]] bool any_primary_attached() noexcept {
  48. return g_handles[kFreshnessHandle].attached || g_handles[kFamily4Handle].attached;
  49. }
  50. /** Clears call targets and the once-per-lifecycle log flags after full detach. */
  51. void clear_runtime() noexcept {
  52. g_originalFreshness.store(nullptr, std::memory_order_release);
  53. g_originalFamily4Lookup.store(nullptr, std::memory_order_release);
  54. g_rebuildArmed.store(false, std::memory_order_release);
  55. g_reportedRebuild.store(false, std::memory_order_release);
  56. g_committedFamily4.store(nullptr, std::memory_order_release);
  57. g_pendingFamily4Publications.store(0, std::memory_order_release);
  58. }
  59. /**
  60. * Reports stale once after a real replicated-object commit.
  61. * @param accessor Borrowed derived-state accessor.
  62. * @return Stale once while armed, otherwise the native verdict.
  63. */
  64. __declspec(noinline) char __fastcall freshness(void* accessor) noexcept {
  65. const Freshness original = g_originalFreshness.load(std::memory_order_acquire);
  66. // The native verdict performs the Family-4 lookup. That lookup is what arms the initial
  67. // rebuild, so it must run before the arm is consumed; checking first left the arm stranded
  68. // when sign-on made only one freshness query and every Triumph card kept its stale action.
  69. const char nativeVerdict = original != nullptr ? original(accessor) : kStale;
  70. if (g_rebuildArmed.exchange(false, std::memory_order_acq_rel)) {
  71. if (!g_reportedRebuild.exchange(true, std::memory_order_relaxed)) {
  72. core::log::write(core::log::Channel::client,
  73. core::log::Level::info,
  74. "ev=investment stage=derived result=rebuilt");
  75. }
  76. return kStale;
  77. }
  78. return nativeVerdict;
  79. }
  80. /**
  81. * Arms a rebuild whenever the state-three lookup observes a different committed Family-4 object.
  82. * The freshness verdict itself performs this lookup, so a simple "nonnull" test would re-arm on
  83. * every query and keep the derived state permanently stale. Object identity changes only when the
  84. * queuez replacement has committed, which gives initial sign-on and later account after-images the
  85. * same boundary without an unrelated Family-5 publication.
  86. * @param key Borrowed account key.
  87. * @return The native lookup result, unchanged.
  88. */
  89. __declspec(noinline) void* __fastcall family4_lookup(std::uint64_t* key) noexcept {
  90. const Family4Lookup original = g_originalFamily4Lookup.load(std::memory_order_acquire);
  91. void* const resolved = original != nullptr ? original(key) : nullptr;
  92. if (resolved != nullptr) {
  93. std::uint32_t pending = g_pendingFamily4Publications.load(std::memory_order_acquire);
  94. while (pending != 0
  95. && !g_pendingFamily4Publications.compare_exchange_weak(
  96. pending, pending - 1, std::memory_order_acq_rel, std::memory_order_acquire)) {}
  97. if (pending != 0) {
  98. arm_derived_rebuild();
  99. core::log::write(core::log::Channel::client,
  100. core::log::Level::info,
  101. "ev=investment stage=family4_commit result=armed source=publication");
  102. }
  103. }
  104. void* previous = g_committedFamily4.load(std::memory_order_acquire);
  105. if (resolved != nullptr && resolved != previous
  106. && g_committedFamily4.compare_exchange_strong(
  107. previous, resolved, std::memory_order_acq_rel, std::memory_order_acquire)) {
  108. arm_derived_rebuild();
  109. core::log::write(core::log::Channel::client,
  110. core::log::Level::info,
  111. "ev=investment stage=family4_commit result=armed");
  112. }
  113. return resolved;
  114. }
  115. } // namespace
  116. /** Arms one derived-state rebuild, used up by the next freshness verdict. */
  117. void arm_derived_rebuild() noexcept {
  118. g_rebuildArmed.store(true, std::memory_order_release);
  119. }
  120. /** Carries an exact committed account publication to its next native Family-4 lookup. */
  121. void notify_family4_publication() noexcept {
  122. g_pendingFamily4Publications.fetch_add(1, std::memory_order_release);
  123. core::log::write(core::log::Channel::client,
  124. core::log::Level::info,
  125. "ev=investment stage=family4_publication result=pending");
  126. }
  127. /** @return True when freshness and both real-arrival rebuild arms are attached. */
  128. bool install() noexcept {
  129. if (is_installed()) {
  130. return true;
  131. }
  132. if (has_ownership()) {
  133. core::log::write(core::log::Channel::client,
  134. core::log::Level::warn,
  135. "ev=investment stage=install result=fail reason=ownership");
  136. return false;
  137. }
  138. std::byte* const freshnessTarget =
  139. scan_main_image_unique(kFreshnessSignature, "investment_derived_freshness");
  140. std::byte* const family4Call =
  141. scan_main_image_unique(kFamily4CallSignature, "queuez_family4_readiness_call");
  142. if (freshnessTarget == nullptr || family4Call == nullptr) {
  143. core::log::write(core::log::Channel::client,
  144. core::log::Level::warn,
  145. "ev=investment stage=install result=fail reason=target");
  146. return false;
  147. }
  148. std::byte* const family4Target =
  149. resolve_relative(family4Call + kFamily4CallOperandOffset,
  150. family4Call + kFamily4CallOperandOffset + kNearCallOperandSize);
  151. if (family4Target == nullptr) {
  152. core::log::write(core::log::Channel::client,
  153. core::log::Level::warn,
  154. "ev=investment stage=install result=fail reason=operand");
  155. return false;
  156. }
  157. const std::array specs{
  158. hooking::detour::Spec{freshnessTarget, reinterpret_cast<void*>(&freshness)},
  159. hooking::detour::Spec{family4Target, reinterpret_cast<void*>(&family4_lookup)},
  160. };
  161. std::array<hooking::detour::Handle, 2> installed{};
  162. if (!hooking::detour::install(specs, installed)) {
  163. core::log::write(core::log::Channel::client,
  164. core::log::Level::warn,
  165. "ev=investment stage=install result=fail reason=attach");
  166. return false;
  167. }
  168. g_handles = installed;
  169. g_originalFreshness.store(reinterpret_cast<Freshness>(g_handles[kFreshnessHandle].original),
  170. std::memory_order_release);
  171. g_originalFamily4Lookup.store(
  172. reinterpret_cast<Family4Lookup>(g_handles[kFamily4Handle].original),
  173. std::memory_order_release);
  174. if (!install_family5_rearm()) {
  175. if (hooking::detour::uninstall(g_handles)) {
  176. clear_runtime();
  177. } else {
  178. core::log::write(core::log::Channel::client,
  179. core::log::Level::warn,
  180. "ev=investment stage=install result=fail reason=rollback");
  181. }
  182. return false;
  183. }
  184. core::log::write(core::log::Channel::client,
  185. core::log::Level::info,
  186. "ev=investment stage=install result=ok");
  187. return true;
  188. }
  189. /** @return True when every investment rebuild detour is absent. */
  190. bool uninstall() noexcept {
  191. restore_lore_visibility();
  192. restore_socket_menu_routing();
  193. if (!uninstall_family5_rearm()) {
  194. core::log::write(core::log::Channel::client,
  195. core::log::Level::warn,
  196. "ev=investment stage=uninstall result=fail reason=family5");
  197. return false;
  198. }
  199. if (any_primary_attached()
  200. && (!g_handles[kFreshnessHandle].attached || !g_handles[kFamily4Handle].attached
  201. || !hooking::detour::uninstall(g_handles))) {
  202. core::log::write(core::log::Channel::client,
  203. core::log::Level::warn,
  204. "ev=investment stage=uninstall result=fail reason=detach");
  205. return false;
  206. }
  207. clear_runtime();
  208. return true;
  209. }
  210. /** @return True while freshness and both real-arrival rebuild arms are attached. */
  211. bool is_installed() noexcept {
  212. return g_handles[kFreshnessHandle].attached && g_handles[kFamily4Handle].attached
  213. && family5_rearm_is_installed();
  214. }
  215. /** @return True while any investment rebuild detour still needs cleanup. */
  216. bool has_ownership() noexcept {
  217. return any_primary_attached() || family5_rearm_is_installed();
  218. }
  219. } // namespace sunrise::client::hooks::network::investment