Skip to content

Commit 580a7f0

Browse files
feat(flutter): deobfuscate exception value occurrences of Instance of 'xyz' (#97668)
Currently for dart/flutter we get the symbol map and map the obfuscated exception type to the de-obfuscated one. Additionally `Instance of 'xyz'` where xyz is obfuscated also occurs very often in the exception value. This PR adds support for de-obfuscating these occurrences within the exception value as well. --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent d7e0a99 commit 580a7f0

File tree

3 files changed

+88
-29
lines changed

3 files changed

+88
-29
lines changed

src/sentry/lang/dart/utils.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
import re
45
from collections.abc import MutableMapping
56
from typing import Any
67

@@ -16,6 +17,7 @@
1617
# the values to be more complicated such as "_xyz", so the regex should capture
1718
# any values other than "<" and ">".
1819
# VIEW_HIERARCHY_TYPE_REGEX = re.compile(r"([^<>]+)(?:<([^<>]+)>)?")
20+
INSTANCE_OF_VALUE_RE = re.compile(r"Instance of '([^']+)'")
1921

2022

2123
def get_debug_meta_image_ids(event: dict[str, Any]) -> set[str]:
@@ -63,9 +65,13 @@ def generate_dart_symbols_map(debug_ids: list[str], project: Project):
6365

6466
def deobfuscate_exception_type(data: MutableMapping[str, Any]):
6567
"""
66-
Deobfuscates exception types in-place.
68+
Deobfuscates exception types and certain values in-place.
6769
68-
If we're unable to fetch a dart symbols mapping file, then the exception types remain unmodified.
70+
- Exception type: replaced directly via symbol map lookup
71+
- Exception value: deobfuscate the quoted symbol for all occurrences of the
72+
pattern "Instance of 'obfuscated_symbol'" in the value.
73+
74+
If we're unable to fetch a dart symbols mapping file, then the exception data remains unmodified.
6975
"""
7076
project = Project.objects.get_from_cache(id=data["project"])
7177

@@ -84,20 +90,25 @@ def deobfuscate_exception_type(data: MutableMapping[str, Any]):
8490

8591
for exception in exceptions:
8692
exception_type = exception.get("type")
87-
if exception_type is None:
88-
continue
89-
90-
deobfuscated_type = symbol_map.get(exception_type)
91-
if deobfuscated_type is not None:
92-
exception["type"] = deobfuscated_type
93-
94-
# TODO(buenaflor): Future enhancement - deobfuscate exception values
95-
#
96-
# Exception values may contain obfuscated symbols in patterns like:
97-
# - "Instance of 'obfuscated_symbol'"
98-
# - General text containing obfuscated symbols
99-
# This could be implemented by extracting symbols from these patterns
100-
# and looking them up in the symbol map for replacement.
93+
if isinstance(exception_type, str):
94+
mapped_type = symbol_map.get(exception_type)
95+
if mapped_type is not None:
96+
exception["type"] = mapped_type
97+
98+
# Deobfuscate occurrences of "Instance of 'xYz'" in the exception value
99+
exception_value = exception.get("value")
100+
if isinstance(exception_value, str):
101+
102+
def replace_symbol(match: re.Match[str]) -> str:
103+
symbol = match.group(1)
104+
deobfuscated_symbol = symbol_map.get(symbol)
105+
if deobfuscated_symbol is None:
106+
return match.group(0)
107+
return f"Instance of '{deobfuscated_symbol}'"
108+
109+
new_value = re.sub(INSTANCE_OF_VALUE_RE, replace_symbol, exception_value)
110+
if new_value != exception_value:
111+
exception["value"] = new_value
101112

102113

103114
# TODO(buenaflor): Add this back in when we decide to deobfuscate view hierarchies

tests/sentry/lang/dart/test_plugin_deobfuscation.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,11 @@ def test_dart_exception_deobfuscation_direct(self) -> None:
8989
preprocessor = preprocessors[0]
9090
preprocessor(data)
9191

92-
# Verify the exception type was deobfuscated, value remains unchanged
92+
# Verify the exception type and value were deobfuscated
9393
assert data["exception"]["values"][0]["type"] == "NetworkException"
94-
assert data["exception"]["values"][0]["value"] == "Instance of 'xyz' was thrown"
94+
assert (
95+
data["exception"]["values"][0]["value"] == "Instance of 'NetworkException' was thrown"
96+
)
9597

9698
def test_dart_multiple_exceptions_deobfuscation_direct(self) -> None:
9799
"""Test that multiple Dart exceptions are deobfuscated."""

tests/sentry/lang/dart/test_utils.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -412,28 +412,29 @@ def test_deobfuscate_exception_type_instance_of_pattern() -> None:
412412
):
413413
deobfuscate_exception_type(data)
414414

415-
# Only exception types should be deobfuscated, values remain unchanged
415+
# Exception types should be deobfuscated
416416
assert data["exception"]["values"][0]["type"] == "NetworkException"
417-
assert data["exception"]["values"][0]["value"] == "Instance of 'xyz'"
417+
# Value starts with pattern: quoted symbol should be deobfuscated
418+
assert data["exception"]["values"][0]["value"] == "Instance of 'NetworkException'"
418419

419420
assert data["exception"]["values"][1]["type"] == "DatabaseException"
421+
# Pattern can appear anywhere: deobfuscate occurrences
420422
assert (
421423
data["exception"]["values"][1]["value"]
422-
== "Unhandled Exception: Instance of 'abc' was thrown"
424+
== "Unhandled Exception: Instance of 'DatabaseException' was thrown"
423425
)
424426

425427
assert data["exception"]["values"][2]["type"] == "FileException"
426-
# No 'Instance of' pattern, so value should remain unchanged
428+
# No pattern, value should remain unchanged
427429
assert (
428430
data["exception"]["values"][2]["value"]
429431
== "Error: def occurred outside of Instance pattern"
430432
)
431433

432434
assert data["exception"]["values"][3]["type"] == "IOException"
433-
# Values remain unchanged
434435
assert (
435436
data["exception"]["values"][3]["value"]
436-
== "Instance of 'xyz' and Instance of 'ghi' both occurred"
437+
== "Instance of 'NetworkException' and Instance of 'IOException' both occurred"
437438
)
438439

439440

@@ -480,21 +481,66 @@ def test_deobfuscate_exception_type_special_regex_chars() -> None:
480481
):
481482
deobfuscate_exception_type(data)
482483

483-
# Exception types deobfuscated, values remain unchanged
484+
# Exception types deobfuscated; values with the pattern are updated
484485
assert data["exception"]["values"][0]["type"] == "NetworkException"
485-
assert data["exception"]["values"][0]["value"] == "Instance of 'a.b'"
486+
assert data["exception"]["values"][0]["value"] == "Instance of 'NetworkException'"
486487

487488
assert data["exception"]["values"][1]["type"] == "MathException"
488-
assert data["exception"]["values"][1]["value"] == "Instance of 'x+y' occurred"
489+
assert data["exception"]["values"][1]["value"] == "Instance of 'MathException' occurred"
489490

490491
assert data["exception"]["values"][2]["type"] == "ArrayException"
491-
# Values remain unchanged
492492
assert (
493493
data["exception"]["values"][2]["value"]
494-
== "Instance of 'test[0]' and Instance of 'other' patterns"
494+
== "Instance of 'ArrayException' and Instance of 'other' patterns"
495495
)
496496

497497

498+
def test_deobfuscate_exception_value_without_type() -> None:
499+
"""Values should be deobfuscated even if the exception type is missing or None."""
500+
mock_project = mock.Mock(id=123)
501+
502+
data: dict[str, Any] = {
503+
"project": 123,
504+
"debug_meta": {"images": [{"debug_id": "test-debug-id"}]},
505+
"exception": {
506+
"values": [
507+
{"type": None, "value": "Instance of 'xyz'"},
508+
{"value": "Unhandled Exception: Instance of 'xyz' was thrown"},
509+
{"type": None, "value": "No pattern here"},
510+
]
511+
},
512+
}
513+
514+
mock_map = {"xyz": "NetworkException"}
515+
516+
with (
517+
mock.patch(
518+
"sentry.models.Project.objects.get_from_cache",
519+
return_value=mock_project,
520+
),
521+
mock.patch(
522+
"sentry.lang.dart.utils.generate_dart_symbols_map",
523+
return_value=mock_map,
524+
),
525+
):
526+
deobfuscate_exception_type(data)
527+
528+
# First: type is None, value should be deobfuscated, type remains None
529+
assert data["exception"]["values"][0]["type"] is None
530+
assert data["exception"]["values"][0]["value"] == "Instance of 'NetworkException'"
531+
532+
# Second: type key missing, value should be deobfuscated
533+
assert (
534+
data["exception"]["values"][1]["value"]
535+
== "Unhandled Exception: Instance of 'NetworkException' was thrown"
536+
)
537+
assert "type" not in data["exception"]["values"][1]
538+
539+
# Third: no pattern; unchanged
540+
assert data["exception"]["values"][2]["type"] is None
541+
assert data["exception"]["values"][2]["value"] == "No pattern here"
542+
543+
498544
# @mock.patch("sentry.lang.dart.utils.generate_dart_symbols_map", return_value=MOCK_DEBUG_MAP)
499545
# @mock.patch("sentry.lang.dart.utils.get_debug_meta_image_ids", return_value=["test-uuid"])
500546
# def test_view_hierarchy_deobfuscation(

0 commit comments

Comments
 (0)