Skip to content

Commit c3f5261

Browse files
authored
Sandbox blocks output to default plugin output directory when it's under <pkgdir>/.build (#4009)
The sandbox rules introduced in #3996 made the entire package directory read-only, but that isn't appropriate when `.build` is inside the package directory. The rules for applying read-only directories were intended to shadow the implicit rules added by specifying a writable temporary directory, not to block the ones specified by explicit writable directories. This fixes that ordering, and adds the missing unit test that would have caught the problem. A future change reworks Sandbox completely so that it becomes a struct that can carry around the sandbox profile configuration until it is applied — this change is intended to be small enough to be nominatable rdar://87417780
1 parent 95ebea1 commit c3f5261

File tree

2 files changed

+44
-10
lines changed

2 files changed

+44
-10
lines changed

Sources/Basics/Sandbox.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public enum Sandbox {
2121
/// - Parameters:
2222
/// - command: The command line to sandbox (including executable as first argument)
2323
/// - strictness: The basic strictness level of the standbox.
24-
/// - writableDirectories: Paths under which writing should be allowed.
25-
/// - 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`.
24+
/// - writableDirectories: Paths under which writing should be allowed, even if they would otherwise be read-only based on the strictness or paths in `readOnlyDirectories`.
25+
/// - readOnlyDirectories: Paths under which writing should be denied, even if they would have otherwise been allowed by the rules implied by the strictness level.
2626
public static func apply(
2727
command: [String],
2828
strictness: Strictness = .default,
@@ -79,9 +79,8 @@ fileprivate func macOSSandboxProfile(
7979
}
8080

8181
// Allow writing only to certain directories.
82-
var writableDirectoriesExpression = writableDirectories.map {
83-
"(subpath \(resolveSymlinks($0).quotedAsSubpathForSandboxProfile))"
84-
}
82+
var writableDirectoriesExpression: [String] = []
83+
8584
// The following accesses are only needed when interpreting the manifest (versus running a compiled version).
8685
if strictness == .manifest_pre_53 {
8786
writableDirectoriesExpression += Platform.threadSafeDarwinCacheDirectories.get().map {
@@ -114,6 +113,15 @@ fileprivate func macOSSandboxProfile(
114113
contents += ")\n"
115114
}
116115

116+
// Emit rules for paths under which writing is allowed, even if they are descendants directories that are otherwise read-only.
117+
if writableDirectories.count > 0 {
118+
contents += "(allow file-write*\n"
119+
for path in writableDirectories {
120+
contents += " (subpath \(resolveSymlinks(path).quotedAsSubpathForSandboxProfile))\n"
121+
}
122+
contents += ")\n"
123+
}
124+
117125
return contents
118126
}
119127

Tests/BasicsTests/SandboxTests.swift

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,19 +133,20 @@ final class SandboxTest: XCTestCase {
133133

134134
func testWritingToReadOnlyInsideWritableNotAllowed() throws {
135135
#if !os(macOS)
136-
try XCTSkipIf(true, "test is only supported on macOS")
136+
try XCTSkipIf(true, "sandboxing is only supported on macOS")
137137
#endif
138138

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

145-
// Check that we cannot write into a read-only directory inside it.
146+
// Check that we cannot write into a read-only directory inside a writable temporary directory.
146147
let readOnlyDir = writableDir.appending(component: "ShouldBeReadOnly")
147148
try localFileSystem.createDirectory(readOnlyDir)
148-
let deniedCommand = Sandbox.apply(command: ["touch", readOnlyDir.pathString], strictness: .default, writableDirectories: [tmpDir], readOnlyDirectories: [readOnlyDir])
149+
let deniedCommand = Sandbox.apply(command: ["touch", readOnlyDir.pathString], strictness: .writableTemporaryDirectory, readOnlyDirectories: [readOnlyDir])
149150
XCTAssertThrowsError(try Process.checkNonZeroExit(arguments: deniedCommand)) { error in
150151
guard case ProcessResult.Error.nonZeroExit(let result) = error else {
151152
return XCTFail("invalid error \(error)")
@@ -154,4 +155,29 @@ final class SandboxTest: XCTestCase {
154155
}
155156
}
156157
}
158+
159+
func testWritingToWritableInsideReadOnlyAllowed() throws {
160+
#if !os(macOS)
161+
try XCTSkipIf(true, "sandboxing is only supported on macOS")
162+
#endif
163+
164+
try withTemporaryDirectory { tmpDir in
165+
// Check that we cannot write into a read-only directory, but into a writable directory underneath it.
166+
let readOnlyDir = tmpDir.appending(component: "ShouldBeReadOnly")
167+
try localFileSystem.createDirectory(readOnlyDir)
168+
let deniedCommand = Sandbox.apply(command: ["touch", readOnlyDir.pathString], strictness: .writableTemporaryDirectory, readOnlyDirectories: [readOnlyDir])
169+
XCTAssertThrowsError(try Process.checkNonZeroExit(arguments: deniedCommand)) { error in
170+
guard case ProcessResult.Error.nonZeroExit(let result) = error else {
171+
return XCTFail("invalid error \(error)")
172+
}
173+
XCTAssertMatch(try! result.utf8stderrOutput(), .contains("Operation not permitted"))
174+
}
175+
176+
// Check that we can write into a writable directory underneath it.
177+
let writableDir = readOnlyDir.appending(component: "ShouldBeWritable")
178+
try localFileSystem.createDirectory(writableDir)
179+
let allowedCommand = Sandbox.apply(command: ["touch", writableDir.pathString], strictness: .default, writableDirectories:[writableDir], readOnlyDirectories: [readOnlyDir])
180+
XCTAssertNoThrow(try Process.checkNonZeroExit(arguments: allowedCommand))
181+
}
182+
}
157183
}

0 commit comments

Comments
 (0)