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(