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..2157d75934aa8 --- /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: } + +// Possibly 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: }