Skip to content

Sandbox for manifests and plugins should provide a unique path specified by TMPDIR instead of allowing /tmp #4307

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

Draft
wants to merge 2 commits 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: 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 @@ -26,7 +26,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
17 changes: 15 additions & 2 deletions Sources/Build/BuildOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -554,10 +554,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 = 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 self.fileSystem.tempDirectory)])
commandLine = try sandboxProfile.apply(to: commandLine)
}
let processResult = try TSCBasic.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"] = try self.fileSystem.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
15 changes: 13 additions & 2 deletions Sources/Build/LLBuildManifestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -621,16 +621,27 @@ 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 self.fileSystem.tempDirectory)])
commandLine = try 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"] = try self.fileSystem.tempDirectory.pathString

manifest.addShellCmd(
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
description: displayName,
inputs: 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
35 changes: 31 additions & 4 deletions Sources/PackageLoading/ManifestLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -635,22 +635,49 @@ 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
do {
cmd = try Sandbox.apply(command: cmd, strictness: strictness, writableDirectories: cacheDirectories)
var sandbox = SandboxProfile()

// Allow writing inside the temporary directory.
sandbox.pathAccessRules.append(.writable(try localFileSystem.tempDirectory))

// 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))
}
}

// 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
do {
environment["TMPDIR"] = try localFileSystem.tempDirectory.pathString
} catch {
return completion(.failure(error))
}

// 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()
TSCBasic.Process.popen(arguments: cmd, environment: environment, queue: callbackQueue) { result in
dispatchPrecondition(condition: .onQueue(callbackQueue))
Expand Down
Loading