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: }