mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 13:57:05 +02:00
Crash reports: the game's own fatal crashes come with the memory their stacks point at
Most crashes are caught by the game's own fatal handlers, which write a dump of thread stacks only; the launcher could only copy it after the game had exited. A crash on a freed or overwritten object then showed the faulting registers but not the object or the list it came from. - game_fatal_dump: the game's MiniDumpWriteDump import points at ReSkate. Before the game writes its dump, the crash reporter takes its own of the still-running game for the faulting thread; the game's dump follows as before. Reports say capture.source "game-fatal-handler". - The reporter's dumps include the memory the stacks point at (MiniDumpWithIndirectlyReferencedMemory), a few MB more a report. - The shared report says who asked for the dump (protocol version 4). - Tests: a game-handler crash is captured with a heap block only its stack points at; ReSkate's own capture keeps it too.
This commit is contained in:
1 parent
ea3f081367
commit
3249f747dc
10 files changed
+169
-18
No files matched your search
@@ -8,6 +8,7 @@
|
||||
#include "Engine/Core/Debug/trace.h"
|
||||
#include "crash_report_config.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <format>
|
||||
#include <iostream>
|
||||
@@ -30,6 +31,19 @@ LONG WINAPI native_child_handler(EXCEPTION_POINTERS* pointers) {
|
||||
CopyFileW(path.c_str(), (native_fixture_directory / L"duplicate.dmp").c_str(), TRUE);
|
||||
ExitProcess(42);
|
||||
}
|
||||
// A heap block only the faulting stack points at: present in a dump only with the memory the
|
||||
// stacks reference. Made at run time, so its bytes are nowhere else in the process.
|
||||
// (Filled in place: a copy left on a stack would be in every dump.)
|
||||
__declspec(noinline) void fill_marker(std::array<char, 64>& bytes) {
|
||||
for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast<char>((i * 37 + 11) ^ 0xa5);
|
||||
}
|
||||
// The game's own fatal handler: ReSkate's import hook asks for its dump first, then the game
|
||||
// writes its own (game_fatal_dump.cpp).
|
||||
LONG WINAPI fatal_child_handler(EXCEPTION_POINTERS* pointers) {
|
||||
capture_game_fatal(GetCurrentThreadId(), pointers);
|
||||
return native_child_handler(pointers);
|
||||
}
|
||||
enum class Mode { direct, native, fatal };
|
||||
std::string read(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||
@@ -190,7 +204,8 @@ void queue_checks(const std::filesystem::path& root) {
|
||||
upload_pending(L"test", queue, failure);
|
||||
check(!std::filesystem::exists(expired) && !std::filesystem::exists(log_attachment_path(expired)), "Expired report or attachment retained");
|
||||
}
|
||||
void crash_check(const std::filesystem::path& root, bool native) {
|
||||
void crash_check(const std::filesystem::path& root, Mode mode) {
|
||||
const bool native = mode != Mode::direct;
|
||||
std::filesystem::create_directories(root);
|
||||
std::array<wchar_t, 32768> executable{};
|
||||
GetModuleFileNameW(nullptr, executable.data(), static_cast<DWORD>(executable.size()));
|
||||
@@ -200,7 +215,8 @@ void crash_check(const std::filesystem::path& root, bool native) {
|
||||
std::filesystem::copy_file(executable.data(), child_path);
|
||||
std::filesystem::copy_file(std::filesystem::path(executable.data()).parent_path() / L"ReSkateLauncher.exe", root / L"ReSkateLauncher.exe");
|
||||
}
|
||||
auto command = std::format(L"\"{}\" {} \"{}\"", child_path.wstring(), native ? L"--native-crash" : L"--crash", root.wstring());
|
||||
auto command = std::format(L"\"{}\" {} \"{}\"", child_path.wstring(),
|
||||
mode == Mode::fatal ? L"--fatal-crash" : native ? L"--native-crash" : L"--crash", root.wstring());
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
check(CreateProcessW(child_path.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process) != FALSE, "Cannot start crash fixture");
|
||||
@@ -226,7 +242,13 @@ void crash_check(const std::filesystem::path& root, bool native) {
|
||||
check(MiniDumpReadDumpStream(bytes.data(), ModuleListStream, &stream, &data, &size) != FALSE, "Minidump module list missing");
|
||||
check(Json::parse(read(dump.wstring() + L".json")).at("application").string() == "ReSkate", "Crash attributes missing");
|
||||
check(Json::parse(read(dump.wstring() + L".json")).at("capture.source").string() ==
|
||||
(native ? "game-native" : "unhandled-exception"), "Wrong capture source");
|
||||
(mode == Mode::fatal ? "game-fatal-handler" : native ? "game-native" : "unhandled-exception"), "Wrong capture source");
|
||||
if (mode != Mode::native) {
|
||||
std::array<char, 64> marker{};
|
||||
fill_marker(marker);
|
||||
check(bytes.find(std::string_view(marker.data(), marker.size())) != std::string::npos,
|
||||
"Dump lacks the heap memory the faulting stack points at");
|
||||
}
|
||||
check(Json::parse(read(dump.wstring() + L".json")).at("attachment.log").string() == "complete" &&
|
||||
read(log_attachment_path(dump)) == log, "Crash did not snapshot its session log");
|
||||
const auto tags = Json::parse(read(dump.wstring() + L".json"));
|
||||
@@ -234,7 +256,7 @@ void crash_check(const std::filesystem::path& root, bool native) {
|
||||
tags.at("game.destination").string() == "Levels/Game/DingoLevel_Root/DingoLevel_Root + levels/game/dingolevel_reskate_lsc_b2/dingolevel_reskate_lsc_b2 @ DefaultStartPoint" &&
|
||||
tags.at("gpu.name").string() == "Fixture GPU" && tags.at("gpu.driver").string() == "32.0.21025.1",
|
||||
"Crash was not tagged with its level and graphics card");
|
||||
if (native) {
|
||||
if (mode == Mode::native) {
|
||||
const auto source = root / L"local" / L"ReSkate" / L"Game" / L"Skate" / L"CrashDumps";
|
||||
const auto pid = GetProcessId(child.value), created = process_creation_seconds(child.value);
|
||||
const auto selected = find_native_dump(source, pid, created);
|
||||
@@ -289,8 +311,10 @@ int wmain(int argc, wchar_t** argv) {
|
||||
check(result.accepted, "Sentry did not confirm test report");
|
||||
return 0;
|
||||
}
|
||||
if (argc == 3 && (std::wstring_view(argv[1]) == L"--crash" || std::wstring_view(argv[1]) == L"--native-crash")) {
|
||||
const bool native = std::wstring_view(argv[1]) == L"--native-crash";
|
||||
if (argc == 3 && (std::wstring_view(argv[1]) == L"--crash" || std::wstring_view(argv[1]) == L"--native-crash" ||
|
||||
std::wstring_view(argv[1]) == L"--fatal-crash")) {
|
||||
const bool fatal = std::wstring_view(argv[1]) == L"--fatal-crash";
|
||||
const bool native = fatal || std::wstring_view(argv[1]) == L"--native-crash";
|
||||
if (native) {
|
||||
const auto local = std::filesystem::path(argv[2]) / L"local";
|
||||
SetEnvironmentVariableW(L"LOCALAPPDATA", local.c_str());
|
||||
@@ -302,10 +326,13 @@ int wmain(int argc, wchar_t** argv) {
|
||||
SetUnhandledExceptionFilter(finish_child);
|
||||
logging::Options options; options.directory = argv[2]; options.truncate_file = true;
|
||||
check(logging::initialize(options), "Fixture logging failed");
|
||||
if (native) SetUnhandledExceptionFilter(native_child_handler); // Reproduce the game's later handler registration.
|
||||
if (native) SetUnhandledExceptionFilter(fatal ? fatal_child_handler : native_child_handler); // Reproduce the game's later handler registration.
|
||||
set_destination(L"Levels/Game/DingoLevel_Root/DingoLevel_Root + levels/game/dingolevel_reskate_lsc_b2/dingolevel_reskate_lsc_b2 @ DefaultStartPoint");
|
||||
set_gpu(L"Fixture GPU", L"32.0.21025.1");
|
||||
RaiseException(test_exception, EXCEPTION_NONCONTINUABLE, 0, nullptr);
|
||||
auto* const volatile marker = new std::array<char, 64>;
|
||||
fill_marker(*marker);
|
||||
const ULONG_PTR arguments[]{reinterpret_cast<ULONG_PTR>(marker)};
|
||||
RaiseException(test_exception, EXCEPTION_NONCONTINUABLE, 1, arguments);
|
||||
return 3;
|
||||
}
|
||||
check(argc == 2, "Pass a fixture root");
|
||||
@@ -320,8 +347,9 @@ int wmain(int argc, wchar_t** argv) {
|
||||
endpoint_checks();
|
||||
attachment_checks(root);
|
||||
queue_checks(root);
|
||||
crash_check(root / L"direct", false);
|
||||
crash_check(root / L"native", true);
|
||||
std::cout << "Timing envelopes, endpoint, receipts, multipart attachments, log snapshots, retries, retention, opt-out, exception chaining, direct capture and native-handler fallback checks passed.\n";
|
||||
crash_check(root / L"direct", Mode::direct);
|
||||
crash_check(root / L"native", Mode::native);
|
||||
crash_check(root / L"fatal", Mode::fatal);
|
||||
std::cout << "Timing envelopes, endpoint, receipts, multipart attachments, log snapshots, retries, retention, opt-out, exception chaining, direct capture, native-handler fallback and game fatal-handler capture checks passed.\n";
|
||||
} catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; }
|
||||
}
|
||||
@@ -104,16 +104,25 @@ StartResult start(const std::filesystem::path& log_directory) noexcept {
|
||||
return StartResult::ready;
|
||||
} catch (...) { return StartResult::failed; }
|
||||
}
|
||||
void capture(EXCEPTION_POINTERS* exception) noexcept {
|
||||
if (!exception || InterlockedCompareExchange(&capturing, 1, 0) != 0) return;
|
||||
report->thread_id = GetCurrentThreadId();
|
||||
namespace {
|
||||
void capture_on(DWORD thread_id, EXCEPTION_POINTERS* exception, CaptureSource source) noexcept {
|
||||
if (!exception || !thread_id || InterlockedCompareExchange(&capturing, 1, 0) != 0) return;
|
||||
report->thread_id = thread_id;
|
||||
report->exception = exception;
|
||||
report->source = source;
|
||||
MemoryBarrier();
|
||||
if (SetEvent(handles[requested])) {
|
||||
const HANDLE wait_for[]{handles[captured], helper};
|
||||
WaitForMultipleObjects(2, wait_for, FALSE, capture_timeout_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
void capture(EXCEPTION_POINTERS* exception) noexcept {
|
||||
capture_on(GetCurrentThreadId(), exception, CaptureSource::unhandled_exception);
|
||||
}
|
||||
void capture_game_fatal(DWORD thread_id, EXCEPTION_POINTERS* exception) noexcept {
|
||||
capture_on(thread_id, exception, CaptureSource::game_fatal_handler);
|
||||
}
|
||||
void set_destination(std::wstring_view destination) noexcept {
|
||||
if (auto* const shared = report) copy_cut(shared->destination, destination);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ StartResult start(const std::filesystem::path& log_directory) noexcept;
|
||||
void stop() noexcept;
|
||||
// Last-chance path: only signals the prestarted helper and waits at most 15 seconds.
|
||||
void capture(EXCEPTION_POINTERS* exception) noexcept;
|
||||
// The same, from the game's own fatal handler just before it writes its dump: thread_id is the
|
||||
// thread that faulted, which need not be the calling one. The first capture of either kind wins.
|
||||
void capture_game_fatal(DWORD thread_id, EXCEPTION_POINTERS* exception) noexcept;
|
||||
// The level being loaded ("root + sublevel @ start point") and the graphics card the game renders
|
||||
// on, for a report's tags: game.map, game.destination, gpu.name and gpu.driver. Nothing while
|
||||
// reporting is off; too long a value is cut short.
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
#include <string>
|
||||
|
||||
namespace dingosdk::crash_report {
|
||||
inline constexpr DWORD protocol_version = 3;
|
||||
inline constexpr DWORD protocol_version = 4;
|
||||
// Who asked for the dump: ReSkate's last-chance filter, or the game's own fatal handler about to
|
||||
// write its dump (which it does without ever reaching that filter).
|
||||
enum class CaptureSource : DWORD { unhandled_exception, game_fatal_handler };
|
||||
inline constexpr DWORD capture_timeout_ms = 15000;
|
||||
struct SharedReport {
|
||||
DWORD version{protocol_version};
|
||||
@@ -20,6 +23,7 @@ struct SharedReport {
|
||||
std::array<wchar_t, 512> destination{};
|
||||
std::array<wchar_t, 128> gpu_name{};
|
||||
std::array<wchar_t, 32> gpu_driver{};
|
||||
CaptureSource source{};
|
||||
};
|
||||
// Explicitly inherited unnamed handles; no token or paths on the command line.
|
||||
enum HandleIndex : std::size_t { mapping, requested, captured, stopping, ready, parent, handle_count };
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Reviewed native contract for supported_build::game_sha256 (September 29 2026).
|
||||
// The game's fatal handlers catch most crashes themselves (ReSkate's last-chance filter never
|
||||
// sees them) and write their own minidump, of thread stacks only, in-process: three callers
|
||||
// (0x18e3ea1 "Frostbite MiniDump", 0x2d28c9d, 0x2d2bfee) through one import thunk.
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace dingosdk::game::build::v20260929::game_fatal_dump {
|
||||
// MiniDumpWriteDump's import slot (api-ms-win-core-debug-minidump-l1-1-0.dll).
|
||||
inline constexpr std::uintptr_t import_slot = 0x05feb2e0;
|
||||
// The thunk every caller goes through: jmp qword ptr [import_slot].
|
||||
inline constexpr std::uintptr_t thunk = 0x0546dbc6;
|
||||
inline constexpr std::array<std::uint8_t, 6> thunk_bytes{0xff, 0x25, 0x14, 0xd7, 0xb7, 0x00};
|
||||
} // namespace dingosdk::game::build::v20260929::game_fatal_dump
|
||||
Reference in new issue
Block a user