Skip to content
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
4 changes: 4 additions & 0 deletions .Jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 - [Denial of Service via Pipe Deadlock]
**Vulnerability:** The application executed `Foundation.Process` shell commands and waited for them to exit (`process.waitUntilExit()`) before attempting to read the standard output and error pipes.
**Learning:** If a child process writes more data than the operating system's pipe buffer can hold (typically ~64KB), the child process will block waiting for the parent to read the data. If the parent is blocked on `waitUntilExit()`, a deadlock occurs, resulting in a Denial of Service.
**Prevention:** Always read data from process pipes (e.g., using `fileHandleForReading.readDataToEndOfFile()`) *before* calling `process.waitUntilExit()` to ensure the pipe buffer is drained and the child process can finish executing. However, ensure that this fix does not introduce deadlocks when used with handlers like `readabilityHandler` or processes that don't close their streams until parent exit.
Original file line number Diff line number Diff line change
Expand Up @@ -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 [] }
Comment on lines +1408 to 1410

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and locate the target area
ast-grep outline XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift --view expanded || true

# Show the nearby lines around the cited snippet
sed -n '1365,1435p' XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift

# Find the method / helper that contains the process handling
rg -n "standardError|readDataToEndOfFile|waitUntilExit|terminationStatus" XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift

Repository: NSEvent/xbox-controller-mapper

Length of output: 13092


Drain or discard standardError here too. readDataToEndOfFile() only consumes stdout; if tailscale status --json writes enough to stderr, the child can block on the unread error pipe and this call can hang indefinitely. Use FileHandle.nullDevice for standardError or read both pipes concurrently.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift`
around lines 1408 - 1410, Update the process setup surrounding the
`readDataToEndOfFile()` call to drain or discard `standardError` as well as
stdout, preferably by assigning `FileHandle.nullDevice` before waiting for the
process. Preserve the existing termination-status check and empty-array
behavior.


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 []
Expand Down
Loading