Skip to content
Open
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 @@ -31,3 +31,7 @@
**Vulnerability:** External shell command executed in `listLocalSnapshots()` triggered a deadlock when `tmutil` output exceeded 64KB, because stdout and stderr were read synchronously inside the process termination handler.
**Learning:** In Swift, reading from a process pipe synchronously inside a `terminationHandler` can result in a permanent deadlock if the child blocks writing to a full pipe, preventing it from exiting.
**Prevention:** Asynchronously drain pipes continuously while the process is running using background queues.
## 2026-05-25 - TOCTOU Vulnerability via chmod
**Vulnerability:** Found `chmod(path, 0o600)` being used to enforce file permissions on a configuration path that could be user-controlled.
**Learning:** `chmod()` is vulnerable to Time-of-Check to Time-of-Use (TOCTOU) attacks because it follows symlinks. If an attacker replaces the target file with a symlink before `chmod()` executes, permissions of an arbitrary file could be altered.
**Prevention:** To safely apply permissions, obtain a file descriptor using `open()` with the `O_NOFOLLOW` flag to guarantee the target is not a symlink, then apply permissions safely using `fchmod()`. Always use `URL.withUnsafeFileSystemRepresentation` to pass paths to POSIX APIs.
11 changes: 9 additions & 2 deletions Sources/Cacheout/Headless/DaemonMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -966,8 +966,15 @@ public actor DaemonMode: StatusSocket.DataSource {
return
}

// Enforce 0600 permissions
chmod(path, 0o600)
// Enforce 0600 permissions safely to avoid TOCTOU symlink vulnerabilities
URL(fileURLWithPath: path).withUnsafeFileSystemRepresentation { ptr in
guard let ptr = ptr else { return }
let fd = open(ptr, O_RDONLY | O_NOFOLLOW | O_CLOEXEC)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Open config without read bit before applying fchmod

Opening the config with O_RDONLY before calling fchmod introduces a regression: if the file exists but is owner-write-only (for example mode 0200), open fails with EACCES, so permissions are never corrected to 0600 and the subsequent read path fails. The previous chmod(path, 0o600) flow could repair such files first, then read them. This can break reloads for mis-permissioned but otherwise valid config files.

Useful? React with πŸ‘Β / πŸ‘Ž.

if fd >= 0 {
fchmod(fd, 0o600)
close(fd)
}
}
Comment on lines +973 to +977
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fail closed when secure open with O_NOFOLLOW fails

If open(..., O_NOFOLLOW, ...) fails (for example because path is a symlink), the code silently skips permission enforcement and still proceeds to contents(atPath:), which follows the path and reads data anyway. That means the hardening step can be bypassed entirely in exactly the symlink case this change is trying to secure, and config loading continues without ever enforcing 0600 on the file actually consumed.

Useful? React with πŸ‘Β / πŸ‘Ž.


// Read file
guard let data = FileManager.default.contents(atPath: path) else {
Expand Down
Loading