Skip to content

Commit ccdc849

Browse files
committed
Sandbox for manifests and plugins should block /tmp and instead provide a unique path set by TMPDIR
The `/tmp` temporary directory is particularly unsafe since it's so noisy (with various processes with various ownerships all writing and reading files there), and so it's better to use the per-user directory returned by `confstr(_CS_DARWIN_USER_TEMP_DIR)`. By code inspection, Foundation's `NSTemporaryDirectory()` respects the `TMPDIR` environment variable both in the open source and Apple implementations (via `confstr()` in the latter). So 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 should cause the inferior's use of Foundation to respect this directory, and that should also be true for any command line tools it calls. If `TMPDIR` is set in SwiftPM's own environment, this 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 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 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, 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 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://91575256
1 parent f9f74f1 commit ccdc849

File tree

9 files changed

+250
-199
lines changed

9 files changed

+250
-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: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
// Convenience initializer to make it easier to construct sandbox profiles with conditional rules.
39+
public init(_ pathAccessRules: PathAccessRule?..., allowNetwork: Bool = false) {
40+
self.init(pathAccessRules.compactMap{ $0 }, allowNetwork: allowNetwork)
41+
}
42+
}
43+
44+
extension SandboxProfile {
45+
/// Applies the sandbox profile to the given command line (if the platform supports it), and returns the modified
46+
/// command line. On platforms that don't support sandboxing, the unmodified command line is returned.
47+
public func apply(to command: [String]) -> [String] {
48+
#if os(macOS)
49+
return ["/usr/bin/sandbox-exec", "-p", self.generateMacOSSandboxProfileString()] + command
50+
#else
51+
// rdar://40235432, rdar://75636874 tracks implementing sandboxes for other platforms.
52+
return command
53+
#endif
54+
}
55+
}
56+
57+
// MARK: - macOS
58+
59+
#if os(macOS)
60+
fileprivate extension SandboxProfile {
61+
/// Private function that generates a Darwin sandbox profile suitable for passing to `sandbox-exec(1)`.
62+
func generateMacOSSandboxProfileString() -> String {
63+
var contents = "(version 1)\n"
64+
65+
// Deny everything by default.
66+
contents += "(deny default)\n"
67+
68+
// Import the system sandbox profile.
69+
contents += "(import \"system.sb\")\n"
70+
71+
// Allow operations on subprocesses.
72+
contents += "(allow process*)\n"
73+
74+
// Optionally allow network access (inbound and outbound).
75+
if allowNetwork {
76+
contents += "(system-network)\n"
77+
contents += "(allow network*)\n"
78+
}
79+
80+
// Allow reading any file that isn't protected by TCC or permissions (ideally we'd only allow a specific set
81+
// of readable locations, and can maybe tighten this in the future).
82+
contents += "(allow file-read*)\n"
83+
84+
// Apply customized rules for specific file system locations. Everything is readonly by default, so we just
85+
// either allow or deny writing, in order. Later rules have precedence over earlier rules.
86+
for rule in pathAccessRules {
87+
switch rule {
88+
case .noaccess(let path):
89+
contents += "(deny file-* (subpath \(resolveSymlinksAndQuotePath(path))))\n"
90+
case .readonly(let path):
91+
contents += "(deny file-write* (subpath \(resolveSymlinksAndQuotePath(path))))\n"
92+
case .writable(let path):
93+
contents += "(allow file-write* (subpath \(resolveSymlinksAndQuotePath(path))))\n"
94+
}
95+
}
96+
return contents
97+
}
98+
99+
/// Private helper function that resolves an AbsolutePath and returns it as a string quoted for use as a subpath
100+
/// in a .sb sandbox profile.
101+
func resolveSymlinksAndQuotePath(_ path: AbsolutePath) -> String {
102+
return "\"" + resolveSymlinks(path).pathString
103+
.replacingOccurrences(of: "\\", with: "\\\\")
104+
.replacingOccurrences(of: "\"", with: "\\\"")
105+
+ "\""
106+
}
107+
}
108+
#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: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -827,18 +827,34 @@ 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+
cmd = SandboxProfile(
831+
// Allow writing in the temporary directory (whether or not it's overridden by TMPDIR).
832+
.writable(localFileSystem.tempDirectory),
833+
834+
// Allow writing in the database cache directory, if we have one.
835+
self.databaseCacheDir.map{ .writable($0) },
836+
837+
// Allow writing in the module cache path, if there is one.
838+
moduleCachePath.map{ .writable($0) },
839+
840+
// But do not allow writing in the directory that contains the manifest, even if it is
841+
// inside one of the writable directories.
842+
.readonly(manifestPath.parentDirectory)
843+
).apply(to: cmd)
833844
}
834845

835-
// Run the compiled manifest.
846+
// Set up the environment so that the manifest inherits our own environment, but make sure that
847+
// `TMPDIR` is set to whatever the temporary directory is that is allowed in the sandbox.
836848
var environment = ProcessEnv.vars
849+
environment["TMPDIR"] = localFileSystem.tempDirectory.pathString
850+
851+
// On Windows, also make sure that the `Path` is set up correctly in the environment.
837852
#if os(Windows)
838853
let windowsPathComponent = runtimePath.pathString.replacingOccurrences(of: "/", with: "\\")
839854
environment["Path"] = "\(windowsPathComponent);\(environment["Path"] ?? "")"
840855
#endif
841856

857+
// Run the compiled manifest.
842858
let cleanupAfterRunning = cleanupIfError.delay()
843859
Process.popen(arguments: cmd, environment: environment, queue: callbackQueue) { result in
844860
dispatchPrecondition(condition: .onQueue(callbackQueue))

0 commit comments

Comments
 (0)