From 3faa58bb42ddf9a36aa9f45deda1e9534810f128 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Thu, 15 Oct 2026 18:16:22 -0500 Subject: [PATCH] Render-resource pool: the other renderer is allocated room for its metadata pointers 719cba1 stopped writing the metadata pointers into the other renderer (0x2daa00 bytes, Skate.exe+0x30c64cc, kept at 0x75f2700), taking them for the state owner's alone. They are not: the other renderer's own code reads them through the patched view (0x030e6bff, called from 0x30a7e18) and append (0x03e94fe6, called from 0x30d8ebb). Without the write, it read whatever lay 18 MB past its end. With it, as in every release before, that memory was overwritten, or the write faulted when nothing was mapped there (the store_field crash). - The other renderer's allocation is patched to the state owner's 0x11ab080 bytes (0x030c64cc, mov ecx), so +0x11aae28/+0x11aae30 are inside it. - Both renderers' metadata pointers are written again, before and after the native constructor. The resource-state and relation-mask vectors stay the state owner's alone: the other renderer keeps fields of its own at +0x2da920. - The contract's comment says which renderer reads what, and where. --- .../Build/20260929/render_resource_pool.h | 16 +++++++++++---- .../Assets/native_render_resource_pool.cpp | 20 +++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Engine/Game/Build/20260929/render_resource_pool.h b/Engine/Game/Build/20260929/render_resource_pool.h index 463e504..bf6fece 100644 --- a/Engine/Game/Build/20260929/render_resource_pool.h +++ b/Engine/Game/Build/20260929/render_resource_pool.h @@ -29,6 +29,9 @@ inline constexpr auto constructor_entry = hex_bytes( inline constexpr auto index_table_constructor_entry = hex_bytes( "4053555657415641574883ec78c5f9efc033ed488bf948896c2428488d4c2440"); +// mov ecx, 0x2daa00 -> mov ecx, 0x11ab080 (the state owner's size, 0x3efd347). +inline constexpr auto other_renderer_size_before = hex_bytes("b900aa2d00"); +inline constexpr auto other_renderer_size_after = hex_bytes("b980b01a01"); inline constexpr auto vector_begin_before = hex_bytes("488d81b0000000"); inline constexpr auto vector_begin_after = hex_bytes("488b8190000000"); inline constexpr auto vector_bytes_before = hex_bytes("480500001e00"); @@ -92,15 +95,20 @@ inline constexpr std::ptrdiff_t relation_mask_vector_offset = 0x114ab60; inline constexpr std::uintptr_t constructor_rva = 0x03e7f300; // The constructor is the base of two renderers. Only the one constructed here // (vtable 0x64f2c10, 0x11ab080 bytes) has the resource-state and relation-mask -// vectors and the metadata fields: every read of owner+0x2da920, owner+0x114ab60 -// and owner+0x11aae28 is in its code. The other (constructed at 0x030a0a46, -// vtable 0x6414168) is 0x2daa00 bytes (Skate.exe+0x30c64cc): it keeps fields of -// its own at +0x2da920, and the rest are past its end. +// vectors: every read of owner+0x2da920 and owner+0x114ab60 is in its code. The +// other (constructed at 0x030a0a46, vtable 0x6414168, kept at 0x75f2700) keeps +// fields of its own at +0x2da920. Both use the metadata: the patched view +// (0x030e6bff, called from 0x30a7e18 with the other renderer) and append +// (0x03e94fe6, called from 0x30d8ebb) read its pointers at +0x11aae28/+0x11aae30. +// The other renderer was allocated 0x2daa00 bytes, which ends before them, so it +// is allocated the state owner's 0x11ab080 instead (other_renderer_size, above). inline constexpr std::uintptr_t state_owner_call_rva = 0x03f33a46; inline constexpr std::uintptr_t index_table_constructor_rva = 0x03e7e400; inline constexpr std::uintptr_t resource_flags_priority_rva = 0x03f56073; inline constexpr std::uintptr_t resource_flags_visibility_rva = 0x03f5789a; inline constexpr Patch patches[]{ + // The other renderer's allocation, large enough for the metadata pointers. + {0x030c64cc, other_renderer_size_before, other_renderer_size_after}, {0x03e7f33c, vector_begin_before, vector_begin_after}, {0x03e7f351, vector_bytes_before, vector_bytes_after}, {0x03e94e99, capacity_before, capacity_after}, diff --git a/Extension/Assets/native_render_resource_pool.cpp b/Extension/Assets/native_render_resource_pool.cpp index b2ede62..2e47243 100644 --- a/Extension/Assets/native_render_resource_pool.cpp +++ b/Extension/Assets/native_render_resource_pool.cpp @@ -308,13 +308,13 @@ void* __fastcall construct_native_renderer(void* self, void* configuration) { // The patched constructor loads its initial vector pointer from +0x90, // constructs the native default record there, and advances +0x98 itself. store_field(self, vector_begin_offset, buffers.records); - // The metadata fields, like the state vectors, are the state owner's alone: the other - // renderer is 0x2daa00 bytes (Skate.exe+0x30c64cc), and +0x11aae28 is 18 MB past its end. - // Written there for every launch until 2026-10-15, they went into whatever memory lay there - // (a crash in store_field when it was not mapped). + // Both renderers read the metadata through these pointers (render_resource_pool.h). The other + // renderer was allocated 0x2daa00 bytes, and they were written 18 MB past its end, into + // whatever lay there (a crash in store_field when nothing did): it is allocated the state + // owner's size now (the patch at 0x030c64cc), so they are its own. + store_field(self, metadata_begin_offset, buffers.metadata); + store_field(self, metadata_end_offset, metadata_end); if (states) { - store_field(self, metadata_begin_offset, buffers.metadata); - store_field(self, metadata_end_offset, metadata_end); store_field(self, resource_state_vector_offset, buffers.resource_states); store_field(self, relation_mask_vector_offset, relation_mask_begin); } @@ -323,11 +323,9 @@ void* __fastcall construct_native_renderer(void* self, void* configuration) { // The side-array fields are initialized by later renderer setup on the // stock build. Keep the external values authoritative across construction. - if (states) { - store_field(self, metadata_begin_offset, buffers.metadata); - store_field(self, metadata_end_offset, metadata_end); - state_owner.store(reinterpret_cast(self), std::memory_order_release); - } + store_field(self, metadata_begin_offset, buffers.metadata); + store_field(self, metadata_end_offset, metadata_end); + if (states) state_owner.store(reinterpret_cast(self), std::memory_order_release); if (load_field(self, vector_begin_offset) != buffers.records || load_field(self, vector_capacity_offset) != record_end || load_field(self, vector_end_offset) < buffers.records ||