Skip to content

Commit 0963c8d

Browse files
committed
Sandbox for manifests and plugins should block /tmp and instead provide a unique path specified by TMPDIR
The `/tmp` temporary directory is particularly unsafe because it is noisy — various processes with various ownerships write and read files there. So it's better to use the per-user directory returned by `NSTemporaryDirectory()`. We can improve several things here by passing through the result of calling `NSTemporaryDirectory()` in the sandbox instead of `/tmp` and also setting `TMPDIR` in the environment (regardless of whether or not it was set in the inherited one). This will cause the inferior's use of Foundation to respect this directory (the implementation of `NSTemporaryDirectory()` looks at `TMPDIR` on all platforms), and that should also be true for any command line tools it calls, as long as it passes through the environment. If `TMPDIR` is set in SwiftPM's own environment, it will control SwiftPM's own `NSTemporaryDirectory()` will be respected all the way through. As part of this we also take the opportunity to clean up the interface a little. In particular, this change turns the stateless `Sandbox` enum into a `SandboxProfile` struct that's a bit more flexible: - there is a list of path rules allowing a mixed order of `writable`, `readonly`, and `noaccess` rules - 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 - there is separate control for whether or not to allow network access The new `SandboxProfile` is also a bit more ergonomic — as a struct, it can be copied, mutated, and carried around in a property before being used. Having the sandbox profile be a struct that generates the platform specifics as needed also could also provide a place with which to associate any cached/compiled representation for profiles that are largely static, for any platform that supports that. 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, no matter what tools version the package specifies. In this commit, the call sites have been adjusted so that they use the modified SandboxProfile API, but they still construct the profiles on-the-fly as before. A future change will 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://91575256
1 parent 52b4c24 commit 0963c8d

File tree

9 files changed

+255
-199
lines changed

9 files changed

+255
-199
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 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
@@ -24,7 +24,7 @@ add_library(Basics
2424
JSONDecoder+Extensions.swift
2525
Netrc.swift
2626
Observability.swift
27-
Sandbox.swift
27+
SandboxProfile.swift
2828
String+Extensions.swift
2929
Triple+Extensions.swift
3030
SwiftVersion.swift

Sources/Basics/Sandbox.swift

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

Sources/Basics/SandboxProfile.swift

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

Sources/Build/BuildOperation.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,10 +452,23 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
452452
// TODO: We need to also use any working directory, but that support isn't yet available on all platforms at a lower level.
453453
var commandLine = [command.configuration.executable.pathString] + command.configuration.arguments
454454
if !self.disableSandboxForPluginCommands {
455-
commandLine = Sandbox.apply(command: commandLine, strictness: .writableTemporaryDirectory, writableDirectories: [pluginResult.pluginOutputDirectory])
455+
// Allow access to the plugin's output directory as well as to the local temporary directory.
456+
let sandboxProfile = SandboxProfile([
457+
.writable(pluginResult.pluginOutputDirectory),
458+
.writable(localFileSystem.tempDirectory)])
459+
commandLine = sandboxProfile.apply(to: commandLine)
456460
}
457-
let processResult = try Process.popen(arguments: commandLine, environment: command.configuration.environment)
461+
462+
// Pass `TMPDIR` in the environment, in addition to anything the plugin specifies, in case we have an
463+
// override in our own environment.
464+
var environment = command.configuration.environment
465+
environment["TMPDIR"] = localFileSystem.tempDirectory.pathString
466+
467+
// Run the command and wait for it to finish.
468+
let processResult = try Process.popen(arguments: commandLine, environment: environment)
458469
let output = try processResult.utf8Output() + processResult.utf8stderrOutput()
470+
471+
// Throw an error if it failed.
459472
if processResult.exitStatus != .terminated(code: 0) {
460473
throw StringError("failed: \(command)\n\n\(output)")
461474
}

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,16 +619,26 @@ extension LLBuildManifestBuilder {
619619
let uniquedName = ([execPath.pathString] + command.configuration.arguments).joined(separator: "|")
620620
let displayName = command.configuration.displayName ?? execPath.basename
621621
var commandLine = [execPath.pathString] + command.configuration.arguments
622+
623+
// Apply the sandbox, if appropriate.
622624
if !self.disableSandboxForPluginCommands {
623-
commandLine = Sandbox.apply(command: commandLine, strictness: .writableTemporaryDirectory, writableDirectories: [result.pluginOutputDirectory])
625+
let sandboxProfile = SandboxProfile([
626+
.writable(result.pluginOutputDirectory),
627+
.writable(localFileSystem.tempDirectory)])
628+
commandLine = sandboxProfile.apply(to: commandLine)
624629
}
630+
// Pass `TMPDIR` in the environment, in addition to anything the plugin specifies, in case we have an
631+
// override in our own environment.
632+
var environment = command.configuration.environment
633+
environment["TMPDIR"] = localFileSystem.tempDirectory.pathString
634+
625635
manifest.addShellCmd(
626636
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
627637
description: displayName,
628638
inputs: [.file(execPath)] + command.inputFiles.map{ .file($0) },
629639
outputs: command.outputFiles.map{ .file($0) },
630640
arguments: commandLine,
631-
environment: command.configuration.environment,
641+
environment: environment,
632642
workingDirectory: command.configuration.workingDirectory?.pathString)
633643
}
634644
}

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -827,18 +827,41 @@ public final class ManifestLoader: ManifestLoaderProtocol {
827827
// This provides some safety against arbitrary code execution when parsing manifest files.
828828
// We only allow the permissions which are absolutely necessary.
829829
if self.isManifestSandboxEnabled {
830-
let cacheDirectories = [self.databaseCacheDir, moduleCachePath].compactMap{ $0 }
831-
let strictness: Sandbox.Strictness = toolsVersion < .v5_3 ? .manifest_pre_53 : .default
832-
cmd = Sandbox.apply(command: cmd, strictness: strictness, writableDirectories: cacheDirectories)
830+
var sandbox = SandboxProfile()
831+
832+
// Allow writing inside the temporary directory.
833+
sandbox.pathAccessRules.append(.writable(localFileSystem.tempDirectory))
834+
835+
// Allow writing in the database cache directory, if we have one.
836+
if let databaseCacheDir = self.databaseCacheDir {
837+
sandbox.pathAccessRules.append(.writable(databaseCacheDir))
838+
}
839+
840+
// Allow writing in the module cache path, if there is one.
841+
if let moduleCachePath = moduleCachePath {
842+
sandbox.pathAccessRules.append(.writable(moduleCachePath))
843+
}
844+
845+
// But do not allow writing in the directory that contains the manifest, even if it is
846+
// inside one of the otherwise writable directories.
847+
sandbox.pathAccessRules.append(.readonly(manifestPath.parentDirectory))
848+
849+
// Finally apply the sandbox.
850+
cmd = sandbox.apply(to: cmd)
833851
}
834852

835-
// Run the compiled manifest.
853+
// Set up the environment so that the manifest inherits our own environment, but make sure that
854+
// `TMPDIR` is set to whatever the temporary directory is that is allowed in the sandbox.
836855
var environment = ProcessEnv.vars
856+
environment["TMPDIR"] = localFileSystem.tempDirectory.pathString
857+
858+
// On Windows, also make sure that the `Path` is set up correctly in the environment.
837859
#if os(Windows)
838860
let windowsPathComponent = runtimePath.pathString.replacingOccurrences(of: "/", with: "\\")
839861
environment["Path"] = "\(windowsPathComponent);\(environment["Path"] ?? "")"
840862
#endif
841863

864+
// Run the compiled manifest.
842865
let cleanupAfterRunning = cleanupIfError.delay()
843866
Process.popen(arguments: cmd, environment: environment, queue: callbackQueue) { result in
844867
dispatchPrecondition(condition: .onQueue(callbackQueue))

0 commit comments

Comments
 (0)