diff --git a/.Jules/sentinel.md b/.Jules/sentinel.md index 70f67594..c875fe00 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 - [Process Pipe Buffer Deadlock] +**Vulnerability:** Parent processes executing `Process()` (NSTask) with piped standard output/error were deadlocking if the child output exceeded the OS pipe buffer size (~64KB). The parent process called `waitUntilExit()` before reading the output, causing a classic IPC deadlock. +**Learning:** This is a DoS risk if an attacker can control or pad the output of commands executed by the application. +**Prevention:** Always read all data from `Pipe()` objects (e.g., using `readDataToEndOfFile()`) *before* calling `process.waitUntilExit()`. 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..98e4061a 100644 --- a/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift +++ b/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift @@ -1405,10 +1405,11 @@ 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 []