Skip to content

Commit 42f8604

Browse files
authored
Extend ManifestLoader with the ability to restrict imports (#5888)
This allows passing a minimal list of allowed modules and the minimum tools-version for applying the restricton to `ManifestLoader` and `WorkspaceConfiguration` (which passes it to the manifest loader). This allows clients of libSwiftPM to choose to be more restrictive about the modules package manifests are allowed to import. rdar://92879696
1 parent 0ade4d8 commit 42f8604

File tree

9 files changed

+183
-19
lines changed

9 files changed

+183
-19
lines changed

Sources/Basics/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ add_library(Basics
2020
FileSystem+Extensions.swift
2121
HTTPClient+URLSession.swift
2222
HTTPClient.swift
23+
ImportScanning.swift
2324
JSON+Extensions.swift
2425
JSONDecoder+Extensions.swift
2526
Netrc.swift

Sources/Basics/ImportScanning.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Dispatch
14+
15+
import class Foundation.JSONDecoder
16+
import struct TSCBasic.AbsolutePath
17+
import class TSCBasic.Process
18+
19+
private let defaultImports = ["Swift", "SwiftOnoneSupport", "_Concurrency", "_StringProcessing"]
20+
21+
private struct Imports: Decodable {
22+
let imports: [String]
23+
}
24+
25+
public protocol ImportScanner {
26+
func scanImports(_ filePathToScan: AbsolutePath, callbackQueue: DispatchQueue, completion: @escaping (Result<[String], Error>) -> Void)
27+
}
28+
29+
public struct SwiftcImportScanner: ImportScanner {
30+
private let swiftCompilerEnvironment: EnvironmentVariables
31+
private let swiftCompilerFlags: [String]
32+
private let swiftCompilerPath: AbsolutePath
33+
34+
public init(swiftCompilerEnvironment: EnvironmentVariables, swiftCompilerFlags: [String], swiftCompilerPath: AbsolutePath) {
35+
self.swiftCompilerEnvironment = swiftCompilerEnvironment
36+
self.swiftCompilerFlags = swiftCompilerFlags
37+
self.swiftCompilerPath = swiftCompilerPath
38+
}
39+
40+
public func scanImports(_ filePathToScan: AbsolutePath,
41+
callbackQueue: DispatchQueue,
42+
completion: @escaping (Result<[String], Error>) -> Void) {
43+
let cmd = [swiftCompilerPath.pathString,
44+
filePathToScan.pathString,
45+
"-scan-dependencies", "-Xfrontend", "-import-prescan"] + self.swiftCompilerFlags
46+
47+
TSCBasic.Process.popen(arguments: cmd, environment: self.swiftCompilerEnvironment, queue: callbackQueue) { result in
48+
dispatchPrecondition(condition: .onQueue(callbackQueue))
49+
50+
do {
51+
let stdout = try result.get().utf8Output()
52+
let imports = try JSONDecoder.makeWithDefaults().decode(Imports.self, from: stdout).imports
53+
.filter { !defaultImports.contains($0) }
54+
55+
callbackQueue.async {
56+
completion(.success(imports))
57+
}
58+
} catch {
59+
callbackQueue.async {
60+
completion(.failure(error))
61+
}
62+
}
63+
}
64+
}
65+
}

Sources/CoreCommands/SwiftTool.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,8 @@ public final class SwiftTool {
378378
additionalFileRules: isXcodeBuildSystemEnabled ? FileRuleDescription.xcbuildFileTypes : FileRuleDescription.swiftpmFileTypes,
379379
sharedDependenciesCacheEnabled: self.options.caching.useDependenciesCache,
380380
fingerprintCheckingMode: self.options.security.fingerprintCheckingMode,
381-
sourceControlToRegistryDependencyTransformation: self.options.resolver.sourceControlToRegistryDependencyTransformation.workspaceConfiguration
381+
sourceControlToRegistryDependencyTransformation: self.options.resolver.sourceControlToRegistryDependencyTransformation.workspaceConfiguration,
382+
restrictImports: .none
382383
),
383384
cancellator: self.cancellator,
384385
initializationWarningHandler: { self.observabilityScope.emit(warning: $0) },
@@ -704,7 +705,8 @@ public final class SwiftTool {
704705
toolchain: self.getHostToolchain(),
705706
isManifestSandboxEnabled: !self.options.security.shouldDisableSandbox,
706707
cacheDir: cachePath,
707-
extraManifestFlags: extraManifestFlags
708+
extraManifestFlags: extraManifestFlags,
709+
restrictImports: nil
708710
)
709711
})
710712
}()

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ public enum ManifestParseError: Swift.Error, Equatable {
2525
// TODO: Test this error.
2626
/// The manifest was successfully loaded by swift interpreter but there were runtime issues.
2727
case runtimeManifestErrors([String])
28+
29+
/// The manifest loader specified import restrictions that the given manifest violated.
30+
case importsRestrictedModules([String])
2831
}
2932

3033
// used to output the errors via the observability system
@@ -37,6 +40,8 @@ extension ManifestParseError: CustomStringConvertible {
3740
return "invalid manifest\n\(error)"
3841
case .runtimeManifestErrors(let errors):
3942
return "invalid manifest (evaluation failed)\n\(errors.joined(separator: "\n"))"
43+
case .importsRestrictedModules(let modules):
44+
return "invalid manifest, imports restricted modules: \(modules.joined(separator: ", "))"
4045
}
4146
}
4247
}
@@ -149,6 +154,7 @@ public final class ManifestLoader: ManifestLoaderProtocol {
149154
private let isManifestSandboxEnabled: Bool
150155
private let delegate: ManifestLoaderDelegate?
151156
private let extraManifestFlags: [String]
157+
private let restrictImports: (startingToolsVersion: ToolsVersion, allowedImports: [String])?
152158

153159
private let databaseCacheDir: AbsolutePath?
154160

@@ -165,13 +171,15 @@ public final class ManifestLoader: ManifestLoaderProtocol {
165171
isManifestSandboxEnabled: Bool = true,
166172
cacheDir: AbsolutePath? = nil,
167173
delegate: ManifestLoaderDelegate? = nil,
168-
extraManifestFlags: [String] = []
174+
extraManifestFlags: [String] = [],
175+
restrictImports: (startingToolsVersion: ToolsVersion, allowedImports: [String])? = .none
169176
) {
170177
self.toolchain = toolchain
171178
self.serializedDiagnostics = serializedDiagnostics
172179
self.isManifestSandboxEnabled = isManifestSandboxEnabled
173180
self.delegate = delegate
174181
self.extraManifestFlags = extraManifestFlags
182+
self.restrictImports = restrictImports
175183

176184
self.databaseCacheDir = try? cacheDir.map(resolveSymlinks)
177185

@@ -426,6 +434,53 @@ public final class ManifestLoader: ManifestLoaderProtocol {
426434
}
427435
}
428436

437+
private func validateImports(
438+
manifestPath: AbsolutePath,
439+
toolsVersion: ToolsVersion,
440+
callbackQueue: DispatchQueue,
441+
completion: @escaping (Result<Void, Error>) -> Void) {
442+
// If there are no import restrictions, we do not need to validate.
443+
guard let restrictImports = restrictImports, toolsVersion >= restrictImports.startingToolsVersion else {
444+
return callbackQueue.async {
445+
completion(.success(()))
446+
}
447+
}
448+
449+
// Allowed are the expected defaults, plus anything allowed by the configured restrictions.
450+
let allowedImports = ["PackageDescription", "Swift", "SwiftOnoneSupport"] + restrictImports.allowedImports
451+
452+
// wrap the completion to free concurrency control semaphore
453+
let completion: (Result<Void, Error>) -> Void = { result in
454+
self.concurrencySemaphore.signal()
455+
callbackQueue.async {
456+
completion(result)
457+
}
458+
}
459+
460+
// we must not block the calling thread (for concurrency control) so nesting this in a queue
461+
self.evaluationQueue.addOperation {
462+
do {
463+
// park the evaluation thread based on the max concurrency allowed
464+
self.concurrencySemaphore.wait()
465+
466+
let importScanner = SwiftcImportScanner(swiftCompilerEnvironment: self.toolchain.swiftCompilerEnvironment,
467+
swiftCompilerFlags: self.extraManifestFlags,
468+
swiftCompilerPath: self.toolchain.swiftCompilerPathForManifests)
469+
470+
importScanner.scanImports(manifestPath, callbackQueue: callbackQueue) { result in
471+
do {
472+
let imports = try result.get().filter { !allowedImports.contains($0) }
473+
guard imports.isEmpty else {
474+
throw ManifestParseError.importsRestrictedModules(imports)
475+
}
476+
} catch {
477+
completion(.failure(error))
478+
}
479+
}
480+
}
481+
}
482+
}
483+
429484
/// Compiler the manifest at the given path and retrieve the JSON.
430485
fileprivate func evaluateManifest(
431486
packageIdentity: PackageIdentity,
@@ -454,17 +509,30 @@ public final class ManifestLoader: ManifestLoaderProtocol {
454509
VFSOverlay.File(name: manifestPath.pathString, externalContents: manifestTempFilePath.pathString)
455510
]).write(to: vfsOverlayTempFilePath, fileSystem: localFileSystem)
456511

457-
try self.evaluateManifest(
458-
at: manifestPath,
459-
vfsOverlayPath: vfsOverlayTempFilePath,
460-
packageIdentity: packageIdentity,
461-
toolsVersion: toolsVersion,
462-
delegateQueue: delegateQueue,
463-
callbackQueue: callbackQueue
464-
) { result in
512+
validateImports(manifestPath: manifestTempFilePath, toolsVersion: toolsVersion, callbackQueue: callbackQueue) { result in
465513
dispatchPrecondition(condition: .onQueue(callbackQueue))
466-
cleanupTempDir(tempDir)
467-
completion(result)
514+
515+
do {
516+
try result.get()
517+
518+
try self.evaluateManifest(
519+
at: manifestPath,
520+
vfsOverlayPath: vfsOverlayTempFilePath,
521+
packageIdentity: packageIdentity,
522+
toolsVersion: toolsVersion,
523+
delegateQueue: delegateQueue,
524+
callbackQueue: callbackQueue
525+
) { result in
526+
dispatchPrecondition(condition: .onQueue(callbackQueue))
527+
cleanupTempDir(tempDir)
528+
completion(result)
529+
}
530+
} catch {
531+
cleanupTempDir(tempDir)
532+
callbackQueue.async {
533+
completion(.failure(error))
534+
}
535+
}
468536
}
469537
}
470538
} catch {

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,8 @@ public final class MockWorkspace {
266266
additionalFileRules: WorkspaceConfiguration.default.additionalFileRules,
267267
sharedDependenciesCacheEnabled: WorkspaceConfiguration.default.sharedDependenciesCacheEnabled,
268268
fingerprintCheckingMode: .strict,
269-
sourceControlToRegistryDependencyTransformation: self.sourceControlToRegistryDependencyTransformation
269+
sourceControlToRegistryDependencyTransformation: self.sourceControlToRegistryDependencyTransformation,
270+
restrictImports: .none
270271
),
271272
customFingerprints: self.fingerprints,
272273
customMirrors: self.mirrors,

Sources/Workspace/Workspace+Configuration.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,9 @@ public struct WorkspaceConfiguration {
663663
/// Whether to create a product for use in the Swift REPL
664664
public var createREPLProduct: Bool
665665

666+
/// Whether or not there should be import restrictions applied when loading manifests
667+
public var restrictImports: (startingToolsVersion: ToolsVersion, allowedImports: [String])?
668+
666669
public init(
667670
skipDependenciesUpdates: Bool,
668671
prefetchBasedOnResolvedFile: Bool,
@@ -671,7 +674,8 @@ public struct WorkspaceConfiguration {
671674
additionalFileRules: [FileRuleDescription],
672675
sharedDependenciesCacheEnabled: Bool,
673676
fingerprintCheckingMode: FingerprintCheckingMode,
674-
sourceControlToRegistryDependencyTransformation: SourceControlToRegistryDependencyTransformation
677+
sourceControlToRegistryDependencyTransformation: SourceControlToRegistryDependencyTransformation,
678+
restrictImports: (startingToolsVersion: ToolsVersion, allowedImports: [String])?
675679
) {
676680
self.skipDependenciesUpdates = skipDependenciesUpdates
677681
self.prefetchBasedOnResolvedFile = prefetchBasedOnResolvedFile
@@ -681,6 +685,7 @@ public struct WorkspaceConfiguration {
681685
self.sharedDependenciesCacheEnabled = sharedDependenciesCacheEnabled
682686
self.fingerprintCheckingMode = fingerprintCheckingMode
683687
self.sourceControlToRegistryDependencyTransformation = sourceControlToRegistryDependencyTransformation
688+
self.restrictImports = restrictImports
684689
}
685690

686691
/// Default instance of WorkspaceConfiguration
@@ -693,7 +698,8 @@ public struct WorkspaceConfiguration {
693698
additionalFileRules: [],
694699
sharedDependenciesCacheEnabled: true,
695700
fingerprintCheckingMode: .strict,
696-
sourceControlToRegistryDependencyTransformation: .disabled
701+
sourceControlToRegistryDependencyTransformation: .disabled,
702+
restrictImports: .none
697703
)
698704
}
699705

Sources/Workspace/Workspace.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,8 @@ public class Workspace {
437437
let location = try Location(forRootPackage: packagePath, fileSystem: fileSystem)
438438
let manifestLoader = ManifestLoader(
439439
toolchain: customHostToolchain,
440-
cacheDir: location.sharedManifestsCacheDirectory
440+
cacheDir: location.sharedManifestsCacheDirectory,
441+
restrictImports: configuration?.restrictImports
441442
)
442443
try self.init(
443444
fileSystem: fileSystem,
@@ -541,7 +542,8 @@ public class Workspace {
541542
let hostToolchain = try customHostToolchain ?? UserToolchain(destination: .hostDestination())
542543
var manifestLoader = customManifestLoader ?? ManifestLoader(
543544
toolchain: hostToolchain,
544-
cacheDir: location.sharedManifestsCacheDirectory
545+
cacheDir: location.sharedManifestsCacheDirectory,
546+
restrictImports: configuration?.restrictImports
545547
)
546548

547549
let configuration = configuration ?? .default

Tests/PackageLoadingTests/PDLoadingTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class PackageDescriptionLoadingTests: XCTestCase, ManifestLoaderDelegate {
5252
_ bytes: ByteString,
5353
toolsVersion: ToolsVersion? = nil,
5454
packageKind: PackageReference.Kind? = nil,
55+
customManifestLoader: ManifestLoader? = nil,
5556
observabilityScope: ObservabilityScope,
5657
file: StaticString = #file,
5758
line: UInt = #line
@@ -73,7 +74,7 @@ class PackageDescriptionLoadingTests: XCTestCase, ManifestLoaderDelegate {
7374
let fileSystem = InMemoryFileSystem()
7475
let manifestPath = packagePath.appending(component: Manifest.filename)
7576
try fileSystem.writeFileContents(manifestPath, bytes: bytes)
76-
let manifest = try manifestLoader.load(
77+
let manifest = try (customManifestLoader ?? manifestLoader).load(
7778
manifestPath: manifestPath,
7879
packageKind: packageKind,
7980
toolsVersion: toolsVersion,

Tests/PackageLoadingTests/PD_5_7_LoadingTests .swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,22 @@ class PackageDescription5_7LoadingTests: PackageDescriptionLoadingTests {
172172
PlatformDescription(name: "driverkit", version: "22.0"),
173173
])
174174
}
175+
176+
func testImportRestrictions() throws {
177+
let content = """
178+
import PackageDescription
179+
import BestModule
180+
let package = Package(name: "Foo")
181+
"""
182+
183+
let observability = ObservabilitySystem.makeForTesting()
184+
let manifestLoader = ManifestLoader(toolchain: try UserToolchain.default, restrictImports: (.v5_7, []))
185+
XCTAssertThrowsError(try loadAndValidateManifest(ByteString(encodingAsUTF8: content), customManifestLoader: manifestLoader, observabilityScope: observability.topScope)) { error in
186+
if case ManifestParseError.importsRestrictedModules(let modules) = error {
187+
XCTAssertEqual(modules, ["BestModule"])
188+
} else {
189+
XCTFail("unexpected error: \(error)")
190+
}
191+
}
192+
}
175193
}

0 commit comments

Comments
 (0)