diff --git a/.Jules/sentinel.md b/.Jules/sentinel.md index 70f67594..2a56df96 100644 --- a/.Jules/sentinel.md +++ b/.Jules/sentinel.md @@ -18,3 +18,7 @@ **Vulnerability:** The application was falling back to storing OBS passwords as plaintext strings inside exported/saved JSON models (`Macro.swift` and `SystemCommand.swift`) if saving to the macOS Keychain failed. **Learning:** Saving secrets to unencrypted formats simply because secure storage fails is a critical anti-pattern known as "failing open" that results in data exposure. **Prevention:** Always fail securely. If secure storage operations fail, discard the sensitive credential in memory rather than writing it insecurely to disk, even if it requires the user to re-authenticate later. +## 2026-06-25 - [Process Output Pipe Deadlock (DoS)] +**Vulnerability:** Calling `process.waitUntilExit()` on a `Foundation.Process` while concurrently having the child process write to standard output or standard error without draining the associated `Pipe` can result in a thread deadlock. If the child process output exceeds the OS pipe buffer size (typically ~64KB), the child blocks on writing, and the parent blocks on waiting for the child to exit, freezing the application thread (Denial of Service). +**Learning:** System pipes have finite capacity. A parent process waiting on a child must actively drain the child's pipes to ensure the child can complete execution and exit. +**Prevention:** Always read from the `pipe.fileHandleForReading` (e.g., using `readDataToEndOfFile()`) *before* invoking `process.waitUntilExit()`, or use asynchronous pipe reading mechanisms (like `readabilityHandler`). diff --git a/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift b/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift index 59854205..5be153d1 100644 --- a/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift +++ b/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift @@ -354,9 +354,8 @@ public final class AutomationExecutor { } catch { return .failure("\(name) launch failed: \(error.localizedDescription)") } - process.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() let output = String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" @@ -729,13 +728,14 @@ private final class ShellCommandRunner: @unchecked Sendable { let pipe = Pipe() pgrep.standardOutput = pipe pgrep.standardError = FileHandle.nullDevice + let data: Data do { try pgrep.run() + data = pipe.fileHandleForReading.readDataToEndOfFile() pgrep.waitUntilExit() } catch { return [] } - let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" return output .split(whereSeparator: \.isNewline) diff --git a/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift b/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift index 40e97b37..15ecd7cb 100644 --- a/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift +++ b/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift @@ -1405,10 +1405,9 @@ final class UniversalControlMouseRelay: @unchecked Sendable { NSLog("[UCMouseRelay] Could not run tailscale status: %@", String(describing: error)) return [] } + let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() guard process.terminationStatus == 0 else { return [] } - - let data = pipe.fileHandleForReading.readDataToEndOfFile() guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let peers = object["Peer"] as? [String: Any] else { return [] diff --git a/XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift b/XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift index 722ba696..bbd7478a 100644 --- a/XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift +++ b/XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift @@ -167,9 +167,9 @@ private enum OBSMediaMTXManager { which.standardOutput = outPipe which.standardError = Pipe() try? which.run() + let data = outPipe.fileHandleForReading.readDataToEndOfFile() which.waitUntilExit() if which.terminationStatus == 0 { - let data = outPipe.fileHandleForReading.readDataToEndOfFile() if let path = String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines), !path.isEmpty,