Skip to content

Commit 112d2cb

Browse files
committed
RUM-12420 Generate backtrace using KSCrash
1 parent 51503d5 commit 112d2cb

File tree

5 files changed

+240
-12
lines changed

5 files changed

+240
-12
lines changed

Datadog/Datadog.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,10 @@
14491449
D24C9C6129A7CB0C002057CF /* DatadogLogsFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222F244E1BE900902D19 /* DatadogLogsFeatureTests.swift */; };
14501450
D24EC3D92DD1F117007A7E8F /* SessionReplayCoreContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24EC3D82DD1F117007A7E8F /* SessionReplayCoreContext.swift */; };
14511451
D24EC3DA2DD1F117007A7E8F /* SessionReplayCoreContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24EC3D82DD1F117007A7E8F /* SessionReplayCoreContext.swift */; };
1452+
D253E65B2EBA31C40074BEC4 /* KSCrashBacktrace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253E65A2EBA31C40074BEC4 /* KSCrashBacktrace.swift */; };
1453+
D253E65C2EBA31C40074BEC4 /* KSCrashBacktrace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253E65A2EBA31C40074BEC4 /* KSCrashBacktrace.swift */; };
1454+
D253E65E2EBA478E0074BEC4 /* KSCrashBacktraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253E65D2EBA478E0074BEC4 /* KSCrashBacktraceTests.swift */; };
1455+
D253E65F2EBA478E0074BEC4 /* KSCrashBacktraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253E65D2EBA478E0074BEC4 /* KSCrashBacktraceTests.swift */; };
14521456
D253EE962B988CA90010B589 /* ViewCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253EE952B988CA90010B589 /* ViewCache.swift */; };
14531457
D253EE972B988CA90010B589 /* ViewCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253EE952B988CA90010B589 /* ViewCache.swift */; };
14541458
D253EE9B2B98B37B0010B589 /* ViewCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253EE982B98B3690010B589 /* ViewCacheTests.swift */; };
@@ -3405,6 +3409,8 @@
34053409
D24985A12728048B00B4F72D /* SwiftUIViewHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIViewHandler.swift; sourceTree = "<group>"; };
34063410
D24C9C3E29A79772002057CF /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
34073411
D24EC3D82DD1F117007A7E8F /* SessionReplayCoreContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayCoreContext.swift; sourceTree = "<group>"; };
3412+
D253E65A2EBA31C40074BEC4 /* KSCrashBacktrace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSCrashBacktrace.swift; sourceTree = "<group>"; };
3413+
D253E65D2EBA478E0074BEC4 /* KSCrashBacktraceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSCrashBacktraceTests.swift; sourceTree = "<group>"; };
34083414
D253EE952B988CA90010B589 /* ViewCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCache.swift; sourceTree = "<group>"; };
34093415
D253EE982B98B3690010B589 /* ViewCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCacheTests.swift; sourceTree = "<group>"; };
34103416
D2546BF029AF4F550054E00B /* DatadogTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogTracer.swift; sourceTree = "<group>"; };
@@ -6555,6 +6561,7 @@
65556561
isa = PBXGroup;
65566562
children = (
65576563
D22689C22EB12D3D00875E44 /* KSCrashPlugin.swift */,
6564+
D253E65A2EBA31C40074BEC4 /* KSCrashBacktrace.swift */,
65586565
D2268AD42EB4DA5800875E44 /* CrashFieldDictionary.swift */,
65596566
D2268AEC2EB51F6300875E44 /* DatadogCrashReportFilter.swift */,
65606567
D2268AD72EB4DAC400875E44 /* AnyCrashReport.swift */,
@@ -6574,6 +6581,7 @@
65746581
D2268AE32EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift */,
65756582
D2268AE92EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift */,
65766583
D2268AEF2EB51FB600875E44 /* DatadogCrashReportFilterTests.swift */,
6584+
D253E65D2EBA478E0074BEC4 /* KSCrashBacktraceTests.swift */,
65776585
);
65786586
path = KSCrashIntegration;
65796587
sourceTree = "<group>";
@@ -9409,6 +9417,7 @@
94099417
isa = PBXSourcesBuildPhase;
94109418
buildActionMask = 2147483647;
94119419
files = (
9420+
D253E65C2EBA31C40074BEC4 /* KSCrashBacktrace.swift in Sources */,
94129421
D214DA8929DF2D6A004D0AE8 /* CrashReportSender.swift in Sources */,
94139422
D2268AD22EB4DA4100875E44 /* DatadogTypeSafeFilter.swift in Sources */,
94149423
D293302C2A137DAD0029C9EA /* CrashReportingFeature.swift in Sources */,
@@ -9447,6 +9456,7 @@
94479456
61FDBA15269722B4001D9D43 /* CrashReportInfoMinifierTests.swift in Sources */,
94489457
D22689C82EB2151F00875E44 /* KSCrashPluginTests.swift in Sources */,
94499458
61E95D882695C00200EA3115 /* DDCrashReportExporterTests.swift in Sources */,
9459+
D253E65F2EBA478E0074BEC4 /* KSCrashBacktraceTests.swift in Sources */,
94509460
61B7886225C180CB002675B5 /* CrashReportingPluginTests.swift in Sources */,
94519461
615CC4132695957C0005F08C /* CrashReportInfoTests.swift in Sources */,
94529462
D2268AE42EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift in Sources */,
@@ -10629,6 +10639,7 @@
1062910639
isa = PBXSourcesBuildPhase;
1063010640
buildActionMask = 2147483647;
1063110641
files = (
10642+
D253E65B2EBA31C40074BEC4 /* KSCrashBacktrace.swift in Sources */,
1063210643
D214DA8C29DF2D6B004D0AE8 /* CrashReportSender.swift in Sources */,
1063310644
D2268AD32EB4DA4100875E44 /* DatadogTypeSafeFilter.swift in Sources */,
1063410645
D293302D2A137DAD0029C9EA /* CrashReportingFeature.swift in Sources */,
@@ -10667,6 +10678,7 @@
1066710678
D2CB6FDD27C5352300A62B57 /* CrashReportInfoMinifierTests.swift in Sources */,
1066810679
D22689C72EB2151F00875E44 /* KSCrashPluginTests.swift in Sources */,
1066910680
D2CB6FDE27C5352300A62B57 /* DDCrashReportExporterTests.swift in Sources */,
10681+
D253E65E2EBA478E0074BEC4 /* KSCrashBacktraceTests.swift in Sources */,
1067010682
D2CB6FE027C5352300A62B57 /* CrashReportingPluginTests.swift in Sources */,
1067110683
D2CB6FE127C5352300A62B57 /* CrashReportInfoTests.swift in Sources */,
1067210684
D2268AE52EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift in Sources */,

DatadogCrashReporting/Sources/KSCrashIntegration/DatadogCrashReportFilter.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,7 @@ internal final class DatadogCrashReportFilter: NSObject, CrashReportFilter {
121121
return nil
122122
}
123123

124-
// Detect system libraries based on path patterns
125-
#if targetEnvironment(simulator)
126-
// Simulator: system images are in Xcode.app/Contents/Developer/Platforms/ or .simruntime bundles (Xcode 16+)
127-
let isSystemImage = path.contains("/Contents/Developer/Platforms/") || path.contains("simruntime")
128-
#else
129-
// Device: user images are in /var/containers/Bundle/Application/, everything else is system
130-
let isSystemImage = !path.contains("/Bundle/Application/")
131-
#endif
124+
132125

133126
let imageAddress: UInt64 = try image.value(forKey: .imageAddress)
134127
let imageSize: UInt64 = try image.value(forKey: .imageSize)
@@ -202,3 +195,16 @@ internal final class DatadogCrashReportFilter: NSObject, CrashReportFilter {
202195
)
203196
}
204197
}
198+
199+
extension BinaryImage {
200+
// Detect system libraries based on path patterns
201+
static func isSystemLibraryPath(_ path: NSString) -> Bool {
202+
#if targetEnvironment(simulator)
203+
// Simulator: system images are in Xcode.app/Contents/Developer/Platforms/ or .simruntime bundles (Xcode 16+)
204+
return path.contains("/Contents/Developer/Platforms/") || path.contains("simruntime")
205+
#else
206+
// Device: user images are in /var/containers/Bundle/Application/, everything else is system
207+
return !path.contains("/Bundle/Application/")
208+
#endif
209+
}
210+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2019-Present Datadog, Inc.
5+
*/
6+
7+
import Foundation
8+
import DatadogInternal
9+
10+
// swiftlint:disable duplicate_imports
11+
#if COCOAPODS
12+
import KSCrash
13+
#elseif swift(>=6.0)
14+
internal import KSCrashRecording
15+
#else
16+
@_implementationOnly import KSCrashRecording
17+
#endif
18+
// swiftlint:enable duplicate_imports
19+
20+
internal struct KSCrashBacktrace: BacktraceReporting {
21+
func generateBacktrace(threadID: DatadogInternal.ThreadID) throws -> DatadogInternal.BacktraceReport? {
22+
// Convert Mach thread_t to pthread_t
23+
guard let pthread = pthread_from_mach_thread_np(threadID) else {
24+
return nil
25+
}
26+
27+
// Capture backtrace for the thread
28+
var count: Int32 = 200
29+
var addresses = [uintptr_t](repeating: 0, count: Int(count))
30+
count = captureBacktrace(thread: pthread, addresses: &addresses, count: count)
31+
32+
guard count > 0 else {
33+
return nil
34+
}
35+
36+
var binaryImages: [UInt: BinaryImage] = [:]
37+
let nb = Int(count)
38+
let stack = (0..<nb).compactMap { index in
39+
let address = addresses[index]
40+
41+
var symbolInfo = SymbolInformation()
42+
guard
43+
symbolicate(address: address, result: &symbolInfo),
44+
let imageName = symbolInfo.imageName,
45+
let path = NSString(utf8String: imageName),
46+
let imageUUID = symbolInfo.imageUUID
47+
else {
48+
return String(format: "%-4ld ??? 0x%016llx 0x0 + 0", index, address) // no binary image info
49+
}
50+
51+
let libraryName = path.lastPathComponent
52+
let loadAddress = symbolInfo.imageAddress
53+
54+
let uuid = UUID(uuid: imageUUID.withMemoryRebound(to: uuid_t.self, capacity: 1) { $0.pointee })
55+
56+
if binaryImages[loadAddress] == nil {
57+
let binaryImage = BinaryImage(
58+
libraryName: libraryName,
59+
uuid: uuid.uuidString,
60+
architecture: String(cString: kscpu_currentArch()),
61+
isSystemLibrary: BinaryImage.isSystemLibraryPath(path),
62+
loadAddress: String(format: "0x%016lx", loadAddress),
63+
maxAddress: String(format: "0x%016lx", loadAddress + UInt(symbolInfo.imageSize))
64+
)
65+
binaryImages[loadAddress] = binaryImage
66+
}
67+
68+
// Format: frame_index (4 chars left-aligned) + library_name (35 chars left-aligned) + addresses + offset
69+
return String(format: "%-4ld %-35@ 0x%016llx 0x%016llx + %lld", index, libraryName, address, loadAddress, address - loadAddress)
70+
}
71+
.joined(separator: "\n")
72+
73+
// Create thread info
74+
let thread = DDThread(
75+
name: getThreadName(pthread: pthread) ?? "Thread \(threadID)",
76+
stack: stack,
77+
crashed: false,
78+
state: nil
79+
)
80+
81+
return BacktraceReport(
82+
stack: stack,
83+
threads: [thread],
84+
binaryImages: Array(binaryImages.values),
85+
wasTruncated: false
86+
)
87+
}
88+
89+
/// Get the name of a pthread
90+
private func getThreadName(pthread: pthread_t) -> String? {
91+
var buffer = [CChar](repeating: 0, count: 256)
92+
guard pthread_getname_np(pthread, &buffer, buffer.count) == KERN_SUCCESS, buffer[0] != 0 else {
93+
return nil // fails or empty
94+
}
95+
return String(cString: buffer)
96+
}
97+
}

DatadogCrashReporting/Sources/KSCrashIntegration/KSCrashPlugin.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ internal class KSCrashPlugin: NSObject, CrashReportingPlugin {
7474
kscrash.userInfo = [CrashField.dd.rawValue: contextBase64]
7575
}
7676

77-
var backtraceReporter: BacktraceReporting? {
78-
/* no-op */
79-
return nil
80-
}
77+
var backtraceReporter: BacktraceReporting? { KSCrashBacktrace() }
8178
}
8279

8380
extension KSCrashConfiguration {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2019-Present Datadog, Inc.
5+
*/
6+
7+
import XCTest
8+
import DatadogInternal
9+
@testable import DatadogCrashReporting
10+
11+
class KSCrashBacktraceTests: XCTestCase {
12+
/// Regex pattern to match stack frame format: index library_name address load_address + offset
13+
/// Library name can contain spaces (e.g., "DatadogCrashReportingTests iOS")
14+
let regex = try! NSRegularExpression(pattern: #"^\d+\s+.+?\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+\+\s+\d+$"#, options: [.anchorsMatchLines])
15+
16+
// MARK: - Current Thread Tests
17+
18+
func testCurrentThreadStackContainsTestFrames() throws {
19+
// Given
20+
let backtrace = KSCrashBacktrace()
21+
let currentThreadID = Thread.currentThreadID
22+
23+
// When
24+
let report = try XCTUnwrap(try backtrace.generateBacktrace(threadID: currentThreadID))
25+
26+
// Then
27+
report.stack.split(separator: "\n").forEach { line in
28+
let range = NSRange(line.startIndex..., in: line)
29+
30+
// Validate each line matches the expected format
31+
XCTAssertNotNil(
32+
regex.firstMatch(in: String(line), range: range),
33+
"Stack line should match format 'index library address load_address + offset': \(line)"
34+
)
35+
}
36+
37+
let userImage = report.binaryImages.first(where: { $0.libraryName.contains("DatadogCrashReportingTests") })
38+
let systemImage = report.binaryImages.first(where: { $0.libraryName == "xctest" })
39+
40+
XCTAssertFalse(userImage?.isSystemLibrary ?? true, "Should include current binary image")
41+
XCTAssertTrue(systemImage?.isSystemLibrary ?? false, "Should include xctest system image")
42+
XCTAssertEqual(report.threads.count, 1, "Should have one thread")
43+
XCTAssertEqual(report.stack, report.threads[0].stack)
44+
XCTAssertFalse(report.threads[0].name.isEmpty, "Thread should have a name")
45+
}
46+
47+
// MARK: - Other Thread Tests
48+
49+
func testGenerateBacktraceForBackgroundThread() throws {
50+
// Given
51+
let backtrace = KSCrashBacktrace()
52+
let expectation = XCTestExpectation(description: "Background thread backtrace")
53+
var backgroundThreadID: ThreadID?
54+
var capturedReport: BacktraceReport?
55+
56+
// When - Create a background thread
57+
Thread.detachNewThread {
58+
let semaphore = DispatchSemaphore(value: 0)
59+
backgroundThreadID = Thread.currentThreadID
60+
61+
// Capture backtrace from main thread
62+
DispatchQueue.global().async {
63+
do {
64+
capturedReport = try backtrace.generateBacktrace(threadID: backgroundThreadID!)
65+
expectation.fulfill()
66+
} catch {
67+
XCTFail("Failed to generate backtrace: \(error)")
68+
expectation.fulfill()
69+
}
70+
71+
semaphore.signal()
72+
}
73+
74+
// keep thread alive to generate its backtrace
75+
semaphore.wait()
76+
}
77+
78+
wait(for: [expectation], timeout: 5.0)
79+
80+
// Then
81+
XCTAssertNotNil(capturedReport, "Should generate backtrace for background thread")
82+
let report = try XCTUnwrap(capturedReport)
83+
84+
report.stack.split(separator: "\n").forEach { line in
85+
let range = NSRange(line.startIndex..., in: line)
86+
87+
// Validate each line matches the expected format
88+
XCTAssertNotNil(
89+
regex.firstMatch(in: String(line), range: range),
90+
"Stack line should match format 'index library address load_address + offset': \(line)"
91+
)
92+
}
93+
94+
let userImage = report.binaryImages.first(where: { $0.libraryName.contains("DatadogCrashReportingTests") })
95+
let systemImage = report.binaryImages.first(where: { $0.libraryName == "Foundation" })
96+
97+
XCTAssertFalse(userImage?.isSystemLibrary ?? true, "Should include current binary image")
98+
XCTAssertTrue(systemImage?.isSystemLibrary ?? false, "Should include Foundation system image")
99+
XCTAssertFalse(report.binaryImages.contains(where: { $0.libraryName == "xctest" }), "Should not include xctest")
100+
XCTAssertEqual(report.threads.count, 1, "Should have one thread")
101+
XCTAssertEqual(report.stack, report.threads[0].stack)
102+
XCTAssertFalse(report.threads[0].name.isEmpty, "Thread should have a name")
103+
}
104+
105+
func testInvalidThreadIDReturnsNil() throws {
106+
// Given - An invalid thread ID
107+
let backtrace = KSCrashBacktrace()
108+
let invalidThreadID: ThreadID = 0
109+
110+
// When
111+
let report = try backtrace.generateBacktrace(threadID: invalidThreadID)
112+
113+
// Then
114+
XCTAssertNil(report, "Should return nil for invalid thread ID")
115+
}
116+
}

0 commit comments

Comments
 (0)