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.
## 2024-05-22 - TOCTOU Symlink Vulnerability via chmod
**Vulnerability:** Calling `chmod()` on a file path directly is susceptible to Time-of-Check to Time-of-Use (TOCTOU) symlink attacks.
**Learning:** An attacker can replace the target file with a symlink between the check and the `chmod` execution, tricking the application into altering the permissions of an unintended file.
**Prevention:** Use `open(2)` with the `O_NOFOLLOW` flag to safely obtain a file descriptor without resolving symlinks, and then apply permissions using `fchmod()`.
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 securely (prevent TOCTOU symlink attacks)
URL(fileURLWithPath: path).withUnsafeFileSystemRepresentation { pathPtr in
guard let ptr = pathPtr 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.

P2 Badge Open descriptor without requiring read permission

Using open(..., O_RDONLY | O_NOFOLLOW | O_CLOEXEC) means the permission-fix path now depends on read access to the file. If the config file exists but is owner-unreadable (for example mode 000/200), open fails, fchmod is skipped, and the subsequent contents(atPath:) read fails, so the daemon cannot recover by restoring 0600 as it did before with direct chmod(path, 0o600). This turns previously recoverable permission states into hard config-load errors.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Open config nonblocking to avoid FIFO deadlock

Opening with O_RDONLY and no O_NONBLOCK can block indefinitely if the path is a FIFO with no writer. In that case config reload/load can hang inside this actor before any status update, whereas the previous chmod(path, ...) path could not block. A malformed or replaced config file type can therefore stall daemon operation.

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

if fd >= 0 {
fchmod(fd, 0o600)
close(fd)
}
Comment on lines +973 to +976
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 is rejected

When open fails (notably O_NOFOLLOW on a symlink), the code silently skips permission enforcement and continues to contents(atPath:), which still reads via the pathname. That means a symlinked config is still accepted even though the secure-open check failed, so the hardening is bypassed instead of treating the config path as invalid.

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

}

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