From 1cf3a7a4ab062e8b2cf2013eb65dea7e5c4bcc54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 20:10:39 +0000 Subject: [PATCH 1/2] fix: crash footer + 4MB scan cap so safe-mode triggers after Vulkan SIGSEGV Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/47bccd72-9ce0-4acd-b9af-3a7ba592a5bb Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/CrashDiagnostics.cs | 101 +++++++++++++++++++++++++-- osu.Android/Native/crash_handler.cpp | 50 +++++++++---- 2 files changed, 132 insertions(+), 19 deletions(-) diff --git a/osu.Android/CrashDiagnostics.cs b/osu.Android/CrashDiagnostics.cs index 8097fe8a14fc..489e94f86e48 100644 --- a/osu.Android/CrashDiagnostics.cs +++ b/osu.Android/CrashDiagnostics.cs @@ -485,11 +485,21 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa } } - // Bound the read used by DetectPreviousDrawThreadNativeCrash. The native crash - // block + register dump + backtrace is ~2 KB; 256 KiB easily covers the most - // recent block even when the file also contains many ALIVE markers and a - // HangWatchdog dump from the previous process. - private const long crash_log_scan_byte_cap = 256L * 1024; + // Bound the read used by DetectPreviousDrawThreadNativeCrash. + // + // Footer path (new, fast): crash_handler.cpp appends a compact + // "=== CRASH FOOTER ===" line AFTER the /proc/self/maps dump, so it + // always lands in the last ~1 KiB of the log. We scan only the last + // crash_log_footer_scan_byte_cap bytes to find it quickly. + // + // Header-block fallback (legacy): older builds without the footer + // require scanning far enough back to reach the "[osu!] NATIVE CRASH" + // marker, which can be hundreds of KiB from the end because the + // /proc/self/maps section is typically 400–500 KiB on Android. The + // log is bounded at ~3 MiB by the rotation logic, so 4 MiB covers + // the entire file in the worst case. + private const long crash_log_footer_scan_byte_cap = 32L * 1024; + private const long crash_log_scan_byte_cap = 4L * 1024 * 1024; /// /// Inspect the on-disk native_crash.log for the most recent @@ -534,6 +544,25 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa string path = Path.Combine(dir, CRASH_LOG_NAME); if (!File.Exists(path)) return null; + // --- Fast path: look for the compact footer line appended by + // crash_handler.cpp after the /proc/self/maps dump. It is + // always in the last few KiB of the log, so a small read is + // enough. Falls through to the legacy full-header scan if the + // footer is absent (older native builds). + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + long footerStart = Math.Max(0, fs.Length - crash_log_footer_scan_byte_cap); + fs.Seek(footerStart, SeekOrigin.Begin); + using var sr = new StreamReader(fs); + string footerTail = sr.ReadToEnd(); + + var fromFooter = tryParseFooter(footerTail); + if (fromFooter != null) return fromFooter; + } + + // --- Legacy path: the full-header "[osu!] NATIVE CRASH" block. + // The /proc/self/maps section can be 400–500 KiB, so we scan + // the last 4 MiB (the log rotation cap) to guarantee we reach it. string tail; using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) @@ -598,6 +627,68 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa } } + // Parse the compact "=== CRASH FOOTER ===" line appended by crash_handler.cpp + // after the /proc/self/maps section. Format: + // === CRASH FOOTER sig= pid= uptime_ns= thread= === + // Returns null if no valid footer line is found or the crash is not a + // fatal Draw-thread event. + private static DrawThreadNativeCrashInfo? tryParseFooter(string tail) + { + const string footer_marker = "=== CRASH FOOTER "; + int last = tail.LastIndexOf(footer_marker, StringComparison.Ordinal); + if (last < 0) return null; + + int lineEnd = tail.IndexOf('\n', last); + if (lineEnd < 0) lineEnd = tail.Length; + string line = tail.Substring(last, lineEnd - last); + + string? signal = extractFooterField(line, " sig=", " "); + string? pid = extractFooterField(line, " pid=", " "); + string? uptime = extractFooterField(line, " uptime_ns=", " "); + string? thread = extractFooterField(line, " thread=", " ==="); + + if (signal == null || thread == null) return null; + + bool isFatalSignal = signal.StartsWith("SIGSEGV", StringComparison.Ordinal) + || signal.StartsWith("SIGBUS", StringComparison.Ordinal) + || signal.StartsWith("SIGABRT", StringComparison.Ordinal); + if (!isFatalSignal) return null; + + if (!thread.StartsWith("Draw", StringComparison.Ordinal)) return null; + + string fingerprint = (uptime != null && pid != null) + ? $"u{uptime}-p{pid}" + : "footer:" + ((uint)line.GetHashCode()).ToString("x"); + + // The footer does not carry a top-frame symbol — report it as such. + return new DrawThreadNativeCrashInfo(fingerprint, signal, thread, "(footer — no top frame)"); + } + + // Extract a field value from a single footer line. + // Reads from after `key` to either the first occurrence of `stopBefore` + // or the end of the line (whichever comes first). + private static string? extractFooterField(string line, string key, string? stopBefore) + { + int idx = line.IndexOf(key, StringComparison.Ordinal); + if (idx < 0) return null; + + int start = idx + key.Length; + int end; + + if (stopBefore != null) + { + end = line.IndexOf(stopBefore, start, StringComparison.Ordinal); + if (end < 0) end = line.Length; + } + else + { + end = line.IndexOf(' ', start); + if (end < 0) end = line.Length; + } + + return line.Substring(start, end - start); + } + private static string? extractField(string block, string keyWithEquals) { int idx = block.IndexOf(keyWithEquals, StringComparison.Ordinal); diff --git a/osu.Android/Native/crash_handler.cpp b/osu.Android/Native/crash_handler.cpp index 7b84e40636a7..28fdb4d19a57 100644 --- a/osu.Android/Native/crash_handler.cpp +++ b/osu.Android/Native/crash_handler.cpp @@ -1013,6 +1013,21 @@ static void crashHandler(int sig, siginfo_t* info, void* ucontext) { } g_dumpWritten = 1; + // Capture crash metadata once so both the header block and the compact + // footer written after /proc/self/maps use identical values. The footer + // is what CrashDiagnostics.scanForDrawThreadCrash now looks for first — + // it always lands in the last few KB of the log even when the memory-map + // section is several hundred KB long. + long long crash_uptime_ns; + { + struct timespec ts{}; + clock_gettime(CLOCK_BOOTTIME, &ts); + crash_uptime_ns = (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec; + } + const long long crash_pid = (long long)getpid(); + char crash_thread_name[32] = {}; + (void)pthread_getname_np(pthread_self(), crash_thread_name, sizeof(crash_thread_name)); + // Open the dump file (append). If g_logPath is empty we still log to logcat. // // Pre-rotate runaway: if the existing log is more than 4× the soft cap @@ -1051,21 +1066,9 @@ static void crashHandler(int sig, siginfo_t* info, void* ucontext) { writeStr(fd, "\n pid = "); writeDec(fd, (long long)getpid()); writeStr(fd, "\n uptime_ns = "); - { - struct timespec ts; - clock_gettime(CLOCK_BOOTTIME, &ts); - writeDec(fd, (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec); - } + writeDec(fd, crash_uptime_ns); writeStr(fd, "\n thread_name = "); - { - char name[32] = {}; - // pthread_getname_np is signal-safe in bionic (it's a thin wrapper - // over a /proc/self/task//comm read). - if (pthread_getname_np(pthread_self(), name, sizeof(name)) == 0) - writeStr(fd, name); - else - writeStr(fd, "?"); - } + writeStr(fd, crash_thread_name[0] ? crash_thread_name : "?"); writeStr(fd, "\n"); // Logcat header (so users with logcat access also see something useful). @@ -1137,6 +1140,25 @@ static void crashHandler(int sig, siginfo_t* info, void* ucontext) { writeStr(fd, "=========================================================\n"); writeStr(fd, "=== END OF CRASH DUMP ===\n"); + // Compact one-line footer, written AFTER the /proc/self/maps section. + // The C# scanner (CrashDiagnostics.scanForDrawThreadCrash) looks for this + // footer FIRST in the last 32 KiB of the log. Without it the scanner + // would need to scan hundreds of KiB backwards past the memory map just to + // reach the "[osu!] NATIVE CRASH" header — causing safe-mode detection to + // silently fail and the app to keep relaunching into the same Vulkan crash. + // The uptime_ns and pid values here MATCH the header block exactly (both + // were captured at handler entry above) so the fingerprint computed by the + // C# scanner is identical regardless of which block it reads. + writeStr(fd, "=== CRASH FOOTER sig="); + writeStr(fd, signalName(sig)); + writeStr(fd, " pid="); + writeDec(fd, crash_pid); + writeStr(fd, " uptime_ns="); + writeDec(fd, crash_uptime_ns); + writeStr(fd, " thread="); + writeStr(fd, crash_thread_name[0] ? crash_thread_name : "?"); + writeStr(fd, " ===\n"); + if (fd >= 0) { fsync(fd); close(fd); From 9d71cd0db890b82a83877fd2aaa59d8ed2380b4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 21:08:55 +0000 Subject: [PATCH 2/2] fix: bump framework to 2026.502.4, demote SetFormat-skipped log to Debug Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/2b0680fd-aeb5-4b8a-b119-8da1afbb018d Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android.props | 2 +- osu.Android/OsuGameActivity.cs | 2 +- osu.Game/osu.Game.csproj | 4 ++-- osu.iOS.props | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 07bb54aff5ef..575166194fab 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -99,7 +99,7 @@ - +