diff --git a/Package.swift b/Package.swift index a3724596aca..8743fa69bcc 100644 --- a/Package.swift +++ b/Package.swift @@ -319,6 +319,19 @@ let package = Package( ] ), + .target( + /** API for inspecting symbols defined in binaries */ + name: "BinarySymbols", + dependencies: [ + "Basics", + .product(name: "TSCBasic", package: "swift-tools-support-core"), + ], + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] + ), + // MARK: Project Model .target( @@ -580,6 +593,7 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "OrderedCollections", package: "swift-collections"), "Basics", + "BinarySymbols", "Build", "CoreCommands", "PackageGraph", @@ -940,6 +954,10 @@ let package = Package( name: "SwiftFixItTests", dependencies: ["SwiftFixIt", "_InternalTestSupport"] ), + .testTarget( + name: "BinarySymbolsTests", + dependencies: ["BinarySymbols", "_InternalTestSupport"] + ), .testTarget( name: "XCBuildSupportTests", dependencies: ["XCBuildSupport", "_InternalTestSupport", "_InternalBuildTestSupport"], diff --git a/Sources/BinarySymbols/CMakeLists.txt b/Sources/BinarySymbols/CMakeLists.txt new file mode 100644 index 00000000000..a82e40ae8fe --- /dev/null +++ b/Sources/BinarySymbols/CMakeLists.txt @@ -0,0 +1,19 @@ +# This source file is part of the Swift open source project +# +# Copyright (c) 2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +add_library(BinarySymbols STATIC + ClangHostDefaultObjectsDetector.swift + LLVMObjdumpSymbolProvider.swift + ReferencedSymbols.swift + SymbolProvider.swift) +target_link_libraries(BinarySymbols PUBLIC + Basics) + +# NOTE(compnerd) workaround for CMake not setting up include flags yet +set_target_properties(BinarySymbols PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/BinarySymbols/ClangHostDefaultObjectsDetector.swift b/Sources/BinarySymbols/ClangHostDefaultObjectsDetector.swift new file mode 100644 index 00000000000..4e777de23e8 --- /dev/null +++ b/Sources/BinarySymbols/ClangHostDefaultObjectsDetector.swift @@ -0,0 +1,90 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import Basics +import Foundation + +import protocol TSCBasic.WritableByteStream + +package func detectDefaultObjects( + clang: AbsolutePath, fileSystem: any FileSystem, hostTriple: Triple +) async throws -> [AbsolutePath] { + let clangProcess = AsyncProcess(args: clang.pathString, "-###", "-x", "c", "-") + let stdinStream = try clangProcess.launch() + stdinStream.write( + #""" + #include + int main(int argc, char *argv[]) { + printf("Hello world!\n") + return 0; + } + """# + ) + stdinStream.flush() + try stdinStream.close() + let clangResult = try await clangProcess.waitUntilExit() + guard case .terminated(let status) = clangResult.exitStatus, + status == 0 + else { + throw StringError("Couldn't run clang on sample hello world program") + } + let commandsStrings = try clangResult.utf8stderrOutput().split(whereSeparator: \.isNewline) + + let commands = commandsStrings.map { $0.split(whereSeparator: \.isWhitespace) } + guard let linkerCommand = commands.last(where: { $0.first?.contains("ld") == true }) else { + throw StringError("Couldn't find default link command") + } + + // TODO: This logic doesn't support Darwin and Windows based, c.f. https://github.com/swiftlang/swift-package-manager/issues/8753 + let libraryExtensions = [hostTriple.staticLibraryExtension, hostTriple.dynamicLibraryExtension] + var objects: Set = [] + var searchPaths: [AbsolutePath] = [] + + var linkerArguments = linkerCommand.dropFirst().map { + $0.replacingOccurrences(of: "\"", with: "") + } + + if hostTriple.isLinux() { + // Some platform still separate those out... + linkerArguments.append(contentsOf: ["-lm", "-lpthread", "-ldl"]) + } + + for argument in linkerArguments { + if argument.hasPrefix("-L") { + searchPaths.append(try AbsolutePath(validating: String(argument.dropFirst(2)))) + } else if argument.hasPrefix("-l") && !argument.hasSuffix("lto_library") { + let libraryName = argument.dropFirst(2) + let potentialLibraries = searchPaths.flatMap { path in + if libraryName == "gcc_s" && hostTriple.isLinux() { + // Try and pick this up first as libgcc_s tends to be either this or a GNU ld script that pulls this in. + return [path.appending("libgcc_s.so.1")] + } else { + return libraryExtensions.map { ext in path.appending("\(hostTriple.dynamicLibraryPrefix)\(libraryName)\(ext)") } + } + } + + guard let library = potentialLibraries.first(where: { fileSystem.isFile($0) }) else { + throw StringError("Couldn't find library: \(libraryName)") + } + + objects.insert(library) + } else if try argument.hasSuffix(".o") + && fileSystem.isFile(AbsolutePath(validating: argument)) + { + objects.insert(try AbsolutePath(validating: argument)) + } else if let dotIndex = argument.firstIndex(of: "."), + libraryExtensions.first(where: { argument[dotIndex...].contains($0) }) != nil + { + objects.insert(try AbsolutePath(validating: argument)) + } + } + + return objects.compactMap { $0 } +} diff --git a/Sources/BinarySymbols/LLVMObjdumpSymbolProvider.swift b/Sources/BinarySymbols/LLVMObjdumpSymbolProvider.swift new file mode 100644 index 00000000000..70db3c39e82 --- /dev/null +++ b/Sources/BinarySymbols/LLVMObjdumpSymbolProvider.swift @@ -0,0 +1,137 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import Basics +import RegexBuilder + +package struct LLVMObjdumpSymbolProvider: SymbolProvider { + private let objdumpPath: AbsolutePath + + package init(objdumpPath: AbsolutePath) { + self.objdumpPath = objdumpPath + } + + package func symbols(for binary: AbsolutePath, symbols: inout ReferencedSymbols, recordUndefined: Bool = true) async throws { + let objdumpProcess = AsyncProcess(args: objdumpPath.pathString, "-t", "-T", binary.pathString) + try objdumpProcess.launch() + let result = try await objdumpProcess.waitUntilExit() + guard case .terminated(let status) = result.exitStatus, + status == 0 else { + throw InternalError("Unable to run llvm-objdump") + } + + try parse(output: try result.utf8Output(), symbols: &symbols, recordUndefined: recordUndefined) + } + + package func parse(output: String, symbols: inout ReferencedSymbols, recordUndefined: Bool = true) throws { + let visibility = Reference() + let weakLinkage = Reference() + let section = Reference() + let name = Reference() + let symbolLineRegex = Regex { + Anchor.startOfLine + Repeat(CharacterClass.hexDigit, count: 16) // The address of the symbol + CharacterClass.whitespace + Capture(as: visibility) { + ChoiceOf { + "l" + "g" + "u" + "!" + " " + } + } + Capture(as: weakLinkage) { // Whether the symbol is weak or strong + ChoiceOf { + "w" + " " + } + } + ChoiceOf { + "C" + " " + } + ChoiceOf { + "W" + " " + } + ChoiceOf { + "I" + "i" + " " + } + ChoiceOf { + "D" + "d" + " " + } + ChoiceOf { + "F" + "f" + "O" + " " + } + OneOrMore{ + .whitespace + } + Capture(as: section) { // The section the symbol appears in + ZeroOrMore { + .whitespace.inverted + } + } + ZeroOrMore { + .anyNonNewline + } + CharacterClass.whitespace + Capture(as: name) { // The name of symbol + OneOrMore { + .whitespace.inverted + } + } + Anchor.endOfLine + } + for line in output.split(whereSeparator: \.isNewline) { + guard let match = try symbolLineRegex.wholeMatch(in: line) else { + // This isn't a symbol definition line + continue + } + + switch match[section] { + case "*UND*": + guard recordUndefined else { + continue + } + // Weak symbols are optional + if match[weakLinkage] != "w" { + symbols.addUndefined(String(match[name])) + } + default: + symbols.addDefined(String(match[name])) + } + } + } + + private func name(line: Substring) -> Substring? { + guard let lastspace = line.lastIndex(where: \.isWhitespace) else { return nil } + return line[line.index(after: lastspace)...] + } + + private func section(line: Substring) throws -> Substring { + guard line.count > 25 else { + throw InternalError("Unable to run llvm-objdump") + } + let sectionStart = line.index(line.startIndex, offsetBy: 25) + guard let sectionEnd = line[sectionStart...].firstIndex(where: \.isWhitespace) else { + throw InternalError("Unable to run llvm-objdump") + } + return line[sectionStart.. + package private(set) var undefined: Set + + package init() { + self.defined = [] + self.undefined = [] + } + + mutating func addUndefined(_ name: String) { + guard !self.defined.contains(name) else { + return + } + self.undefined.insert(name) + } + + mutating func addDefined(_ name: String) { + self.defined.insert(name) + self.undefined.remove(name) + } +} diff --git a/Sources/BinarySymbols/SymbolProvider.swift b/Sources/BinarySymbols/SymbolProvider.swift new file mode 100644 index 00000000000..29f4ee98c26 --- /dev/null +++ b/Sources/BinarySymbols/SymbolProvider.swift @@ -0,0 +1,21 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import Basics + +package protocol SymbolProvider { + func symbols(for: AbsolutePath, symbols: inout ReferencedSymbols, recordUndefined: Bool) async throws +} + +extension SymbolProvider { + package func symbols(for binary: AbsolutePath, symbols: inout ReferencedSymbols) async throws { + try await self.symbols(for: binary, symbols: &symbols, recordUndefined: true) + } +} diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index ec8a339aa33..809eb98067e 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -10,6 +10,7 @@ add_compile_definitions(USE_IMPL_ONLY_IMPORTS) add_subdirectory(_AsyncFileSystem) add_subdirectory(Basics) +add_subdirectory(BinarySymbols) add_subdirectory(Build) add_subdirectory(Commands) add_subdirectory(CompilerPluginSupport) diff --git a/Sources/Commands/CMakeLists.txt b/Sources/Commands/CMakeLists.txt index 2bcf5b44d0f..c62c7424ed2 100644 --- a/Sources/Commands/CMakeLists.txt +++ b/Sources/Commands/CMakeLists.txt @@ -8,12 +8,13 @@ add_library(Commands PackageCommands/AddDependency.swift - PackageCommands/AddProduct.swift + PackageCommands/AddProduct.swift PackageCommands/AddTarget.swift PackageCommands/AddTargetDependency.swift PackageCommands/AddSetting.swift PackageCommands/APIDiff.swift PackageCommands/ArchiveSource.swift + PackageCommands/AuditBinaryArtifact.swift PackageCommands/CompletionCommand.swift PackageCommands/ComputeChecksum.swift PackageCommands/Config.swift @@ -59,6 +60,7 @@ target_link_libraries(Commands PUBLIC SwiftCollections::OrderedCollections ArgumentParser Basics + BinarySymbols Build CoreCommands LLBuildManifest diff --git a/Sources/Commands/PackageCommands/AuditBinaryArtifact.swift b/Sources/Commands/PackageCommands/AuditBinaryArtifact.swift new file mode 100644 index 00000000000..0b5119fe733 --- /dev/null +++ b/Sources/Commands/PackageCommands/AuditBinaryArtifact.swift @@ -0,0 +1,136 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Basics +import BinarySymbols +import CoreCommands +import Foundation +import PackageModel +import SPMBuildCore +import Workspace + +import struct TSCBasic.StringError + +struct AuditBinaryArtifact: AsyncSwiftCommand { + static let configuration = CommandConfiguration( + commandName: "experimental-audit-binary-artifact", + abstract: "Audit a static library binary artifact for undefined symbols." + ) + + @OptionGroup(visibility: .hidden) + var globalOptions: GlobalOptions + + @Argument(help: "The absolute or relative path to the binary artifact.") + var path: AbsolutePath + + func run(_ swiftCommandState: SwiftCommandState) async throws { + let hostToolchain = try swiftCommandState.getHostToolchain() + let clang = try hostToolchain.getClangCompiler() + let objdump = try hostToolchain.getLLVMObjdump() + let hostTriple = try Triple.getHostTriple( + usingSwiftCompiler: hostToolchain.swiftCompilerPath) + let fileSystem = swiftCommandState.fileSystem + + guard !(hostTriple.isDarwin() || hostTriple.isWindows()) else { + throw StringError( + "experimental-audit-binary-artifact is not supported on Darwin and Windows platforms." + ) + } + + var hostDefaultSymbols = ReferencedSymbols() + let symbolProvider = LLVMObjdumpSymbolProvider(objdumpPath: objdump) + for binary in try await detectDefaultObjects( + clang: clang, fileSystem: fileSystem, hostTriple: hostTriple) + { + try await symbolProvider.symbols( + for: binary, symbols: &hostDefaultSymbols, recordUndefined: false) + } + + let extractedArtifact = try await extractArtifact( + fileSystem: fileSystem, scratchDirectory: swiftCommandState.scratchDirectory) + + guard + let artifactKind = try Workspace.BinaryArtifactsManager.deriveBinaryArtifactKind( + fileSystem: fileSystem, + path: extractedArtifact, + observabilityScope: swiftCommandState.observabilityScope + ) + else { + throw StringError("Invalid binary artifact provided at \(path)") + } + + let module = BinaryModule( + name: path.basenameWithoutExt, kind: artifactKind, path: extractedArtifact, + origin: .local) + for library in try module.parseLibraryArtifactArchives( + for: hostTriple, fileSystem: fileSystem) + { + var symbols = hostDefaultSymbols + try await symbolProvider.symbols(for: library.libraryPath, symbols: &symbols) + + guard symbols.undefined.isEmpty else { + print( + "Invalid artifact binary \(library.libraryPath.pathString), found undefined symbols:" + ) + for name in symbols.undefined { + print("- \(name)") + } + throw ExitCode(1) + } + } + + print( + "Artifact is safe to use on the platforms runtime compatible with triple: \(hostTriple.tripleString)" + ) + } + + private func extractArtifact(fileSystem: any FileSystem, scratchDirectory: AbsolutePath) + async throws -> AbsolutePath + { + let archiver = UniversalArchiver(fileSystem) + + guard let lastPathComponent = path.components.last, + archiver.isFileSupported(lastPathComponent) + else { + let supportedExtensionList = archiver.supportedExtensions.joined(separator: ", ") + throw StringError( + "unexpected file type; supported extensions are: \(supportedExtensionList)") + } + + // Ensure that the path with the accepted extension is a file. + guard fileSystem.isFile(path) else { + throw StringError("file not found at path: \(path.pathString)") + } + + let archiveDirectory = scratchDirectory.appending( + components: "artifact-auditing", + path.basenameWithoutExt, UUID().uuidString + ) + try fileSystem.forceCreateDirectory(at: archiveDirectory) + + try await archiver.extract(from: path, to: archiveDirectory) + + let artifacts = try fileSystem.getDirectoryContents(archiveDirectory) + .map { archiveDirectory.appending(component: $0) } + .filter { + fileSystem.isDirectory($0) + && $0.extension == BinaryModule.Kind.artifactsArchive(types: []).fileExtension + } + + guard artifacts.count == 1 else { + throw StringError("Could not find an artifact bundle in the archive") + } + + return artifacts.first! + } +} diff --git a/Sources/Commands/PackageCommands/SwiftPackageCommand.swift b/Sources/Commands/PackageCommands/SwiftPackageCommand.swift index 06bfbf67111..da253850404 100644 --- a/Sources/Commands/PackageCommands/SwiftPackageCommand.swift +++ b/Sources/Commands/PackageCommands/SwiftPackageCommand.swift @@ -38,6 +38,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand { AddTarget.self, AddTargetDependency.self, AddSetting.self, + AuditBinaryArtifact.self, Clean.self, PurgeCache.self, Reset.self, @@ -49,7 +50,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand { Install.self, Uninstall.self, - + APIDiff.self, DeprecatedAPIDiff.self, DumpSymbolGraph.self, diff --git a/Sources/PackageModel/UserToolchain.swift b/Sources/PackageModel/UserToolchain.swift index 1ff61309ddd..c0924657a48 100644 --- a/Sources/PackageModel/UserToolchain.swift +++ b/Sources/PackageModel/UserToolchain.swift @@ -429,6 +429,15 @@ public final class UserToolchain: Toolchain { ) } + /// Returns the path to llvm-objdump tool. + package func getLLVMObjdump() throws -> AbsolutePath { + try UserToolchain.getTool( + "llvm-objdump", + binDirectories: [self.swiftCompilerPath.parentDirectory], + fileSystem: self.fileSystem + ) + } + public func getSwiftAPIDigester() throws -> AbsolutePath { if let envValue = UserToolchain.lookup( variable: "SWIFT_API_DIGESTER", diff --git a/Sources/Workspace/Workspace+BinaryArtifacts.swift b/Sources/Workspace/Workspace+BinaryArtifacts.swift index b1f045dffb1..308c3e7d43f 100644 --- a/Sources/Workspace/Workspace+BinaryArtifacts.swift +++ b/Sources/Workspace/Workspace+BinaryArtifacts.swift @@ -770,7 +770,7 @@ extension Workspace.BinaryArtifactsManager { return results } - private static func deriveBinaryArtifactKind( + package static func deriveBinaryArtifactKind( fileSystem: FileSystem, path: AbsolutePath, observabilityScope: ObservabilityScope diff --git a/Tests/BinarySymbolsTests/LLVMObjdumpSymbolProviderTests.swift b/Tests/BinarySymbolsTests/LLVMObjdumpSymbolProviderTests.swift new file mode 100644 index 00000000000..3bd20328824 --- /dev/null +++ b/Tests/BinarySymbolsTests/LLVMObjdumpSymbolProviderTests.swift @@ -0,0 +1,54 @@ +import Testing +import BinarySymbols +import Basics + +@Suite +struct LLVMObjdumpSymbolProviderTests { + private func getSymbols(_ dump: String) throws -> ReferencedSymbols { + var symbols = ReferencedSymbols() + // Placeholder executable path since we won't actually run it + try LLVMObjdumpSymbolProvider(objdumpPath: AbsolutePath.root).parse(output: dump, symbols: &symbols) + return symbols + } + + @Test + func ignoresHeaderLines() throws { + let output = try getSymbols( + """ + + /usr/lib/aarch64-linux-gnu/libc.so.6: file format elf64-littleaarch64 + + SYMBOL TABLE: + + DYNAMIC SYMBOL TABLE: + """ + ) + + #expect(output.defined.isEmpty) + #expect(output.undefined.isEmpty) + } + + @Test + func detectsDefinedSymbol() throws { + let output = try getSymbols("00000000000e0618 g DF .text 0000000000000018 GLIBC_2.17 __ppoll_chk") + + #expect(output.defined.contains("__ppoll_chk")) + #expect(output.undefined.isEmpty) + } + + @Test + func detectsUndefinedSymbol() throws { + let output = try getSymbols("0000000000000000 *UND* 0000000000000000 calloc") + + #expect(output.defined.isEmpty) + #expect(output.undefined.contains("calloc")) + } + + @Test + func treatsCommonSymbolsAsDefined() throws { + let output = try getSymbols("0000000000000004 O *COM* 0000000000000004 __libc_enable_secure_decided") + + #expect(output.defined.contains("__libc_enable_secure_decided")) + #expect(output.undefined.isEmpty) + } +}