diff --git a/.jules/sentinel.md b/.jules/sentinel.md index fd58571..7af48c9 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -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-21 - TOCTOU Vulnerability via Data.write and setAttributes +**Vulnerability:** Found `try data.write(to: tmpURL)` followed by `try FileManager.default.setAttributes([.posixPermissions: 0o600], ...)` in `flushState()`, which introduces a TOCTOU window where the file is exposed with default umask permissions before being tightened. +**Learning:** `Data.write(to:)` is vulnerable to TOCTOU if permissions need to be restricted after creation. +**Prevention:** Use POSIX `open()` with `O_CREAT | O_WRONLY | O_EXCL | O_CLOEXEC` and the `0o600` mode to securely create files without relying on the process umask, then wrap the file descriptor in a `FileHandle`. diff --git a/Sources/CacheoutHelperLib/SysctlJournal.swift b/Sources/CacheoutHelperLib/SysctlJournal.swift index a14c902..ec1ba5e 100644 --- a/Sources/CacheoutHelperLib/SysctlJournal.swift +++ b/Sources/CacheoutHelperLib/SysctlJournal.swift @@ -307,14 +307,24 @@ public final class SysctlJournal { do { let data = try PropertyListEncoder().encode(state) - // Write to temp file (non-atomic — we control the rename ourselves). - try data.write(to: tmpURL) + try? FileManager.default.removeItem(at: tmpURL) - // Set permissions to 0600 (root-only) on temp file before rename. - try FileManager.default.setAttributes( - [.posixPermissions: 0o600], - ofItemAtPath: tmpURL.path - ) + let fd = tmpURL.withUnsafeFileSystemRepresentation { pathPtr -> Int32 in + guard let ptr = pathPtr else { return -1 } + return open(ptr, O_CREAT | O_WRONLY | O_EXCL | O_CLOEXEC, S_IRUSR | S_IWUSR) + } + guard fd >= 0 else { + logger.error("Failed to securely open temp journal file: errno \(errno, privacy: .public)") + return false + } + + let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: true) + handle.write(data) + if #available(macOS 10.15.4, *) { + try? handle.close() + } else { + handle.closeFile() + } // Atomic rename(2) — atomicity on APFS/HFS+. if rename(tmpURL.path, url.path) != 0 {