Skip to content

Fix a deadlock where we call OSLog while on OperationSystemAdaptor.queue, ... #470

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
May 1, 2025
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
16 changes: 10 additions & 6 deletions Sources/SWBBuildSystem/BuildOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1850,15 +1850,19 @@ internal final class OperationSystemAdaptor: SWBLLBuild.BuildSystemDelegate, Act
return
}

guard let outputDelegate = (queue.blocking_sync { self.commandOutputDelegates.removeValue(forKey: command) }) else {
// If there's no outputDelegate, the command never started (i.e. it was skipped by shouldCommandStart().
return
}

// We can call this here because we're on an llbuild worker thread. This shouldn't be used while on `self.queue` because we have Swift async work elsewhere which blocks on that queue.
let sandboxViolations = task.isSandboxed && result == .failed ? task.extractSandboxViolationMessages_ASYNC_UNSAFE(startTime: outputDelegate.startTime) : []

queue.async {
// Find the output delegate, and remove it from the active set.
guard let outputDelegate = self.commandOutputDelegates.removeValue(forKey: command) else {
// If there's no outputDelegate, the command never started (i.e. it was skipped by shouldCommandStart().
return
for message in sandboxViolations {
outputDelegate.emit(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData(message)))
}

outputDelegate.emitSandboxingViolations(task: task, commandResult: result)

// This may be updated by commandProcessFinished if it was an
// ExternalCommand, so only update the exit status in output delegate if
// it is nil. However, always update the status if the result is failed,
Expand Down
27 changes: 11 additions & 16 deletions Sources/SWBBuildSystem/SandboxViolations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,33 @@
//
//===----------------------------------------------------------------------===//

import Foundation
package import Foundation
import SWBUtil
package import SWBCore
package import SWBTaskExecution

#if os(macOS)
import OSLog
#endif

extension TaskOutputDelegate {
package func emitSandboxingViolations(task: any ExecutableTask, commandResult result: CommandResult) {
guard task.isSandboxed && result == .failed else {
return
}

for message in task.extractSandboxViolationMessages(startTime: startTime) {
emit(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData(message)))
}
}
}

extension ExecutableTask {
/// Whether or not this task is being executed within a sandbox which restricts filesystem access to declared inputs and outputs.
///
/// - note: Currently this will be true for any task whose executable is `sandbox-exec`, not only tasks which were created using _Swift Builds_ sandboxing mechanism and therefore whose diagnostics contain the message sentinel that we look for. However, this shouldn't really matter in practice at this time as if this property is true for a task not sandboxed _by Swift Build_, we'll simply not extract any diagnostics for it. The only way to get into this situation is to create such tasks in the PIF, which end-users can't do.
fileprivate var isSandboxed: Bool {
package var isSandboxed: Bool {
return commandLine.first?.asByteString == ByteString(encodingAsUTF8: "/usr/bin/sandbox-exec")
}

fileprivate func extractSandboxViolationMessages(startTime: Date) -> [String] {
/// This must be called from threads which aren't Swift async worker threads. This func uses OSLog which kicks off async work and waits for it on a semaphore, causing deadlocks when invoked from Swift Concurrency worker threads.
@available(*, noasync)
package func extractSandboxViolationMessages_ASYNC_UNSAFE(startTime: Date) -> [String] {
var res: [String] = []
#if os(macOS)
withUnsafeCurrentTask { task in
if task != nil {
preconditionFailure("This function should not be invoked from the Swift Concurrency thread pool as it may lead to deadlock via thread starvation.")
}
}

if let store = try? OSLogStore.local() {
let query = String("((processID == 0 AND senderImagePath CONTAINS[c] \"/Sandbox\") OR (process == \"sandboxd\" AND subsystem == \"com.apple.sandbox.reporting\")) AND (eventMessage CONTAINS[c] %@)")
let endTime = Date()
Expand Down
Loading