Skip to content

Commit 5e22d1b

Browse files
authored
Ensure package plugin sandbox contents is deterministic (#7876)
Previously, nondeterministic item replacement directory paths could be included in the sandbox content. This can lead to spurious incremental rebuilds of plugin outputs. Because `itemReplacementDirectories` returns a unique temporary directory on each call, I don't believe including it in the sandbox profile had the intended effect, so removing it should not break existing plugins.
1 parent cdac335 commit 5e22d1b

File tree

2 files changed

+41
-4
lines changed

2 files changed

+41
-4
lines changed

Sources/Basics/Sandbox.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,19 @@ fileprivate func macOSSandboxProfile(
217217
// Emit rules for paths under which writing is allowed, even if they are descendants directories that are otherwise read-only.
218218
if writableDirectories.count > 0 {
219219
contents += "(allow file-write*\n"
220-
// For any explicit writable directories, also include the relevant item replacement directories so that Foundation APIs using atomic writes are not blocked by the sandbox.
221-
for path in writableDirectories + Set(writableDirectories.compactMap { try? fileSystem.itemReplacementDirectories(for: $0) }.flatMap { $0}) {
220+
for path in writableDirectories {
222221
contents += " (subpath \(try resolveSymlinks(path).quotedAsSubpathForSandboxProfile))\n"
222+
223+
// `itemReplacementDirectories` may return a combination of stable directory paths, and subdirectories which are unique on every call. Avoid including unnecessary subdirectories in the Sandbox profile which may lead to nondeterminism in its construction.
224+
if let itemReplacementDirectories = try? fileSystem.itemReplacementDirectories(for: path).sorted(by: { $0.pathString.count < $1.pathString.count }) {
225+
var stableItemReplacementDirectories: [AbsolutePath] = []
226+
for directory in itemReplacementDirectories {
227+
if !stableItemReplacementDirectories.contains(where: { $0.isAncestorOfOrEqual(to: directory) }) {
228+
stableItemReplacementDirectories.append(directory)
229+
contents += " (subpath \(try resolveSymlinks(directory).quotedAsSubpathForSandboxProfile))\n"
230+
}
231+
}
232+
}
223233
}
224234
contents += ")\n"
225235
}

Tests/BasicsTests/SandboxTests.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,32 @@ final class SandboxTest: XCTestCase {
217217
XCTAssertNoThrow(try AsyncProcess.checkNonZeroExit(arguments: allowedCommand))
218218
}
219219
}
220+
221+
func testDeterministicOrdering() throws {
222+
#if !os(macOS)
223+
try XCTSkipIf(true, "test is only supported on macOS")
224+
#endif
225+
try withTemporaryDirectory { path in
226+
// Ensure the contents of the produced sandbox directory is deterministic, in order
227+
// to avoid spurious incremental rebuilds. This test is not guaranteed to catch rare
228+
// cases of nondeterminism, but in practice guards well against past issues in this area.
229+
let writeable1 = path.appending(component: "w1")
230+
let writeable2 = path.appending(component: "w2")
231+
let writeable3 = path.appending(component: "w3")
232+
let writeable4 = path.appending(component: "w4")
233+
234+
let readable1 = path.appending(component: "r1")
235+
let readable2 = path.appending(component: "r2")
236+
let readable3 = path.appending(component: "r3")
237+
let readable4 = path.appending(component: "r4")
238+
239+
let command = try Sandbox.apply(command: ["echo", "hello"], strictness: .default, writableDirectories: [writeable1, writeable2, writeable3, writeable4], readOnlyDirectories: [readable1, readable2, readable3, readable4], fileSystem: localFileSystem)
240+
for _ in 0..<10 {
241+
let newCommand = try Sandbox.apply(command: ["echo", "hello"], strictness: .default, writableDirectories: [writeable1, writeable2, writeable3, writeable4], readOnlyDirectories: [readable1, readable2, readable3, readable4],fileSystem: localFileSystem)
242+
XCTAssertEqual(command, newCommand)
243+
}
244+
}
245+
}
220246
}
221247

222248
extension Sandbox {
@@ -225,11 +251,12 @@ extension Sandbox {
225251
strictness: Strictness = .default,
226252
writableDirectories: [AbsolutePath] = [],
227253
readOnlyDirectories: [AbsolutePath] = [],
228-
allowNetworkConnections: [SandboxNetworkPermission] = []
254+
allowNetworkConnections: [SandboxNetworkPermission] = [],
255+
fileSystem: FileSystem = InMemoryFileSystem()
229256
) throws -> [String] {
230257
return try self.apply(
231258
command: command,
232-
fileSystem: InMemoryFileSystem(),
259+
fileSystem: fileSystem,
233260
strictness: strictness,
234261
writableDirectories: writableDirectories,
235262
readOnlyDirectories: readOnlyDirectories,

0 commit comments

Comments
 (0)