Skip to content

Commit 0bd9968

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 `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. Note: if `TMPDIR` happens to be set in SwiftPM's own environment, it will control SwiftPM's own uses of `NSTemporaryDirectory()`, so the callers `TMPDIR` will be respected all the way through (down to manifests and plugins). rdar://91575256
1 parent 9ff90b0 commit 0bd9968

File tree

5 files changed

+58
-9
lines changed

5 files changed

+58
-9
lines changed

Sources/Build/BuildOperation.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,12 +557,20 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
557557
// Allow access to the plugin's output directory as well as to the local temporary directory.
558558
let sandboxProfile = SandboxProfile([
559559
.writable(pluginResult.pluginOutputDirectory),
560-
.writable(try AbsolutePath(validating: "/tmp")),
561560
.writable(try self.fileSystem.tempDirectory)])
562561
commandLine = try sandboxProfile.apply(to: commandLine)
563562
}
564-
let processResult = try TSCBasic.Process.popen(arguments: commandLine, environment: command.configuration.environment)
563+
564+
// Pass `TMPDIR` in the environment, in addition to anything the plugin specifies, in case we have an
565+
// override in our own environment.
566+
var environment = command.configuration.environment
567+
environment["TMPDIR"] = try self.fileSystem.tempDirectory.pathString
568+
569+
// Run the command and wait for it to finish.
570+
let processResult = try Process.popen(arguments: commandLine, environment: environment)
565571
let output = try processResult.utf8Output() + processResult.utf8stderrOutput()
572+
573+
// Throw an error if it failed.
566574
if processResult.exitStatus != .terminated(code: 0) {
567575
throw StringError("failed: \(command)\n\n\(output)")
568576
}

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,17 +626,22 @@ extension LLBuildManifestBuilder {
626626
if !self.disableSandboxForPluginCommands {
627627
let sandboxProfile = SandboxProfile([
628628
.writable(result.pluginOutputDirectory),
629-
.writable(try AbsolutePath(validating: "/tmp")),
630629
.writable(try self.fileSystem.tempDirectory)])
631630
commandLine = try sandboxProfile.apply(to: commandLine)
632631
}
632+
633+
// Pass `TMPDIR` in the environment, in addition to anything the plugin specifies, in case we have an
634+
// override in our own environment.
635+
var environment = command.configuration.environment
636+
environment["TMPDIR"] = try self.fileSystem.tempDirectory.pathString
637+
633638
manifest.addShellCmd(
634639
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
635640
description: displayName,
636641
inputs: command.inputFiles.map{ .file($0) },
637642
outputs: command.outputFiles.map{ .file($0) },
638643
arguments: commandLine,
639-
environment: command.configuration.environment,
644+
environment: environment,
640645
workingDirectory: command.configuration.workingDirectory?.pathString)
641646
}
642647
}

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,7 @@ public final class ManifestLoader: ManifestLoaderProtocol {
638638
do {
639639
var sandbox = SandboxProfile()
640640

641-
// Allow writing inside the temporary directories.
642-
sandbox.pathAccessRules.append(.writable(try AbsolutePath(validating: "/tmp")))
641+
// Allow writing inside the temporary directory.
643642
sandbox.pathAccessRules.append(.writable(try localFileSystem.tempDirectory))
644643

645644
// Allow writing in the database cache directory, if we have one.
@@ -663,13 +662,22 @@ public final class ManifestLoader: ManifestLoaderProtocol {
663662
}
664663
}
665664

666-
// Run the compiled manifest.
665+
// Set up the environment so that the manifest inherits our own environment, but make sure that
666+
// `TMPDIR` is set to whatever the temporary directory is that is allowed in the sandbox.
667667
var environment = ProcessEnv.vars
668+
do {
669+
environment["TMPDIR"] = try localFileSystem.tempDirectory.pathString
670+
} catch {
671+
return completion(.failure(error))
672+
}
673+
674+
// On Windows, also make sure that the `Path` is set up correctly in the environment.
668675
#if os(Windows)
669676
let windowsPathComponent = runtimePath.pathString.replacingOccurrences(of: "/", with: "\\")
670677
environment["Path"] = "\(windowsPathComponent);\(environment["Path"] ?? "")"
671678
#endif
672679

680+
// Run the compiled manifest.
673681
let cleanupAfterRunning = cleanupIfError.delay()
674682
TSCBasic.Process.popen(arguments: cmd, environment: environment, queue: callbackQueue) { result in
675683
dispatchPrecondition(condition: .onQueue(callbackQueue))

Sources/Workspace/DefaultPluginScriptRunner.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,7 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable {
425425
do {
426426
var sandbox = SandboxProfile()
427427

428-
// Allow writing inside the temporary directories.
429-
sandbox.pathAccessRules.append(.writable(try AbsolutePath(validating: "/tmp")))
428+
// Allow writing inside the temporary directory.
430429
sandbox.pathAccessRules.append(.writable(try self.fileSystem.tempDirectory))
431430

432431
// But prevent writing in any read-only directories.
@@ -464,6 +463,16 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable {
464463
#endif
465464
process.currentDirectoryURL = workingDirectory.asURL
466465

466+
// Pass `TMPDIR` in the environment, in addition to anything the plugin specifies. This lets us respect our own
467+
// override, and also makes sure that the plugin always knows what temporary directory we opened up for it.
468+
var environment = ProcessInfo.processInfo.environment
469+
do {
470+
environment["TMPDIR"] = try localFileSystem.tempDirectory.pathString
471+
} catch {
472+
return completion(.failure(error))
473+
}
474+
process.environment = environment
475+
467476
// Set up a pipe for sending structured messages to the plugin on its stdin.
468477
let stdinPipe = Pipe()
469478
let outputHandle = stdinPipe.fileHandleForWriting

Tests/CommandsTests/PackageToolTests.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,6 +1621,22 @@ final class PackageToolTests: CommandsTestCase {
16211621
let targetNames = argExtractor.extractOption(named: "target")
16221622
let targets = try context.package.targets(named: targetNames)
16231623
1624+
// Check that we are able to write to the package plugin directory.
1625+
print("Trying to write to the plugin output directory...")
1626+
let outputPath = context.pluginWorkDirectory.appending("Foo")
1627+
guard FileManager.default.createFile(atPath: outputPath.string, contents: Data("Hello".utf8)) else {
1628+
throw "Couldn’t create file at path \\(outputPath)"
1629+
}
1630+
print("... successfully created file at path \\(outputPath)")
1631+
1632+
// Check that we cannot write to `/tmp/`.
1633+
print("Trying to write inside `/tmp/`...")
1634+
let outputPath2 = Path("/tmp/_this_file_should_not_be_possible_to_create_")
1635+
if FileManager.default.createFile(atPath: outputPath2.string, contents: Data("Hello".utf8)) {
1636+
throw "Unexpectedly could create file at path \\(outputPath2)"
1637+
}
1638+
print("... correctly couldn't create file at path \\(outputPath2)")
1639+
16241640
// Print out the source files so that we can check them.
16251641
if let sourceFiles = (targets.first{ $0.name == "MyLibrary" } as? SourceModuleTarget)?.sourceFiles {
16261642
for file in sourceFiles {
@@ -1629,6 +1645,7 @@ final class PackageToolTests: CommandsTestCase {
16291645
}
16301646
}
16311647
}
1648+
extension String: Error {}
16321649
"""
16331650
}
16341651

@@ -1677,6 +1694,8 @@ final class PackageToolTests: CommandsTestCase {
16771694
let output = try result.utf8Output() + result.utf8stderrOutput()
16781695
XCTAssertEqual(result.exitStatus, .terminated(code: 0), "output: \(output)")
16791696
XCTAssertMatch(output, .contains("This is MyCommandPlugin."))
1697+
XCTAssertMatch(output, .contains("successfully created file at path \(try resolveSymlinks(packageDir).appending(components: ".build", "plugins", "MyPlugin", "outputs", "Foo"))"))
1698+
XCTAssertMatch(output, .contains("correctly couldn't create file at path /tmp/_this_file_should_not_be_possible_to_create_"))
16801699
}
16811700

16821701
// Check that we can also invoke it without the "plugin" subcommand.

0 commit comments

Comments
 (0)