Skip to content

Sandbox blocks output to default plugin output directory when it's under <pkgdir>/.build #4009

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

Merged
merged 1 commit into from
Jan 13, 2022
Merged
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
18 changes: 13 additions & 5 deletions Sources/Basics/Sandbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public enum Sandbox {
/// - Parameters:
/// - command: The command line to sandbox (including executable as first argument)
/// - strictness: The basic strictness level of the standbox.
/// - writableDirectories: Paths under which writing should be allowed.
/// - readOnlyDirectories: Paths under which writing should be denied, even if they would have otherwise been allowed by either the strictness level or paths in `writableDirectories`.
/// - writableDirectories: Paths under which writing should be allowed, even if they would otherwise be read-only based on the strictness or paths in `readOnlyDirectories`.
/// - readOnlyDirectories: Paths under which writing should be denied, even if they would have otherwise been allowed by the rules implied by the strictness level.
public static func apply(
command: [String],
strictness: Strictness = .default,
Expand Down Expand Up @@ -79,9 +79,8 @@ fileprivate func macOSSandboxProfile(
}

// Allow writing only to certain directories.
var writableDirectoriesExpression = writableDirectories.map {
"(subpath \(resolveSymlinks($0).quotedAsSubpathForSandboxProfile))"
}
var writableDirectoriesExpression: [String] = []

// The following accesses are only needed when interpreting the manifest (versus running a compiled version).
if strictness == .manifest_pre_53 {
writableDirectoriesExpression += Platform.threadSafeDarwinCacheDirectories.get().map {
Expand Down Expand Up @@ -114,6 +113,15 @@ fileprivate func macOSSandboxProfile(
contents += ")\n"
}

// Emit rules for paths under which writing is allowed, even if they are descendants directories that are otherwise read-only.
if writableDirectories.count > 0 {
contents += "(allow file-write*\n"
for path in writableDirectories {
contents += " (subpath \(resolveSymlinks(path).quotedAsSubpathForSandboxProfile))\n"
}
contents += ")\n"
}

return contents
}

Expand Down
36 changes: 31 additions & 5 deletions Tests/BasicsTests/SandboxTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,20 @@ final class SandboxTest: XCTestCase {

func testWritingToReadOnlyInsideWritableNotAllowed() throws {
#if !os(macOS)
try XCTSkipIf(true, "test is only supported on macOS")
try XCTSkipIf(true, "sandboxing is only supported on macOS")
#endif

try withTemporaryDirectory { tmpDir in
// Check that we can write into it, but not to a read-only directory underneath it.
// Check that we can write into the temporary directory, but not into a read-only directory underneath it.
let writableDir = tmpDir.appending(component: "ShouldBeWritable")
let allowedCommand = Sandbox.apply(command: ["mkdir", writableDir.pathString], strictness: .default, writableDirectories: [tmpDir])
try localFileSystem.createDirectory(writableDir)
let allowedCommand = Sandbox.apply(command: ["touch", writableDir.pathString], strictness: .default, writableDirectories: [writableDir])
XCTAssertNoThrow(try Process.checkNonZeroExit(arguments: allowedCommand))

// Check that we cannot write into a read-only directory inside it.
// Check that we cannot write into a read-only directory inside a writable temporary directory.
let readOnlyDir = writableDir.appending(component: "ShouldBeReadOnly")
try localFileSystem.createDirectory(readOnlyDir)
let deniedCommand = Sandbox.apply(command: ["touch", readOnlyDir.pathString], strictness: .default, writableDirectories: [tmpDir], readOnlyDirectories: [readOnlyDir])
let deniedCommand = Sandbox.apply(command: ["touch", readOnlyDir.pathString], strictness: .writableTemporaryDirectory, readOnlyDirectories: [readOnlyDir])
XCTAssertThrowsError(try Process.checkNonZeroExit(arguments: deniedCommand)) { error in
guard case ProcessResult.Error.nonZeroExit(let result) = error else {
return XCTFail("invalid error \(error)")
Expand All @@ -154,4 +155,29 @@ final class SandboxTest: XCTestCase {
}
}
}

func testWritingToWritableInsideReadOnlyAllowed() throws {
#if !os(macOS)
try XCTSkipIf(true, "sandboxing is only supported on macOS")
#endif

try withTemporaryDirectory { tmpDir in
// Check that we cannot write into a read-only directory, but into a writable directory underneath it.
let readOnlyDir = tmpDir.appending(component: "ShouldBeReadOnly")
try localFileSystem.createDirectory(readOnlyDir)
let deniedCommand = Sandbox.apply(command: ["touch", readOnlyDir.pathString], strictness: .writableTemporaryDirectory, readOnlyDirectories: [readOnlyDir])
XCTAssertThrowsError(try Process.checkNonZeroExit(arguments: deniedCommand)) { error in
guard case ProcessResult.Error.nonZeroExit(let result) = error else {
return XCTFail("invalid error \(error)")
}
XCTAssertMatch(try! result.utf8stderrOutput(), .contains("Operation not permitted"))
}

// Check that we can write into a writable directory underneath it.
let writableDir = readOnlyDir.appending(component: "ShouldBeWritable")
try localFileSystem.createDirectory(writableDir)
let allowedCommand = Sandbox.apply(command: ["touch", writableDir.pathString], strictness: .default, writableDirectories:[writableDir], readOnlyDirectories: [readOnlyDir])
XCTAssertNoThrow(try Process.checkNonZeroExit(arguments: allowedCommand))
}
}
}