Skip to content

Commit 1cf3a7a

Browse files
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>
1 parent 347426e commit 1cf3a7a

2 files changed

Lines changed: 132 additions & 19 deletions

File tree

osu.Android/CrashDiagnostics.cs

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -485,11 +485,21 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
485485
}
486486
}
487487

488-
// Bound the read used by DetectPreviousDrawThreadNativeCrash. The native crash
489-
// block + register dump + backtrace is ~2 KB; 256 KiB easily covers the most
490-
// recent block even when the file also contains many ALIVE markers and a
491-
// HangWatchdog dump from the previous process.
492-
private const long crash_log_scan_byte_cap = 256L * 1024;
488+
// Bound the read used by DetectPreviousDrawThreadNativeCrash.
489+
//
490+
// Footer path (new, fast): crash_handler.cpp appends a compact
491+
// "=== CRASH FOOTER ===" line AFTER the /proc/self/maps dump, so it
492+
// always lands in the last ~1 KiB of the log. We scan only the last
493+
// crash_log_footer_scan_byte_cap bytes to find it quickly.
494+
//
495+
// Header-block fallback (legacy): older builds without the footer
496+
// require scanning far enough back to reach the "[osu!] NATIVE CRASH"
497+
// marker, which can be hundreds of KiB from the end because the
498+
// /proc/self/maps section is typically 400–500 KiB on Android. The
499+
// log is bounded at ~3 MiB by the rotation logic, so 4 MiB covers
500+
// the entire file in the worst case.
501+
private const long crash_log_footer_scan_byte_cap = 32L * 1024;
502+
private const long crash_log_scan_byte_cap = 4L * 1024 * 1024;
493503

494504
/// <summary>
495505
/// Inspect the on-disk <c>native_crash.log</c> for the most recent
@@ -534,6 +544,25 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
534544
string path = Path.Combine(dir, CRASH_LOG_NAME);
535545
if (!File.Exists(path)) return null;
536546

547+
// --- Fast path: look for the compact footer line appended by
548+
// crash_handler.cpp after the /proc/self/maps dump. It is
549+
// always in the last few KiB of the log, so a small read is
550+
// enough. Falls through to the legacy full-header scan if the
551+
// footer is absent (older native builds).
552+
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
553+
{
554+
long footerStart = Math.Max(0, fs.Length - crash_log_footer_scan_byte_cap);
555+
fs.Seek(footerStart, SeekOrigin.Begin);
556+
using var sr = new StreamReader(fs);
557+
string footerTail = sr.ReadToEnd();
558+
559+
var fromFooter = tryParseFooter(footerTail);
560+
if (fromFooter != null) return fromFooter;
561+
}
562+
563+
// --- Legacy path: the full-header "[osu!] NATIVE CRASH" block.
564+
// The /proc/self/maps section can be 400–500 KiB, so we scan
565+
// the last 4 MiB (the log rotation cap) to guarantee we reach it.
537566
string tail;
538567

539568
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
@@ -598,6 +627,68 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
598627
}
599628
}
600629

630+
// Parse the compact "=== CRASH FOOTER ===" line appended by crash_handler.cpp
631+
// after the /proc/self/maps section. Format:
632+
// === CRASH FOOTER sig=<SIGNAL> pid=<PID> uptime_ns=<NS> thread=<NAME> ===
633+
// Returns null if no valid footer line is found or the crash is not a
634+
// fatal Draw-thread event.
635+
private static DrawThreadNativeCrashInfo? tryParseFooter(string tail)
636+
{
637+
const string footer_marker = "=== CRASH FOOTER ";
638+
int last = tail.LastIndexOf(footer_marker, StringComparison.Ordinal);
639+
if (last < 0) return null;
640+
641+
int lineEnd = tail.IndexOf('\n', last);
642+
if (lineEnd < 0) lineEnd = tail.Length;
643+
string line = tail.Substring(last, lineEnd - last);
644+
645+
string? signal = extractFooterField(line, " sig=", " ");
646+
string? pid = extractFooterField(line, " pid=", " ");
647+
string? uptime = extractFooterField(line, " uptime_ns=", " ");
648+
string? thread = extractFooterField(line, " thread=", " ===");
649+
650+
if (signal == null || thread == null) return null;
651+
652+
bool isFatalSignal = signal.StartsWith("SIGSEGV", StringComparison.Ordinal)
653+
|| signal.StartsWith("SIGBUS", StringComparison.Ordinal)
654+
|| signal.StartsWith("SIGABRT", StringComparison.Ordinal);
655+
if (!isFatalSignal) return null;
656+
657+
if (!thread.StartsWith("Draw", StringComparison.Ordinal)) return null;
658+
659+
string fingerprint = (uptime != null && pid != null)
660+
? $"u{uptime}-p{pid}"
661+
: "footer:" + ((uint)line.GetHashCode()).ToString("x");
662+
663+
// The footer does not carry a top-frame symbol — report it as such.
664+
return new DrawThreadNativeCrashInfo(fingerprint, signal, thread, "(footer — no top frame)");
665+
}
666+
667+
// Extract a field value from a single footer line.
668+
// Reads from after `key` to either the first occurrence of `stopBefore`
669+
// or the end of the line (whichever comes first).
670+
private static string? extractFooterField(string line, string key, string? stopBefore)
671+
{
672+
int idx = line.IndexOf(key, StringComparison.Ordinal);
673+
if (idx < 0) return null;
674+
675+
int start = idx + key.Length;
676+
int end;
677+
678+
if (stopBefore != null)
679+
{
680+
end = line.IndexOf(stopBefore, start, StringComparison.Ordinal);
681+
if (end < 0) end = line.Length;
682+
}
683+
else
684+
{
685+
end = line.IndexOf(' ', start);
686+
if (end < 0) end = line.Length;
687+
}
688+
689+
return line.Substring(start, end - start);
690+
}
691+
601692
private static string? extractField(string block, string keyWithEquals)
602693
{
603694
int idx = block.IndexOf(keyWithEquals, StringComparison.Ordinal);

osu.Android/Native/crash_handler.cpp

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,21 @@ static void crashHandler(int sig, siginfo_t* info, void* ucontext) {
10131013
}
10141014
g_dumpWritten = 1;
10151015

1016+
// Capture crash metadata once so both the header block and the compact
1017+
// footer written after /proc/self/maps use identical values. The footer
1018+
// is what CrashDiagnostics.scanForDrawThreadCrash now looks for first —
1019+
// it always lands in the last few KB of the log even when the memory-map
1020+
// section is several hundred KB long.
1021+
long long crash_uptime_ns;
1022+
{
1023+
struct timespec ts{};
1024+
clock_gettime(CLOCK_BOOTTIME, &ts);
1025+
crash_uptime_ns = (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec;
1026+
}
1027+
const long long crash_pid = (long long)getpid();
1028+
char crash_thread_name[32] = {};
1029+
(void)pthread_getname_np(pthread_self(), crash_thread_name, sizeof(crash_thread_name));
1030+
10161031
// Open the dump file (append). If g_logPath is empty we still log to logcat.
10171032
//
10181033
// 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) {
10511066
writeStr(fd, "\n pid = ");
10521067
writeDec(fd, (long long)getpid());
10531068
writeStr(fd, "\n uptime_ns = ");
1054-
{
1055-
struct timespec ts;
1056-
clock_gettime(CLOCK_BOOTTIME, &ts);
1057-
writeDec(fd, (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec);
1058-
}
1069+
writeDec(fd, crash_uptime_ns);
10591070
writeStr(fd, "\n thread_name = ");
1060-
{
1061-
char name[32] = {};
1062-
// pthread_getname_np is signal-safe in bionic (it's a thin wrapper
1063-
// over a /proc/self/task/<tid>/comm read).
1064-
if (pthread_getname_np(pthread_self(), name, sizeof(name)) == 0)
1065-
writeStr(fd, name);
1066-
else
1067-
writeStr(fd, "?");
1068-
}
1071+
writeStr(fd, crash_thread_name[0] ? crash_thread_name : "?");
10691072
writeStr(fd, "\n");
10701073

10711074
// 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) {
11371140
writeStr(fd, "=========================================================\n");
11381141
writeStr(fd, "=== END OF CRASH DUMP ===\n");
11391142

1143+
// Compact one-line footer, written AFTER the /proc/self/maps section.
1144+
// The C# scanner (CrashDiagnostics.scanForDrawThreadCrash) looks for this
1145+
// footer FIRST in the last 32 KiB of the log. Without it the scanner
1146+
// would need to scan hundreds of KiB backwards past the memory map just to
1147+
// reach the "[osu!] NATIVE CRASH" header — causing safe-mode detection to
1148+
// silently fail and the app to keep relaunching into the same Vulkan crash.
1149+
// The uptime_ns and pid values here MATCH the header block exactly (both
1150+
// were captured at handler entry above) so the fingerprint computed by the
1151+
// C# scanner is identical regardless of which block it reads.
1152+
writeStr(fd, "=== CRASH FOOTER sig=");
1153+
writeStr(fd, signalName(sig));
1154+
writeStr(fd, " pid=");
1155+
writeDec(fd, crash_pid);
1156+
writeStr(fd, " uptime_ns=");
1157+
writeDec(fd, crash_uptime_ns);
1158+
writeStr(fd, " thread=");
1159+
writeStr(fd, crash_thread_name[0] ? crash_thread_name : "?");
1160+
writeStr(fd, " ===\n");
1161+
11401162
if (fd >= 0) {
11411163
fsync(fd);
11421164
close(fd);

0 commit comments

Comments
 (0)