diff --git a/Docs/generation.md b/Docs/generation.md index 6449df9..e6df7ae 100644 --- a/Docs/generation.md +++ b/Docs/generation.md @@ -145,14 +145,15 @@ valid entry. Both preserve outer-table order and validate the outer table and each nonempty inner member table before decoding; an empty inner list does not require an otherwise unused entry size. Regular method, property, and ivar tables use the same finite count and byte budgets, checked arithmetic, and -complete-range validation. Loaded class, protocol, and category root tables -validate both the complete pointer table and each referenced layout before -decoding. One malformed relative list, regular member-table entry, loaded-root -entry, or loaded class/category relationship preserves its readable siblings -and produces a typed degradation; unloaded relative lists are skipped without -warning. These guarantees cover structural table, list, and layout ranges; they -do not validate referenced C strings, loaded class RW-extension arrays, or -file-backed root-section tables. Once that target is published, +complete-range validation. File-backed and loaded class, protocol, and category +root tables validate both the complete pointer table and each referenced layout +before decoding. File-backed roots also validate raw section/segment coordinates +and map cache offsets to their actual subcache backing. One malformed relative +list, regular member-table entry, root entry, or loaded class/category +relationship preserves its readable siblings and produces a typed degradation; +unloaded relative lists are skipped without warning. These guarantees cover +structural table, list, and layout ranges; they do not validate referenced C +strings or loaded class RW-extension arrays. Once that target is published, PrivateHeaderKit reports the precise owner and degradation as an `objc-metadata-warning` and persists the warning in `generation.sqlite`. A bounded diagnostics report records when additional warnings were omitted, so diff --git a/Package.resolved b/Package.resolved index a5f3afe..c9c2268 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "bf9bc82de8b74cbd9b5723f3edfea31631e1790a92b45368c79b146d9e3fe49b", + "originHash" : "2c8b98637386e53b99342869664c9fbadf31a1d488329018a54eb9de7ae90553", "pins" : [ { "identity" : "associatedobject", @@ -59,7 +59,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/lynnswap/MachOObjCSection.git", "state" : { - "revision" : "41a1652d95c34bbb2fdf32ba4b7f5cd9d3ba20ba" + "revision" : "8f0ff76f02c0865422a72662177b5e687e43522d" } }, { @@ -67,7 +67,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/lynnswap/MachOSwiftSection.git", "state" : { - "revision" : "3344bd04e6c0819b17df126bea57bae1bbed3022" + "revision" : "06ed57ab39fc24956f9079beeb407fffa30d50f4" } }, { diff --git a/Package.swift b/Package.swift index 7ff494b..a0d2693 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( ), .package( url: "https://github.com/lynnswap/MachOObjCSection.git", - revision: "41a1652d95c34bbb2fdf32ba4b7f5cd9d3ba20ba" + revision: "8f0ff76f02c0865422a72662177b5e687e43522d" ), .package( url: "https://github.com/MxIris-Reverse-Engineering/swift-objc-dump.git", @@ -45,7 +45,7 @@ let package = Package( ), .package( url: "https://github.com/lynnswap/MachOSwiftSection.git", - revision: "3344bd04e6c0819b17df126bea57bae1bbed3022" + revision: "06ed57ab39fc24956f9079beeb407fffa30d50f4" ), .package( url: "https://github.com/MxIris-Reverse-Engineering/swift-demangling", diff --git a/Sources/PrivateHeaderKitRawDumpCore/PrivateHeaderKitRawDumpMain.swift b/Sources/PrivateHeaderKitRawDumpCore/PrivateHeaderKitRawDumpMain.swift index a78de1d..2343df9 100644 --- a/Sources/PrivateHeaderKitRawDumpCore/PrivateHeaderKitRawDumpMain.swift +++ b/Sources/PrivateHeaderKitRawDumpCore/PrivateHeaderKitRawDumpMain.swift @@ -1159,7 +1159,9 @@ private func dumpObjC( var metadata: CollectedObjCMetadata switch machO { case .file(let file): - metadata = collectObjCMetadata(from: file.objc, in: machO, options: options) + let roots = file.objc.readRoots() + options.objcDiagnostics.append(contentsOf: roots.tableDiagnostics) + metadata = collectObjCMetadata(from: roots, in: machO, options: options) case .loaded(let image): let roots = image.objc.readRoots() options.objcDiagnostics.append(contentsOf: roots.tableDiagnostics) @@ -1216,7 +1218,7 @@ private protocol RawDumpObjCRoots { var categories2_32: [ObjCCategory32]? { get } } -extension MachOFile.ObjectiveC: RawDumpObjCRoots {} +extension ObjCFileRootReadResult: RawDumpObjCRoots {} extension ObjCImageRootReadResult: RawDumpObjCRoots {} @discardableResult diff --git a/Sources/PrivateHeaderKitRawDumpCore/RawDumpObjCDiagnostics.swift b/Sources/PrivateHeaderKitRawDumpCore/RawDumpObjCDiagnostics.swift index 2b917b7..bbfeee3 100644 --- a/Sources/PrivateHeaderKitRawDumpCore/RawDumpObjCDiagnostics.swift +++ b/Sources/PrivateHeaderKitRawDumpCore/RawDumpObjCDiagnostics.swift @@ -279,6 +279,11 @@ private func rawDumpMetadataTableDiagnostic( case let .member(subject, kind): ownerDescription = subjectDescription(subject) metadataDescription = "\(metadataTableMemberKindDescription(kind)) metadata table" + case let .fileRoot(section, pointerWidth): + ownerDescription = "Objective-C file-backed roots" + metadataDescription = + "\(pointerWidthDescription(pointerWidth))" + + " \(rootSectionDescription(section)) root table" case let .loadedImageRoot(section, pointerWidth): ownerDescription = "Objective-C loaded-image roots" metadataDescription = @@ -632,6 +637,17 @@ private func failureDescription( "list pointer could not be rebased" case .missingListBackingData: "list pointer has no readable backing data" + case let .invalidFileRootOffset(sectionAddress, sharedRegionStart): + "root section address \(sectionAddress) cannot be represented relative" + + " to shared-region start \(sharedRegionStart)" + case .missingFileRootBackingData(let sectionAddress): + "root section at address \(sectionAddress) has no readable backing data" + case .unresolvedFileRootPointer(let rawValue): + "root pointer value \(rawValue) could not be rebased" + case .invalidReferencedFileOffset(let offset): + "resolved root offset \(offset) cannot be represented" + case .missingReferencedFileBackingData(let logicalOffset): + "resolved root at logical offset \(logicalOffset) has no readable backing data" case let .unreadableFileHeader(offset, byteCount): "file header at offset \(offset) is not readable for \(byteCount) bytes" case .invalidEntryLogicalOffset: diff --git a/Tests/PrivateHeaderKitHelperProtocolTests/PrivateHeaderKitHelperProtocolTests.swift b/Tests/PrivateHeaderKitHelperProtocolTests/PrivateHeaderKitHelperProtocolTests.swift index 5013fcc..7443cc2 100644 --- a/Tests/PrivateHeaderKitHelperProtocolTests/PrivateHeaderKitHelperProtocolTests.swift +++ b/Tests/PrivateHeaderKitHelperProtocolTests/PrivateHeaderKitHelperProtocolTests.swift @@ -292,7 +292,7 @@ struct PrivateHeaderKitHelperProtocolTests { let state = try #require(pin["state"] as? [String: Any]) #expect(pin["location"] as? String == "https://github.com/lynnswap/MachOObjCSection.git") - #expect(state["revision"] as? String == "41a1652d95c34bbb2fdf32ba4b7f5cd9d3ba20ba") + #expect(state["revision"] as? String == "8f0ff76f02c0865422a72662177b5e687e43522d") #expect(state["version"] == nil) let swiftSectionPin = try #require( @@ -307,7 +307,7 @@ struct PrivateHeaderKitHelperProtocolTests { ) #expect( swiftSectionState["revision"] as? String - == "3344bd04e6c0819b17df126bea57bae1bbed3022" + == "06ed57ab39fc24956f9079beeb407fffa30d50f4" ) #expect(swiftSectionState["version"] == nil) diff --git a/Tests/PrivateHeaderKitRawDumpTests/ObjCMemberListDiagnosticsTests.swift b/Tests/PrivateHeaderKitRawDumpTests/ObjCMemberListDiagnosticsTests.swift index c649bc6..307a794 100644 --- a/Tests/PrivateHeaderKitRawDumpTests/ObjCMemberListDiagnosticsTests.swift +++ b/Tests/PrivateHeaderKitRawDumpTests/ObjCMemberListDiagnosticsTests.swift @@ -6,6 +6,27 @@ import Testing @testable import PrivateHeaderKitRawDumpCore struct ObjCMemberListDiagnosticsTests { + @Test func fileRootDiagnosticsUseTheExistingBoundedMemberChannel() throws { + let fixture = try InvalidFileRootFixture() + let result = fixture.machO.objc.readRoots() + + #expect(result.classes64?.isEmpty == true) + #expect(result.tableDiagnostics.count == 1) + + let accumulator = RawDumpObjCDiagnosticsAccumulator() + accumulator.append(contentsOf: result.tableDiagnostics) + + let report = accumulator.report + #expect(report.diagnostics.count == 1) + #expect(report.omittedDiagnosticCount == 0) + #expect(report.diagnostics.first?.owner == "Objective-C file-backed roots") + #expect( + report.diagnostics.first?.degradation + == "64-bit class-list root table could not be fully read:" + + " section size 9 is not a multiple of pointer size 8" + ) + } + @Test func regularTableDiagnosticsUseTheExistingBoundedMemberChannel() throws { let fixture = try InvalidMemberListFixture( memberDiagnosticCount: 0, @@ -223,6 +244,64 @@ struct ObjCMemberListDiagnosticsTests { } } +private final class InvalidFileRootFixture { + let machO: MachOFile + private let url: URL + + init() throws { + let fileSize = 0x1000 + let rootOffset = 0x400 + let vmAddress: UInt64 = 0x1_0000_0000 + var data = Data(count: fileSize) + + var header = mach_header_64() + header.magic = UInt32(MH_MAGIC_64) + header.cputype = CPU_TYPE_ARM64 + header.cpusubtype = CPU_SUBTYPE_ARM64_ALL + header.filetype = UInt32(MH_DYLIB) + header.ncmds = 1 + header.sizeofcmds = UInt32( + MemoryLayout.size + MemoryLayout.size + ) + data.storeValue(header, at: 0) + + var segment = segment_command_64() + segment.cmd = UInt32(LC_SEGMENT_64) + segment.cmdsize = header.sizeofcmds + segment.vmaddr = vmAddress + segment.vmsize = UInt64(fileSize) + segment.fileoff = 0 + segment.filesize = UInt64(fileSize) + segment.maxprot = VM_PROT_READ + segment.initprot = VM_PROT_READ + segment.nsects = 1 + segment.writeName("__DATA") + data.storeValue(segment, at: MemoryLayout.size) + + var section = section_64() + section.addr = vmAddress + UInt64(rootOffset) + section.size = 9 + section.offset = UInt32(rootOffset) + section.align = 3 + section.writeSectionName("__objc_classlist", segmentName: "__DATA") + data.storeValue( + section, + at: MemoryLayout.size + + MemoryLayout.size + ) + + url = FileManager.default.temporaryDirectory.appendingPathComponent( + "PrivateHeaderKitInvalidFileRoot-\(UUID().uuidString)" + ) + try data.write(to: url) + machO = try MachOFile(url: url) + } + + deinit { + try? FileManager.default.removeItem(at: url) + } +} + private final class InvalidMemberListFixture { let machO: MachOFile let objcClass: ObjCClass64 @@ -449,6 +528,25 @@ private struct RawIvar64 { let size: UInt32 } +private extension segment_command_64 { + mutating func writeName(_ value: String) { + Swift.withUnsafeMutableBytes(of: &segname) { destination in + destination.copyBytes(from: value.utf8) + } + } +} + +private extension section_64 { + mutating func writeSectionName(_ value: String, segmentName: String) { + Swift.withUnsafeMutableBytes(of: §name) { destination in + destination.copyBytes(from: value.utf8) + } + Swift.withUnsafeMutableBytes(of: &segname) { destination in + destination.copyBytes(from: segmentName.utf8) + } + } +} + private extension Data { func loadValue(_ type: Value.Type, at offset: Int) -> Value { self[offset..<(offset + MemoryLayout.size)].withUnsafeBytes {