From 153fa5acfd8735f8284d77065532c4938d905827 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Wed, 22 Jul 2026 10:57:59 +0200 Subject: [PATCH 01/13] fix(switch): Match the event types of other console/gaming errors --- CHANGELOG.md | 4 ++ .../src/processing/errors/errors/nswitch.rs | 63 ++++++++++++++++++- relay-server/src/utils/native.rs | 42 +++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4330611227..fff79c98116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - No longer write the deprecated `sentry.transaction` and `db.system` attributes. ([#6237](https://github.com/getsentry/relay/pull/6237), [#6238](https://github.com/getsentry/relay/pull/6238)) - Allow additional exceptions in minidump and apple crash report events. ([#6241](https://github.com/getsentry/relay/pull/6241)) +**Bug Fixes**: + +- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and render them as fatal and unhandled. ([#6252](https://github.com/getsentry/relay/pull/6252)) + ## 26.7.0 **Features**: diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index 688f9668fe3..d037ca8a54f 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -65,7 +65,7 @@ impl SentryError for Nswitch { let mut attachments = attachments; attachments.extend(dying_message.attachments); - let event = match (event, dying_message.event) { + let mut event = match (event, dying_message.event) { (Some(event), Some(dying_message)) => { metrics.bytes_ingested_event = Annotated::new((event.len() + dying_message.len()) as u64); @@ -76,6 +76,14 @@ impl SentryError for Nswitch { (None, None) => return Err(ProcessingError::NoEventPayload.into()), }; + // Nintendo forwards the crash with the abort result code as the exception `type`, which + // Sentry would otherwise render as the issue title. Reshape it to look like a native + // crash on other platforms: fall the title back to the crashing function and render the + // event as a fatal, unhandled crash. Normalization runs after this and preserves it. + if let Some(event) = event.value_mut() { + crate::utils::reshape_switch_crash(event); + } + Ok(Some(Expansion { event: Box::new(event), attachments, @@ -304,6 +312,7 @@ mod tests { use super::*; use relay_config::{Config, OverridableConfig}; + use relay_event_schema::protocol::Level; use relay_protocol::assert_annotated_snapshot; use std::io::Write; use zstd::bulk::Compressor as ZstdCompressor; @@ -470,4 +479,56 @@ mod tests { let _ = Nswitch::try_expand(&mut items, ctx()).unwrap().unwrap(); } + + #[test] + fn test_switch_crash_is_reshaped() { + // Minimal, empty dying message (magic + version 0 + encoding 0 + length 0): no scope + // patch, so we exercise only the reshaping of the event Nintendo forwards. + let dying_message = Bytes::from("sntr\0\0\0\0"); + + // The parent envelope event is what Nintendo forwards: the abort result code as the + // exception `type`, a readable `value`, the crashing function, and level `error`. + let envelope = r#"{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","dsn":"https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"} +{"type":"event"} +{"level":"error","exception":{"values":[{"type":"2168-0002 ResultAccessViolationData","value":"Data access to an invalid memory region was performed. (2: Access Violation Data)","stacktrace":{"frames":[{"function":"ASentryTowerTurret::Shoot"}]}}]}} +{"type":"attachment","filename":"dying_message.dat","length":} +"# + .replace("", &dying_message.len().to_string()); + + let mut envelope = + Envelope::parse_bytes([Bytes::from(envelope), dying_message].concat().into()).unwrap(); + let mut items = envelope.take_items_by(|_| true).into_vec(); + + let parsed = Nswitch::try_expand(&mut items, ctx()).unwrap().unwrap(); + + let event = parsed.event.value().unwrap(); + + // The crash is rendered as fatal (Nintendo forwarded it as `error`). + assert_eq!(event.level.value(), Some(&Level::Fatal)); + + let exception = event + .exceptions + .value() + .unwrap() + .values + .value() + .unwrap() + .last() + .unwrap() + .value() + .unwrap(); + + // Marked synthetic and unhandled, so Sentry drops the result-code `type` from the + // title (falling back to the crashing function) and renders it as unhandled. + let mechanism = exception.mechanism.value().unwrap(); + assert_eq!(mechanism.synthetic.value(), Some(&true)); + assert_eq!(mechanism.handled.value(), Some(&false)); + + // The result code and its description are preserved; only the `type`'s influence on the + // title is removed. `value` remains as the issue subtitle. + assert_eq!( + exception.ty.value().map(String::as_str), + Some("2168-0002 ResultAccessViolationData") + ); + } } diff --git a/relay-server/src/utils/native.rs b/relay-server/src/utils/native.rs index e54236fecca..ff951c1d3fb 100644 --- a/relay-server/src/utils/native.rs +++ b/relay-server/src/utils/native.rs @@ -308,3 +308,45 @@ pub fn process_apple_crash_report(event: &mut Event, additional_exceptions: Addi }; write_native_placeholder(event, placeholder, additional_exceptions); } + +/// Reshapes a Nintendo Switch crash so it renders like the same crash captured on other +/// native platforms (for example a Windows minidump). +/// +/// Unlike a minidump, a Switch crash reaches Relay as a fully-formed event assembled by +/// Nintendo's crash pipeline: its exception `type` is the raw abort result code (for example +/// `2168-0002 ResultAccessViolationData`) at severity `error`. Left untouched, Sentry renders +/// that result code as the issue title and hides the crashing function. +/// +/// Native crashes that Relay assembles itself (see [`write_native_placeholder`]) mark their +/// exception `synthetic`, which tells Sentry to drop the exception `type` from the title and +/// fall back to the crashing function. We apply the same treatment here, and raise the level +/// to fatal and mark the crash unhandled, so the issue matches its Windows/macOS counterpart. +/// +/// The exception `value` is deliberately preserved: as with minidumps, it remains the issue +/// subtitle. Only the `type`'s influence on the title is removed via the synthetic flag. +pub fn reshape_switch_crash(event: &mut Event) { + // Sentry derives the issue title from the last exception in the list (see `_get_exception` + // in `sentry/eventtypes/error.py`), so that is the one whose `type` we must neutralize. + let Some(exception) = event + .exceptions + .value_mut() + .as_mut() + .and_then(|values| values.values.value_mut().as_mut()) + .and_then(|exceptions| exceptions.last_mut()) + .and_then(|exception| exception.value_mut().as_mut()) + else { + // No exception means there is no crash title to fix; leave the event untouched. + return; + }; + + let mechanism = exception + .mechanism + .value_mut() + .get_or_insert_with(Mechanism::default); + mechanism.synthetic.set_value(Some(true)); + mechanism.handled.set_value(Some(false)); + + // A captured crash is fatal and unhandled. Nintendo forwards it as `error`, so upgrade the + // level explicitly rather than only defaulting it. + event.level.set_value(Some(Level::Fatal)); +} From 49cd2e279547c7512d25132a44861cafe3df840a Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Wed, 22 Jul 2026 10:58:49 +0200 Subject: [PATCH 02/13] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fff79c98116..f2075334efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ **Bug Fixes**: -- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and render them as fatal and unhandled. ([#6252](https://github.com/getsentry/relay/pull/6252)) +- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and render them as fatal and unhandled. ([#6253](https://github.com/getsentry/relay/pull/6253)) ## 26.7.0 From 1dc962c8acb9f020e1ab218dea0ed0603d578540 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Wed, 22 Jul 2026 13:45:47 +0200 Subject: [PATCH 03/13] fix(nswitch): Gate reshape call behind processing feature The native crash helpers are only compiled with the processing feature (pub use self::native::* is cfg-gated), so calling reshape_switch_crash unconditionally broke the default-features build (E0425). Wrap the call in if_processing! like the minidump path, and guard the now-conditional mut binding. --- relay-server/src/processing/errors/errors/nswitch.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index d037ca8a54f..135fc5b08fd 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -65,6 +65,7 @@ impl SentryError for Nswitch { let mut attachments = attachments; attachments.extend(dying_message.attachments); + #[cfg_attr(not(feature = "processing"), expect(unused_mut))] let mut event = match (event, dying_message.event) { (Some(event), Some(dying_message)) => { metrics.bytes_ingested_event = @@ -80,9 +81,11 @@ impl SentryError for Nswitch { // Sentry would otherwise render as the issue title. Reshape it to look like a native // crash on other platforms: fall the title back to the crashing function and render the // event as a fatal, unhandled crash. Normalization runs after this and preserves it. - if let Some(event) = event.value_mut() { - crate::utils::reshape_switch_crash(event); - } + utils::if_processing!(ctx, { + if let Some(event) = event.value_mut() { + crate::utils::reshape_switch_crash(event); + } + }); Ok(Some(Expansion { event: Box::new(event), From 108f6682895449ad0fe355bc6281eebd084ab129 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Wed, 22 Jul 2026 14:55:45 +0200 Subject: [PATCH 04/13] fix(nswitch): Reject dying message with invalid magic instead of panicking Item::attachment_type can return NintendoSwitchDyingMessage from an explicitly set item header, bypassing the starts_with(magic) guard used when inferring the type. expand_dying_message then called Bytes::advance past the payload length, which panics and crashes the processing worker (DoS). Validate the magic up front and return an error instead. Reported by Warden wrdn-dos-review. --- CHANGELOG.md | 1 + .../src/processing/errors/errors/nswitch.rs | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2075334efa..d97d3480bf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ **Bug Fixes**: - Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and render them as fatal and unhandled. ([#6253](https://github.com/getsentry/relay/pull/6253)) +- Reject Nintendo Switch dying message attachments with an invalid magic number instead of panicking on a short payload. ([#6253](https://github.com/getsentry/relay/pull/6253)) ## 26.7.0 diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index 135fc5b08fd..46030530b15 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -135,6 +135,8 @@ pub enum SwitchProcessingError { EnvelopeParsing(#[from] EnvelopeError), #[error("unexpected EOF, expected {expected:?}")] UnexpectedEof { expected: &'static str }, + #[error("invalid magic number")] + InvalidMagic, #[error("invalid {0:?} ({1:?})")] InvalidValue(&'static str, usize), #[error("Zstandard error")] @@ -177,6 +179,14 @@ struct ExpandedDyingMessage { /// Parses DyingMessage contents and updates the envelope. /// See dying_message.md for the documentation. fn expand_dying_message(mut payload: Bytes) -> Result { + // `Item::attachment_type` may report `NintendoSwitchDyingMessage` from an explicitly set item + // header, which bypasses the `starts_with(magic)` guard it applies when inferring the type. + // Validate the magic here so a crafted short payload can't panic the `advance` below: + // `Bytes::advance` panics when the count exceeds the remaining length, and `starts_with` is + // already false for any payload shorter than the magic. + if !payload.starts_with(NNSWITCH_SENTRY_MAGIC) { + return Err(SwitchProcessingError::InvalidMagic); + } payload.advance(NNSWITCH_SENTRY_MAGIC.len()); let version = payload .try_get_u8() @@ -483,6 +493,24 @@ mod tests { let _ = Nswitch::try_expand(&mut items, ctx()).unwrap().unwrap(); } + #[test] + fn test_expand_dying_message_rejects_short_or_invalid_magic() { + // A payload shorter than the 4-byte magic must return an error rather than panic in + // `advance` (reachable when the attachment type is set explicitly in the item header). + for payload in ["", "s", "sn", "snt"] { + assert!(matches!( + expand_dying_message(Bytes::from(payload)), + Err(SwitchProcessingError::InvalidMagic) + )); + } + + // A long-enough payload whose magic doesn't match is rejected too. + assert!(matches!( + expand_dying_message(Bytes::from("xxxx")), + Err(SwitchProcessingError::InvalidMagic) + )); + } + #[test] fn test_switch_crash_is_reshaped() { // Minimal, empty dying message (magic + version 0 + encoding 0 + length 0): no scope From d2bea6e1c7a6540158d8a6a3546114e130bc1e12 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:35:07 +0200 Subject: [PATCH 05/13] fix(nswitch): Drop redundant handled flag from the crash reshape A raw capture of what CRPortal actually delivers (via an Import Destination pointed at a local Relay) showed the forwarded event already carries mechanism.handled: false and level: fatal. The error level seen on stored events comes from the DyingMessage scope patch winning the merge, not from Nintendo. Remove the redundant handled write and correct the reshape docs, test premise, and changelog accordingly: synthetic remains the title fix, and the level write re-asserts the severity the merge downgraded. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 2 +- .../src/processing/errors/errors/nswitch.rs | 18 ++++++++++-------- relay-server/src/utils/native.rs | 15 ++++++++------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d97d3480bf4..ef083c28abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ **Bug Fixes**: -- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and render them as fatal and unhandled. ([#6253](https://github.com/getsentry/relay/pull/6253)) +- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and restore the `fatal` level the DyingMessage merge downgrades. ([#6253](https://github.com/getsentry/relay/pull/6253)) - Reject Nintendo Switch dying message attachments with an invalid magic number instead of panicking on a short payload. ([#6253](https://github.com/getsentry/relay/pull/6253)) ## 26.7.0 diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index 46030530b15..197ee53a73c 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -513,15 +513,16 @@ mod tests { #[test] fn test_switch_crash_is_reshaped() { - // Minimal, empty dying message (magic + version 0 + encoding 0 + length 0): no scope - // patch, so we exercise only the reshaping of the event Nintendo forwards. - let dying_message = Bytes::from("sntr\0\0\0\0"); + // The DyingMessage carries the scope patch our SDK serializes before the crash. It + // includes `level: error`, and since the patch is the merge base it downgrades the + // `fatal` Nintendo forwards (magic + version 0 + encoding 0 + u16 payload length). + let dying_message = Bytes::from("sntr\0\0\0\x22{\"type\":\"event\"}\n{\"level\":\"error\"}"); // The parent envelope event is what Nintendo forwards: the abort result code as the - // exception `type`, a readable `value`, the crashing function, and level `error`. + // exception `type`, a readable `value`, an unhandled mechanism, and level `fatal`. let envelope = r#"{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","dsn":"https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"} {"type":"event"} -{"level":"error","exception":{"values":[{"type":"2168-0002 ResultAccessViolationData","value":"Data access to an invalid memory region was performed. (2: Access Violation Data)","stacktrace":{"frames":[{"function":"ASentryTowerTurret::Shoot"}]}}]}} +{"level":"fatal","exception":{"values":[{"type":"2168-0002 ResultAccessViolationData","value":"Data access to an invalid memory region was performed. (2: Access Violation Data)","mechanism":{"type":"2168-0002","handled":false},"stacktrace":{"frames":[{"function":"ASentryTowerTurret::Shoot"}]}}]}} {"type":"attachment","filename":"dying_message.dat","length":} "# .replace("", &dying_message.len().to_string()); @@ -534,7 +535,8 @@ mod tests { let event = parsed.event.value().unwrap(); - // The crash is rendered as fatal (Nintendo forwarded it as `error`). + // The crash is rendered as fatal: the DyingMessage merge downgraded it to `error`, + // and the reshape re-asserts Nintendo's original severity. assert_eq!(event.level.value(), Some(&Level::Fatal)); let exception = event @@ -549,8 +551,8 @@ mod tests { .value() .unwrap(); - // Marked synthetic and unhandled, so Sentry drops the result-code `type` from the - // title (falling back to the crashing function) and renders it as unhandled. + // Marked synthetic, so Sentry drops the result-code `type` from the title and falls + // back to the crashing function. Nintendo's own `handled: false` passes through. let mechanism = exception.mechanism.value().unwrap(); assert_eq!(mechanism.synthetic.value(), Some(&true)); assert_eq!(mechanism.handled.value(), Some(&false)); diff --git a/relay-server/src/utils/native.rs b/relay-server/src/utils/native.rs index ff951c1d3fb..6899e822a97 100644 --- a/relay-server/src/utils/native.rs +++ b/relay-server/src/utils/native.rs @@ -314,13 +314,14 @@ pub fn process_apple_crash_report(event: &mut Event, additional_exceptions: Addi /// /// Unlike a minidump, a Switch crash reaches Relay as a fully-formed event assembled by /// Nintendo's crash pipeline: its exception `type` is the raw abort result code (for example -/// `2168-0002 ResultAccessViolationData`) at severity `error`. Left untouched, Sentry renders -/// that result code as the issue title and hides the crashing function. +/// `2168-0002 ResultAccessViolationData`). Left untouched, Sentry renders that result code as +/// the issue title and hides the crashing function. /// /// Native crashes that Relay assembles itself (see [`write_native_placeholder`]) mark their /// exception `synthetic`, which tells Sentry to drop the exception `type` from the title and -/// fall back to the crashing function. We apply the same treatment here, and raise the level -/// to fatal and mark the crash unhandled, so the issue matches its Windows/macOS counterpart. +/// fall back to the crashing function. We apply the same treatment here, so the issue matches +/// its Windows/macOS counterpart. Nintendo already marks the mechanism unhandled, so that is +/// left untouched. /// /// The exception `value` is deliberately preserved: as with minidumps, it remains the issue /// subtitle. Only the `type`'s influence on the title is removed via the synthetic flag. @@ -344,9 +345,9 @@ pub fn reshape_switch_crash(event: &mut Event) { .value_mut() .get_or_insert_with(Mechanism::default); mechanism.synthetic.set_value(Some(true)); - mechanism.handled.set_value(Some(false)); - // A captured crash is fatal and unhandled. Nintendo forwards it as `error`, so upgrade the - // level explicitly rather than only defaulting it. + // Nintendo forwards the crash as `fatal`, but the DyingMessage event patch our SDK writes + // is the merge base and its `level: error` wins the merge (see `merge_events` in + // `nswitch.rs`), so re-assert the severity of a captured crash here. event.level.set_value(Some(Level::Fatal)); } From 7b7698ea6c8f0ae60fd29185dab1d0c1ffb30153 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:05:42 +0200 Subject: [PATCH 06/13] ref(nswitch): Fix rustfmt violation in reshape test Co-Authored-By: Claude Fable 5 --- relay-server/src/processing/errors/errors/nswitch.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index 197ee53a73c..fa53e011738 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -516,7 +516,8 @@ mod tests { // The DyingMessage carries the scope patch our SDK serializes before the crash. It // includes `level: error`, and since the patch is the merge base it downgrades the // `fatal` Nintendo forwards (magic + version 0 + encoding 0 + u16 payload length). - let dying_message = Bytes::from("sntr\0\0\0\x22{\"type\":\"event\"}\n{\"level\":\"error\"}"); + let dying_message = + Bytes::from("sntr\0\0\0\x22{\"type\":\"event\"}\n{\"level\":\"error\"}"); // The parent envelope event is what Nintendo forwards: the abort result code as the // exception `type`, a readable `value`, an unhandled mechanism, and level `fatal`. From 6c2c1d08da40262097faaa78ebd9cebf9cac1595 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:57:12 +0200 Subject: [PATCH 07/13] add feature flag --- relay-dynamic-config/src/feature.rs | 3 +++ relay-server/src/processing/errors/errors/nswitch.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/relay-dynamic-config/src/feature.rs b/relay-dynamic-config/src/feature.rs index 8ad4f8422e0..c4a6564a556 100644 --- a/relay-dynamic-config/src/feature.rs +++ b/relay-dynamic-config/src/feature.rs @@ -89,6 +89,9 @@ pub enum Feature { /// Upload non-prosperodmp playstation attachments via the upload endpoint. #[serde(rename = "projects:relay-playstation-uploads")] PlaystationUploads, + /// Enable Nintendo event rewrite. + #[serde(rename = "projects:relay-nintendo-event-rewrite")] + NintendoEventRewrite, /// Stream minidumps to objectstore. #[serde(rename = "projects:relay-minidump-uploads")] MinidumpUploads, diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index fa53e011738..1369dc4e0bb 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -82,7 +82,7 @@ impl SentryError for Nswitch { // crash on other platforms: fall the title back to the crashing function and render the // event as a fatal, unhandled crash. Normalization runs after this and preserves it. utils::if_processing!(ctx, { - if let Some(event) = event.value_mut() { + if let Some(event) = event.value_mut() && ctx.processing.project_info.has_feature(Feature::NintendoEventRewrite) { crate::utils::reshape_switch_crash(event); } }); From 06d77e2ccafe060dc6a30b706770118303434326 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:34:01 +0200 Subject: [PATCH 08/13] cleanup --- .../src/processing/errors/errors/nswitch.rs | 29 +++++++++++++++---- relay-server/src/utils/native.rs | 23 +++++++-------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index 1369dc4e0bb..dc7e7cb98bb 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -76,13 +76,17 @@ impl SentryError for Nswitch { (None, Some(event)) => utils::event_from_json_payload(event, None, &mut metrics, ctx)?, (None, None) => return Err(ProcessingError::NoEventPayload.into()), }; - - // Nintendo forwards the crash with the abort result code as the exception `type`, which - // Sentry would otherwise render as the issue title. Reshape it to look like a native - // crash on other platforms: fall the title back to the crashing function and render the - // event as a fatal, unhandled crash. Normalization runs after this and preserves it. + + // Reshape switch crash title to match other native platforms, and mark it as fatal. utils::if_processing!(ctx, { - if let Some(event) = event.value_mut() && ctx.processing.project_info.has_feature(Feature::NintendoEventRewrite) { + use relay_dynamic_config::Feature; + + if let Some(event) = event.value_mut() + && ctx + .processing + .project_info + .has_feature(Feature::NintendoEventRewrite) + { crate::utils::reshape_switch_crash(event); } }); @@ -325,6 +329,7 @@ mod tests { use super::*; use relay_config::{Config, OverridableConfig}; + use relay_dynamic_config::Feature; use relay_event_schema::protocol::Level; use relay_protocol::assert_annotated_snapshot; use std::io::Write; @@ -333,6 +338,7 @@ mod tests { use crate::constants::NNSWITCH_DYING_MESSAGE_FILENAME; use crate::envelope::Item; use crate::processing; + use crate::services::projects::project::ProjectInfo; fn ctx() -> Context<'static> { static CONFIG: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -346,9 +352,20 @@ mod tests { config }); + static PROJECT_INFO: std::sync::LazyLock = std::sync::LazyLock::new(|| { + let mut project_info = ProjectInfo::default(); + project_info + .config + .features + .0 + .insert(Feature::NintendoEventRewrite); + project_info + }); + Context { processing: processing::Context { config: &CONFIG, + project_info: &PROJECT_INFO, ..processing::Context::for_test() }, ..Context::for_test() diff --git a/relay-server/src/utils/native.rs b/relay-server/src/utils/native.rs index 6899e822a97..24181d59ee4 100644 --- a/relay-server/src/utils/native.rs +++ b/relay-server/src/utils/native.rs @@ -309,22 +309,19 @@ pub fn process_apple_crash_report(event: &mut Event, additional_exceptions: Addi write_native_placeholder(event, placeholder, additional_exceptions); } -/// Reshapes a Nintendo Switch crash so it renders like the same crash captured on other -/// native platforms (for example a Windows minidump). +/// Reshapes a Switch crashes so they render the same as crashes on other platforms. /// -/// Unlike a minidump, a Switch crash reaches Relay as a fully-formed event assembled by -/// Nintendo's crash pipeline: its exception `type` is the raw abort result code (for example -/// `2168-0002 ResultAccessViolationData`). Left untouched, Sentry renders that result code as -/// the issue title and hides the crashing function. +/// Unlike a minidump, a Switch crash reaches Relay straight from the Nintendo crash pipeline: +/// its exception `type` is the raw abort result code (for example +/// `2168-0002 ResultAccessViolationData`). /// /// Native crashes that Relay assembles itself (see [`write_native_placeholder`]) mark their /// exception `synthetic`, which tells Sentry to drop the exception `type` from the title and -/// fall back to the crashing function. We apply the same treatment here, so the issue matches -/// its Windows/macOS counterpart. Nintendo already marks the mechanism unhandled, so that is -/// left untouched. +/// fall back to the crashing function. We apply the same treatment here, so the issue title +/// matches its Windows/macOS counterpart. /// /// The exception `value` is deliberately preserved: as with minidumps, it remains the issue -/// subtitle. Only the `type`'s influence on the title is removed via the synthetic flag. +/// subtitle. pub fn reshape_switch_crash(event: &mut Event) { // Sentry derives the issue title from the last exception in the list (see `_get_exception` // in `sentry/eventtypes/error.py`), so that is the one whose `type` we must neutralize. @@ -346,8 +343,8 @@ pub fn reshape_switch_crash(event: &mut Event) { .get_or_insert_with(Mechanism::default); mechanism.synthetic.set_value(Some(true)); - // Nintendo forwards the crash as `fatal`, but the DyingMessage event patch our SDK writes - // is the merge base and its `level: error` wins the merge (see `merge_events` in - // `nswitch.rs`), so re-assert the severity of a captured crash here. + // Nintendo forwards crashes as `fatal`, but our DyingMessage contains `level: error` + // which wins the merge (see `merge_events` in `nswitch.rs`), so re-assert the + //severity of a captured crash here. event.level.set_value(Some(Level::Fatal)); } From 0232372e2fd5bd8955a04b9971d5383eb8abc6a2 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:56:48 +0200 Subject: [PATCH 09/13] fmt --- relay-server/src/processing/errors/errors/nswitch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index dc7e7cb98bb..1b4a7e49668 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -76,7 +76,7 @@ impl SentryError for Nswitch { (None, Some(event)) => utils::event_from_json_payload(event, None, &mut metrics, ctx)?, (None, None) => return Err(ProcessingError::NoEventPayload.into()), }; - + // Reshape switch crash title to match other native platforms, and mark it as fatal. utils::if_processing!(ctx, { use relay_dynamic_config::Feature; From 5b227ff6624d449c0b975ed3009656ec49709dca Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:59:00 +0200 Subject: [PATCH 10/13] fix changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d68e8f642..80729246006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ **Bug Fixes**: - Store a normalized attachment content type in objectstore so that downloads are served with the correct type. ([#6319](https://github.com/getsentry/relay/pull/6319)) +- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and restore the `fatal` level the DyingMessage merge downgrades. ([#6253](https://github.com/getsentry/relay/pull/6253)) +- Reject Nintendo Switch dying message attachments with an invalid magic number instead of panicking on a short payload. ([#6253](https://github.com/getsentry/relay/pull/6253)) **Internal**: @@ -48,8 +50,6 @@ - Validate streamed minidumps and correctly enforce size limit for buffered minidumps. ([#6282](https://github.com/getsentry/relay/pull/6282)) - Improve `gen_ai` span op inference. ([#6307](https://github.com/getsentry/relay/pull/6307)) - Always set `sentry.client_sample_rate` on span v2 spans, preferring the SDK-provided attribute over the DSC and falling back to `1.0`. ([#6299](https://github.com/getsentry/relay/pull/6299)) -- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and restore the `fatal` level the DyingMessage merge downgrades. ([#6253](https://github.com/getsentry/relay/pull/6253)) -- Reject Nintendo Switch dying message attachments with an invalid magic number instead of panicking on a short payload. ([#6253](https://github.com/getsentry/relay/pull/6253)) **Internal**: From dc8e0135c54150da97a345b5670c974096067271 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:38:55 +0200 Subject: [PATCH 11/13] Apply batched suggestions from code review Co-authored-by: Sebastian Zivota --- relay-server/src/processing/errors/errors/nswitch.rs | 4 +--- relay-server/src/utils/native.rs | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index 1b4a7e49668..477fec7768a 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -185,9 +185,7 @@ struct ExpandedDyingMessage { fn expand_dying_message(mut payload: Bytes) -> Result { // `Item::attachment_type` may report `NintendoSwitchDyingMessage` from an explicitly set item // header, which bypasses the `starts_with(magic)` guard it applies when inferring the type. - // Validate the magic here so a crafted short payload can't panic the `advance` below: - // `Bytes::advance` panics when the count exceeds the remaining length, and `starts_with` is - // already false for any payload shorter than the magic. + // Validate the magic here so a crafted short payload can't panic the `advance` below. if !payload.starts_with(NNSWITCH_SENTRY_MAGIC) { return Err(SwitchProcessingError::InvalidMagic); } diff --git a/relay-server/src/utils/native.rs b/relay-server/src/utils/native.rs index 24181d59ee4..7791084af93 100644 --- a/relay-server/src/utils/native.rs +++ b/relay-server/src/utils/native.rs @@ -309,7 +309,7 @@ pub fn process_apple_crash_report(event: &mut Event, additional_exceptions: Addi write_native_placeholder(event, placeholder, additional_exceptions); } -/// Reshapes a Switch crashes so they render the same as crashes on other platforms. +/// Reshapes a Switch crash so it renders the same as crashes on other platforms. /// /// Unlike a minidump, a Switch crash reaches Relay straight from the Nintendo crash pipeline: /// its exception `type` is the raw abort result code (for example @@ -333,7 +333,6 @@ pub fn reshape_switch_crash(event: &mut Event) { .and_then(|exceptions| exceptions.last_mut()) .and_then(|exception| exception.value_mut().as_mut()) else { - // No exception means there is no crash title to fix; leave the event untouched. return; }; @@ -345,6 +344,6 @@ pub fn reshape_switch_crash(event: &mut Event) { // Nintendo forwards crashes as `fatal`, but our DyingMessage contains `level: error` // which wins the merge (see `merge_events` in `nswitch.rs`), so re-assert the - //severity of a captured crash here. + // severity of a captured crash here. event.level.set_value(Some(Level::Fatal)); } From ef999f251194a3ec4554cf5c69074b7bb776b2ca Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:00:43 +0200 Subject: [PATCH 12/13] add nnswitch crportal fixtures --- .../src/processing/errors/errors/nswitch.rs | 54 +++++--- .../nnswitch_crportal_dying_message_raw.dat | Bin 0 -> 4096 bytes .../native/nnswitch_crportal_event.json | 115 ++++++++++++++++++ 3 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 tests/integration/fixtures/native/nnswitch_crportal_dying_message_raw.dat create mode 100644 tests/integration/fixtures/native/nnswitch_crportal_event.json diff --git a/relay-server/src/processing/errors/errors/nswitch.rs b/relay-server/src/processing/errors/errors/nswitch.rs index 477fec7768a..5a3b2f23485 100644 --- a/relay-server/src/processing/errors/errors/nswitch.rs +++ b/relay-server/src/processing/errors/errors/nswitch.rs @@ -334,7 +334,7 @@ mod tests { use zstd::bulk::Compressor as ZstdCompressor; use crate::constants::NNSWITCH_DYING_MESSAGE_FILENAME; - use crate::envelope::Item; + use crate::envelope::{ContentType, Item}; use crate::processing; use crate::services::projects::project::ProjectInfo; @@ -528,31 +528,45 @@ mod tests { #[test] fn test_switch_crash_is_reshaped() { - // The DyingMessage carries the scope patch our SDK serializes before the crash. It - // includes `level: error`, and since the patch is the merge base it downgrades the - // `fatal` Nintendo forwards (magic + version 0 + encoding 0 + u16 payload length). - let dying_message = - Bytes::from("sntr\0\0\0\x22{\"type\":\"event\"}\n{\"level\":\"error\"}"); - - // The parent envelope event is what Nintendo forwards: the abort result code as the - // exception `type`, a readable `value`, an unhandled mechanism, and level `fatal`. - let envelope = r#"{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","dsn":"https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"} -{"type":"event"} -{"level":"fatal","exception":{"values":[{"type":"2168-0002 ResultAccessViolationData","value":"Data access to an invalid memory region was performed. (2: Access Violation Data)","mechanism":{"type":"2168-0002","handled":false},"stacktrace":{"frames":[{"function":"ASentryTowerTurret::Shoot"}]}}]}} -{"type":"attachment","filename":"dying_message.dat","length":} -"# - .replace("", &dying_message.len().to_string()); + let mut event = Item::new(ItemType::Event); + event.set_payload( + ContentType::Json, + include_bytes!( + "../../../../../tests/integration/fixtures/native/nnswitch_crportal_event.json" + ) + .as_slice(), + ); - let mut envelope = - Envelope::parse_bytes([Bytes::from(envelope), dying_message].concat().into()).unwrap(); - let mut items = envelope.take_items_by(|_| true).into_vec(); + let mut dying_message = Item::new(ItemType::Attachment); + dying_message.set_filename(NNSWITCH_DYING_MESSAGE_FILENAME); + dying_message.set_attachment_type(AttachmentType::NintendoSwitchDyingMessage); + dying_message.set_payload( + ContentType::OctetStream, + include_bytes!( + "../../../../../tests/integration/fixtures/native/nnswitch_crportal_dying_message_raw.dat" + ) + .as_slice(), + ); + + let mut items = vec![event, dying_message]; let parsed = Nswitch::try_expand(&mut items, ctx()).unwrap().unwrap(); let event = parsed.event.value().unwrap(); - // The crash is rendered as fatal: the DyingMessage merge downgraded it to `error`, - // and the reshape re-asserts Nintendo's original severity. + // The DyingMessage scope patch is the merge base and contributes the fields the + // forwarded event lacks. + assert_eq!( + event.release.value().map(|release| release.as_str()), + Some("test-app@1.0.0") + ); + assert_eq!( + event.environment.value().map(String::as_str), + Some("integration-test") + ); + + // The crash is rendered as fatal: both the forwarded event and the scope patch carry + // `level: error`, and the reshape asserts the severity of a captured crash. assert_eq!(event.level.value(), Some(&Level::Fatal)); let exception = event diff --git a/tests/integration/fixtures/native/nnswitch_crportal_dying_message_raw.dat b/tests/integration/fixtures/native/nnswitch_crportal_dying_message_raw.dat new file mode 100644 index 0000000000000000000000000000000000000000..695e364b192a36e80c01db46a8723f58b957a004 GIT binary patch literal 4096 zcmeH`J#XAF42Cmw>aQ@gHgY9@+f$29UD_d?S{wq}mV82N*^uPx9fJGsrR?h^X@_Pm z?BRS8$(Q6q+G=NRZr;3oBnKdq z2OhGJp-Je2GNT@RctwG%qd(mh4!a% z%^sZ&!dTQm2Nv=yjNe5p%2>e@$^_(f#L;Jh<^eikN%i9@*D1H3>#7rO6@I_I=(Ur7 z3!itl&)e|wL?6lSd`ld3ctmE{S!ieFdOhOgRgf! zq!kC%V0DjV7~2K^)#2_p<3A&>1_^`ePgvmJF$3sXLgO1U&m4S8tZ~hF>3%(5?k^2< znaa2=DWo!@Ny<5GG60&%x?m+VJTBM48r+I|JAq!~kV-2gkK!cF@}l%>_he*cQ_Gq| z0w9uDE=CxpUfdhdh#A*3=LL%x&*CyFG3yztt&M=TG4oLS0pm#3fJ4PN?8@&NWd-HQ zHy&3!ui`Z1Jc)Su9W$e!#kE7f&ri!9_}K<1V4P@?#;XM5Jbo$oX`OC>Qs}8-f3nz%Q! Date: Mon, 31 Aug 2026 11:00:57 +0200 Subject: [PATCH 13/13] wording cleanup --- CHANGELOG.md | 2 +- relay-server/src/utils/native.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80729246006..5191ae73bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ **Bug Fixes**: - Store a normalized attachment content type in objectstore so that downloads are served with the correct type. ([#6319](https://github.com/getsentry/relay/pull/6319)) -- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and restore the `fatal` level the DyingMessage merge downgrades. ([#6253](https://github.com/getsentry/relay/pull/6253)) +- Reshape Nintendo Switch crashes so the issue title falls back to the crashing function instead of the raw abort result code, and raise their level to `fatal`. ([#6253](https://github.com/getsentry/relay/pull/6253)) - Reject Nintendo Switch dying message attachments with an invalid magic number instead of panicking on a short payload. ([#6253](https://github.com/getsentry/relay/pull/6253)) **Internal**: diff --git a/relay-server/src/utils/native.rs b/relay-server/src/utils/native.rs index 7791084af93..922f75c6c79 100644 --- a/relay-server/src/utils/native.rs +++ b/relay-server/src/utils/native.rs @@ -342,8 +342,6 @@ pub fn reshape_switch_crash(event: &mut Event) { .get_or_insert_with(Mechanism::default); mechanism.synthetic.set_value(Some(true)); - // Nintendo forwards crashes as `fatal`, but our DyingMessage contains `level: error` - // which wins the merge (see `merge_events` in `nswitch.rs`), so re-assert the - // severity of a captured crash here. + // Events have level `error` by default, so set to `fatal` like for other native crashes. event.level.set_value(Some(Level::Fatal)); }