diff --git a/.Jules/sentinel.md b/.Jules/sentinel.md index 70f67594..3d52446e 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-26 - [Foundation.Process Pipe Deadlock] +**Vulnerability:** Execution frameworks using `Foundation.Process` to run shell commands or scripts were vulnerable to a pipe deadlock and Denial of Service (DoS) if the child process output exceeded the OS pipe buffer (~64KB). +**Learning:** Calling `process.waitUntilExit()` before fully reading the child process's output pipe causes the parent to block waiting for the child, while the child blocks waiting for the parent to drain the pipe buffer. +**Prevention:** Always read data from the pipe (e.g., `pipe.fileHandleForReading.readDataToEndOfFile()`) *before* calling `process.waitUntilExit()` to ensure the pipe is drained as the child writes. diff --git a/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift b/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift index 59854205..bb8eff77 100644 --- a/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift +++ b/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift @@ -354,9 +354,10 @@ 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) ?? "" @@ -731,11 +732,11 @@ private final class ShellCommandRunner: @unchecked Sendable { pgrep.standardError = FileHandle.nullDevice do { try pgrep.run() - pgrep.waitUntilExit() } catch { return [] } let data = pipe.fileHandleForReading.readDataToEndOfFile() + pgrep.waitUntilExit() 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..c2543442 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, @@ -246,6 +246,12 @@ private enum OBSMediaMTXManager { } catch { return false } + if let outPipe = p.standardOutput as? Pipe { + _ = outPipe.fileHandleForReading.readDataToEndOfFile() + } + if let errPipe = p.standardError as? Pipe { + _ = errPipe.fileHandleForReading.readDataToEndOfFile() + } p.waitUntilExit() return p.terminationStatus == 0 }