Files
luma/Sources/LumaCore/Models/ITraceDecoder.swift
Ole André Vadla Ravnås 82c22cf427 core: Route LumaCore prints through engine events
Add an .engine RuntimeEvent source so host-tier issues
(devices, packages, attach, spawn-gating, instruments,
hookpacks, repl, itrace, editor) flow into the existing
event stream instead of stdout. HookPackLibrary takes a
callback for decode errors, and ITraceDecoder returns
collected panic strings so ProcessNode can yield them as
engine events.
2026-05-16 03:14:37 +02:00

610 lines
21 KiB
Swift

import Foundation
public struct DecodedITrace {
public let registerNames: [String]
public var entries: [TraceEntry]
public let blockBytes: [UInt64: Data]
public let functionCalls: [TraceFunctionCall]
public let registerStates: [RegisterState]
public init(
registerNames: [String],
entries: [TraceEntry],
blockBytes: [UInt64: Data],
functionCalls: [TraceFunctionCall],
registerStates: [RegisterState]
) {
self.registerNames = registerNames
self.entries = entries
self.blockBytes = blockBytes
self.functionCalls = functionCalls
self.registerStates = registerStates
}
}
public struct RegisterState {
public let values: [Int: UInt64]
public let changed: Set<Int>
public init(values: [Int: UInt64], changed: Set<Int>) {
self.values = values
self.changed = changed
}
}
public struct TraceFunctionCall {
public let functionName: String
public let startIndex: Int
public let endIndex: Int
public var entryCount: Int { endIndex - startIndex }
public var shortName: String {
if let bang = functionName.firstIndex(of: "!") {
return String(functionName[functionName.index(after: bang)...])
}
return functionName
}
public init(functionName: String, startIndex: Int, endIndex: Int) {
self.functionName = functionName
self.startIndex = startIndex
self.endIndex = endIndex
}
}
public struct TraceEntry {
public let blockAddress: UInt64
public let blockSize: Int
public var blockName: String
public let registerWrites: [RegisterWrite]
public init(blockAddress: UInt64, blockSize: Int, blockName: String, registerWrites: [RegisterWrite]) {
self.blockAddress = blockAddress
self.blockSize = blockSize
self.blockName = blockName
self.registerWrites = registerWrites
}
}
public struct RegisterWrite {
public let blockOffset: Int
public let registerName: String
public let registerIndex: Int
public let value: UInt64
public init(blockOffset: Int, registerName: String, registerIndex: Int, value: UInt64) {
self.blockOffset = blockOffset
self.registerName = registerName
self.registerIndex = registerIndex
self.value = value
}
}
public struct ITraceMetadata: Codable {
public let hookId: String
public let callIndex: Int
public let hookTarget: String?
public let prologueBytes: String?
public let regSpecs: [RegisterSpec]
public var blocks: [BlockSpec]
public struct RegisterSpec: Codable {
public let name: String
public let size: Int
}
public struct BlockSpec: Codable {
public var name: String
public let address: String
public let size: Int
public let bytes: String?
public let module: ModuleRef?
public let writes: [[Int]]
public struct ModuleRef: Codable {
public let path: String
public let base: String
}
public init(name: String, address: String, size: Int, bytes: String?, module: ModuleRef?, writes: [[Int]]) {
self.name = name
self.address = address
self.size = size
self.bytes = bytes
self.module = module
self.writes = writes
}
}
}
public enum ITraceDecoder {
public static func decode(traceData: Data, metadataJSON: Data) throws -> DecodedITrace {
let metadata = try JSONDecoder().decode(ITraceMetadata.self, from: metadataJSON)
let registerNames = metadata.regSpecs.map(\.name)
let blocksByAddress = Dictionary(
uniqueKeysWithValues: metadata.blocks.compactMap { block -> (UInt64, ITraceMetadata.BlockSpec)? in
guard let addr = parseHexAddress(block.address) else { return nil }
return (addr, block)
}
)
var blockBytesMap: [UInt64: Data] = [:]
for block in metadata.blocks {
guard let addr = parseHexAddress(block.address),
let hex = block.bytes, !hex.isEmpty,
let data = hexToData(hex)
else { continue }
blockBytesMap[addr] = data
}
var entries: [TraceEntry] = []
var offset = 0
let bytes = [UInt8](traceData)
while offset + 8 <= bytes.count {
let blockAddress = readUInt64(bytes, at: offset)
offset += 8
guard let block = blocksByAddress[blockAddress] else {
break
}
var writes: [RegisterWrite] = []
for pair in block.writes {
guard pair.count == 2 else { continue }
let blockOffset = pair[0]
let registerIndex = pair[1]
guard registerIndex < metadata.regSpecs.count else {
offset += 8
continue
}
let spec = metadata.regSpecs[registerIndex]
let valueSize = spec.size
guard offset + valueSize <= bytes.count else { break }
let value: UInt64
if valueSize <= 8 {
value = readUIntN(bytes, at: offset, size: valueSize)
} else {
value = readUInt64(bytes, at: offset)
}
offset += valueSize
writes.append(RegisterWrite(
blockOffset: blockOffset,
registerName: spec.name,
registerIndex: registerIndex,
value: value
))
}
entries.append(TraceEntry(
blockAddress: blockAddress,
blockSize: block.size,
blockName: block.name,
registerWrites: writes
))
}
let functionCalls = groupIntoFunctionCalls(entries)
let registerStates = computeRegisterStates(entries)
return DecodedITrace(
registerNames: registerNames,
entries: entries,
blockBytes: blockBytesMap,
functionCalls: functionCalls,
registerStates: registerStates
)
}
// MARK: - Binary Buffer Parsing (frida-itrace v5)
public static func parseRawBuffer(
_ rawData: Data,
hookTarget: String?,
prologueBytes: String?
) -> (traceData: Data, metadataJSON: Data, panics: [String]) {
let bytes = [UInt8](rawData)
var offset = 0
var regSpecs: [[String: Any]] = []
var blocks: [[String: Any]] = []
var traceRecords = Data()
var blockRecordSizes: [UInt64: Int] = [:]
var panics: [String] = []
while offset + 8 <= bytes.count {
let sentinel = readUInt64(bytes, at: offset)
if sentinel != 0 {
guard let recordSize = blockRecordSizes[sentinel] else {
break
}
guard offset + recordSize <= bytes.count else { break }
traceRecords.append(contentsOf: bytes[offset..<(offset + recordSize)])
offset += recordSize
} else {
guard offset + 16 <= bytes.count else { break }
let eventType = readUInt32(bytes, at: offset + 8)
let payloadSize = Int(readUInt32(bytes, at: offset + 12))
let payloadStart = offset + 16
guard payloadStart + payloadSize <= bytes.count else { break }
switch eventType {
case 1:
let block = parseCompileEvent(bytes, at: payloadStart, size: payloadSize)
if let addr = block["address"] as? UInt64,
let recordSize = block["record_size"] as? Int
{
blockRecordSizes[addr] = recordSize
}
blocks.append(serializeBlockForJSON(block))
case 2:
regSpecs = parseStartEvent(bytes, at: payloadStart, size: payloadSize)
case 3:
break
case 4:
let msg = String(bytes: bytes[payloadStart..<(payloadStart + payloadSize)], encoding: .utf8) ?? "unknown"
panics.append(msg)
default:
break
}
offset = payloadStart + payloadSize
}
}
var metadata: [String: Any] = [
"hookId": "",
"callIndex": 0,
"regSpecs": regSpecs,
"blocks": blocks,
]
if let hookTarget { metadata["hookTarget"] = hookTarget }
if let prologueBytes { metadata["prologueBytes"] = prologueBytes }
let metadataJSON = (try? JSONSerialization.data(withJSONObject: metadata)) ?? Data()
return (traceData: traceRecords, metadataJSON: metadataJSON, panics: panics)
}
private static func parseCompileEvent(_ bytes: [UInt8], at start: Int, size: Int) -> [String: Any] {
var o = start
let blockAddress = readUInt64(bytes, at: o); o += 8
let blockSize = readUInt32(bytes, at: o); o += 4
let recordSize = readUInt32(bytes, at: o); o += 4
let numWrites = Int(readUInt16(bytes, at: o)); o += 2
let nameSize = Int(readUInt16(bytes, at: o)); o += 2
_ = readUInt64(bytes, at: o); o += 8
_ = readUInt32(bytes, at: o); o += 4
let moduleBase = readUInt64(bytes, at: o); o += 8
let modulePathSize = Int(readUInt16(bytes, at: o)); o += 2
_ = readUInt16(bytes, at: o); o += 2
var writes: [[Int]] = []
for _ in 0..<numWrites {
let blockOffset = Int(readUInt32(bytes, at: o)); o += 4
let regIndex = Int(readUInt32(bytes, at: o)); o += 4
writes.append([blockOffset, regIndex])
}
let name: String
if nameSize > 0, o + nameSize <= bytes.count {
name = String(bytes: bytes[o..<(o + nameSize)], encoding: .utf8) ?? ""
o += nameSize
} else {
name = String(format: "0x%llx", blockAddress)
}
var modulePath: String?
if modulePathSize > 0, o + modulePathSize <= bytes.count {
modulePath = String(bytes: bytes[o..<(o + modulePathSize)], encoding: .utf8)
o += modulePathSize
}
let bSize = Int(blockSize)
var codeHex = ""
if bSize > 0, o + bSize <= bytes.count {
codeHex = Data(bytes[o..<(o + bSize)]).map { String(format: "%02x", $0) }.joined()
o += bSize
}
var result: [String: Any] = [
"address": blockAddress,
"record_size": Int(recordSize),
"name": name,
"size": bSize,
"writes": writes,
"bytes": codeHex,
]
if moduleBase != 0 {
result["module"] = [
"path": modulePath ?? "",
"base": String(format: "0x%llx", moduleBase),
]
}
return result
}
private static func serializeBlockForJSON(_ block: [String: Any]) -> [String: Any] {
var json = block
if let addr = json["address"] as? UInt64 {
json["address"] = String(format: "0x%llx", addr)
}
json.removeValue(forKey: "record_size")
return json
}
private static func parseStartEvent(_ bytes: [UInt8], at start: Int, size: Int) -> [[String: Any]] {
var o = start
let numRegs = Int(readUInt32(bytes, at: o)); o += 4
_ = readUInt32(bytes, at: o); o += 4
var specs: [[String: Any]] = []
for _ in 0..<numRegs {
guard o + 8 <= bytes.count else { break }
let nameLen = Int(bytes[o]); o += 1
let nameBytes = bytes[o..<(o + min(nameLen, 6))]
let name = String(bytes: nameBytes, encoding: .utf8) ?? ""
o += 6
let regSize = Int(bytes[o]); o += 1
specs.append(["name": name, "size": regSize])
}
return specs
}
private static func readUInt32(_ bytes: [UInt8], at offset: Int) -> UInt32 {
var value: UInt32 = 0
for i in 0..<4 {
value |= UInt32(bytes[offset + i]) << (i * 8)
}
return value
}
private static func readUInt16(_ bytes: [UInt8], at offset: Int) -> UInt16 {
UInt16(bytes[offset]) | (UInt16(bytes[offset + 1]) << 8)
}
public static func cleanupAfterCapture(
traceData: inout Data,
metadataJSON: inout Data
) {
guard var metadata = try? JSONDecoder().decode(ITraceMetadata.self, from: metadataJSON) else { return }
let hookTarget = metadata.hookTarget.flatMap { parseHexAddress($0) }
let prologueData = metadata.prologueBytes.flatMap { hexToData($0) }
let meaningfulAddresses = Set(
metadata.blocks
.filter { $0.name.contains("!") }
.compactMap { parseHexAddress($0.address) }
)
guard !meaningfulAddresses.isEmpty else { return }
let rawTrace = try? decode(traceData: traceData, metadataJSON: metadataJSON)
guard let rawTrace, !rawTrace.entries.isEmpty else { return }
let firstMeaningfulIdx = rawTrace.entries.firstIndex { $0.blockName.contains("!") }
let lastMeaningfulIdx = rawTrace.entries.lastIndex { $0.blockName.contains("!") }
guard let firstMeaningfulIdx, let lastMeaningfulIdx else { return }
var cleanEntries = Array(rawTrace.entries[firstMeaningfulIdx...lastMeaningfulIdx])
if let hookTarget, let prologueData,
firstMeaningfulIdx > 0
{
let firstEntry = cleanEntries[0]
let overwrittenSize = Int(firstEntry.blockAddress - hookTarget)
if overwrittenSize > 0, overwrittenSize <= prologueData.count {
var restoreWrites: [RegisterWrite] = []
if firstMeaningfulIdx >= 2 {
let restoreEntry = rawTrace.entries[firstMeaningfulIdx - 2]
restoreWrites = deduplicateAndSort(Array(restoreEntry.registerWrites.dropFirst()))
.map { RegisterWrite(blockOffset: 0, registerName: $0.registerName, registerIndex: $0.registerIndex, value: $0.value) }
}
let trampolineEntry = rawTrace.entries[firstMeaningfulIdx - 1]
var trampolineWrites = Array(trampolineEntry.registerWrites.dropFirst())
if trampolineWrites.last?.registerName == "x16" {
trampolineWrites.removeLast()
}
trampolineWrites = deduplicateAndSort(trampolineWrites)
let shiftedWrites = Array(firstEntry.registerWrites.dropFirst()).map { write in
RegisterWrite(
blockOffset: write.blockOffset + overwrittenSize,
registerName: write.registerName,
registerIndex: write.registerIndex,
value: write.value
)
}
let mergedWrites = restoreWrites + trampolineWrites + shiftedWrites
var mergedBytesData = Data(prologueData.prefix(overwrittenSize))
if let realHex = metadata.blocks.first(where: { parseHexAddress($0.address) == firstEntry.blockAddress })?.bytes,
let realBytes = hexToData(realHex)
{
mergedBytesData.append(realBytes)
}
let bangIdx = firstEntry.blockName.firstIndex(of: "!")!
let moduleName = firstEntry.blockName[...firstEntry.blockName.index(before: bangIdx)]
let origOffset = parseHexAddress(
String(firstEntry.blockName[firstEntry.blockName.index(after: bangIdx)...]))!
let mergedName = "\(moduleName)!0x\(String(origOffset - UInt64(overwrittenSize), radix: 16))"
let mergedBlockSpec = ITraceMetadata.BlockSpec(
name: mergedName,
address: String(format: "0x%llx", hookTarget),
size: mergedBytesData.count,
bytes: dataToHex(mergedBytesData),
module: metadata.blocks.first { $0.name.contains("!") }?.module,
writes: mergedWrites.map { [$0.blockOffset, $0.registerIndex] }
)
cleanEntries[0] = TraceEntry(
blockAddress: hookTarget,
blockSize: mergedBytesData.count,
blockName: mergedName,
registerWrites: mergedWrites
)
var cleanBlocks = [mergedBlockSpec]
for block in metadata.blocks where block.name.contains("!") {
let addr = parseHexAddress(block.address)
if addr != firstEntry.blockAddress {
cleanBlocks.append(block)
}
}
metadata.blocks = cleanBlocks
}
} else {
metadata.blocks = metadata.blocks.filter { $0.name.contains("!") }
}
traceData = rebuildTraceData(entries: cleanEntries, metadata: metadata)
if let data = try? JSONEncoder().encode(metadata) {
metadataJSON = data
}
}
private static func rebuildTraceData(entries: [TraceEntry], metadata: ITraceMetadata) -> Data {
var out = Data()
for entry in entries {
var addr = entry.blockAddress
out.append(Data(bytes: &addr, count: 8))
for write in entry.registerWrites {
guard write.registerIndex < metadata.regSpecs.count else { continue }
let size = metadata.regSpecs[write.registerIndex].size
var value = write.value
out.append(Data(bytes: &value, count: min(size, 8)))
if size > 8 {
out.append(Data(repeating: 0, count: size - 8))
}
}
}
return out
}
private static func deduplicateAndSort(_ writes: [RegisterWrite]) -> [RegisterWrite] {
var lastByIndex: [Int: RegisterWrite] = [:]
for w in writes { lastByIndex[w.registerIndex] = w }
return lastByIndex.values.sorted { $0.registerIndex < $1.registerIndex }
}
// MARK: - Helpers
private static func computeRegisterStates(_ entries: [TraceEntry]) -> [RegisterState] {
var current: [Int: UInt64] = [:]
var states: [RegisterState] = []
states.reserveCapacity(entries.count)
for entry in entries {
var changed = Set<Int>()
for write in entry.registerWrites {
current[write.registerIndex] = write.value
changed.insert(write.registerIndex)
}
states.append(RegisterState(values: current, changed: changed))
}
return states
}
private static func groupIntoFunctionCalls(_ entries: [TraceEntry]) -> [TraceFunctionCall] {
guard !entries.isEmpty else { return [] }
var calls: [TraceFunctionCall] = []
var currentSymbol = baseSymbol(of: entries[0].blockName)
var startIndex = 0
for i in 1..<entries.count {
let symbol = baseSymbol(of: entries[i].blockName)
if symbol != currentSymbol {
calls.append(TraceFunctionCall(
functionName: currentSymbol,
startIndex: startIndex,
endIndex: i
))
currentSymbol = symbol
startIndex = i
}
}
calls.append(TraceFunctionCall(
functionName: currentSymbol,
startIndex: startIndex,
endIndex: entries.count
))
return calls
}
public static func baseSymbol(of name: String) -> String {
if let plus = name.firstIndex(of: "+") {
return String(name[..<plus])
}
return name
}
private static func readUInt64(_ bytes: [UInt8], at offset: Int) -> UInt64 {
var value: UInt64 = 0
for i in 0..<8 {
value |= UInt64(bytes[offset + i]) << (i * 8)
}
return value
}
private static func readUIntN(_ bytes: [UInt8], at offset: Int, size: Int) -> UInt64 {
var value: UInt64 = 0
for i in 0..<min(size, 8) {
value |= UInt64(bytes[offset + i]) << (i * 8)
}
return value
}
public static func parseHexAddress(_ s: String) -> UInt64? {
let str = s.hasPrefix("0x") ? String(s.dropFirst(2)) : s
return UInt64(str, radix: 16)
}
private static func hexToData(_ hex: String) -> Data? {
var data = Data()
data.reserveCapacity(hex.count / 2)
var chars = hex.makeIterator()
while let hi = chars.next(), let lo = chars.next() {
guard let byte = UInt8(String([hi, lo]), radix: 16) else { return nil }
data.append(byte)
}
return data
}
private static func dataToHex(_ data: Data) -> String {
data.map { String(format: "%02x", $0) }.joined()
}
}