Skip to content

Rework the Sandbox enum into a SandboxProfile struct that can separate out point of configuration from point of application #4005

New issue

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

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

Already on GitHub? Sign in to your account

Closed
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: 2 additions & 2 deletions Sources/Basics/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This source file is part of the Swift open source project
#
# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
# Copyright (c) 2014 - 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
Expand All @@ -24,7 +24,7 @@ add_library(Basics
JSONDecoder+Extensions.swift
Netrc.swift
Observability.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.

108 changes: 108 additions & 0 deletions Sources/Basics/SandboxProfile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
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.
public struct SandboxProfile {
/// 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)
}

/// Whether to allow outbound and inbound network access.
public var allowNetwork: Bool

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

// Convenience initializer to make it easier to construct sandbox profiles with conditional rules.
public init(_ pathAccessRules: PathAccessRule?..., allowNetwork: Bool = false) {
self.init(pathAccessRules.compactMap{ $0 }, allowNetwork: allowNetwork)
}
}

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]) -> [String] {
#if os(macOS)
return ["/usr/bin/sandbox-exec", "-p", 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() -> 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"

// Optionally allow network access (inbound and outbound).
if allowNetwork {
contents += "(system-network)\n"
contents += "(allow network*)\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 \(resolveSymlinksAndQuotePath(path))))\n"
case .readonly(let path):
contents += "(deny file-write* (subpath \(resolveSymlinksAndQuotePath(path))))\n"
case .writable(let path):
contents += "(allow file-write* (subpath \(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) -> String {
return "\"" + resolveSymlinks(path).pathString
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
+ "\""
}
}
#endif
17 changes: 15 additions & 2 deletions Sources/Build/BuildOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,23 @@ 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 = 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(localFileSystem.tempDirectory))
commandLine = sandboxProfile.apply(to: commandLine)
}
let processResult = try Process.popen(arguments: commandLine, environment: command.configuration.environment)

// Pass `TMPDIR` in the environment, in addition to anything the plugin specifies, in case we have an
// override in our own environment.
var environment = command.configuration.environment
environment["TMPDIR"] = localFileSystem.tempDirectory.pathString

// Run the command and wait for it to finish.
let processResult = try Process.popen(arguments: commandLine, environment: environment)
let output = try processResult.utf8Output() + processResult.utf8stderrOutput()

// Throw an error if it failed.
if processResult.exitStatus != .terminated(code: 0) {
throw StringError("failed: \(command)\n\n\(output)")
}
Expand Down
14 changes: 12 additions & 2 deletions Sources/Build/LLBuildManifestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -619,16 +619,26 @@ 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 = Sandbox.apply(command: commandLine, strictness: .writableTemporaryDirectory, writableDirectories: [result.pluginOutputDirectory])
let sandboxProfile = SandboxProfile(
.writable(result.pluginOutputDirectory),
.writable(localFileSystem.tempDirectory))
commandLine = sandboxProfile.apply(to: commandLine)
}
// Pass `TMPDIR` in the environment, in addition to anything the plugin specifies, in case we have an
// override in our own environment.
var environment = command.configuration.environment
environment["TMPDIR"] = localFileSystem.tempDirectory.pathString

manifest.addShellCmd(
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
description: displayName,
inputs: [.file(execPath)] + command.inputFiles.map{ .file($0) },
outputs: command.outputFiles.map{ .file($0) },
arguments: commandLine,
environment: command.configuration.environment,
environment: environment,
workingDirectory: command.configuration.workingDirectory?.pathString)
}
}
Expand Down
24 changes: 20 additions & 4 deletions Sources/PackageLoading/ManifestLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -827,18 +827,34 @@ 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
cmd = Sandbox.apply(command: cmd, strictness: strictness, writableDirectories: cacheDirectories)
cmd = SandboxProfile(
// Allow writing in the temporary directory (whether or not it's overridden by TMPDIR).
.writable(localFileSystem.tempDirectory),

// Allow writing in the database cache directory, if we have one.
self.databaseCacheDir.map{ .writable($0) },

// Allow writing in the module cache path, if there is one.
moduleCachePath.map{ .writable($0) },

// But do not allow writing in the directory that contains the manifest, even if it is
// inside one of the writable directories.
.readonly(manifestPath.parentDirectory)
).apply(to: cmd)
}

// Run the compiled manifest.
// Set up the environment so that the manifest inherits our own environment, but make sure that
// `TMPDIR` is set to whatever the temporary directory is that is allowed in the sandbox.
var environment = ProcessEnv.vars
environment["TMPDIR"] = localFileSystem.tempDirectory.pathString

// On Windows, also make sure that the `Path` is set up correctly in the environment.
#if os(Windows)
let windowsPathComponent = runtimePath.pathString.replacingOccurrences(of: "/", with: "\\")
environment["Path"] = "\(windowsPathComponent);\(environment["Path"] ?? "")"
#endif

// Run the compiled manifest.
let cleanupAfterRunning = cleanupIfError.delay()
Process.popen(arguments: cmd, environment: environment, queue: callbackQueue) { result in
dispatchPrecondition(condition: .onQueue(callbackQueue))
Expand Down
Loading