#include "backtrace.h" #include "protocol.h" #include "backtrace_config.h" #include #include #include namespace dingosdk::backtrace { namespace { std::array handles{}; HANDLE helper{}; SharedReport* report{}; // 0 = ready, 1 = capture owns the IPC, 2 = stopped/initializing. volatile LONG capturing{2}; struct View { void* value{}; ~View() { if (value) UnmapViewOfFile(value); } }; template bool copy(std::array& target, const std::wstring& value) { if (value.size() >= N) return false; std::copy(value.begin(), value.end(), target.begin()); target[value.size()] = 0; return true; } } StartResult start(const std::filesystem::path& log_directory) noexcept { try { if (report) return StartResult::ready; if (environment(L"RESKATE_CRASH_REPORTING") == L"0") return StartResult::disabled; auto url = environment(L"RESKATE_BACKTRACE_URL"); if (url.empty()) url = configured_url; if (url.empty()) return StartResult::disabled; if (!url.starts_with(L"https://") || url.size() >= SharedReport{}.url.size()) return StartResult::failed; HMODULE module{}; if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(&handles), &module)) return StartResult::failed; std::array module_path{}, process_path{}; const auto module_size = GetModuleFileNameW(module, module_path.data(), static_cast(module_path.size())); const auto process_size = GetModuleFileNameW(nullptr, process_path.data(), static_cast(process_path.size())); if (!module_size || module_size >= module_path.size() || !process_size || process_size >= process_path.size()) return StartResult::failed; const auto executable = std::filesystem::path(module_path.data()).parent_path() / L"ReSkateLauncher.exe"; SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE}; std::array owned; owned[mapping].value = CreateFileMappingW(INVALID_HANDLE_VALUE, &security, PAGE_READWRITE, 0, sizeof(SharedReport), nullptr); for (std::size_t i = requested; i <= ready; ++i) owned[i].value = CreateEventW(&security, TRUE, FALSE, nullptr); if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &owned[parent].value, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | SYNCHRONIZE, TRUE, 0)) return StartResult::failed; std::array inherited{}; for (std::size_t i = 0; i < handle_count; ++i) { if (!owned[i].value) return StartResult::failed; inherited[i] = owned[i].value; } View view{MapViewOfFile(owned[mapping].value, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedReport))}; if (!view.value) return StartResult::failed; auto* shared = new (view.value) SharedReport{}; if (!copy(shared->url, url) || !copy(shared->directory, std::filesystem::absolute(log_directory / L"crashes").wstring()) || !copy(shared->application, std::filesystem::path(process_path.data()).filename().wstring())) return StartResult::failed; // Frostbite handles some fatal exceptions itself and writes a native dump // before exiting, so those faults never reach our last-chance filter. The // game's LocalAppData is redirected to ReSkate\Game (Extension/Boot/user_data_redirect.cpp). if (_wcsicmp(shared->application.data(), L"Skate.exe") == 0) { const auto local = environment(L"LOCALAPPDATA"); if (!local.empty() && !copy(shared->native_directory, (std::filesystem::path(local) / L"ReSkate" / L"Game" / L"Skate" / L"CrashDumps").wstring())) return StartResult::failed; } SIZE_T size{}; InitializeProcThreadAttributeList(nullptr, 1, 0, &size); std::vector storage(size); auto* attributes = reinterpret_cast(storage.data()); if (!InitializeProcThreadAttributeList(attributes, 1, 0, &size)) return StartResult::failed; struct Attributes { LPPROC_THREAD_ATTRIBUTE_LIST value; ~Attributes() { DeleteProcThreadAttributeList(value); } } cleanup{attributes}; if (!UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited.data(), sizeof(inherited), nullptr, nullptr)) return StartResult::failed; auto command = std::format(L"\"{}\" --reskate-crash-helper", executable.wstring()); for (const auto handle : inherited) command += std::format(L" {}", reinterpret_cast(handle)); STARTUPINFOEXW startup{}; startup.StartupInfo.cb = sizeof(startup); startup.lpAttributeList = attributes; PROCESS_INFORMATION process{}; if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, nullptr, executable.parent_path().c_str(), &startup.StartupInfo, &process)) return StartResult::failed; Handle child(process.hProcess), thread(process.hThread); // Prevent later child processes from inheriting our IPC and parent handles. for (const auto handle : inherited) SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0); const HANDLE wait_for[]{owned[ready].value, child.value}; if (WaitForMultipleObjects(2, wait_for, FALSE, 5000) != WAIT_OBJECT_0) { SetEvent(owned[stopping].value); return StartResult::failed; } for (std::size_t i = 0; i < handle_count; ++i) handles[i] = owned[i].release(); helper = child.release(); report = shared; view.value = nullptr; InterlockedExchange(&capturing, 0); 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(); report->exception = exception; MemoryBarrier(); if (SetEvent(handles[requested])) { const HANDLE wait_for[]{handles[captured], helper}; WaitForMultipleObjects(2, wait_for, FALSE, capture_timeout_ms); } } void stop() noexcept { // An exception already using these handles owns them until process exit. // Never unmap its report or close its events from a concurrent shutdown. if (InterlockedCompareExchange(&capturing, 2, 0) != 0) return; SetEvent(handles[stopping]); UnmapViewOfFile(report); report = nullptr; for (auto& handle : handles) { CloseHandle(handle); handle = nullptr; } CloseHandle(helper); helper = nullptr; } }