Skip to content

Commit 692e86a

Browse files
committed
Sandbox blocks output to default plugin output directory when it's under <pkgdir>/.build
The sandbox rules introduced in #3996 made the entire package directory read-only, but that isn't appropriate when `.build` is inside the package directory. This change turns the stateless Sandbox enum into a SandboxProfile struct that can be configured and passed around well before being applied, and it makes the configuration more flexible in several regards: - there is a list of path rules allowing a mixed order of `allow` and `deny` rules - the choice of allowing writing to temporary directories is independent of anything other choice - the defaults are built into the initializer rather than a separate `strictness` parameter — this means that they can be queried once the SandboxProfile has been created and are expressed in terms of the choices available to all sandbox profiles. Having the sandbox profile be a struct that generates the platform specifics as needed also provides a place with which to associate any cached/compiled representation for profiles that are largely static. As cleanup, this commit also removes the pre-SwiftPM 5.3 specialities which were specific to running the package manifest in an interpreter rather than compiling and executing it. This functionality is no longer relevant since it isn't possible to run any manifest in the interpreter. In this commit, the call sites have been adjusted so that they use the modified SandboxProfile API, but still construct the profiles on-the-fly as before. A future change could make them instead be configured at an early point and then applied later when the sandboxed process is actually launched. Another future change could add the sandbox profile to Process as a property so that there is no API assumption that applying a sandbox necessarily involves just modifying the command line. rdar://87417780
1 parent 07e6b9c commit 692e86a

File tree

9 files changed

+235
-177
lines changed

9 files changed

+235
-177
lines changed

Sources/Basics/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This source file is part of the Swift.org open source project
22
#
3-
# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
3+
# Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
44
# Licensed under Apache License v2.0 with Runtime Library Exception
55
#
66
# See http://swift.org/LICENSE.txt for license information
@@ -20,7 +20,7 @@ add_library(Basics
2020
JSON+Extensions.swift
2121
JSONDecoder+Extensions.swift
2222
Observability.swift
23-
Sandbox.swift
23+
SandboxProfile.swift
2424
Triple+Extensions.swift
2525
SwiftVersion.swift
2626
SQLiteBackedCache.swift

Sources/Basics/Sandbox.swift

Lines changed: 0 additions & 133 deletions
This file was deleted.

Sources/Basics/SandboxProfile.swift

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 - 2022 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 Foundation
12+
import TSCBasic
13+
import TSCUtility
14+
15+
/// A sandbox profile representing the desired restrictions. The implementation can vary between platforms.
16+
public struct SandboxProfile {
17+
/// Whether to allow outbound and inbound network access.
18+
public var allowNetwork: Bool
19+
20+
/// Whether to allow writing to system-defined temporary directories. Overridden by any rules in `pathRules`.
21+
public var allowWritingToTemporaryDirectories: Bool
22+
23+
/// An ordered list of path rules, where the last rule to cover a particular path "wins". These will be resolved
24+
/// to absolute paths at the time the profile is applied. They are applied after any of the implicit directories
25+
/// referenced by other sandbox profile settings.
26+
public var pathRules: [PathWritabilityRule]
27+
28+
/// Represents a readonly/writable rule for a path and everything under it.
29+
public enum PathWritabilityRule: Equatable {
30+
case readonly(AbsolutePath)
31+
case writable(AbsolutePath)
32+
}
33+
34+
/// Configures a SandboxProfile for blocking network access and writing to the file system except to specifically
35+
/// permitted locations.
36+
public init(
37+
allowNetwork: Bool = false,
38+
allowWritingToTemporaryDirectories: Bool = false,
39+
pathRules: [PathWritabilityRule] = []
40+
) {
41+
self.allowNetwork = allowNetwork
42+
self.allowWritingToTemporaryDirectories = allowWritingToTemporaryDirectories
43+
self.pathRules = pathRules
44+
}
45+
}
46+
47+
extension SandboxProfile {
48+
/// Applies the sandbox profile to the given command line (if the platform supports it), and returns the modified
49+
/// command line. On platforms that don't support sandboxing, the unmodified command line is returned.
50+
public func apply(to command: [String]) -> [String] {
51+
#if os(macOS)
52+
return ["/usr/bin/sandbox-exec", "-p", self.generateMacOSSandboxProfileString()] + command
53+
#else
54+
// rdar://40235432, rdar://75636874 tracks implementing sandboxes for other platforms.
55+
return command
56+
#endif
57+
}
58+
}
59+
60+
// MARK: - macOS
61+
62+
#if os(macOS)
63+
fileprivate extension SandboxProfile {
64+
/// Private function that generates a Darwin sandbox profile suitable for passing to `sandbox-exec(1)`.
65+
func generateMacOSSandboxProfileString() -> String {
66+
var contents = "(version 1)\n"
67+
68+
// Deny everything by default.
69+
contents += "(deny default)\n"
70+
71+
// Import the system sandbox profile.
72+
contents += "(import \"system.sb\")\n"
73+
74+
// Allow reading any file that isn't protected by TCC or permissions (ideally we'd only allow a specific set
75+
// of readable locations, and can maybe tighten this in the future).
76+
contents += "(allow file-read*)\n"
77+
78+
// Allow operations on subprocesses.
79+
contents += "(allow process*)\n"
80+
81+
// Optionally allow network access (inbound and outbound).
82+
if allowNetwork {
83+
contents += "(system-network)\n"
84+
contents += "(allow network*)\n"
85+
}
86+
87+
// Optionally allow writing to the platform-specific temporary directories.
88+
if allowWritingToTemporaryDirectories {
89+
contents += "(allow file-write*\n"
90+
for path in temporaryDirectories {
91+
contents += " (subpath \(resolveSymlinksAndQuotePath(path)))\n"
92+
}
93+
contents += ")\n"
94+
}
95+
96+
// Apply customized rules for specific file system locations. Everything is readonly by default, so we just
97+
// either allow or deny writing, in order. Later rules have precedence over earlier rules.
98+
for rule in pathRules {
99+
switch rule {
100+
case .readonly(let path):
101+
contents += "(deny file-write* (subpath \(resolveSymlinksAndQuotePath(path))))\n"
102+
case .writable(let path):
103+
contents += "(allow file-write* (subpath \(resolveSymlinksAndQuotePath(path))))\n"
104+
}
105+
}
106+
107+
return contents
108+
}
109+
110+
/// Private helper function that returns the temporary directories on macOS.
111+
var temporaryDirectories: [AbsolutePath] {
112+
return [AbsolutePath("/tmp"), AbsolutePath(NSTemporaryDirectory())]
113+
}
114+
115+
/// Private helper function that resolves an AbsolutePath and returns it as a string quoted for use as a subpath
116+
/// in a .sb sandbox profile.
117+
func resolveSymlinksAndQuotePath(_ path: AbsolutePath) -> String {
118+
return "\"" + resolveSymlinks(path).pathString
119+
.replacingOccurrences(of: "\\", with: "\\\\")
120+
.replacingOccurrences(of: "\"", with: "\\\"")
121+
+ "\""
122+
}
123+
}
124+
#endif

Sources/Build/BuildOperation.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
344344
// TODO: We need to also use any working directory, but that support isn't yet available on all platforms at a lower level.
345345
var commandLine = [command.configuration.executable.pathString] + command.configuration.arguments
346346
if !self.disableSandboxForPluginCommands {
347-
commandLine = Sandbox.apply(command: commandLine, strictness: .writableTemporaryDirectory, writableDirectories: [pluginResult.pluginOutputDirectory])
347+
let sandboxProfile = SandboxProfile(allowWritingToTemporaryDirectories: true, pathRules: [.writable(pluginResult.pluginOutputDirectory)])
348+
commandLine = sandboxProfile.apply(to: commandLine)
348349
}
349350
let processResult = try Process.popen(arguments: commandLine, environment: command.configuration.environment)
350351
let output = try processResult.utf8Output() + processResult.utf8stderrOutput()

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,8 @@ extension LLBuildManifestBuilder {
618618
let displayName = command.configuration.displayName ?? execPath.basename
619619
var commandLine = [execPath.pathString] + command.configuration.arguments
620620
if !self.disableSandboxForPluginCommands {
621-
commandLine = Sandbox.apply(command: commandLine, strictness: .writableTemporaryDirectory, writableDirectories: [result.pluginOutputDirectory])
621+
let sandboxProfile = SandboxProfile(allowWritingToTemporaryDirectories: true, pathRules: [.writable(result.pluginOutputDirectory)])
622+
commandLine = sandboxProfile.apply(to: commandLine)
622623
}
623624
manifest.addShellCmd(
624625
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -899,8 +899,8 @@ public final class ManifestLoader: ManifestLoaderProtocol {
899899
// We only allow the permissions which are absolutely necessary.
900900
if self.isManifestSandboxEnabled {
901901
let cacheDirectories = [self.databaseCacheDir, moduleCachePath].compactMap{ $0 }
902-
let strictness: Sandbox.Strictness = toolsVersion < .v5_3 ? .manifest_pre_53 : .default
903-
cmd = Sandbox.apply(command: cmd, strictness: strictness, writableDirectories: cacheDirectories)
902+
let sandboxProfile = SandboxProfile(pathRules: cacheDirectories.map{ .writable($0) })
903+
cmd = sandboxProfile.apply(to: cmd)
904904
}
905905

906906
// Run the compiled manifest.

Sources/Workspace/DefaultPluginScriptRunner.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,10 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
361361

362362
// Optionally wrap the command in a sandbox, which places some limits on what it can do. In particular, it blocks network access and restricts the paths to which the plugin can make file system changes. It does allow writing to temporary directories.
363363
if self.enableSandbox {
364-
command = Sandbox.apply(command: command, strictness: .writableTemporaryDirectory, writableDirectories: writableDirectories + [self.cacheDir], readOnlyDirectories: readOnlyDirectories)
364+
let sandboxProfile = SandboxProfile(allowWritingToTemporaryDirectories: true, pathRules:
365+
readOnlyDirectories.map{ .readonly($0) } +
366+
writableDirectories.map{ .writable($0) } + [.writable(self.cacheDir)])
367+
command = sandboxProfile.apply(to: command)
365368
}
366369

367370
// Create and configure a Process. We set the working directory to the cache directory, so that relative paths end up there.

0 commit comments

Comments
 (0)