Repository navigation
Conversation
…no minidump Unreal's Crash Report Client does not write a minidump on Linux, so these events had no stack trace. The crash context still contains the crashing call stack. Build an exception from it when the report has no minidump or Apple crash report and the event has no exception yet.
Gate the exception behind projects:relay-unreal-crash-context-stacktrace, mark it synthetic and the event native, and fall back to a fatal level after the existing assert and ensure mapping.
7ee2a63 to
3d4c1e2
Compare
…cktrace # Conflicts: # CHANGELOG.md
|
@tustanivsky we now support retaining SDK exceptions in error events accompanying a minidump crash. Do you think for unreal crashes we should parse the stack trace even if there is a minidump or only fall back to parsing if there is no minidump? |
Dav1dde
left a comment
There was a problem hiding this comment.
I've yet to go through the code more deeply, just some quick things I found comparing the parser with the unreal source.
There is also one case we still need to figure out, unsymbolicated stack traces can only be symbolicated if we have a source to fill out the event's debug images. Either we need the SDK to fill this in or somehow extract it from the crash (if that's possible).
| same_module | ||
| && offset.is_none_or(|offset| offset.checked_add(1) == Some(portable.offset)) |
There was a problem hiding this comment.
This seems to not be unconditionally true, from the Unreal code:
// Program counters in the backtrace point to the location from where the execution will be resumed (in all frames except the one where we crashed),
// which results in callstack pointing to the next lines in code. In order to determine the source line where the actual call happened, we need to go
// back to the line that had the "call" instruction. Since x86(-64) instructions vary in length, we cannot do it reliably without disassembling,
// just go back one byte - even if it's not the actual address of the call site.
int OffsetToCallsite = CurrentCallDepth > 0 ? 1 : 0;There was a problem hiding this comment.
Thanks, fixed in d5b4a25. In the sample from #5192 the first frame is adjusted as well (0xb3a7401 in CallStack vs b3a7402 in PCallStack), probably because the crash handler frames are skipped (NumMinidumpFramesToIgnore is 7), so the first frame written is not at depth 0. Since the crash context does not say which case it is, the first frame now matches with or without the adjustment, and all later frames still need it.
| let mut tokens = tokens.as_slice(); | ||
|
|
||
| while !tokens.is_empty() && frames.len() < MAX_NUM_UNREAL_FRAMES { | ||
| let (module, base, offset, rest) = match tokens { |
There was a problem hiding this comment.
I don't think we can unconditionally assume the module name does not contain a whitespace.
There was a problem hiding this comment.
Right, fixed in d5b4a25. A frame is now split at the + between image base and offset: the image base is the last word before it and everything before that is the module, so module names can contain whitespace (or a +, like libstdc++.so).
| return None; | ||
| } | ||
|
|
||
| let (package, mut function) = line.split_once('!').unwrap_or((line, "")); |
There was a problem hiding this comment.
I think it's possible that there are frames without module.
From the source code:
//
// Callstack lines should be written in this standard format
//
// 0xaddress module!func [file]
//
// E.g. 0x045C8D01 OrionClient.self!UEngine::PerformError() [D:\Epic\Orion\Engine\Source\Runtime\Engine\Private\UnrealEngine.cpp:6481]
//
// Module may be omitted, everything else should be present, or substituted with a string that conforms to the expected type
//
// E.g 0x00000000 UnknownFunction []
//
//
There was a problem hiding this comment.
Fixed in d5b4a25. A line without ! is now read as a function when it has arguments or a [file] part, like UnknownFunction [], and still as a module otherwise. When the stacks line up, such a frame takes its module from PCallStack. Since it cannot be checked by its module, it only counts as matching if its offset agrees, or if it has no function or file that could end up on the wrong address.
I'd keep it as a fallback only. The callstack in the crash context describes the same crash as the minidump so it would basically be a duplicate. |
…cktrace # Conflicts: # relay-server/src/processing/errors/integrations/unreal.rs
…n it Address review feedback on the crash context stack trace: - Legacy call stack lines may omit the module, like `UnknownFunction []`. Such frames take the module from the portable call stack, and only count as matching if their offset agrees or they carry no symbol to misplace. - Module names in the portable call stack can contain whitespace, so a frame is split at the `+` between image base and offset. - The engine does not move the crashing frame back to the call site, so the first frame matches with or without that adjustment.
|
Thanks for the review! I pushed fixes for the three comments (d5b4a25) and merged master. On the fallback question: that's how it works now, the stack trace is only added when there is no minidump or Apple crash report. On debug images: in the sample from the issue the crash context has no build IDs ( |
|
I was talking on Slack with @tustanivsky what we can do, and he added a PR which would include the images information in the I am not sure yet if we want to gate the callstack extraction also behind the presence of the debug images if we don't have an already symbolicated stack trace, as otherwise it may quite heavily mess with grouping. |
|
Thanks, and thanks @tustanivsky for the SDK PR! I tried the #5192 sample with Sentry master and Symbolicator. Without symbols, grouping doesn't change: same hash, same issue, only the title becomes So a gate makes sense to me: only add the stack trace if a frame has a function or file name, or the event has One thing for later: in the sample the first frame's address is 1 lower in CallStack than in PCallStack, so Unreal treats it as a return address. In public Linux crash logs I see the same for asserts and fatal errors, while SIGSEGV/SIGABRT keep the first frame as is. So once images arrive, events like this one will need I'll also fix the description: "regroup once" only applies when the frames have names, and the frames do go to Symbolicator. Should I add the gate with a test here? |
Closes #5192.
Unreal's Crash Report Client does not write a minidump on Linux, so these events arrive without a stack trace. The crash context (
CrashContext.runtime-xml) still has the crashing call stack. With the new project featureprojects:relay-unreal-crash-context-stacktrace, Relay builds an exception from it when the report has no minidump or Apple crash report and the event has no exception yet.CallStack: module, plus function and file:line when the build had symbols. Symbols with an offset like__libc_start_main(+0x8a)are the nearest export and are left out as function names, so they don't merge unrelated crashes.PCallStackonly when both stacks agree on every frame's module, and on the offset where the legacy frame has one. Engines older than 4.26 write 32-bit image bases, so they get no addresses. Frames are capped at 256.platform: nativeandmechanism.synthetic: true.levelkeeps the existing assert/ensure mapping frommerge_unreal_context, and since ref(unreal): Migrate unreal processing to its own processor #6578 every Unreal event without a level falls back tofatal, so this PR adds no level handling of its own. That fallback also applies to stalls andVerseRuntimeError, which are reported while the game keeps running (handled: true). Happy to map those towarningif you prefer.UnrealErrorIntegrationProcessor, so the stack trace is added by whichever Relay expands the report.Effect when the flag is on: affected events become error events, so existing Linux crash issues regroup once. Because the exception is synthetic, Sentry ignores its type for grouping and prefers the crashing function as title. For builds without symbols, like the sample in the issue, every frame is
UnknownFunction, so grouping stays on the error message and the title becomesEnsure: <message>. Builds with symbols get the crashing function as title and group by stack. Inbound filters and PII rules on the error message now also see the exception value. Nothing is symbolicated: the XML has no debug IDs, so these native frames have no debug images to resolve against.To turn it on, the flag still needs to be registered on the Sentry side.
Tests: unit tests for parsing, combining both stacks, crash types, the level (including a level sent by the client), and the disabled flag.
test_unreal_stacktrace_without_minidump_with_processingintest_unreal.pyruns with and without the feature against a processing Relay with Kafka, Redis and objectstore; with the feature it fails on master and passes here. I ran the Unreal unit and integration tests locally, plus clippy and fmt. Prepared with AI assistance.Legal Boilerplate
Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.