Skip to content

Improve flexibility of sandbox path rules, and make sandbox profile a struct that's easier to manage #5857

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Swift 5.8

In packages that specify resources using tools version 5.8 or later, the generated resource bundle accessor will import `Foundation.Bundle` for its own implementation only. _Clients_ of such packages therefore no longer silently import `Foundation`, preventing inadvertent use of Foundation extensions to standard library APIs, which helps to avoid unexpected code size increases.

* [#5857]

When running a compiler package manifest, the sandbox on Darwin platforms no longer allows writing to the compiler's module cache directories. These directories were originally writable when running Swift package manifests in the interpreter, but the Swift interpreter has not been used for package manifests (regardless of tools version) for several releases now, so this should have no noticeable impact on existing packages.

Swift 5.7
-----------

Expand Down
2 changes: 1 addition & 1 deletion Sources/Basics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ add_library(Basics
NSLock+Extensions.swift
Observability.swift
SQLite.swift
Sandbox.swift
SandboxProfile.swift
String+Extensions.swift
Triple+Extensions.swift
SwiftVersion.swift
Expand Down
144 changes: 0 additions & 144 deletions Sources/Basics/Sandbox.swift

This file was deleted.

95 changes: 95 additions & 0 deletions Sources/Basics/SandboxProfile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 - 2022 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import TSCBasic

/// A sandbox profile representing the desired restrictions. The implementation can vary between platforms. Currently
/// the only control a client has is in the path rules, but in the future there should also be options for controlling
/// blocking of network access and process launching.
public struct SandboxProfile: Equatable {
/// An ordered list of path rules, where the last rule to cover a particular path "wins". These will be resolved
/// to absolute paths at the time the profile is applied. They are applied after any of the implicit directories
/// referenced by other sandbox profile settings.
public var pathAccessRules: [PathAccessRule]

/// Represents a rule for access to a path and everything under it.
public enum PathAccessRule: Equatable {
case noaccess(AbsolutePath)
case readonly(AbsolutePath)
case writable(AbsolutePath)
}

/// Configures a SandboxProfile for blocking network access and writing to the file system except to specifically
/// permitted locations.
public init(_ pathAccessRules: [PathAccessRule] = []) {
self.pathAccessRules = pathAccessRules
}
}

extension SandboxProfile {
/// Applies the sandbox profile to the given command line (if the platform supports it), and returns the modified
/// command line. On platforms that don't support sandboxing, the unmodified command line is returned.
public func apply(to command: [String]) throws -> [String] {
#if os(macOS)
return ["/usr/bin/sandbox-exec", "-p", try self.generateMacOSSandboxProfileString()] + command
#else
// rdar://40235432, rdar://75636874 tracks implementing sandboxes for other platforms.
return command
#endif
}
}

// MARK: - macOS

#if os(macOS)
fileprivate extension SandboxProfile {
/// Private function that generates a Darwin sandbox profile suitable for passing to `sandbox-exec(1)`.
func generateMacOSSandboxProfileString() throws -> String {
var contents = "(version 1)\n"

// Deny everything by default.
contents += "(deny default)\n"

// Import the system sandbox profile.
contents += "(import \"system.sb\")\n"

// Allow operations on subprocesses.
contents += "(allow process*)\n"

// Allow reading any file that isn't protected by TCC or permissions (ideally we'd only allow a specific set
// of readable locations, and can maybe tighten this in the future).
contents += "(allow file-read*)\n"

// Apply customized rules for specific file system locations. Everything is readonly by default, so we just
// either allow or deny writing, in order. Later rules have precedence over earlier rules.
for rule in pathAccessRules {
switch rule {
case .noaccess(let path):
contents += "(deny file-* (subpath \(try resolveSymlinksAndQuotePath(path))))\n"
case .readonly(let path):
contents += "(deny file-write* (subpath \(try resolveSymlinksAndQuotePath(path))))\n"
case .writable(let path):
contents += "(allow file-write* (subpath \(try resolveSymlinksAndQuotePath(path))))\n"
}
}
return contents
}

/// Private helper function that resolves an AbsolutePath and returns it as a string quoted for use as a subpath
/// in a .sb sandbox profile.
func resolveSymlinksAndQuotePath(_ path: AbsolutePath) throws -> String {
return try "\"" + resolveSymlinks(path).pathString
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
+ "\""
}
}
#endif
7 changes: 6 additions & 1 deletion Sources/Build/BuildOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,12 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
// TODO: We need to also use any working directory, but that support isn't yet available on all platforms at a lower level.
var commandLine = [command.configuration.executable.pathString] + command.configuration.arguments
if !self.disableSandboxForPluginCommands {
commandLine = try Sandbox.apply(command: commandLine, strictness: .writableTemporaryDirectory, writableDirectories: [pluginResult.pluginOutputDirectory])
// Allow access to the plugin's output directory as well as to the local temporary directory.
let sandboxProfile = SandboxProfile([
.writable(pluginResult.pluginOutputDirectory),
.writable(try AbsolutePath(validating: "/tmp")),
.writable(try self.fileSystem.tempDirectory)])
commandLine = try sandboxProfile.apply(to: commandLine)
}
let processResult = try TSCBasic.Process.popen(arguments: commandLine, environment: command.configuration.environment)
let output = try processResult.utf8Output() + processResult.utf8stderrOutput()
Expand Down
8 changes: 7 additions & 1 deletion Sources/Build/LLBuildManifestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -621,8 +621,14 @@ extension LLBuildManifestBuilder {
let uniquedName = ([execPath.pathString] + command.configuration.arguments).joined(separator: "|")
let displayName = command.configuration.displayName ?? execPath.basename
var commandLine = [execPath.pathString] + command.configuration.arguments

// Apply the sandbox, if appropriate.
if !self.disableSandboxForPluginCommands {
commandLine = try Sandbox.apply(command: commandLine, strictness: .writableTemporaryDirectory, writableDirectories: [result.pluginOutputDirectory])
let sandboxProfile = SandboxProfile([
.writable(result.pluginOutputDirectory),
.writable(try AbsolutePath(validating: "/tmp")),
.writable(try self.fileSystem.tempDirectory)])
commandLine = try sandboxProfile.apply(to: commandLine)
}
manifest.addShellCmd(
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
Expand Down
25 changes: 22 additions & 3 deletions Sources/PackageLoading/ManifestLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -708,10 +708,29 @@ public final class ManifestLoader: ManifestLoaderProtocol {
// This provides some safety against arbitrary code execution when parsing manifest files.
// We only allow the permissions which are absolutely necessary.
if self.isManifestSandboxEnabled {
let cacheDirectories = [self.databaseCacheDir, moduleCachePath].compactMap{ $0 }
let strictness: Sandbox.Strictness = toolsVersion < .v5_3 ? .manifest_pre_53 : .default
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change isn't as material as it looks. The pre-5.3 special case in the Sandbox was to allow the manifest to run interpreted, which is something that no longer happens regardless of the tools version. This controlled writability to org.llvm.clang cache directories etc, and is not likely to affect existing manifests.

There is actually a question of whether sandbox rule changes should be based on tools version at all, as this makes it very easy to circumvent tightened sandboxes even in new builds of SwiftPM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably double-check the assumption that this doesn't affect existing libraries by running through our various compatibility suites. I agree in principle that the rules shouldn't depend on the tools-version, on the other hand we need to make sure we don't break existing packages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point.

The other approach would be to add back the basic knobs-and-switches controlled by that specialized setting and just apply them at the call site. I mainly wanted to get the "special sauce" out of the SandboxProfile type.

do {
cmd = try Sandbox.apply(command: cmd, strictness: strictness, writableDirectories: cacheDirectories)
var sandbox = SandboxProfile()

// Allow writing inside the temporary directories.
sandbox.pathAccessRules.append(.writable(try AbsolutePath(validating: "/tmp")))
sandbox.pathAccessRules.append(.writable(try localFileSystem.tempDirectory))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we consider changing FileSystem::tempDirectory to return an array that includes /tmp instead of adding it manually everywhere? alternatively, maybe a function in this area of the code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'd want to change the tempDirectory function itself, since /tmp is discouraged (in favour of the per-user one that NSTemporaryDirectory() returns), and so making it an array adds complexity without any gain (IMO).

I agree that we'd want to avoid the duplication. In this case, I think the hope is that we can remove the /tmp hardcoding when we set TMPDIR in a follow-on PR, which I would actually want to do as an option on Process (along with adding the sandbox as an option). So the duplication here is fairly temporary, I hope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about something like

// deprecate - for compatibility reason 12/2022
FileSystem._legacyTempDirecotry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having discussed this further offline, I think a improvement for this PR would be to follow Tom's suggestion of encapsulating the /tmp part into a FileSystem.legacyTemporaryDirectory or something similar. That way it's clear that it's disfavored compared with the regular temporary directory, but at least the hardcoding is mitigated. Then I also want to open that TSC.Process PR to add the support for the sandbox and for passing through access to the temporary directory there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having thought some more about this, maybe the right thing to do here is to reintroduce the option for whether or not "temporary directories are writable" as a SandboxProfile option in its own right (it was removed in a previous change in order to simplify the sandbox profile). I'm not all that keen on making FileSystem have two separate notions of a temporary directory, which seems conceptually confusing to most clients. The inclusion of /tmp here is really just for backward compatibility and so it seems unfortunate to add semi-deprecated API to FileSystem for it.

The construction of these paths could then be pulled into a single place in the sandbox profile, and clients just get to choose whether or not to enable writing to temporary directories.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok


// Allow writing in the database cache directory, if we have one.
if let databaseCacheDir = self.databaseCacheDir {
sandbox.pathAccessRules.append(.writable(databaseCacheDir))
}

// Allow writing in the module cache path, if there is one.
if let moduleCachePath = moduleCachePath {
sandbox.pathAccessRules.append(.writable(moduleCachePath))
}

// But do not allow writing in the directory that contains the manifest, even if it is
// inside one of the otherwise writable directories.
sandbox.pathAccessRules.append(.readonly(manifestPath.parentDirectory))

// Finally apply the sandbox.
cmd = try sandbox.apply(to: cmd)
} catch {
return completion(.failure(error))
}
Expand Down
18 changes: 17 additions & 1 deletion Sources/Workspace/DefaultPluginScriptRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,23 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable {
// 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.
if self.enableSandbox {
do {
command = try Sandbox.apply(command: command, strictness: .writableTemporaryDirectory, writableDirectories: writableDirectories + [self.cacheDir], readOnlyDirectories: readOnlyDirectories)
var sandbox = SandboxProfile()

// Allow writing inside the temporary directories.
sandbox.pathAccessRules.append(.writable(try AbsolutePath(validating: "/tmp")))
sandbox.pathAccessRules.append(.writable(try self.fileSystem.tempDirectory))

// But prevent writing in any read-only directories.
sandbox.pathAccessRules.append(contentsOf: readOnlyDirectories.map{ .readonly($0) })

// But allow writing in any writable directories.
sandbox.pathAccessRules.append(contentsOf: writableDirectories.map{ .writable($0) })

// And always allow writing to the cache directory, even if it is inside one of the readonly directories.
sandbox.pathAccessRules.append(.writable(self.cacheDir))

// Apply the sandbox to the command.
command = try sandbox.apply(to: command)
} catch {
return callbackQueue.async {
completion(.failure(error))
Expand Down
Loading