Skip to content

[6.2] Binary Static Library Artifact auditing tool #8804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: release/6.2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"],
Expand Down
19 changes: 19 additions & 0 deletions Sources/BinarySymbols/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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})
90 changes: 90 additions & 0 deletions Sources/BinarySymbols/ClangHostDefaultObjectsDetector.swift
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
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<AbsolutePath> = []
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 }
}
137 changes: 137 additions & 0 deletions Sources/BinarySymbols/LLVMObjdumpSymbolProvider.swift
Original file line number Diff line number Diff line change
@@ -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<Substring>()
let weakLinkage = Reference<Substring>()
let section = Reference<Substring>()
let name = Reference<Substring>()
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..<sectionEnd]
}
}

31 changes: 31 additions & 0 deletions Sources/BinarySymbols/ReferencedSymbols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
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
*/

package struct ReferencedSymbols {
package private(set) var defined: Set<String>
package private(set) var undefined: Set<String>

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)
}
}
21 changes: 21 additions & 0 deletions Sources/BinarySymbols/SymbolProvider.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions Sources/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion Sources/Commands/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -59,6 +60,7 @@ target_link_libraries(Commands PUBLIC
SwiftCollections::OrderedCollections
ArgumentParser
Basics
BinarySymbols
Build
CoreCommands
LLBuildManifest
Expand Down
Loading