Files
ReSkate/Extension/World/physics_wake_guard.cpp
T
AyeZeeBB 7dabd99a40 Crashes: the Trainer no longer writes into freed memory, throwdown models go before a level unload, and a gone texture draws nothing instead of crashing
From the first 6k Sentry reports of 55a7342 (2026-10-24), whose top sites were all new since 2.1.1:

- Trainer: scale_underflip_bands wrote the underflip rate bands' MinScale and MaxScale every tick on
  every copy it had ever found, checking only that the address was readable. A level's copies go back
  to the game's heap with it, so 1.0 and 0.6 landed in the upper half of whatever the game made there
  next (Skate.exe+0x157f930, +0x12a1549, +0x10084de, +0x11ef49e, +0x3db81ea, +0x12c21ca: about a
  third of the crashes, every dump with a pointer whose high dword is one of those floats). A copy is
  now written only while it still is that band and holds the game's band or the last one written;
  any other is forgotten.
- Throwdowns: models the page released were destroyed 3 s later by the menu tick, which does not run
  while a level loads, so the game's own flush of the unloaded level's types destroyed them after
  their assets were freed (Skate.exe+0x3dc5eaa, 18%). Before a level unload they are destroyed at
  once, as the native menu's own are.
- Textures: the guard's "default for the id" is the entry itself, so for a dead entry resolve gave a
  null texture, and the texture view cache asked it for its key (Skate.exe+0x3e6ddc1, 17%, every
  report with TheVlixOne-Nike_Pack). A stub at the cache insert hands back the empty view the cache
  already built when the texture is null; the guard is on only when that patch applies.
- Physics: the wake guard widens every pass because ReSkate raises the owner pool to 160000; its log
  line now says so once instead of claiming a prevented crash thousands of times a session.

Built; not yet checked in the game.
2026-10-24 17:19:32 -05:00

78 lines
3.3 KiB
C++

#include "physics_wake_guard.h"
#include "Engine/Core/Hooks/hooks.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Platform/memory.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/physics_wake.h"
#include <array>
#include <atomic>
namespace dingosdk::physics_wake_guard {
namespace {
namespace pw = addr::physics_wake;
using Wake = void (*)(std::uintptr_t);
Wake original{};
std::atomic<bool> installed{};
std::atomic<std::uint32_t> most{};
std::atomic<std::uint64_t> widened{};
// Said once: with the owner pool raised (Extension/World/physics_world_size.cpp) every pass of every
// session is widened, so the line says how the pass is sized, not a crash each time (55a7342's logs
// had it thousands of times per session; 2026-10-24).
__declspec(noinline) void note(std::uint32_t owners, std::uint32_t limit) noexcept {
if (widened.fetch_add(1, std::memory_order_relaxed) != 0) return;
try {
logging::log(logging::Level::info, logging::Channel::runtime,
"Physics: the wake pass makes room for the world's {} physics owners (the game's own size is {}, below the "
"raised owner pool).", owners, limit);
} catch (...) {}
}
// The world's owner count, 0 when it cannot be read (the game's own size is then kept).
std::uint32_t owners_of(std::uintptr_t system) noexcept {
std::uintptr_t world{}, begin{}, end{};
if (!memory::peek(system + pw::world_offset, world) || !world ||
!memory::peek(world + pw::owner_generations_begin, begin) ||
!memory::peek(world + pw::owner_generations_end, end) || end < begin || (end - begin) % 4)
return 0;
const auto count = (end - begin) / 4;
return count > 0x7fffffc0u ? 0 : static_cast<std::uint32_t>(count);
}
void guarded(std::uintptr_t system) {
auto* const limit = system ? reinterpret_cast<std::uint32_t*>(system + pw::owner_limit_offset) : nullptr;
const auto owners = system ? owners_of(system) : 0;
if (!limit || owners <= *limit) return original(system);
// The pass runs on the entity update's thread and is the one reader of the limit, which is
// put back as soon as it returns.
const auto stock = *limit;
*limit = owners;
if (owners > most.load(std::memory_order_relaxed)) most.store(owners, std::memory_order_relaxed);
note(owners, stock);
original(system);
*limit = stock;
}
} // namespace
bool install(std::uintptr_t base) noexcept {
if (installed.exchange(true)) return original != nullptr;
if (!base) return false;
std::array<unsigned char, 32> head{};
if (!memory::peek(base + pw::wake.rva, head) || head != pw::wake.bytes) {
logging::log(logging::Level::warning, logging::Channel::runtime,
"Physics wake guard: this build's physics code differs; it is left as it is.");
return false;
}
const auto target = reinterpret_cast<void*>(base + pw::wake.rva);
if (hook_prepare(target, reinterpret_cast<void*>(&guarded), reinterpret_cast<void**>(&original)) != HookOk ||
hook_enable(target) != HookOk) {
original = nullptr;
logging::log(logging::Level::warning, logging::Channel::runtime, "Physics wake guard: the game's function could not be hooked.");
return false;
}
return true;
}
std::uint32_t widest() noexcept { return most.load(std::memory_order_relaxed); }
} // namespace dingosdk::physics_wake_guard