Skip to content

Commit 257ad9b

Browse files
Detect system GNU ld script pretending to be system libraries (#9296)
When a system library is shimmed with a GNU ld script, pull in the required libraries when auditing binary artifacts ### Motivation: Basic C runtime libraries appear to sometimes be replaced by a GNU ld script that pulls it some other libraries as a backwards compatibility mechanism on certain Linux distros. This makes it difficult for the binary auditing tool to find all required libraries. The only previously known case of this was libgcc_s so we used to just special case for it, but now that Ubuntu does this for splitting up libm, a more general solution is required. ### Modifications: Add basic support for detecting such GNU ld script and parsing out libraries they pull in. The subset of features supported is consistent with how we have seen libraries being repackaged. ### Result: Binary auditing tool will be more resilient.
1 parent a8d89fe commit 257ad9b

File tree

2 files changed

+64
-10
lines changed

2 files changed

+64
-10
lines changed

Sources/BinarySymbols/ClangHostDefaultObjectsDetector.swift

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import Foundation
1414
import protocol TSCBasic.WritableByteStream
1515

1616
package func detectDefaultObjects(
17-
clang: AbsolutePath, fileSystem: any FileSystem, hostTriple: Triple
17+
clang: AbsolutePath, fileSystem: any FileSystem, hostTriple: Triple,
18+
observabilityScope: ObservabilityScope
1819
) async throws -> [AbsolutePath] {
1920
let clangProcess = AsyncProcess(args: clang.pathString, "-###", "-x", "c", "-")
2021
let stdinStream = try clangProcess.launch()
@@ -56,25 +57,71 @@ package func detectDefaultObjects(
5657
linkerArguments.append(contentsOf: ["-lm", "-lpthread", "-ldl"])
5758
}
5859

59-
for argument in linkerArguments {
60+
func handleArgument(_ argument: String) throws {
6061
if argument.hasPrefix("-L") {
6162
searchPaths.append(try AbsolutePath(validating: String(argument.dropFirst(2))))
6263
} else if argument.hasPrefix("-l") && !argument.hasSuffix("lto_library") {
6364
let libraryName = argument.dropFirst(2)
6465
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)") }
66+
return libraryExtensions.map { ext in
67+
path.appending("\(hostTriple.dynamicLibraryPrefix)\(libraryName)\(ext)")
7068
}
7169
}
7270

7371
guard let library = potentialLibraries.first(where: { fileSystem.isFile($0) }) else {
74-
throw StringError("Couldn't find library: \(libraryName)")
72+
observabilityScope.emit(warning: "Could not find library: \(libraryName)")
73+
return
74+
}
75+
76+
// Try and detect if this a GNU ld linker script.
77+
if let fileContents = try fileSystem.readFileContents(library).validDescription {
78+
let lines = fileContents.split(whereSeparator: \.isNewline)
79+
guard lines.contains(where: { $0.contains("GNU ld script") }) else {
80+
objects.insert(library)
81+
return
82+
}
83+
84+
// If it is try and parse GROUP/INPUT commands as documented in https://sourceware.org/binutils/docs/ld/File-Commands.html
85+
// Empirically it seems like GROUP is the only used such directive for libraries of interest.
86+
// Empirically it looks like packaging linker scripts use spaces around parenthesis which greatly simplifies parsing.
87+
let inputs = lines.filter { $0.hasPrefix("GROUP") || $0.hasPrefix("INPUT") }
88+
let words = inputs.flatMap { $0.split(whereSeparator: \.isWhitespace) }
89+
let newArguments = words.filter {
90+
!["GROUP", "AS_NEEDED", "INPUT"].contains($0) && $0 != "(" && $0 != ")"
91+
}.map(String.init)
92+
93+
for arg in newArguments {
94+
if arg.hasPrefix("-l") {
95+
try handleArgument(arg)
96+
} else {
97+
// First try and locate the file relative to the linker script.
98+
let siblingPath = try AbsolutePath(
99+
validating: arg,
100+
relativeTo: try AbsolutePath(validating: library.dirname))
101+
if fileSystem.isFile(siblingPath) {
102+
try handleArgument(siblingPath.pathString)
103+
} else {
104+
// If this fails the file needs to be resolved relative to the search paths.
105+
guard
106+
let library = searchPaths.map({ $0.appending(arg) }).first(where: {
107+
fileSystem.isFile($0)
108+
})
109+
else {
110+
observabilityScope.emit(
111+
warning:
112+
"Malformed linker script at \(library): found no library named \(arg)"
113+
)
114+
continue
115+
}
116+
try handleArgument(library.pathString)
117+
}
118+
}
119+
}
120+
121+
} else {
122+
objects.insert(library)
75123
}
76124

77-
objects.insert(library)
78125
} else if try argument.hasSuffix(".o")
79126
&& fileSystem.isFile(AbsolutePath(validating: argument))
80127
{
@@ -86,5 +133,9 @@ package func detectDefaultObjects(
86133
}
87134
}
88135

136+
for argument in linkerArguments {
137+
try handleArgument(argument)
138+
}
139+
89140
return objects.compactMap { $0 }
90141
}

Sources/Commands/PackageCommands/AuditBinaryArtifact.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ struct AuditBinaryArtifact: AsyncSwiftCommand {
5050
var hostDefaultSymbols = ReferencedSymbols()
5151
let symbolProvider = LLVMObjdumpSymbolProvider(objdumpPath: objdump)
5252
for binary in try await detectDefaultObjects(
53-
clang: clang, fileSystem: fileSystem, hostTriple: hostTriple)
53+
clang: clang, fileSystem: fileSystem, hostTriple: hostTriple,
54+
observabilityScope:
55+
swiftCommandState.observabilityScope.makeChildScope(
56+
description: "DefaultObjectsDetector"))
5457
{
5558
try await symbolProvider.symbols(
5659
for: binary, symbols: &hostDefaultSymbols, recordUndefined: false)

0 commit comments

Comments
 (0)