Skip to content

Commit 2a3d3cb

Browse files
authored
Merge pull request #69916 from w6sec/swift-inspect-json
[swift-inspect] Add JSON output option for dump-generic-metadata
2 parents d8fc855 + 4af03a8 commit 2a3d3cb

File tree

9 files changed

+366
-77
lines changed

9 files changed

+366
-77
lines changed

tools/swift-inspect/Sources/swift-inspect/DarwinRemoteProcess.swift

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ internal final class DarwinRemoteProcess: RemoteProcess {
2020
public typealias ProcessHandle = task_t
2121

2222
private var task: task_t
23+
internal var processIdentifier: ProcessIdentifier
24+
internal lazy var processName = getProcessName(processId: processIdentifier) ?? "<unknown process>"
2325

2426
public var process: ProcessHandle { task }
2527
public private(set) var context: SwiftReflectionContextRef!
@@ -114,20 +116,32 @@ internal final class DarwinRemoteProcess: RemoteProcess {
114116
}
115117

116118
init?(processId: ProcessIdentifier, forkCorpse: Bool) {
119+
processIdentifier = processId
117120
var task: task_t = task_t()
118121
let taskResult = task_for_pid(mach_task_self_, processId, &task)
119122
guard taskResult == KERN_SUCCESS else {
120-
print("unable to get task for pid \(processId): \(String(cString: mach_error_string(taskResult))) \(hex: taskResult)")
123+
print("unable to get task for pid \(processId): \(String(cString: mach_error_string(taskResult))) \(hex: taskResult)",
124+
to: &Std.err)
121125
return nil
122126
}
123127

124128
if forkCorpse {
125129
var corpse = task_t()
126-
let corpseResult = task_generate_corpse(task, &corpse)
127-
if corpseResult == KERN_SUCCESS {
128-
task = corpse
129-
} else {
130-
print("unable to fork corpse for pid \(processId): \(String(cString: mach_error_string(corpseResult))) \(hex: corpseResult)")
130+
let maxRetry = 6
131+
for retry in 0..<maxRetry {
132+
let corpseResult = task_generate_corpse(task, &corpse)
133+
if corpseResult == KERN_SUCCESS {
134+
task_stop_peeking(task)
135+
mach_port_deallocate(mach_task_self_, task)
136+
task = corpse
137+
break
138+
}
139+
if corpseResult != KERN_RESOURCE_SHORTAGE || retry == maxRetry {
140+
print("unable to fork corpse for pid \(processId): \(String(cString: mach_error_string(corpseResult))) \(hex: corpseResult)",
141+
to: &Std.err)
142+
return nil
143+
}
144+
sleep(UInt32(1 << retry))
131145
}
132146
}
133147

@@ -161,6 +175,7 @@ internal final class DarwinRemoteProcess: RemoteProcess {
161175
deinit {
162176
task_stop_peeking(self.task)
163177
mach_port_deallocate(mach_task_self_, self.task)
178+
mach_port_mod_refs(mach_task_self_, self.task, MACH_PORT_RIGHT_SEND, -1);
164179
}
165180

166181
func symbolicate(_ address: swift_addr_t) -> (module: String?, symbol: String?) {

tools/swift-inspect/Sources/swift-inspect/Operations/DumpGenericMetadata.swift

Lines changed: 177 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,80 @@
1212

1313
import ArgumentParser
1414
import SwiftRemoteMirror
15+
import Foundation
1516

16-
private struct Metadata {
17+
private struct Metadata: Encodable {
1718
let ptr: swift_reflection_ptr_t
1819
var allocation: swift_metadata_allocation_t?
19-
2020
let name: String
2121
let isArrayOfClass: Bool
22-
22+
var garbage: Bool = false
2323
var offset: Int? { allocation.map { Int(self.ptr - $0.ptr) } }
24+
var backtrace: String?
25+
26+
enum CodingKeys: String, CodingKey {
27+
case ptr = "address"
28+
case allocation
29+
case name
30+
case isArrayOfClass
31+
case garbage
32+
case offset
33+
case backtrace
34+
}
35+
36+
func encode(to encoder: Encoder) throws {
37+
var container = encoder.container(keyedBy: CodingKeys.self)
38+
try container.encode(ptr, forKey: .ptr)
39+
try container.encode(name, forKey: .name)
40+
if isArrayOfClass {
41+
try container.encode(isArrayOfClass, forKey: .isArrayOfClass)
42+
}
43+
if garbage {
44+
try container.encode(garbage, forKey: .garbage)
45+
}
46+
if let offset {
47+
try container.encode(offset, forKey: .offset)
48+
}
49+
if let backtrace {
50+
try container.encode(backtrace, forKey: .backtrace)
51+
}
52+
if let allocation {
53+
try container.encode(allocation, forKey: .allocation)
54+
}
55+
}
56+
}
57+
58+
private struct ProcessMetadata: Encodable {
59+
var name: String
60+
var pid: ProcessIdentifier
61+
var metadata: [Metadata]
62+
}
63+
64+
private struct MetadataSummary: Encodable {
65+
var totalSize: Int
66+
var processes: Set<String>
67+
}
68+
69+
internal struct Output: TextOutputStream {
70+
let fileHandle: FileHandle
71+
init(_ outputFile: String? = nil) throws {
72+
if let outputFile {
73+
if FileManager().createFile(atPath: outputFile, contents: nil) {
74+
self.fileHandle = FileHandle(forWritingAtPath: outputFile)!
75+
} else {
76+
print("Unable to create file \(outputFile)", to: &Std.err)
77+
exit(1)
78+
}
79+
} else {
80+
self.fileHandle = FileHandle.standardOutput
81+
}
82+
}
83+
84+
mutating func write(_ string: String) {
85+
if let encodedString = string.data(using: .utf8) {
86+
fileHandle.write(encodedString)
87+
}
88+
}
2489
}
2590

2691
internal struct DumpGenericMetadata: ParsableCommand {
@@ -37,56 +102,131 @@ internal struct DumpGenericMetadata: ParsableCommand {
37102
var genericMetadataOptions: GenericMetadataOptions
38103

39104
func run() throws {
105+
disableStdErrBuffer()
106+
var metadataSummary = [String: MetadataSummary]()
107+
var allProcesses = [ProcessMetadata]()
40108
try inspect(options: options) { process in
41109
let allocations: [swift_metadata_allocation_t] =
42110
try process.context.allocations.sorted()
43111

44-
let generics: [Metadata] = allocations.compactMap { allocation -> Metadata? in
112+
let stacks: [swift_reflection_ptr_t:[swift_reflection_ptr_t]] =
113+
backtraceOptions.style == nil
114+
? [swift_reflection_ptr_t:[swift_reflection_ptr_t]]()
115+
: try process.context.allocationStacks
116+
117+
let generics: [Metadata] = allocations.compactMap { allocation in
45118
let pointer = swift_reflection_allocationMetadataPointer(process.context, allocation)
46119
if pointer == 0 { return nil }
120+
let allocation = allocations.last(where: { pointer >= $0.ptr && pointer < $0.ptr + UInt64($0.size) })
121+
let garbage = (allocation == nil && swift_reflection_ownsAddressStrict(process.context, UInt(pointer)) == 0)
122+
var currentBacktrace: String?
123+
if let style = backtraceOptions.style, let allocation, let stack = stacks[allocation.ptr] {
124+
currentBacktrace = backtrace(stack, style: style, process.symbolicate)
125+
}
47126

48127
return Metadata(ptr: pointer,
49-
allocation: allocations.last(where: { pointer >= $0.ptr && pointer < $0.ptr + swift_reflection_ptr_t($0.size) }),
50-
name: (process.context.name(type: pointer, mangled: genericMetadataOptions.mangled) ?? "<unknown>"),
51-
isArrayOfClass: process.context.isArrayOfClass(pointer))
128+
allocation: allocation,
129+
name: process.context.name(type: pointer, mangled: genericMetadataOptions.mangled) ?? "<unknown>",
130+
isArrayOfClass: process.context.isArrayOfClass(pointer),
131+
garbage: garbage,
132+
backtrace: currentBacktrace)
133+
} // generics
134+
135+
// Update summary
136+
generics.forEach { metadata in
137+
if let allocation = metadata.allocation {
138+
let name = metadata.name
139+
if metadataSummary.keys.contains(name) {
140+
metadataSummary[name]!.totalSize += allocation.size
141+
metadataSummary[name]!.processes.insert(process.processName)
142+
} else {
143+
metadataSummary[name] = MetadataSummary(totalSize: allocation.size,
144+
processes: Set([process.processName]))
145+
}
146+
}
52147
}
53148

54-
let stacks: [swift_reflection_ptr_t:[swift_reflection_ptr_t]]? =
55-
backtraceOptions.style == nil
56-
? nil
57-
: try process.context.allocationStacks
58-
59-
var errorneousMetadata: [(ptr: swift_reflection_ptr_t, name: String)] = []
60-
61-
print("Address", "Allocation", "Size", "Offset", "isArrayOfClass", "Name", separator: "\t")
62-
generics.forEach {
63-
print("\(hex: $0.ptr)", terminator: "\t")
64-
if let allocation = $0.allocation, let offset = $0.offset {
65-
print("\(hex: allocation.ptr)\t\(allocation.size)\t\(offset)", terminator: "\t")
66-
} else {
67-
if (swift_reflection_ownsAddressStrict(process.context, UInt($0.ptr))) == 0 {
68-
errorneousMetadata.append((ptr: $0.ptr, name: $0.name))
69-
}
70-
print("???\t??\t???", terminator: "\t")
149+
if genericMetadataOptions.json {
150+
let processMetadata = ProcessMetadata(name: process.processName,
151+
pid: process.processIdentifier as! ProcessIdentifier,
152+
metadata: generics)
153+
allProcesses.append(processMetadata)
154+
} else if !genericMetadataOptions.summary {
155+
try dumpText(process: process, generics: generics)
71156
}
72-
print($0.isArrayOfClass, terminator: "\t")
73-
print($0.name)
74-
if let style = backtraceOptions.style, let allocation = $0.allocation {
75-
if let stack = stacks?[allocation.ptr] {
76-
print(backtrace(stack, style: style, process.symbolicate))
77-
} else {
78-
print(" No stacktrace available")
79-
}
157+
} // inspect
158+
159+
if genericMetadataOptions.json {
160+
if genericMetadataOptions.summary {
161+
try dumpJson(of: metadataSummary)
162+
} else {
163+
try dumpJson(of: allProcesses)
80164
}
81-
}
165+
} else if genericMetadataOptions.summary {
166+
try dumpTextSummary(of: metadataSummary)
167+
}
168+
}
82169

83-
if errorneousMetadata.count > 0 {
84-
print("Error: The following metadata was not found in any DATA or AUTH segments, may be garbage.")
85-
errorneousMetadata.forEach {
86-
print("\(hex: $0.ptr)\t\($0.name)")
170+
private func dumpText(process: any RemoteProcess, generics: [Metadata]) throws {
171+
var errorneousMetadata: [(ptr: swift_reflection_ptr_t, name: String)] = []
172+
var output = try Output(genericMetadataOptions.outputFile)
173+
print("\(process.processName)(\(process.processIdentifier)):\n", to: &output)
174+
print("Address", "Allocation", "Size", "Offset", "isArrayOfClass", "Name", separator: "\t", to: &output)
175+
generics.forEach {
176+
print("\(hex: $0.ptr)", terminator: "\t", to: &output)
177+
if let allocation = $0.allocation, let offset = $0.offset {
178+
print("\(hex: allocation.ptr)\t\(allocation.size)\t\(offset)", terminator: "\t", to: &output)
179+
} else {
180+
if $0.garbage {
181+
errorneousMetadata.append((ptr: $0.ptr, name: $0.name))
87182
}
183+
print("???\t??\t???", terminator: "\t", to: &output)
88184
}
185+
print($0.isArrayOfClass, terminator: "\t", to: &output)
186+
print($0.name, to: &output)
187+
if let _ = backtraceOptions.style, let _ = $0.allocation {
188+
print($0.backtrace ?? " No stacktrace available", to: &output)
189+
}
190+
}
191+
192+
if errorneousMetadata.count > 0 {
193+
print("Warning: The following metadata was not found in any DATA or AUTH segments, may be garbage.", to: &output)
194+
errorneousMetadata.forEach {
195+
print("\(hex: $0.ptr)\t\($0.name)", to: &output)
196+
}
197+
}
198+
print("", to: &output)
199+
}
89200

201+
private func dumpJson(of: (any Encodable)) throws {
202+
let encoder = JSONEncoder()
203+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
204+
let data = try encoder.encode(of)
205+
let jsonOutput = String(data: data, encoding: .utf8)!
206+
if let outputFile = genericMetadataOptions.outputFile {
207+
try jsonOutput.write(toFile: outputFile, atomically: true, encoding: .utf8)
208+
} else {
209+
print(jsonOutput)
210+
}
211+
}
212+
213+
private func dumpTextSummary(of: [String: MetadataSummary]) throws {
214+
var output = try Output(genericMetadataOptions.outputFile)
215+
print("Size", "Owners", "Name", separator: "\t", to: &output)
216+
var totalSize = 0
217+
var unknownSize = 0
218+
of.sorted { first, second in
219+
first.value.processes.count > second.value.processes.count
220+
}
221+
.forEach { summary in
222+
totalSize += summary.value.totalSize
223+
if summary.key == "<unknown>" {
224+
unknownSize += summary.value.totalSize
225+
}
226+
print(summary.value.totalSize, summary.value.processes.count, summary.key,
227+
separator: "\t", to: &output)
90228
}
229+
print("\nTotal size:\t\(totalSize / 1024) KiB", to: &output)
230+
print("Unknown size:\t\(unknownSize / 1024) KiB", to: &output)
91231
}
92232
}

tools/swift-inspect/Sources/swift-inspect/Process.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,62 @@ private func refersToSelf(_ str: String) -> Bool {
4343
// No match.
4444
return false
4545
}
46+
47+
internal func getRemoteProcess(processId: ProcessIdentifier,
48+
options: UniversalOptions) -> (any RemoteProcess)? {
49+
return DarwinRemoteProcess(processId: processId,
50+
forkCorpse: options.forkCorpse)
51+
}
52+
53+
internal func getProcessName(processId: ProcessIdentifier) -> String? {
54+
var info = proc_bsdinfo()
55+
let bsdinfoSize = Int32(MemoryLayout<proc_bsdinfo>.stride)
56+
let size = proc_pidinfo(processId, PROC_PIDTBSDINFO, 0, &info, bsdinfoSize)
57+
if (size != bsdinfoSize) {
58+
return nil
59+
}
60+
let processName = withUnsafeBytes(of: info.pbi_name) { buffer in
61+
let nonnullBuffer = buffer.prefix { $0 != 0 }
62+
return String(decoding: nonnullBuffer, as: UTF8.self)
63+
}
64+
return processName
65+
}
66+
67+
internal func getAllProcesses(options: UniversalOptions) -> [ProcessIdentifier]? {
68+
var ProcessIdentifiers = [ProcessIdentifier]()
69+
let kinfo_stride = MemoryLayout<kinfo_proc>.stride
70+
var bufferSize: Int = 0
71+
var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL]
72+
73+
guard sysctl(&name, u_int(name.count), nil, &bufferSize, nil, 0) == 0 else {
74+
return nil
75+
}
76+
let count = bufferSize / kinfo_stride
77+
var buffer = Array(repeating: kinfo_proc(), count: count)
78+
guard sysctl(&name, u_int(name.count), &buffer, &bufferSize, nil, 0) == 0 else {
79+
return nil
80+
}
81+
let newCount = bufferSize / kinfo_stride
82+
if count > newCount {
83+
buffer.dropLast(count - newCount)
84+
}
85+
let sorted = buffer.sorted { first, second in
86+
first.kp_proc.p_pid > second.kp_proc.p_pid
87+
}
88+
let myPid = getpid()
89+
for kinfo in sorted {
90+
let pid = kinfo.kp_proc.p_pid
91+
if pid <= 1 {
92+
break
93+
}
94+
if pid == myPid { // skip self
95+
continue
96+
}
97+
ProcessIdentifiers.append(pid)
98+
}
99+
return ProcessIdentifiers
100+
}
101+
46102
#elseif os(Windows)
47103
import WinSDK
48104

@@ -81,6 +137,12 @@ internal func process(matching: String) -> ProcessIdentifier? {
81137

82138
return matches.first?.0
83139
}
140+
141+
internal func getRemoteProcess(processId: ProcessIdentifier,
142+
options: UniversalOptions) -> (any RemoteProcess)? {
143+
return WindowsRemoteProcess(processId: processId)
144+
}
145+
84146
#else
85147
#error("Unsupported platform")
86148
#endif

0 commit comments

Comments
 (0)