From bcd0b9088d5f60962c859ad9e64f9a4962b85235 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Thu, 27 Feb 2025 16:35:42 +0000 Subject: [PATCH 1/2] [Backtracing] Add JSON backtrace output Add an option to use JSON for the crash log output; this makes it easy to ingest crash logs using tools like Splunk or Elastic, which is extremely useful in server environments. rdar://121430255 --- docs/Backtracing.rst | 206 ++++++++++ stdlib/public/Backtracing/Backtrace.swift | 25 ++ .../Backtracing/BacktraceFormatter.swift | 3 +- .../Backtracing/SymbolicatedBacktrace.swift | 3 + .../libexec/swift-backtrace/CMakeLists.txt | 2 + .../public/libexec/swift-backtrace/JSON.swift | 373 ++++++++++++++++++ .../swift-backtrace/OSReleaseScanner.swift | 186 +++++++++ .../libexec/swift-backtrace/TargetLinux.swift | 2 +- .../libexec/swift-backtrace/TargetMacOS.swift | 2 +- .../libexec/swift-backtrace/Themes.swift | 2 +- .../libexec/swift-backtrace/Utils.swift | 200 +++++++++- .../public/libexec/swift-backtrace/main.swift | 115 ++++-- stdlib/public/runtime/Backtrace.cpp | 28 +- stdlib/public/runtime/BacktracePrivate.h | 6 + test/Backtracing/JSON.swift | 235 +++++++++++ test/Backtracing/JSONAsync.swift | 220 +++++++++++ 16 files changed, 1559 insertions(+), 49 deletions(-) create mode 100644 stdlib/public/libexec/swift-backtrace/JSON.swift create mode 100644 stdlib/public/libexec/swift-backtrace/OSReleaseScanner.swift create mode 100644 test/Backtracing/JSON.swift create mode 100644 test/Backtracing/JSONAsync.swift diff --git a/docs/Backtracing.rst b/docs/Backtracing.rst index 352e7b0999772..2ae557f20b26e 100644 --- a/docs/Backtracing.rst +++ b/docs/Backtracing.rst @@ -92,9 +92,17 @@ follows: | | | only has effect on platforms that have a symbol | | | | cache that can be controlled by the runtime. | +-----------------+---------+--------------------------------------------------+ +| format | text | Set to ``json`` to output JSON crash logs rather | +| | | than plain text. | ++-----------------+---------+--------------------------------------------------+ | output-to | stdout | Set to ``stderr`` to send the backtrace to the | | | | standard error instead of standard output. This | | | | may be useful in some CI systems. | +| | | | +| | | You may also specify a path; if this points at a | +| | | directory, the backtracer will generate unique | +| | | filenames within that directory. Otherwise it | +| | | is assumed to be a filename. | +-----------------+---------+--------------------------------------------------+ | symbolicate | full | Options are ``full``, ``fast``, or ``off``. | | | | Full means to look up source locations and | @@ -319,3 +327,201 @@ of the backtracer using If the runtime is unable to locate the backtracer, it will allow your program to crash as it would have done anyway. + +JSON Crash Logs +--------------- + +JSON crash logs are a structured crash log format that the backtracer is able +to output. Note that addresses are represented in this format as hexadecimal +strings, rather than as numbers, in order to avoid representational issues. +Additionally, boolean fields that are ``false``, as well as fields whose +values are unknown or empty, will normally be completely omitted to save space. + +Where hexadecimal *values* are output, they will normally be prefixed with +a ``0x`` prefix. Hexadecimal *data*, by contrast, such as captured memory or +build IDs, will not have a prefix and will be formatted as a string with no +whitespace. + +Note that since JSON does not officially support hexadecimal, hexadecimal +values will always be output as strings. + +JSON crash logs will always contain the following top level fields: + ++-------------------+--------------------------------------------------------+ +| Field | Value | ++===================+========================================================+ +| timestamp | An ISO-8601 formatted timestamp, as a string. | ++-------------------+--------------------------------------------------------+ +| kind | The string ``crashReport``. | ++-------------------+--------------------------------------------------------+ +| description | A textual description of the crash or runtime failure. | ++-------------------+--------------------------------------------------------+ +| faultAddress | The fault address associated with the crash. | ++-------------------+--------------------------------------------------------+ +| platform | A string describing the platform; the first token | +| | identifies the platform itself and is followed by | +| | platform specific version information. | +| | | +| | e.g. "macOS 13.0 (22A380)", | +| | "linux (Ubuntu 22.04.5 LTS)" | ++-------------------+--------------------------------------------------------+ +| architecture | The name of the processor architecture for this crash. | ++-------------------+--------------------------------------------------------+ +| threads | An array of thread records, one for each thread. | ++-------------------+--------------------------------------------------------+ + +These will be followed by some or all of the following, according to the +backtracer settings: + ++-------------------+--------------------------------------------------------+ +| Field | Value | ++===================+========================================================+ +| omittedThreads | A count of the number of threads that were omitted, if | +| | the backtracer is set to give a backtrace only for the | +| | crashed thread. Omitted if zero. | ++-------------------+--------------------------------------------------------+ +| capturedMemory | A dictionary containing captured memory contents, if | +| | any. This will not be present if the ``sanitize`` | +| | setting is enabled, or if no data was captured. | +| | | +| | The dictionary is keyed by hexadecimal addresses, as | +| | strings (with a ``0x`` prefix); the captured data is | +| | also given as a hexadecimal string, but with no prefix | +| | and no inter-byte whitespace. | +| | | +| | You should make no assumptions about the number of | +| | bytes captured at each address; the backtracer will | +| | currently attempt to grab 16 bytes, but this may | +| | change if only a shorter range is available or in | +| | future according to configuration parameters. | ++-------------------+--------------------------------------------------------+ +| omittedImages | If ``images`` is set to ``mentioned``, this is an | +| | integer giving the number of images whose details were | +| | omitted from the crash log. | ++-------------------+--------------------------------------------------------+ +| images | Unless ``images`` is ``none``, an array of records | +| | describing the loaded images in the crashed process. | ++-------------------+--------------------------------------------------------+ +| backtraceTime | The time taken to generate the crash report, in | +| | seconds. | ++-------------------+--------------------------------------------------------+ + +Thread Records +^^^^^^^^^^^^^^ + +A thread record is a dictionary with the following fields: + ++-------------------+--------------------------------------------------------+ +| Field | Value | ++===================+========================================================+ +| name | The name of the thread. Omitted if no name. | ++-------------------+--------------------------------------------------------+ +| crashed | ``true`` if the thread is the one that crashed, | +| | omitted otherwise. | ++-------------------+--------------------------------------------------------+ +| registers | A dictionary containing the register contents on the | +| | crashed thread. | +| | | +| | The dictionary is keyed by architecture specific | +| | register name; values are given as hexadecimal | +| | strings (with a ``0x`` prefix). | +| | | +| | This field may be omitted for threads other than the | +| | crashed thread, if the ``registers`` setting is set | +| | to ``crashed``. | ++-------------------+--------------------------------------------------------+ +| frames | An array of frames forming the backtrace for the | +| | thread. | ++-------------------+--------------------------------------------------------+ + +Each frame in the backtrace is described by a dictionary containing the +following fields: + ++-------------------+--------------------------------------------------------+ +| Field | Value | ++===================+========================================================+ +| kind | ``programCounter`` if the frame address is a directly | +| | captured program counter/instruction pointer. | +| | | +| | ``returnAddress`` if the frame address is a return | +| | address. | +| | | +| | ``asyncResumePoint`` if the frame address is a | +| | resumption point in an ``async`` function. | +| | | +| | ``omittedFrames`` if this is a frame omission record. | +| | | +| | ``truncated`` to indicate that the backtrace is | +| | truncated at this point and that more frames were | +| | available but not captured. | ++-------------------+--------------------------------------------------------+ +| address | The frame address as a string (for records containing | +| | an address). | ++-------------------+--------------------------------------------------------+ +| count | The number of frames omitted at this point in the | +| | backtrace (``omittedFrames`` only). | ++-------------------+--------------------------------------------------------+ + +If the backtrace is symbolicated, the frame record may also contain the +following additional information: + ++-------------------+--------------------------------------------------------+ +| Field | Value | ++===================+========================================================+ +| inlined | ``true`` if this frame is inlined, omitted otherwise. | ++-------------------+--------------------------------------------------------+ +| runtimeFailure | ``true`` if this frame represents a Swift runtime | +| | failure, omitted otherwise. | ++-------------------+--------------------------------------------------------+ +| thunk | ``true`` if this frame is a compiler-generated thunk | +| | function, omitted otherwise. | ++-------------------+--------------------------------------------------------+ +| system | ``true`` if this frame is a system frame, omitted | +| | otherwise. | ++-------------------+--------------------------------------------------------+ + +If symbol lookup succeeded for the frame address, the following additional +fields will be present: + ++-------------------+--------------------------------------------------------+ +| Field | Value | ++===================+========================================================+ +| symbol | The mangled name of the symbol corresponding to the | +| | frame address. | ++-------------------+--------------------------------------------------------+ +| offset | The offset from the symbol to the frame address. | ++-------------------+--------------------------------------------------------+ +| description | If demangling is enabled, a human readable description | +| | of the frame address, otherwise omitted. | ++-------------------+--------------------------------------------------------+ +| image | The name of the image in which the symbol was found; | +| | omitted if no corresponding image exists. | ++-------------------+--------------------------------------------------------+ +| sourceLocation | If the source location of the symbol is known, a | +| | dictionary containing ``file``, ``line`` and | +| | ``column`` keys that identify the location of the | +| | symbol in the source files. | ++-------------------+--------------------------------------------------------+ + +Image Records +^^^^^^^^^^^^^ + +An image record is a dictionary with the following fields: + ++-------------------+--------------------------------------------------------+ +| Field | Value | ++===================+========================================================+ +| name | The name of the image (omitted if not known). | ++-------------------+--------------------------------------------------------+ +| buildId | The build ID (aka unique ID) of the image (omitted if | +| | not known). Build IDs are formatted as un-prefixed | +| | hexadecimal strings, with no inter-byte whitespace. | ++-------------------+--------------------------------------------------------+ +| path | The path to the image (omitted if not known). | ++-------------------+--------------------------------------------------------+ +| baseAddress | The base address of the image text, as a hexadecimal | +| | string. | ++-------------------+--------------------------------------------------------+ +| endOfText | The end of the image text, as a hexadecimal string. | ++-------------------+--------------------------------------------------------+ + diff --git a/stdlib/public/Backtracing/Backtrace.swift b/stdlib/public/Backtracing/Backtrace.swift index c512cebe5c0c7..0b2ecb19f117a 100644 --- a/stdlib/public/Backtracing/Backtrace.swift +++ b/stdlib/public/Backtracing/Backtrace.swift @@ -148,6 +148,31 @@ public struct Backtrace: CustomStringConvertible, Sendable { public var description: String { return description(width: MemoryLayout
.size * 2) } + + /// A JSON description of this frame, less the surrounding braces. + /// This is useful if you want to add extra information. + @_spi(Internal) + public var jsonBody: String { + let width = MemoryLayout
.size * 2 + switch self { + case let .programCounter(addr): + return "\"kind\": \"programCounter\", \"address\": \"\(hex(addr, width: width))\"" + case let .returnAddress(addr): + return "\"kind\": \"returnAddress\", \"address\": \"\(hex(addr, width: width))\"" + case let .asyncResumePoint(addr): + return "\"kind\": \"asyncResumePoint\", \"address\": \"\(hex(addr, width: width))\"" + case let .omittedFrames(count): + return "\"kind\": \"omittedFrames\", \"count\": \(count)" + case .truncated: + return "\"kind\": \"truncated\"" + } + } + + /// A JSON description of this frame. + @_spi(Internal) + public var jsonDescription: String { + return "{ \(jsonBody) }" + } } /// Represents an image loaded in the process's address space diff --git a/stdlib/public/Backtracing/BacktraceFormatter.swift b/stdlib/public/Backtracing/BacktraceFormatter.swift index 95f49d3248f54..f48bff7b61dc9 100644 --- a/stdlib/public/Backtracing/BacktraceFormatter.swift +++ b/stdlib/public/Backtracing/BacktraceFormatter.swift @@ -407,7 +407,8 @@ private func untabify(_ s: String, tabWidth: Int = 8) -> String { /// @param path The path to sanitize. /// /// @returns A string containing the sanitized path. -private func sanitizePath(_ path: String) -> String { +@_spi(Formatting) +public func sanitizePath(_ path: String) -> String { #if os(macOS) return CRCopySanitizedPath(path, kCRSanitizePathGlobAllTypes diff --git a/stdlib/public/Backtracing/SymbolicatedBacktrace.swift b/stdlib/public/Backtracing/SymbolicatedBacktrace.swift index 5302f8a92bac6..e0d8355cc0ac5 100644 --- a/stdlib/public/Backtracing/SymbolicatedBacktrace.swift +++ b/stdlib/public/Backtracing/SymbolicatedBacktrace.swift @@ -252,6 +252,9 @@ public struct SymbolicatedBacktrace: CustomStringConvertible { return backtrace.addressWidth } + /// The architecture on which this backtrace was captured. + public var architecture: String { return backtrace.architecture } + /// A list of captured frame information. public var frames: [Frame] diff --git a/stdlib/public/libexec/swift-backtrace/CMakeLists.txt b/stdlib/public/libexec/swift-backtrace/CMakeLists.txt index 6ec0836513d34..72294a93b226c 100644 --- a/stdlib/public/libexec/swift-backtrace/CMakeLists.txt +++ b/stdlib/public/libexec/swift-backtrace/CMakeLists.txt @@ -31,6 +31,8 @@ set(BACKTRACING_COMPILE_FLAGS set(BACKTRACING_SOURCES main.swift AnsiColor.swift + JSON.swift + OSReleaseScanner.swift TargetMacOS.swift TargetLinux.swift Themes.swift diff --git a/stdlib/public/libexec/swift-backtrace/JSON.swift b/stdlib/public/libexec/swift-backtrace/JSON.swift new file mode 100644 index 0000000000000..9bfd898b0c3d0 --- /dev/null +++ b/stdlib/public/libexec/swift-backtrace/JSON.swift @@ -0,0 +1,373 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(CRT) +import CRT +#endif + +@_spi(Contexts) import _Backtracing +@_spi(Registers) import _Backtracing +@_spi(Formatting) import _Backtracing +@_spi(Internal) import _Backtracing +@_spi(MemoryReaders) import _Backtracing + +extension SwiftBacktrace { + + static func outputJSONCrashLog() { + guard let target = target else { + print("swift-backtrace: unable to get target", + to: &standardError) + return + } + + let crashingThread = target.threads[target.crashingThreadNdx] + + let description: String + + if case let .symbolicated(symbolicated) = crashingThread.backtrace, + let failure = symbolicated.swiftRuntimeFailure { + description = failure + } else { + description = target.signalDescription + } + + let architecture: String + switch crashingThread.backtrace { + case let .raw(backtrace): + architecture = backtrace.architecture + case let .symbolicated(backtrace): + architecture = backtrace.architecture + } + + write(""" + { \ + "timestamp": "\(formatISO8601(now))", \ + "kind": "crashReport", \ + "description": "\(escapeJSON(description))", \ + "faultAddress": "\(hex(target.faultAddress))", \ + "platform": "\(escapeJSON(getPlatform()))", \ + "architecture": "\(escapeJSON(architecture))" + """) + + var mentionedImages = Set() + var capturedBytes: [UInt64:Array] = [:] + + func outputJSONRegister( + name: String, value: T, first: Bool = false + ) { + if !first { + write(", ") + } + write("\"\(name)\": \"\(hex(value))\"") + + if let bytes = try? target.reader.fetch( + from: RemoteMemoryReader.Address(value), + count: 16, + as: UInt8.self) { + capturedBytes[UInt64(truncatingIfNeeded: value)] = bytes + } + } + + func outputJSONRegister( + name: String, context: C, register: C.Register, first: Bool = false + ) { + let value = context.getRegister(register)! + outputJSONRegister(name: name, value: value, first: first) + } + + func outputJSONGPRs(_ context: C, range: Rs) + where Rs.Element == C.Register + { + var first = true + for reg in range { + outputJSONRegister(name: "\(reg)", context: context, register: reg, + first: first) + if first { + first = false + } + } + } + + func outputJSONRegisterDump(_ context: X86_64Context) { + outputJSONGPRs(context, range: .rax ... .r15) + outputJSONRegister(name: "rip", value: context.programCounter) + outputJSONRegister(name: "rflags", context: context, register: .rflags) + outputJSONRegister(name: "cs", context: context, register: .cs) + outputJSONRegister(name: "fs", context: context, register: .fs) + outputJSONRegister(name: "gs", context: context, register: .gs) + } + + func outputJSONRegisterDump(_ context: I386Context) { + outputJSONGPRs(context, range: .eax ... .edi) + outputJSONRegister(name: "eip", value: context.programCounter) + outputJSONRegister(name: "eflags", context: context, register: .eflags) + outputJSONRegister(name: "es", context: context, register: .es) + outputJSONRegister(name: "cs", context: context, register: .cs) + outputJSONRegister(name: "ss", context: context, register: .ss) + outputJSONRegister(name: "ds", context: context, register: .ds) + outputJSONRegister(name: "fs", context: context, register: .fs) + outputJSONRegister(name: "gs", context: context, register: .gs) + } + + func outputJSONRegisterDump(_ context: ARM64Context) { + outputJSONGPRs(context, range: .x0 ..< .x29) + outputJSONRegister(name: "fp", context: context, register: .x29) + outputJSONRegister(name: "lr", context: context, register: .x30) + outputJSONRegister(name: "sp", context: context, register: .sp) + outputJSONRegister(name: "pc", context: context, register: .pc) + } + + func outputJSONRegisterDump(_ context: ARMContext) { + outputJSONGPRs(context, range: .r0 ... .r10) + outputJSONRegister(name: "fp", context: context, register: .r11) + outputJSONRegister(name: "ip", context: context, register: .r12) + outputJSONRegister(name: "sp", context: context, register: .r13) + outputJSONRegister(name: "lr", context: context, register: .r14) + outputJSONRegister(name: "pc", context: context, register: .r15) + } + + func outputJSONThread(ndx: Int, thread: TargetThread) { + write("{ ") + + if !thread.name.isEmpty { + write("\"name\": \"\(escapeJSON(thread.name))\", ") + } + if thread.id == target.crashingThread { + write(#""crashed": true, "#) + } + if args.registers! == .all || thread.id == target.crashingThread { + if let context = thread.context { + write(#""registers": {"#) + outputJSONRegisterDump(context) + write(" }, ") + } + } + + write(#""frames": ["#) + var first = true + switch thread.backtrace { + case let .raw(backtrace): + for frame in backtrace.frames { + if first { + first = false + } else { + write(",") + } + + write(" { \(frame.jsonBody) }") + } + case let .symbolicated(backtrace): + for frame in backtrace.frames { + if first { + first = false + } else { + write(",") + } + + write(" { ") + + write(frame.captured.jsonBody) + + if frame.inlined { + write(#", "inlined": true"#) + } + if frame.isSwiftRuntimeFailure { + write(#", "runtimeFailure": true"#) + } + if frame.isSwiftThunk { + write(#", "thunk": true"#) + } + if frame.isSystem { + write(#", "system": true"#) + } + + if let symbol = frame.symbol { + write(""" + , "symbol": "\(escapeJSON(symbol.rawName))"\ + , "offset": \(symbol.offset) + """) + + if args.demangle { + let formattedOffset: String + if symbol.offset > 0 { + formattedOffset = " + \(symbol.offset)" + } else if symbol.offset < 0 { + formattedOffset = " - \(symbol.offset)" + } else { + formattedOffset = "" + } + + write(""" + , "description": \"\(escapeJSON(symbol.name))\(formattedOffset)\" + """) + } + + if symbol.imageIndex >= 0 { + write(", \"image\": \"\(symbol.imageName)\"") + } + + if var sourceLocation = symbol.sourceLocation { + if args.sanitize ?? false { + sourceLocation.path = sanitizePath(sourceLocation.path) + } + write(#", "sourceLocation": { "#) + + write(""" + "file": "\(escapeJSON(sourceLocation.path))", \ + "line": \(sourceLocation.line), \ + "column": \(sourceLocation.column) + """) + + write(" }") + } + } + write(" }") + } + } + write(" ] ") + + write("}") + + if args.showImages! == .mentioned { + switch thread.backtrace { + case let .raw(backtrace): + for frame in backtrace.frames { + let address = frame.adjustedProgramCounter + if let imageNdx = target.images.firstIndex( + where: { address >= $0.baseAddress + && address < $0.endOfText } + ) { + mentionedImages.insert(imageNdx) + } + } + case let .symbolicated(backtrace): + for frame in backtrace.frames { + if let symbol = frame.symbol, symbol.imageIndex >= 0 { + mentionedImages.insert(symbol.imageIndex) + } + } + } + } + } + + write(#", "threads": [ "#) + if args.threads! { + var first = true + for (ndx, thread) in target.threads.enumerated() { + if first { + first = false + } else { + write(", ") + } + outputJSONThread(ndx: ndx, thread: thread) + } + } else { + outputJSONThread(ndx: target.crashingThreadNdx, + thread: crashingThread) + } + write(" ]") + + if !args.threads! && target.threads.count > 1 { + write(", \"omittedThreads\": \(target.threads.count - 1)") + } + + if !capturedBytes.isEmpty && !(args.sanitize ?? false) { + write(#", "capturedMemory": {"#) + var first = true + for (address, bytes) in capturedBytes { + let formattedBytes = bytes + .map{ hex($0, withPrefix: false) } + .joined(separator: "") + if first { + first = false + } else { + write(",") + } + write(" \"\(hex(address))\": \"\(formattedBytes)\"") + } + write(" }") + } + + func outputJSONImage(_ image: Backtrace.Image, first: Bool) { + if !first { + write(", ") + } + + write("{ ") + + if !image.name.isEmpty { + write("\"name\": \"\(escapeJSON(image.name))\", ") + } + + if let bytes = image.buildID { + let buildID = hex(bytes) + write("\"buildId\": \"\(buildID)\", ") + } + + if !image.path.isEmpty { + var path = image.path + if args.sanitize ?? false { + path = sanitizePath(path) + } + write("\"path\": \"\(path)\", ") + } + + write(""" + "baseAddress": "\(hex(image.baseAddress))", \ + "endOfText": "\(hex(image.endOfText))" + """) + + write(" }") + } + + switch args.showImages! { + case .none: + break + case .mentioned: + let images = mentionedImages.sorted().map{ target.images[$0] } + let omitted = target.images.count - images.count + write(", \"omittedImages\": \(omitted), \"images\": [ ") + var first = true + for image in images { + outputJSONImage(image, first: first) + if first { + first = false + } + } + write(" ] ") + case .all: + write(#", "images": [ "#) + var first = true + for image in target.images { + outputJSONImage(image, first: first) + if first { + first = false + } + } + write(" ] ") + } + + let secs = Double(backtraceDuration.tv_sec) + + 1.0e-9 * Double(backtraceDuration.tv_nsec) + + write(", \"backtraceTime\": \(secs) ") + + writeln("}") + } + +} diff --git a/stdlib/public/libexec/swift-backtrace/OSReleaseScanner.swift b/stdlib/public/libexec/swift-backtrace/OSReleaseScanner.swift new file mode 100644 index 0000000000000..e5d1d1a76f34b --- /dev/null +++ b/stdlib/public/libexec/swift-backtrace/OSReleaseScanner.swift @@ -0,0 +1,186 @@ +//===--- OSReleaseScanner.swift --------------------------------*- swift -*-===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +// +// Defines OSReleaseScanner, which is for scanning the /etc/os-release +// file on Linux. +// +//===----------------------------------------------------------------------===// + +#if os(Linux) + +import Swift + +// Lines in /etc/os-release consist of KEY=VALUE pairs. +// +// The VALUE may be quoted with single quotes, in which case its contents +// are left alone. +// +// It may also be quoted with double quotes, in which case slash escapes +// are processed. +// +// If it is unquoted, whitespace will be stripped. + +struct OSReleaseScanner: Sequence, IteratorProtocol { + typealias SS = S.SubSequence + + private enum State { + case normal + case badLine + case comment + case key + case beforeEquals + case beforeValue + case value + case valueWhitespace + case singleQuote + case doubleQuote + case escape + case awaitingNewline + } + + private var asString: S + private var asUTF8: S.UTF8View + private var pos: S.UTF8View.Index + private var state: State + + init(_ string: S) { + asString = string + asUTF8 = string.utf8 + pos = asUTF8.startIndex + state = .normal + } + + mutating func next() -> (String, String)? { + var chunkStart = pos + var whitespaceStart = pos + var key: String = "" + var quotedValue: String = "" + + while pos < asUTF8.endIndex { + let ch = asUTF8[pos] + switch state { + case .normal: + if ch == 32 || ch == 9 || ch == 13 || ch == 10 { + break + } + if ch == UInt8(ascii: "#") { + state = .comment + break + } + chunkStart = pos + state = .key + case .badLine, .comment, .awaitingNewline: + if ch == 13 || ch == 10 { + state = .normal + } + case .key: + if ch == 32 || ch == 9 { + key = String(asString[chunkStart..(_ value: T, + width: Int, + radix: Int = 10, + uppercase: Bool = false) { + let digits = String(value, radix: radix, uppercase: uppercase) + if digits.count >= width { + self = digits + return + } + + let padding = String(repeating: "0", + count: width - digits.count) + self = padding + digits + } +} + internal func hex(_ value: T, withPrefix: Bool = true) -> String { - let digits = String(value, radix: 16) - let padTo = value.bitWidth / 4 - let padding = digits.count >= padTo ? "" : String(repeating: "0", - count: padTo - digits.count) - let prefix = withPrefix ? "0x" : "" + let formatted = String(value, width: value.bitWidth / 4, radix: 16) - return "\(prefix)\(padding)\(digits)" + if withPrefix { + return "0x" + formatted + } else { + return formatted + } } internal func hex(_ bytes: [UInt8]) -> String { @@ -96,6 +113,7 @@ internal func recursiveRemoveContents(_ dir: String) throws { } } +/// Remove a directory and its contents. internal func recursiveRemove(_ dir: String) throws { try recursiveRemoveContents(dir) @@ -104,8 +122,25 @@ internal func recursiveRemove(_ dir: String) throws { } } -internal func withTemporaryDirectory(pattern: String, shouldDelete: Bool = true, - body: (String) throws -> ()) throws { +/// Run a closure, passing in the name of a temporary directory that will +/// (optionally) be deleted automatically when the closure returns. +/// +/// Parameters: +/// +/// - pattern: A string with some number of trailing 'X's giving the name +/// to use for the directory; the 'X's will be replaced with a +/// unique combination of alphanumeric characters. +/// - shouldDelete: If `true` (the default), the directory and its contents +/// will be removed automatically when the closure returns. +/// - body: The closure to execute. +/// +/// Returns: +/// +/// This function returns whatever the closure returns. +internal func withTemporaryDirectory( + pattern: String, shouldDelete: Bool = true, + body: (String) throws -> R +) throws -> R { var buf = Array(pattern.utf8) buf.append(0) @@ -124,9 +159,10 @@ internal func withTemporaryDirectory(pattern: String, shouldDelete: Bool = true, } } - try body(dir) + return try body(dir) } +/// Start a program with the specified arguments internal func spawn(_ path: String, args: [String]) throws { var cargs = args.map{ strdup($0) } cargs.append(nil) @@ -143,6 +179,7 @@ internal func spawn(_ path: String, args: [String]) throws { #endif // os(macOS) +/// Test if the specified path is a directory internal func isDir(_ path: String) -> Bool { var st = stat() guard stat(path, &st) == 0 else { @@ -151,6 +188,7 @@ internal func isDir(_ path: String) -> Bool { return (st.st_mode & S_IFMT) == S_IFDIR } +/// Test if the specified path exists internal func exists(_ path: String) -> Bool { var st = stat() guard stat(path, &st) == 0 else { @@ -177,3 +215,145 @@ struct CFileStream: TextOutputStream { var standardOutput = CFileStream(fp: stdout) var standardError = CFileStream(fp: stderr) + +/// Format a timespec as an ISO8601 date/time +func formatISO8601(_ time: timespec) -> String { + var exploded = tm() + var secs = time.tv_sec + + gmtime_r(&secs, &exploded) + + let isoTime = """ +\(String(exploded.tm_year + 1900, width: 4))-\ +\(String(exploded.tm_mon + 1, width: 2))-\ +\(String(exploded.tm_mday, width: 2))T\ +\(String(exploded.tm_hour, width: 2)):\ +\(String(exploded.tm_min, width: 2)):\ +\(String(exploded.tm_sec, width: 2)).\ +\(String(time.tv_nsec / 1000, width: 6))Z +""" + + return isoTime +} + +/// Escape a JSON string +func escapeJSON(_ s: String) -> String { + var result = "" + let utf8View = s.utf8 + var chunk = utf8View.startIndex + var pos = chunk + let end = utf8View.endIndex + + result.reserveCapacity(utf8View.count) + + while pos != end { + let scalar = utf8View[pos] + switch scalar { + case 0x22, 0x5c, 0x00...0x1f: + result += s[chunk.. String? { + return withUnsafeTemporaryAllocation(byteCount: 256, alignment: 16) { + (buffer: UnsafeMutableRawBufferPointer) -> String? in + + var len = buffer.count + let ret = sysctlbyname(name, + buffer.baseAddress, &len, + nil, 0) + if ret != 0 { + return nil + } + + return String(validatingUTF8: + buffer.baseAddress!.assumingMemoryBound(to: CChar.self)) + } +} + +func getPlatform() -> String { + + #if os(macOS) + var platform = "macOS" + #elseif os(iOS) + var platform = "iOS" + #elseif os(watchOS) + var platform = "watchOS" + #elseif os(tvOS) + var platform = "tvOS" + #elseif os(visionOS) + var platform = "visionOS" + #endif + + let osVersion = getSysCtlString("kern.osversion") ?? "" + let osProductVersion = getSysCtlString("kern.osproductversion") ?? "" + + return "\(platform) \(osProductVersion) (\(osVersion))" +} +#elseif os(Linux) +fileprivate func readOSRelease(fd: CInt) -> [String:String]? { + let len = lseek(fd, 0, SEEK_END) + guard len >= 0 else { + return nil + } + return withUnsafeTemporaryAllocation(byteCount: len, alignment: 16) { + (buffer: UnsafeMutableRawBufferPointer) -> [String:String]? in + + _ = lseek(fd, 0, SEEK_SET) + let bytesRead = read(fd, buffer.baseAddress, buffer.count) + guard bytesRead == buffer.count else { + return nil + } + + let asString = String(decoding: buffer, as: UTF8.self) + return Dictionary(OSReleaseScanner(asString), + uniquingKeysWith: { $1 }) + } +} + +fileprivate func readOSRelease() -> [String:String]? { + var fd = open("/etc/os-release", O_RDONLY) + if fd == -1 { + fd = open("/usr/lib/os-release", O_RDONLY) + } + if fd == -1 { + return nil + } + defer { + close(fd) + } + + return readOSRelease(fd: fd) +} + +func getPlatform() -> String { + guard let info = readOSRelease(), + let pretty = info["PRETTY_NAME"] else { + return "Linux (unknown)" + } + + return "Linux (\(pretty))" +} +#elseif os(Windows) +func getPlatform() -> String { + return "Windows" +} +#else +func getPlatform() -> String { + return "Unknown" +} +#endif diff --git a/stdlib/public/libexec/swift-backtrace/main.swift b/stdlib/public/libexec/swift-backtrace/main.swift index 1eb939f9231ef..4db40d2201939 100644 --- a/stdlib/public/libexec/swift-backtrace/main.swift +++ b/stdlib/public/libexec/swift-backtrace/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Copyright (c) 2023-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -65,6 +65,11 @@ internal struct SwiftBacktrace { case full } + enum OutputFormat { + case text + case json + } + struct Arguments { var unwindAlgorithm: UnwindAlgorithm = .precise var demangle = false @@ -82,6 +87,7 @@ internal struct SwiftBacktrace { var cache = true var outputTo: OutputTo = .stdout var symbolicate: Symbolication = .full + var format: OutputFormat = .text var outputPath: String = "/tmp" } @@ -91,6 +97,9 @@ internal struct SwiftBacktrace { static var target: Target? = nil static var currentThread: Int = 0 + static var now = timespec(tv_sec: 0, tv_nsec: 0) + static var backtraceDuration = timespec(tv_sec: 0, tv_nsec: 0) + static var theme: any Theme { if args.color { return Themes.color @@ -209,6 +218,9 @@ Generate a backtrace for the parent process. the path points to a directory, a unique filename will be generated automatically. +--format Set the output format. Options are "text" and "json"; + the default is "text". + --crashinfo -a Provide a pointer to a platform specific CrashInfo structure. should be in hexadecimal. @@ -447,6 +459,23 @@ Generate a backtrace for the parent process. } else { args.symbolicate = .full } + case "--format": + if let v = value { + switch v.lowercased() { + case "text": + args.format = .text + case "json": + args.format = .json + default: + print("swift-backtrace: unknown output format '\(v)'", + to: &standardError) + } + } else { + print("swift-backtrace: missing format value", + to: &standardError) + usage() + exit(1) + } default: print("swift-backtrace: unknown argument '\(arg)'", to: &standardError) @@ -530,7 +559,7 @@ Generate a backtrace for the parent process. // Target's initializer fetches and symbolicates backtraces, so // we want to time that part here. - let duration = measureDuration { + backtraceDuration = measureDuration { target = Target(crashInfoAddr: crashInfoAddr, limit: args.limit, top: args.top, cache: args.cache, @@ -539,6 +568,13 @@ Generate a backtrace for the parent process. currentThread = target!.crashingThreadNdx } + // Grab the current wall clock time; if clock_gettime() fails, get a + // lower resolution version instead. + if clock_gettime(CLOCK_REALTIME, &now) != 0 { + now.tv_sec = time(nil) + now.tv_nsec = 0 + } + // Set up the output stream var didOpenOutput = false switch args.outputTo { @@ -551,23 +587,27 @@ Generate a backtrace for the parent process. // If the output path is a directory, generate a filename let name = target!.name let pid = target!.pid - var now = timespec() - - if clock_gettime(CLOCK_REALTIME, &now) != 0 { - now.tv_sec = time(nil) - now.tv_nsec = 0 + let now = timespec() + + let ext: String + switch args.format { + case .text: + ext = "log" + case .json: + ext = "json" } var filename = - "\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec).log" + "\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec).\(ext)" var fd = open(filename, O_RDWR|O_CREAT|O_EXCL, 0o644) var ndx = 1 + while fd < 0 && (errno == EEXIST || errno == EINTR) { if errno != EINTR { ndx += 1 + filename = "\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec)-\(ndx).\(ext)" } - filename = "\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec)-\(ndx).log" fd = open(filename, O_RDWR|O_CREAT|O_EXCL, 0o644) } @@ -604,14 +644,33 @@ Generate a backtrace for the parent process. } } - printCrashLog() + // Clear (or complete) the message written by the crash handler; this + // is always on stdout or stderr, even if you specify a file for output. + var handlerOut: CFileStream + if args.outputTo == .stdout { + handlerOut = standardOutput + } else { + handlerOut = standardError + } + if args.color { + print("\r\u{1b}[0K", terminator: "", to: &handlerOut) + } else { + print(" done ***\n\n", terminator: "", to: &handlerOut) + } - writeln("") + switch args.format { + case .text: + printCrashLog() - let formattedDuration = format(duration: duration) + writeln("") - writeln("Backtrace took \(formattedDuration)s") - writeln("") + let formattedDuration = format(duration: backtraceDuration) + + writeln("Backtrace took \(formattedDuration)s") + writeln("") + case .json: + outputJSONCrashLog() + } #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) // On Darwin, if Developer Mode is turned off, or we can't tell if it's @@ -771,20 +830,6 @@ Generate a backtrace for the parent process. description = "Program crashed: \(target.signalDescription) at \(hex(target.faultAddress))" } - // Clear (or complete) the message written by the crash handler; this - // is always on stdout or stderr, even if you specify a file for output. - var handlerOut: CFileStream - if args.outputTo == .stdout { - handlerOut = standardOutput - } else { - handlerOut = standardError - } - if args.color { - print("\r\u{1b}[0K", terminator: "", to: &handlerOut) - } else { - print(" done ***\n\n", terminator: "", to: &handlerOut) - } - writeln(theme.crashReason(description)) var mentionedImages = Set() @@ -1345,11 +1390,15 @@ Generate a backtrace for the parent process. from: RemoteMemoryReader.Address(value), count: 16, as: UInt8.self) { - let formattedBytes = theme.data(bytes.map{ - hex($0, withPrefix: false) - }.joined(separator: " ")) - let printedBytes = printableBytes(from: bytes) - writeln("\(reg) \(hexValue) \(formattedBytes) \(printedBytes)") + if args.sanitize ?? false { + writeln("\(reg) \(hexValue) ") + } else { + let formattedBytes = theme.data(bytes.map{ + hex($0, withPrefix: false) + }.joined(separator: " ")) + let printedBytes = printableBytes(from: bytes) + writeln("\(reg) \(hexValue) \(formattedBytes) \(printedBytes)") + } } else { let decValue = theme.decimalValue("\(value)") writeln("\(reg) \(hexValue) \(decValue)") diff --git a/stdlib/public/runtime/Backtrace.cpp b/stdlib/public/runtime/Backtrace.cpp index ee8e41e2894a3..dd16a4d8b70be 100644 --- a/stdlib/public/runtime/Backtrace.cpp +++ b/stdlib/public/runtime/Backtrace.cpp @@ -120,7 +120,7 @@ SWIFT_RUNTIME_STDLIB_INTERNAL BacktraceSettings _swift_backtraceSettings = { // top 16, - // sanitize, + // sanitize SanitizePaths::Preset, // preset @@ -129,12 +129,15 @@ SWIFT_RUNTIME_STDLIB_INTERNAL BacktraceSettings _swift_backtraceSettings = { // cache true, - // outputTo, + // outputTo OutputTo::Auto, // symbolicate Symbolication::Full, + // format + OutputFormat::Text, + // swiftBacktracePath NULL, @@ -748,6 +751,16 @@ _swift_processBacktracingSetting(llvm::StringRef key, } } else if (key.equals_insensitive("symbolicate")) { _swift_backtraceSettings.symbolicate = parseSymbolication(value); + } else if (key.equals_insensitive("format")) { + if (value.equals_insensitive("text")) { + _swift_backtraceSettings.format = OutputFormat::Text; + } else if (value.equals_insensitive("json")) { + _swift_backtraceSettings.format = OutputFormat::JSON; + } else { + swift::warning(0, + "swift runtime: unknown backtrace format '%.*s'\n", + static_cast(value.size()), value.data()); + } #if !defined(SWIFT_RUNTIME_FIXED_BACKTRACER_PATH) } else if (key.equals_insensitive("swift-backtrace")) { size_t len = value.size(); @@ -1054,6 +1067,8 @@ const char *backtracer_argv[] = { "stdout", // 30 "--symbolicate", // 31 "full", // 32 + "--format", // 33 + "text", // 34 NULL }; @@ -1191,6 +1206,15 @@ _swift_spawnBacktracer(CrashInfo *crashInfo) break; } + switch (_swift_backtraceSettings.format) { + case OutputFormat::Text: + backtracer_argv[34] = "text"; + break; + case OutputFormat::JSON: + backtracer_argv[34] = "json"; + break; + } + _swift_formatUnsigned(_swift_backtraceSettings.timeout, timeout_buf); if (_swift_backtraceSettings.limit < 0) diff --git a/stdlib/public/runtime/BacktracePrivate.h b/stdlib/public/runtime/BacktracePrivate.h index 15dc36d9989ef..238098a2b3673 100644 --- a/stdlib/public/runtime/BacktracePrivate.h +++ b/stdlib/public/runtime/BacktracePrivate.h @@ -104,6 +104,11 @@ enum class Symbolication { Full = 2, }; +enum class OutputFormat { + Text = 0, + JSON = 1 +}; + struct BacktraceSettings { UnwindAlgorithm algorithm; OnOffTty enabled; @@ -121,6 +126,7 @@ struct BacktraceSettings { bool cache; OutputTo outputTo; Symbolication symbolicate; + OutputFormat format; const char *swiftBacktracePath; const char *outputPath; }; diff --git a/test/Backtracing/JSON.swift b/test/Backtracing/JSON.swift new file mode 100644 index 0000000000000..acd1b948e7f2e --- /dev/null +++ b/test/Backtracing/JSON.swift @@ -0,0 +1,235 @@ +// RUN: %empty-directory(%t) +// RUN: %target-build-swift %s -parse-as-library -Onone -g -o %t/Crash +// RUN: %target-codesign %t/Crash +// RUN: env SWIFT_BACKTRACE=enable=yes,cache=no,format=json,output-to=%t/crash.json %target-run %t/Crash 2>&1 || true +// RUN: %validate-json %t/crash.json | %FileCheck %s + + +// Also check that we generate valid JSON with various different options set. + +// RUN: env SWIFT_BACKTRACE=enable=yes,cache=no,format=json,output-to=%t/crash2.json,threads=crashed %target-run %t/Crash 2>&1 || true +// RUN: env SWIFT_BACKTRACE=enable=yes,cache=no,format=json,output-to=%t/crash3.json,registers=all %target-run %t/Crash 2>&1 || true +// RUN: env SWIFT_BACKTRACE=enable=yes,cache=no,format=json,output-to=%t/crash4.json,sanitize=yes %target-run %t/Crash 2>&1 || true +// RUN: env SWIFT_BACKTRACE=enable=yes,cache=no,format=json,output-to=%t/crash5.json,images=none %target-run %t/Crash 2>&1 || true +// RUN: env SWIFT_BACKTRACE=enable=yes,cache=no,format=json,output-to=%t/crash6.json,images=all %target-run %t/Crash 2>&1 || true +// RUN: env SWIFT_BACKTRACE=enable=yes,cache=no,format=json,output-to=%t/crash7.json,symbolicate=off %target-run %t/Crash 2>&1 || true +// RUN: env SWIFT_BACKTRACE=enable=yes,cache=no,format=json,output-to=%t/crash8.json,demangle=no %target-run %t/Crash 2>&1 || true +// RUN: %validate-json %t/crash2.json +// RUN: %validate-json %t/crash3.json +// RUN: %validate-json %t/crash4.json +// RUN: %validate-json %t/crash5.json +// RUN: %validate-json %t/crash6.json +// RUN: %validate-json %t/crash7.json +// RUN: %validate-json %t/crash8.json + +// UNSUPPORTED: use_os_stdlib +// UNSUPPORTED: back_deployment_runtime +// UNSUPPORTED: asan +// REQUIRES: executable_test +// REQUIRES: backtracing +// REQUIRES: OS=macosx || OS=linux-gnu + +func level1() { + level2() +} + +func level2() { + level3() +} + +func level3() { + level4() +} + +func level4() { + level5() +} + +func level5() { + print("About to crash") + let ptr = UnsafeMutablePointer(bitPattern: 4)! + ptr.pointee = 42 +} + +@main +struct Crash { + static func main() { + level1() + } +} + +// CHECK: { + +// JSON logs start with an ISO timestamp + +// CHECK-NEXT: "timestamp": "{{[0-9]{4,}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.[0-9]{6}Z}}", + + +// Then a tag identifying this as a crash report + +// CHECK-NEXT: "kind": "crashReport", + + +// This is followed by a description and the fault address + +// CHECK-NEXT: "description": "Bad pointer dereference", +// CHECK-NEXT: "faultAddress": "0x{{0+}}4", + + +// And then by the platform and architecture +// CHECK-NEXT: "platform": "{{.*}}", +// CHECK-NEXT: "architecture": "{{.*}}", + + +// And then a list of threads + +// CHECK-NEXT: "threads": [ +// CHECK-NEXT: { + +// On Linux there's a name here + +// CHECK: "crashed": true, +// CHECK-NEXT: "registers": { +// CHECK-NEXT: "{{.*}}": "0x{{[0-9a-f]+}}", + +// More registers here, but the number is system specific + +// CHECK: }, +// CHECK-NEXT: "frames": [ +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "programCounter", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s5Crash6level5yyF", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "description": "level5() + [[OFFSET]]", +// CHECK-NEXT: "image": "Crash", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSON.swift", +// CHECK-NEXT: "line": 51, +// CHECK-NEXT: "column": 15 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "returnAddress", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s5Crash6level4yyF", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "description": "level4() + [[OFFSET]]", +// CHECK-NEXT: "image": "Crash", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSON.swift", +// CHECK-NEXT: "line": 45, +// CHECK-NEXT: "column": 3 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "returnAddress", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s5Crash6level3yyF", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "description": "level3() + [[OFFSET]]", +// CHECK-NEXT: "image": "Crash", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSON.swift", +// CHECK-NEXT: "line": 41, +// CHECK-NEXT: "column": 3 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "returnAddress", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s5Crash6level2yyF", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "description": "level2() + [[OFFSET]]", +// CHECK-NEXT: "image": "Crash", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSON.swift", +// CHECK-NEXT: "line": 37, +// CHECK-NEXT: "column": 3 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "returnAddress", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s5Crash6level1yyF", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "description": "level1() + [[OFFSET]]", +// CHECK-NEXT: "image": "Crash", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSON.swift", +// CHECK-NEXT: "line": 33, +// CHECK-NEXT: "column": 3 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "returnAddress", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s5CrashAAV4mainyyFZ", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "description": "static Crash.main() + [[OFFSET]]", +// CHECK-NEXT: "image": "Crash", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSON.swift", +// CHECK-NEXT: "line": 57, +// CHECK-NEXT: "column": 5 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "returnAddress", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "system": true, +// CHECK-NEXT: "symbol": "{{_?}}$s5CrashAAV5$mainyyFZ", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "description": "static Crash.$main() + [[OFFSET]]", +// CHECK-NEXT: "image": "Crash", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{/*}}", +// CHECK-NEXT: "line": 0, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "returnAddress", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "system": true, +// CHECK-NEXT: "symbol": "{{_?main}}", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "description": "main + [[OFFSET]]", +// CHECK-NEXT: "image": "Crash", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSON.swift", +// CHECK-NEXT: "line": 0, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, + +// More frames here, but they're system specific + +// CHECK: ] +// CHECK: } +// CHECK-NEXT: ], +// CHECK-NEXT: "capturedMemory": { +// CHECK-NEXT: "0x{{[[0-9a-f]+}}": "{{([0-9a-f][0-9a-f])+}}", + +// More captures here, but system specific + +// CHECK: }, +// CHECK-NEXT: "omittedImages": {{[0-9]+}}, +// CHECK-NEXT: "images": [ +// CHECK-NEXT: { + +// Maybe multiple images before this one + +// CHECK: "name": "Crash", +// "buildId": ... is optional +// CHECK: "path": "{{.*}}/Crash", +// CHECK-NEXT: "baseAddress": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "endOfText": "0x{{[0-9a-f]+}}" +// CHECK-NEXT: } + +// Maybe multiple images after this one + +// CHECK: ], +// CHECK-NEXT: "backtraceTime": {{[0-9]+(\.[0-9]+)?}} + +// CHECK-NEXT: } diff --git a/test/Backtracing/JSONAsync.swift b/test/Backtracing/JSONAsync.swift new file mode 100644 index 0000000000000..c5aeebca60193 --- /dev/null +++ b/test/Backtracing/JSONAsync.swift @@ -0,0 +1,220 @@ +// RUN: %empty-directory(%t) +// RUN: %target-build-swift %s -parse-as-library -Onone -g -o %t/JSONAsync +// RUN: %target-codesign %t/JSONAsync + +// RUN: env SWIFT_BACKTRACE=enable=yes,demangle=no,cache=no,format=json,output-to=%t/crash.json %target-run %t/JSONAsync 2>&1 || true +// RUN: %validate-json %t/crash.json | %FileCheck %s + +// UNSUPPORTED: use_os_stdlib +// UNSUPPORTED: back_deployment_runtime +// UNSUPPORTED: asan +// REQUIRES: executable_test +// REQUIRES: backtracing +// REQUIRES: OS=macosx || OS=linux-gnu + +@available(SwiftStdlib 5.1, *) +func crash() { + let ptr = UnsafeMutablePointer(bitPattern: 4)! + ptr.pointee = 42 +} + +@available(SwiftStdlib 5.1, *) +func level(_ n: Int) async { + if n < 5 { + await level(n + 1) + } else { + crash() + } +} + +@available(SwiftStdlib 5.1, *) +@main +struct JSONAsync { + static func main() async { + await level(1) + } +} + +// CHECK: { + +// JSON logs start with an ISO timestamp + +// CHECK-NEXT: "timestamp": "{{[0-9]{4,}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.[0-9]{6}Z}}", + + +// Then a tag identifying this as a crash report + +// CHECK-NEXT: "kind": "crashReport", + + +// This is followed by a description and the fault address + +// CHECK-NEXT: "description": "Bad pointer dereference", +// CHECK-NEXT: "faultAddress": "0x{{0+}}4", + + +// And then by the platform and architecture + +// CHECK-NEXT: "platform": "{{.*}}", +// CHECK-NEXT: "architecture": "{{.*}}", + + +// And then a list of threads + +// CHECK-NEXT: "threads": [ +// CHECK-NEXT: { + +// The crashing thread isn't necessarily the first + +// CHECK: "crashed": true, +// CHECK-NEXT: "registers": { +// CHECK-NEXT: "{{.*}}": "0x{{[0-9a-f]+}}", + +// More registers here, but the number is system specific + +// CHECK: }, +// CHECK-NEXT: "frames": [ +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "programCounter", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s9JSONAsync5crashyyF", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSONAsync.swift", +// CHECK-NEXT: "line": 18, +// CHECK-NEXT: "column": 15 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "returnAddress", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s9JSONAsync5levelyySiYaFTY0_", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSONAsync.swift", +// CHECK-NEXT: "line": 26, +// CHECK-NEXT: "column": 5 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "asyncResumePoint", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s9JSONAsync5levelyySiYaFTQ1_", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSONAsync.swift", +// CHECK-NEXT: "line": 24, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "asyncResumePoint", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s9JSONAsync5levelyySiYaFTQ1_", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSONAsync.swift", +// CHECK-NEXT: "line": 24, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "asyncResumePoint", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s9JSONAsync5levelyySiYaFTQ1_", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSONAsync.swift", +// CHECK-NEXT: "line": 24, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "asyncResumePoint", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s9JSONAsync5levelyySiYaFTQ1_", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSONAsync.swift", +// CHECK-NEXT: "line": 24, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "asyncResumePoint", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "symbol": "{{_?}}$s9JSONAsyncAAV4mainyyYaFZTQ0_", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{.*}}/JSONAsync.swift", +// CHECK-NEXT: "line": 34, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "asyncResumePoint", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "system": true, +// CHECK-NEXT: "symbol": "{{_?}}$s9JSONAsyncAAV5$mainyyYaFZTQ0_", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{/*}}", +// CHECK-NEXT: "line": 0, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "kind": "asyncResumePoint", +// CHECK-NEXT: "address": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "system": true, +// CHECK-NEXT: "symbol": "{{_?}}async_MainTQ0_", +// CHECK-NEXT: "offset": [[OFFSET:[0-9]+]], +// CHECK-NEXT: "image": "JSONAsync", +// CHECK-NEXT: "sourceLocation": { +// CHECK-NEXT: "file": "{{/*}}", +// CHECK-NEXT: "line": 0, +// CHECK-NEXT: "column": 0 +// CHECK-NEXT: } +// CHECK-NEXT: }, + +// More frames here, but they're system specific + +// CHECK: ] +// CHECK-NEXT: } + +// Potentially more threads here + +// CHECK: ], +// CHECK-NEXT: "capturedMemory": { +// CHECK-NEXT: "0x{{[[0-9a-f]+}}": "{{([0-9a-f][0-9a-f])+}}", + +// More captures here, but system specific + +// CHECK: }, +// CHECK-NEXT: "omittedImages": {{[0-9]+}}, +// CHECK-NEXT: "images": [ +// CHECK-NEXT: { + +// Maybe multiple images before this one + +// CHECK: "name": "JSONAsync", +// "buildId": ... is optional +// CHECK: "path": "{{.*}}/JSONAsync", +// CHECK-NEXT: "baseAddress": "0x{{[0-9a-f]+}}", +// CHECK-NEXT: "endOfText": "0x{{[0-9a-f]+}}" +// CHECK-NEXT: } + +// Maybe multiple images after this one + +// CHECK: ], +// CHECK-NEXT: "backtraceTime": {{[0-9]+(\.[0-9]+)?}} + +// CHECK-NEXT: } From 565022d1f06d8130e46d3e4b0d34b703eef0c974 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Fri, 28 Mar 2025 08:44:31 +0000 Subject: [PATCH 2/2] [Tests][Backtracing] Fix JSON test failure on Linux. It's possible that the last frame in the test is genuinely the last one we see in the backtrace, in which case there won't be a `,` after the `}`. rdar://121430255 --- test/Backtracing/JSON.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Backtracing/JSON.swift b/test/Backtracing/JSON.swift index acd1b948e7f2e..2157d75934aa8 100644 --- a/test/Backtracing/JSON.swift +++ b/test/Backtracing/JSON.swift @@ -201,9 +201,9 @@ struct Crash { // CHECK-NEXT: "line": 0, // CHECK-NEXT: "column": 0 // CHECK-NEXT: } -// CHECK-NEXT: }, +// CHECK-NEXT: } -// More frames here, but they're system specific +// Possibly more frames here, but they're system specific // CHECK: ] // CHECK: }