Skip to content

Commit 7c5b0bc

Browse files
Binary Static Library Artifact auditing tool (#8741)
This is an implementation of the auditing tool described in [SE-0482](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0482-swiftpm-static-library-binary-target-non-apple-platforms.md). ### Motivation: As part of SE-0482 and #8639 we introduced the ability to depend on prebuilt static libraries (that expose a C interface) and that don't have any dependencies outside of the C standard library. This PR introduces an auditing tool that checks the ABI of static library artifact bundle and checks if it's compatible with the current host platform. ### Modifications: - Create a new package subcommand that checks a local artifact bundle for unexpected external dependencies. - New internal APIs to inspect the ABI of a binary object (object file, static archive, dynamic library). ### Result: Users will be able to validate that their static library binary artifacts won't cause runtime issues for users.
1 parent b6fc37b commit 7c5b0bc

13 files changed

+522
-3
lines changed

Package.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,19 @@ let package = Package(
338338
]
339339
),
340340

341+
.target(
342+
/** API for inspecting symbols defined in binaries */
343+
name: "BinarySymbols",
344+
dependencies: [
345+
"Basics",
346+
.product(name: "TSCBasic", package: "swift-tools-support-core"),
347+
],
348+
exclude: ["CMakeLists.txt"],
349+
swiftSettings: commonExperimentalFeatures + [
350+
.unsafeFlags(["-static"]),
351+
]
352+
),
353+
341354
// MARK: Project Model
342355

343356
.target(
@@ -598,6 +611,7 @@ let package = Package(
598611
.product(name: "ArgumentParser", package: "swift-argument-parser"),
599612
.product(name: "OrderedCollections", package: "swift-collections"),
600613
"Basics",
614+
"BinarySymbols",
601615
"Build",
602616
"CoreCommands",
603617
"PackageGraph",
@@ -958,6 +972,10 @@ let package = Package(
958972
name: "SwiftFixItTests",
959973
dependencies: ["SwiftFixIt", "_InternalTestSupport"]
960974
),
975+
.testTarget(
976+
name: "BinarySymbolsTests",
977+
dependencies: ["BinarySymbols", "_InternalTestSupport"]
978+
),
961979
.testTarget(
962980
name: "XCBuildSupportTests",
963981
dependencies: ["XCBuildSupport", "_InternalTestSupport", "_InternalBuildTestSupport"],

Sources/BinarySymbols/CMakeLists.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This source file is part of the Swift open source project
2+
#
3+
# Copyright (c) 2025 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See http://swift.org/LICENSE.txt for license information
7+
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
8+
9+
add_library(BinarySymbols STATIC
10+
ClangHostDefaultObjectsDetector.swift
11+
LLVMObjdumpSymbolProvider.swift
12+
ReferencedSymbols.swift
13+
SymbolProvider.swift)
14+
target_link_libraries(BinarySymbols PUBLIC
15+
Basics)
16+
17+
# NOTE(compnerd) workaround for CMake not setting up include flags yet
18+
set_target_properties(BinarySymbols PROPERTIES
19+
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Basics
12+
import Foundation
13+
14+
import protocol TSCBasic.WritableByteStream
15+
16+
package func detectDefaultObjects(
17+
clang: AbsolutePath, fileSystem: any FileSystem, hostTriple: Triple
18+
) async throws -> [AbsolutePath] {
19+
let clangProcess = AsyncProcess(args: clang.pathString, "-###", "-x", "c", "-")
20+
let stdinStream = try clangProcess.launch()
21+
stdinStream.write(
22+
#"""
23+
#include <stdio.h>
24+
int main(int argc, char *argv[]) {
25+
printf("Hello world!\n")
26+
return 0;
27+
}
28+
"""#
29+
)
30+
stdinStream.flush()
31+
try stdinStream.close()
32+
let clangResult = try await clangProcess.waitUntilExit()
33+
guard case .terminated(let status) = clangResult.exitStatus,
34+
status == 0
35+
else {
36+
throw StringError("Couldn't run clang on sample hello world program")
37+
}
38+
let commandsStrings = try clangResult.utf8stderrOutput().split(whereSeparator: \.isNewline)
39+
40+
let commands = commandsStrings.map { $0.split(whereSeparator: \.isWhitespace) }
41+
guard let linkerCommand = commands.last(where: { $0.first?.contains("ld") == true }) else {
42+
throw StringError("Couldn't find default link command")
43+
}
44+
45+
// TODO: This logic doesn't support Darwin and Windows based, c.f. https://github.com/swiftlang/swift-package-manager/issues/8753
46+
let libraryExtensions = [hostTriple.staticLibraryExtension, hostTriple.dynamicLibraryExtension]
47+
var objects: Set<AbsolutePath> = []
48+
var searchPaths: [AbsolutePath] = []
49+
50+
var linkerArguments = linkerCommand.dropFirst().map {
51+
$0.replacingOccurrences(of: "\"", with: "")
52+
}
53+
54+
if hostTriple.isLinux() {
55+
// Some platform still separate those out...
56+
linkerArguments.append(contentsOf: ["-lm", "-lpthread", "-ldl"])
57+
}
58+
59+
for argument in linkerArguments {
60+
if argument.hasPrefix("-L") {
61+
searchPaths.append(try AbsolutePath(validating: String(argument.dropFirst(2))))
62+
} else if argument.hasPrefix("-l") && !argument.hasSuffix("lto_library") {
63+
let libraryName = argument.dropFirst(2)
64+
let potentialLibraries = searchPaths.flatMap { path in
65+
if libraryName == "gcc_s" && hostTriple.isLinux() {
66+
// Try and pick this up first as libgcc_s tends to be either this or a GNU ld script that pulls this in.
67+
return [path.appending("libgcc_s.so.1")]
68+
} else {
69+
return libraryExtensions.map { ext in path.appending("\(hostTriple.dynamicLibraryPrefix)\(libraryName)\(ext)") }
70+
}
71+
}
72+
73+
guard let library = potentialLibraries.first(where: { fileSystem.isFile($0) }) else {
74+
throw StringError("Couldn't find library: \(libraryName)")
75+
}
76+
77+
objects.insert(library)
78+
} else if try argument.hasSuffix(".o")
79+
&& fileSystem.isFile(AbsolutePath(validating: argument))
80+
{
81+
objects.insert(try AbsolutePath(validating: argument))
82+
} else if let dotIndex = argument.firstIndex(of: "."),
83+
libraryExtensions.first(where: { argument[dotIndex...].contains($0) }) != nil
84+
{
85+
objects.insert(try AbsolutePath(validating: argument))
86+
}
87+
}
88+
89+
return objects.compactMap { $0 }
90+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Basics
12+
import RegexBuilder
13+
14+
package struct LLVMObjdumpSymbolProvider: SymbolProvider {
15+
private let objdumpPath: AbsolutePath
16+
17+
package init(objdumpPath: AbsolutePath) {
18+
self.objdumpPath = objdumpPath
19+
}
20+
21+
package func symbols(for binary: AbsolutePath, symbols: inout ReferencedSymbols, recordUndefined: Bool = true) async throws {
22+
let objdumpProcess = AsyncProcess(args: objdumpPath.pathString, "-t", "-T", binary.pathString)
23+
try objdumpProcess.launch()
24+
let result = try await objdumpProcess.waitUntilExit()
25+
guard case .terminated(let status) = result.exitStatus,
26+
status == 0 else {
27+
throw InternalError("Unable to run llvm-objdump")
28+
}
29+
30+
try parse(output: try result.utf8Output(), symbols: &symbols, recordUndefined: recordUndefined)
31+
}
32+
33+
package func parse(output: String, symbols: inout ReferencedSymbols, recordUndefined: Bool = true) throws {
34+
let visibility = Reference<Substring>()
35+
let weakLinkage = Reference<Substring>()
36+
let section = Reference<Substring>()
37+
let name = Reference<Substring>()
38+
let symbolLineRegex = Regex {
39+
Anchor.startOfLine
40+
Repeat(CharacterClass.hexDigit, count: 16) // The address of the symbol
41+
CharacterClass.whitespace
42+
Capture(as: visibility) {
43+
ChoiceOf {
44+
"l"
45+
"g"
46+
"u"
47+
"!"
48+
" "
49+
}
50+
}
51+
Capture(as: weakLinkage) { // Whether the symbol is weak or strong
52+
ChoiceOf {
53+
"w"
54+
" "
55+
}
56+
}
57+
ChoiceOf {
58+
"C"
59+
" "
60+
}
61+
ChoiceOf {
62+
"W"
63+
" "
64+
}
65+
ChoiceOf {
66+
"I"
67+
"i"
68+
" "
69+
}
70+
ChoiceOf {
71+
"D"
72+
"d"
73+
" "
74+
}
75+
ChoiceOf {
76+
"F"
77+
"f"
78+
"O"
79+
" "
80+
}
81+
OneOrMore{
82+
.whitespace
83+
}
84+
Capture(as: section) { // The section the symbol appears in
85+
ZeroOrMore {
86+
.whitespace.inverted
87+
}
88+
}
89+
ZeroOrMore {
90+
.anyNonNewline
91+
}
92+
CharacterClass.whitespace
93+
Capture(as: name) { // The name of symbol
94+
OneOrMore {
95+
.whitespace.inverted
96+
}
97+
}
98+
Anchor.endOfLine
99+
}
100+
for line in output.split(whereSeparator: \.isNewline) {
101+
guard let match = try symbolLineRegex.wholeMatch(in: line) else {
102+
// This isn't a symbol definition line
103+
continue
104+
}
105+
106+
switch match[section] {
107+
case "*UND*":
108+
guard recordUndefined else {
109+
continue
110+
}
111+
// Weak symbols are optional
112+
if match[weakLinkage] != "w" {
113+
symbols.addUndefined(String(match[name]))
114+
}
115+
default:
116+
symbols.addDefined(String(match[name]))
117+
}
118+
}
119+
}
120+
121+
private func name(line: Substring) -> Substring? {
122+
guard let lastspace = line.lastIndex(where: \.isWhitespace) else { return nil }
123+
return line[line.index(after: lastspace)...]
124+
}
125+
126+
private func section(line: Substring) throws -> Substring {
127+
guard line.count > 25 else {
128+
throw InternalError("Unable to run llvm-objdump")
129+
}
130+
let sectionStart = line.index(line.startIndex, offsetBy: 25)
131+
guard let sectionEnd = line[sectionStart...].firstIndex(where: \.isWhitespace) else {
132+
throw InternalError("Unable to run llvm-objdump")
133+
}
134+
return line[sectionStart..<sectionEnd]
135+
}
136+
}
137+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
package struct ReferencedSymbols {
12+
package private(set) var defined: Set<String>
13+
package private(set) var undefined: Set<String>
14+
15+
package init() {
16+
self.defined = []
17+
self.undefined = []
18+
}
19+
20+
mutating func addUndefined(_ name: String) {
21+
guard !self.defined.contains(name) else {
22+
return
23+
}
24+
self.undefined.insert(name)
25+
}
26+
27+
mutating func addDefined(_ name: String) {
28+
self.defined.insert(name)
29+
self.undefined.remove(name)
30+
}
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Basics
12+
13+
package protocol SymbolProvider {
14+
func symbols(for: AbsolutePath, symbols: inout ReferencedSymbols, recordUndefined: Bool) async throws
15+
}
16+
17+
extension SymbolProvider {
18+
package func symbols(for binary: AbsolutePath, symbols: inout ReferencedSymbols) async throws {
19+
try await self.symbols(for: binary, symbols: &symbols, recordUndefined: true)
20+
}
21+
}

Sources/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ add_compile_definitions(USE_IMPL_ONLY_IMPORTS)
1010

1111
add_subdirectory(_AsyncFileSystem)
1212
add_subdirectory(Basics)
13+
add_subdirectory(BinarySymbols)
1314
add_subdirectory(Build)
1415
add_subdirectory(Commands)
1516
add_subdirectory(CompilerPluginSupport)

Sources/Commands/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88

99
add_library(Commands
1010
PackageCommands/AddDependency.swift
11-
PackageCommands/AddProduct.swift
11+
PackageCommands/AddProduct.swift
1212
PackageCommands/AddTarget.swift
1313
PackageCommands/AddTargetDependency.swift
1414
PackageCommands/AddSetting.swift
1515
PackageCommands/APIDiff.swift
1616
PackageCommands/ArchiveSource.swift
17+
PackageCommands/AuditBinaryArtifact.swift
1718
PackageCommands/CompletionCommand.swift
1819
PackageCommands/ComputeChecksum.swift
1920
PackageCommands/Config.swift
@@ -59,6 +60,7 @@ target_link_libraries(Commands PUBLIC
5960
SwiftCollections::OrderedCollections
6061
ArgumentParser
6162
Basics
63+
BinarySymbols
6264
Build
6365
CoreCommands
6466
LLBuildManifest

0 commit comments

Comments
 (0)