diff --git a/requirements-base.txt b/requirements-base.txt
index 1eace7b88c3915..7ced01c4a65e39 100644
--- a/requirements-base.txt
+++ b/requirements-base.txt
@@ -58,7 +58,7 @@ six>=1.11.0,<1.12.0
sqlparse>=0.2.0,<0.3.0
statsd>=3.1.0,<3.2.0
structlog==17.1.0
-symbolic>=7.3.5,<8.0.0
+symbolic>=8.0.0,<9.0.0
toronado>=0.0.11,<0.1.0
ua-parser>=0.10.0,<0.11.0
unidiff>=0.5.4
diff --git a/src/sentry/constants.py b/src/sentry/constants.py
index 49957da6aa9f05..b1e976ba286dc6 100644
--- a/src/sentry/constants.py
+++ b/src/sentry/constants.py
@@ -282,6 +282,7 @@ def get_all_languages():
"application/x-elf-binary": "elf",
"application/x-dosexec": "pe",
"application/x-ms-pdb": "pdb",
+ "application/wasm": "wasm",
"text/x-proguard+plain": "proguard",
"application/x-sentry-bundle+zip": "sourcebundle",
}
diff --git a/src/sentry/interfaces/stacktrace.py b/src/sentry/interfaces/stacktrace.py
index ab85617171311c..70635e05873a61 100644
--- a/src/sentry/interfaces/stacktrace.py
+++ b/src/sentry/interfaces/stacktrace.py
@@ -144,6 +144,7 @@ def to_python(cls, data, raw=False):
"image_addr",
"in_app",
"instruction_addr",
+ "addr_mode",
"lineno",
"module",
"package",
@@ -172,6 +173,7 @@ def to_json(self):
"symbol": self.symbol,
"symbol_addr": self.symbol_addr,
"instruction_addr": self.instruction_addr,
+ "addr_mode": self.addr_mode,
"trust": self.trust,
"in_app": self.in_app,
"context_line": self.context_line,
@@ -214,6 +216,10 @@ def get_api_context(self, is_public=False, pad_addr=None, platform=None):
}
if not is_public:
data["vars"] = self.vars
+
+ if self.addr_mode and self.addr_mode != "abs":
+ data["addrMode"] = self.addr_mode
+
# TODO(dcramer): abstract out this API
if self.data and "sourcemap" in data:
data.update(
diff --git a/src/sentry/lang/native/processing.py b/src/sentry/lang/native/processing.py
index 9c8c15684ca93c..203b1880256ec0 100644
--- a/src/sentry/lang/native/processing.py
+++ b/src/sentry/lang/native/processing.py
@@ -24,6 +24,8 @@
from sentry.stacktraces.processing import find_stacktraces_in_data
from sentry.utils.compat import zip
+from symbolic import normalize_debug_id, ParseDebugIdError
+
logger = logging.getLogger(__name__)
@@ -73,6 +75,13 @@ def _merge_frame(new_frame, symbolicated):
new_frame["context_line"] = symbolicated["context_line"]
if symbolicated.get("post_context"):
new_frame["post_context"] = symbolicated["post_context"]
+
+ addr_mode = symbolicated.get("addr_mode")
+ if addr_mode is None:
+ new_frame.pop("addr_mode", None)
+ else:
+ new_frame["addr_mode"] = addr_mode
+
if symbolicated.get("status"):
frame_meta = new_frame.setdefault("data", {})
frame_meta["symbolicator_status"] = symbolicated["status"]
@@ -283,6 +292,51 @@ def _handles_frame(data, frame):
return is_native_platform(platform) and "instruction_addr" in frame
+def get_frames_for_symbolication(frames, data, modules):
+ modules_by_debug_id = None
+ rv = []
+
+ for frame in reversed(frames):
+ if not _handles_frame(data, frame):
+ continue
+ s_frame = dict(frame)
+
+ # validate and expand addressing modes. If we can't validate and
+ # expand it, we keep None which is absolute. That's not great but
+ # at least won't do damage.
+ addr_mode = s_frame.pop("addr_mode", None)
+ sanitized_addr_mode = None
+
+ # None and abs mean absolute addressing explicitly.
+ if addr_mode in (None, "abs"):
+ pass
+ # this is relative addressing to module by index or debug id.
+ elif addr_mode.startswith("rel:"):
+ arg = addr_mode[4:]
+ idx = None
+
+ if modules_by_debug_id is None:
+ modules_by_debug_id = dict(
+ (x.get("debug_id"), idx) for idx, x in enumerate(modules)
+ )
+ try:
+ idx = modules_by_debug_id.get(normalize_debug_id(arg))
+ except ParseDebugIdError:
+ pass
+
+ if idx is None and arg.isdigit():
+ idx = int(arg)
+
+ if idx is not None:
+ sanitized_addr_mode = "rel:%d" % idx
+
+ if sanitized_addr_mode is not None:
+ s_frame["addr_mode"] = sanitized_addr_mode
+ rv.append(s_frame)
+
+ return rv
+
+
def process_payload(data):
project = Project.objects.get_from_cache(id=data["project"])
@@ -294,12 +348,14 @@ def process_payload(data):
if any(is_native_platform(x) for x in stacktrace.platforms)
]
+ modules = native_images_from_data(data)
+
stacktraces = [
{
"registers": sinfo.stacktrace.get("registers") or {},
- "frames": [
- f for f in reversed(sinfo.stacktrace.get("frames") or ()) if _handles_frame(data, f)
- ],
+ "frames": get_frames_for_symbolication(
+ sinfo.stacktrace.get("frames") or (), data, modules
+ ),
}
for sinfo in stacktrace_infos
]
@@ -307,7 +363,6 @@ def process_payload(data):
if not any(stacktrace["frames"] for stacktrace in stacktraces):
return
- modules = native_images_from_data(data)
signal = signal_from_data(data)
response = symbolicator.process_payload(stacktraces=stacktraces, modules=modules, signal=signal)
diff --git a/src/sentry/lang/native/utils.py b/src/sentry/lang/native/utils.py
index c7a2e074bc5b54..6886cf0c4c70d3 100644
--- a/src/sentry/lang/native/utils.py
+++ b/src/sentry/lang/native/utils.py
@@ -24,6 +24,7 @@
"elf", # Linux
"macho", # macOS, iOS
"pe", # Windows
+ "wasm", # WASM
)
# Default disables storing crash reports.
@@ -40,8 +41,6 @@ def is_native_image(image):
return (
bool(image)
and image.get("type") in NATIVE_IMAGE_TYPES
- and image.get("image_addr") is not None
- and image.get("image_size") is not None
and (image.get("debug_id") or image.get("id") or image.get("uuid")) is not None
)
diff --git a/src/sentry/models/debugfile.py b/src/sentry/models/debugfile.py
index 00d3883da8e9ae..d8b4b38661748e 100644
--- a/src/sentry/models/debugfile.py
+++ b/src/sentry/models/debugfile.py
@@ -149,6 +149,8 @@ def file_extension(self):
return ".pdb"
if self.file_format == "sourcebundle":
return ".src.zip"
+ if self.file_format == "wasm":
+ return ".wasm"
return ""
@@ -190,7 +192,7 @@ def create_dif_from_id(project, meta, fileobj=None, file=None):
"""
if meta.file_format == "proguard":
object_name = "proguard-mapping"
- elif meta.file_format in ("macho", "elf", "pdb", "pe", "sourcebundle"):
+ elif meta.file_format in ("macho", "elf", "pdb", "pe", "wasm", "sourcebundle"):
object_name = meta.name
elif meta.file_format == "breakpad":
object_name = meta.name[:-4] if meta.name.endswith(".sym") else meta.name
diff --git a/src/sentry/static/sentry/app/components/events/interfaces/debugMeta/debugImage.tsx b/src/sentry/static/sentry/app/components/events/interfaces/debugMeta/debugImage.tsx
index fbf9f212e23285..4361442347d66e 100644
--- a/src/sentry/static/sentry/app/components/events/interfaces/debugMeta/debugImage.tsx
+++ b/src/sentry/static/sentry/app/components/events/interfaces/debugMeta/debugImage.tsx
@@ -176,9 +176,13 @@ const DebugImage = React.memo(({image, orgId, projectId, showDetails, style}: Pr
{renderIconElement()}
- {formatAddress(startAddress, IMAGE_ADDR_LEN)} –{' '}
-
- {formatAddress(endAddress, IMAGE_ADDR_LEN)}
+ {startAddress && endAddress ? (
+
+ {formatAddress(startAddress, IMAGE_ADDR_LEN)} –{' '}
+
+ {formatAddress(endAddress, IMAGE_ADDR_LEN)}
+
+ ) : null}
diff --git a/src/sentry/static/sentry/app/components/events/interfaces/debugMeta/index.tsx b/src/sentry/static/sentry/app/components/events/interfaces/debugMeta/index.tsx
index 01e56551ed2cac..230a5a81cc7bf4 100644
--- a/src/sentry/static/sentry/app/components/events/interfaces/debugMeta/index.tsx
+++ b/src/sentry/static/sentry/app/components/events/interfaces/debugMeta/index.tsx
@@ -55,6 +55,10 @@ type State = {
panelBodyHeight?: number;
};
+function normalizeId(id: string | undefined): string {
+ return id ? id.trim().toLowerCase().replace(/[- ]/g, '') : '';
+}
+
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 81,
@@ -159,19 +163,24 @@ class DebugMeta extends React.PureComponent {
}
// When searching for an address, check for the address range of the image
- // instead of an exact match.
+ // instead of an exact match. Note that images cannot be found by index
+ // if they are at 0x0. For those relative addressing has to be used.
if (searchTerm.indexOf('0x') === 0) {
const needle = parseAddress(searchTerm);
- if (needle > 0) {
+ if (needle > 0 && image.image_addr !== '0x0') {
const [startAddress, endAddress] = getImageRange(image);
return needle >= startAddress && needle < endAddress;
}
}
+ // the searchTerm ending at "!" is the end of the ID search.
+ const relMatch = normalizeId(searchTerm).match(/^\s*(.*?)!/);
+ const idSearchTerm = normalizeId((relMatch && relMatch[1]) || searchTerm);
+
return (
// Prefix match for identifiers
- (image.code_id?.toLowerCase() || '').indexOf(searchTerm) === 0 ||
- (image.debug_id?.toLowerCase() || '').indexOf(searchTerm) === 0 ||
+ normalizeId(image.code_id).indexOf(idSearchTerm) === 0 ||
+ normalizeId(image.debug_id).indexOf(idSearchTerm) === 0 ||
// Any match for file paths
(image.code_file?.toLowerCase() || '').indexOf(searchTerm) >= 0 ||
(image.debug_file?.toLowerCase() || '').indexOf(searchTerm) >= 0
@@ -214,9 +223,27 @@ class DebugMeta extends React.PureComponent {
return undefined;
}
- const searchTerm = this.state.filter.toLowerCase();
+ const searchTerm = normalizeId(this.state.filter.toLowerCase());
+ const relMatch = searchTerm.match(/^\s*(.*?)!(.*)$/);
- return frames.find(frame => frame.instructionAddr?.toLowerCase() === searchTerm);
+ if (relMatch) {
+ const debugImages = this.getDebugImages().map(
+ (image, idx) => [idx, image] as [number, Image]
+ );
+ const filteredImages = debugImages.filter(([_, image]) => this.filterImage(image));
+ if (filteredImages.length === 1) {
+ return frames.find(frame => {
+ return (
+ frame.addrMode === `rel:${filteredImages[0][0]}` &&
+ frame.instructionAddr?.toLowerCase() === relMatch[2]
+ );
+ });
+ } else {
+ return undefined;
+ }
+ } else {
+ return frames.find(frame => frame.instructionAddr?.toLowerCase() === searchTerm);
+ }
}
getDebugImages() {
diff --git a/src/sentry/static/sentry/app/components/events/interfaces/frame/line.tsx b/src/sentry/static/sentry/app/components/events/interfaces/frame/line.tsx
index 93178ba523e995..b22f392cd61dba 100644
--- a/src/sentry/static/sentry/app/components/events/interfaces/frame/line.tsx
+++ b/src/sentry/static/sentry/app/components/events/interfaces/frame/line.tsx
@@ -56,6 +56,18 @@ type State = {
isExpanded?: boolean;
};
+function makeFilter(
+ addr: string,
+ addrMode: string | undefined,
+ image?: React.ComponentProps['image']
+): string {
+ let filter = addr;
+ if (!(!addrMode || addrMode === 'abs') && image) {
+ filter = image.debug_id + '!' + filter;
+ }
+ return filter;
+}
+
export class Line extends React.Component {
static defaultProps = {
isExpanded: false,
@@ -147,7 +159,12 @@ export class Line extends React.Component {
scrollToImage = event => {
event.stopPropagation(); // to prevent collapsing if collapsable
- DebugMetaActions.updateFilter(this.props.data.instructionAddr);
+ const {instructionAddr, addrMode} = this.props.data;
+ if (instructionAddr) {
+ DebugMetaActions.updateFilter(
+ makeFilter(instructionAddr, addrMode, this.props.image)
+ );
+ }
scrollToElement('#packages');
};
diff --git a/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.tsx b/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.tsx
index 30d60ab50ccbc4..cb5a15d0f347d1 100644
--- a/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.tsx
+++ b/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.tsx
@@ -78,14 +78,18 @@ export default class StacktraceContent extends React.Component {
);
};
- findImageForAddress(address: Frame['instructionAddr']) {
+ findImageForAddress(address: Frame['instructionAddr'], addrMode: Frame['addrMode']) {
const images = this.props.event.entries.find(entry => entry.type === 'debugmeta')
?.data?.images;
return images && address
- ? images.find(img => {
- const [startAddress, endAddress] = getImageRange(img);
- return address >= startAddress && address < endAddress;
+ ? images.find((img, idx) => {
+ if (!addrMode || addrMode === 'abs') {
+ const [startAddress, endAddress] = getImageRange(img);
+ return address >= startAddress && address < endAddress;
+ } else {
+ return addrMode === `rel:${idx}`;
+ }
})
: null;
}
@@ -152,7 +156,10 @@ export default class StacktraceContent extends React.Component {
const maxLengthOfAllRelativeAddresses = data.frames.reduce(
(maxLengthUntilThisPoint, frame) => {
- const correspondingImage = this.findImageForAddress(frame.instructionAddr);
+ const correspondingImage = this.findImageForAddress(
+ frame.instructionAddr,
+ frame.addrMode
+ );
try {
const relativeAddress = (
@@ -188,7 +195,7 @@ export default class StacktraceContent extends React.Component {
}
if (this.frameIsVisible(frame, nextFrame) && !repeatedFrame) {
- const image = this.findImageForAddress(frame.instructionAddr);
+ const image = this.findImageForAddress(frame.instructionAddr, frame.addrMode);
frames.push(