From 9b79fd78299bd3985a151fc687dcbdf7e7dd68d1 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sun, 11 Oct 2026 18:00:30 -0500 Subject: [PATCH] Guard a crash when the game describes a failed load Guard a crash when the game describes a failed load When a load fails, the game writes "load
failed with error:" and describes each part of the load's error in turn. That list can hold parts that were never set, and the game calls through them: six crash reports from one player on 2.0.5 and 2.0.6 all end at Skate.exe+0x2e3ab9c, after eight parts that read "no error". The guard hooks that one function. Before the game describes the list, every part that is not one of the game's own objects is emptied, so it reads "no error" as an empty part does; the description also runs under an exception handler. The failed load is then the game's to deal with, as it meant to. What the game wrote goes to the log as a warning ("The game described the error of a load that failed"), with how many parts were emptied, so content that does not load can be traced. The game's message names no asset, only an address. --- Engine/Game/Build/20260929/load_errors.h | 21 +++++ Extension/World/load_error_guard.cpp | 115 +++++++++++++++++++++++ Extension/World/load_error_guard.h | 16 ++++ Runtime/runtime_init.cpp | 2 + cmake/Runtime.cmake | 1 + 5 files changed, 155 insertions(+) create mode 100644 Engine/Game/Build/20260929/load_errors.h create mode 100644 Extension/World/load_error_guard.cpp create mode 100644 Extension/World/load_error_guard.h diff --git a/Engine/Game/Build/20260929/load_errors.h b/Engine/Game/Build/20260929/load_errors.h new file mode 100644 index 0000000..bcf82f7 --- /dev/null +++ b/Engine/Game/Build/20260929/load_errors.h @@ -0,0 +1,21 @@ +#pragma once +#include "Engine/Game/Build/fingerprint.h" +#include + +// The game's errors of a load made of several (Extension/World/load_error_guard.cpp). +namespace dingosdk::game::build::v20260929::load_errors { +// describe(errors, text): a virtual of the error that holds one error per part of a load. It +// writes "[i]:" and each part's own description (its virtual at + 8), or "no error" for a null +// one, a line each. The parts are an array of pointers (errors + 0x18) and their count +// (errors + 0x20); the text is the game's fixed text writer, whose first field is its buffer +// with flags in the two low bits. +// +// The game calls it when a load fails, to write "load
failed with error: ..." +// (0x1aa75e0), and the array can hold parts that were never set: crash reports of 2026-10-11 +// (one player, six times, 2.0.5 and 2.0.6) all end in the call through part 8 after eight that +// read "no error" - once a null vtable, else garbage that was read or run. +inline constexpr Fingerprint describe{0x2e3ab40, { + 0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x33, + 0xff,0x48,0x8b,0xda,0x48,0x8b,0xf1,0x48,0x39,0x79,0x20,0x76,0x6b,0x0f,0x1f,0x00}}; +inline constexpr std::uintptr_t parts_offset = 0x18, count_offset = 0x20; +} diff --git a/Extension/World/load_error_guard.cpp b/Extension/World/load_error_guard.cpp new file mode 100644 index 0000000..432954f --- /dev/null +++ b/Extension/World/load_error_guard.cpp @@ -0,0 +1,115 @@ +#include "load_error_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/load_errors.h" +#include +#include +#include +#include + +namespace dingosdk::load_error_guard { +namespace { +namespace le = addr::load_errors; +using Describe = void (*)(void* errors, void* text); +Describe original{}; +std::uintptr_t image_begin{}, image_end{}; +std::atomic installed{}; +std::atomic empties{}, faults{}, told{}; +thread_local unsigned depth{}; // a part can be a list of its own: only the outermost is logged +constexpr std::uint64_t most_parts = 4096, most_told = 40; + +bool in_image(std::uintptr_t address) noexcept { return address >= image_begin && address < image_end; } + +// One of the game's own objects: its vtable and the virtual that describes it are in the image. +bool a_part(std::uintptr_t part) noexcept { + std::uintptr_t vtable{}, describe{}; + return memory::read(part, vtable) && in_image(vtable) && !(vtable & 7) && memory::read(vtable + 8, describe) && in_image(describe); +} +// Empties the parts that are not: how many. A list that cannot be read is left alone (the +// handler around the description is what is left for it). +unsigned empty_bad_parts(std::uintptr_t errors) noexcept { + std::uintptr_t parts{}; + std::uint64_t count{}; + if (!memory::read(errors + le::parts_offset, parts) || !memory::read(errors + le::count_offset, count) || !parts || count > most_parts) return 0; + unsigned emptied{}; + for (std::uint64_t i = 0; i < count; ++i) { + std::uintptr_t part{}; + if (!memory::read(parts + i * 8, part)) break; + if (!part || a_part(part)) continue; + __try { + *reinterpret_cast(parts + i * 8) = 0; + ++emptied; + } __except (EXCEPTION_EXECUTE_HANDLER) { + break; + } + } + return emptied; +} +bool describe_guarded(void* errors, void* text) noexcept { + __try { + original(errors, text); + return true; + } __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { + return false; + } +} +// What the game wrote, for the log: the writer's buffer, on one line. +__declspec(noinline) void tell(std::uintptr_t text, unsigned emptied, bool whole) noexcept { + if (told.fetch_add(1, std::memory_order_relaxed) >= most_told) return; + try { + std::array buffer{}; + std::uintptr_t begin{}; + std::string said; + if (memory::read(text, begin) && (begin &= ~std::uintptr_t{3})) { + if (memory::peek_cstring(begin, buffer.data(), buffer.size()) < 0) buffer.back() = '\0'; + said = buffer.data(); + } + for (auto& c : said) + if (c == '\n' || c == '\r' || c == '\t') c = ' '; + std::string note; + if (emptied) note += " " + std::to_string(emptied) + " part(s) of its error list were never set, where the game would have crashed; they read as no error."; + if (!whole) note += " The description faulted and was cut short."; + logging::log(logging::Level::warning, logging::Channel::runtime, + "The game described the error of a load that failed (content that is missing or broken, often a mod's): {}{}", + said.empty() ? std::string("(nothing readable)") : said, note); + } catch (...) {} +} +void guarded(void* errors, void* text) { + const auto emptied = empty_bad_parts(reinterpret_cast(errors)); + if (emptied) empties.fetch_add(emptied, std::memory_order_relaxed); + ++depth; + const bool whole = describe_guarded(errors, text); + --depth; + if (!whole) faults.fetch_add(1, std::memory_order_relaxed); + if (!depth) tell(reinterpret_cast(text), emptied, whole); +} +} // namespace + +bool install(std::uintptr_t base) noexcept { + if (installed.exchange(true)) return original != nullptr; + if (!base) return false; + std::array head{}; + IMAGE_DOS_HEADER dos{}; + IMAGE_NT_HEADERS64 nt{}; + if (!memory::peek(base + le::describe.rva, head) || head != le::describe.bytes || !memory::peek(base, dos) || dos.e_lfanew <= 0 || + !memory::peek(base + static_cast(dos.e_lfanew), nt) || !nt.OptionalHeader.SizeOfImage) { + logging::log(logging::Level::warning, logging::Channel::runtime, + "Load error guard: this build's load error code differs; it is left as it is."); + return false; + } + image_begin = base; + image_end = base + nt.OptionalHeader.SizeOfImage; + const auto target = reinterpret_cast(base + le::describe.rva); + if (hook_prepare(target, reinterpret_cast(&guarded), reinterpret_cast(&original)) != HookOk || + hook_enable(target) != HookOk) { + original = nullptr; + logging::log(logging::Level::warning, logging::Channel::runtime, "Load error guard: the game's function could not be hooked."); + return false; + } + return true; +} + +std::uint64_t emptied() noexcept { return empties.load(std::memory_order_relaxed); } +} // namespace dingosdk::load_error_guard diff --git a/Extension/World/load_error_guard.h b/Extension/World/load_error_guard.h new file mode 100644 index 0000000..6b30553 --- /dev/null +++ b/Extension/World/load_error_guard.h @@ -0,0 +1,16 @@ +#pragma once +#include + +// A load that failed could crash the game instead: to say why, the game describes each part of +// the load's error in turn, and the list can hold parts that were never set +// (Engine/Game/Build//load_errors.h). The guard looks at the list first and empties every +// part that is not one of the game's own objects, so those read "no error" as an empty part +// does; the description itself runs under an exception handler as well. The failed load is then +// the game's to deal with, as it meant to. What the game wrote is put in the log, so a mod +// whose content does not load can be found. +namespace dingosdk::load_error_guard { +// Checks this build's function and hooks it; once. False when the build differs (no guard). +bool install(std::uintptr_t base) noexcept; +// How many parts were emptied since the game started. +std::uint64_t emptied() noexcept; +} diff --git a/Runtime/runtime_init.cpp b/Runtime/runtime_init.cpp index 1a39da0..8e6e295 100644 --- a/Runtime/runtime_init.cpp +++ b/Runtime/runtime_init.cpp @@ -6,6 +6,7 @@ #include "Engine/Core/Profiling/profiler.h" #include "Extension/Settings/engine_tweaks.h" #include "Extension/Settings/replay_guard.h" +#include "Extension/World/load_error_guard.h" #include "Extension/World/mesh_unload_guard.h" #include "Extension/Console/commands.h" #include "Extension/UI/ui_pointer_skip.h" @@ -317,6 +318,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() { (void)dingosdk::engine_tweaks::install(r.base); (void)dingosdk::replay_guard::install(r.base); (void)dingosdk::mesh_unload_guard::install(r.base); + (void)dingosdk::load_error_guard::install(r.base); dingosdk::console::start_memory_trace_from_environment(); const bool fixed_stop_provider = (global_offline_hook && progression_guard_armed) && dingosdk::prepare_fixed_stop_entitlement_provider(r.base); diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index 3942e45..b282683 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -70,6 +70,7 @@ add_library(dingosdk_runtime SHARED Extension/Settings/job_spin.cpp Extension/Settings/engine_tweaks.cpp Extension/Settings/replay_guard.cpp + Extension/World/load_error_guard.cpp Extension/World/mesh_unload_guard.cpp Extension/Settings/replay_drafts.cpp Extension/UI/ui_pointer_skip.cpp