From bd7b11f0242ddcb9f96010c784cd548d58ce65d2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:39:53 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL/H?= =?UTF-8?q?IGH]=20Fix=20TOCTOU=20vulnerability=20in=20file=20creation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ Sources/Cacheout/Cleaner/CacheCleaner.swift | 24 +++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index fd58571..38ef279 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -31,3 +31,8 @@ **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-02 - Fix TOCTOU vulnerability in file creation +**Vulnerability:** High-level Swift file APIs (`FileHandle(forWritingTo:)`, `String.write(to:atomically:)`) follow symlinks by default, leading to Time-of-Check to Time-of-Use (TOCTOU) symlink vulnerabilities. +**Learning:** High-level APIs are inherently insecure when writing to paths that could be tampered with. The vulnerability can be exploited to overwrite arbitrary files if an attacker replaces a file or directory with a symlink before the application writes to it. +**Prevention:** Use POSIX `open(2)` with flags like `O_CREAT | O_WRONLY | O_APPEND | O_NOFOLLOW | O_CLOEXEC` to explicitly refuse following symlinks, securely create the file, and then wrap the resulting file descriptor in a `FileHandle`. diff --git a/Sources/Cacheout/Cleaner/CacheCleaner.swift b/Sources/Cacheout/Cleaner/CacheCleaner.swift index 2111e6a..ebd352a 100644 --- a/Sources/Cacheout/Cleaner/CacheCleaner.swift +++ b/Sources/Cacheout/Cleaner/CacheCleaner.swift @@ -34,6 +34,7 @@ import Foundation import AppKit +import Darwin actor CacheCleaner { private let fileManager = FileManager.default @@ -255,18 +256,27 @@ actor CacheCleaner { private func logCleanup(category: String, bytesFreed: Int64) { let logDir = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent(".cacheout") - try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true) + try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700]) let logFile = logDir.appendingPathComponent("cleanup.log") let size = ByteCountFormatter.sharedFile.string(fromByteCount: bytesFreed) let entry = "[\(ISO8601DateFormatter.shared.string(from: Date()))] Cleaned \(category): \(size)\n" - if let handle = try? FileHandle(forWritingTo: logFile) { - handle.seekToEndOfFile() - handle.write(entry.data(using: .utf8) ?? Data()) - handle.closeFile() - } else { - try? entry.write(to: logFile, atomically: true, encoding: .utf8) + _ = logFile.withUnsafeFileSystemRepresentation { pathPtr -> Int32 in + guard let pathPtr = pathPtr else { return -1 } + let fd = open(pathPtr, O_CREAT | O_WRONLY | O_APPEND | O_NOFOLLOW | O_CLOEXEC, 0o600) + if fd != -1 { + let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: true) + let data = entry.data(using: .utf8) ?? Data() + if #available(macOS 10.15.4, *) { + try? handle.write(contentsOf: data) + try? handle.close() + } else { + handle.write(data) + handle.closeFile() + } + } + return fd } } }