Skip to content

🛡️ Sentinel: [HIGH] Fix TOCTOU vulnerability in file archival move - #302

Open
matta wants to merge 1 commit into
mainfrom
fix/sentinel-archive-toctou-7993948144143463845
Open

🛡️ Sentinel: [HIGH] Fix TOCTOU vulnerability in file archival move#302
matta wants to merge 1 commit into
mainfrom
fix/sentinel-archive-toctou-7993948144143463845

Conversation

@matta

@matta matta commented Mar 16, 2026

Copy link
Copy Markdown
Owner

🚨 Severity: HIGH
💡 Vulnerability: The run_archive function used fs::rename to move tasks into the archive directory. It checked for existence first (exists(&new_path)) and then moved the file. This creates a Time-of-Check to Time-of-Use (TOCTOU) vulnerability where a file or symlink could be created at the destination in between the check and the move. Because fs::rename silently overwrites files, this could lead to unintended data loss or file clobbering.
🎯 Impact: An attacker or concurrent process could cause an existing task or file to be overwritten, leading to data loss or tampering.
🔧 Fix: Replaced fs::rename with a robust safe_move_file function. This function uses fs::hard_link followed by fs::remove_file to ensure an atomic move. If the hard link fails due to the file already existing, it bubbles up an AlreadyExists error to the retry loop. If hard linking fails for other reasons (like crossing device boundaries), it falls back to an atomic copy using OpenOptions::new().write(true).create_new(true). Added a test using tempdir to verify the new move_to_archive function.
✅ Verification: Ran unit and integration tests successfully using just check and just test. Tested fallback behavior and unique naming logic locally.


PR created automatically by Jules for task 7993948144143463845 started by @matta

Co-authored-by: matta <37314+matta@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@trunk-io

trunk-io Bot commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a high-severity Time-of-Check to Time-of-Use (TOCTOU) vulnerability in the file archiving mechanism. The changes introduce a new atomic file movement utility that safeguards against race conditions and potential data loss or corruption during file operations, significantly enhancing the system's robustness and security posture.

Highlights

  • TOCTOU Vulnerability Fix: Addressed a Time-of-Check to Time-of-Use (TOCTOU) vulnerability in the run_archive function by replacing fs::rename with a new, robust safe_move_file function.
  • Atomic File Move Implementation: Introduced safe_move_file which primarily uses fs::hard_link followed by fs::remove_file for atomic moves, ensuring data integrity and preventing race conditions.
  • Fallback for Cross-Device Moves: Implemented a fallback mechanism in safe_move_file to perform an atomic copy using OpenOptions::new().write(true).create_new(true) if hard-linking fails (e.g., across different file systems).
  • Improved Unique Naming Logic: Modified the move_to_archive function to integrate safe_move_file's AlreadyExists error handling, allowing it to retry with a unique filename suffix (-2, -3, etc.) until a successful move is achieved.
  • New Test Coverage: Added a dedicated test case for move_to_archive using tempdir to verify the new atomic move and unique naming logic.
  • Security Documentation Update: Documented the TOCTOU vulnerability and its prevention strategies in .jules/sentinel.md.
Changelog
  • .jules/sentinel.md
    • Added a new entry detailing the TOCTOU race condition vulnerability in file archival and its prevention.
  • crates/pebble/src/commands_archive.rs
    • Added safe_move_file function for atomic file operations.
    • Replaced fs::rename with move_to_archive in run_archive.
    • Refactored move_to_archive to use safe_move_file and handle AlreadyExists errors for unique naming.
    • Updated and expanded unit tests for move_to_archive.
Activity
  • PR created automatically by Jules for task 7993948144143463845 started by @matta.
  • Unit and integration tests were run successfully using just check and just test.
  • Fallback behavior and unique naming logic were tested locally.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request effectively resolves the Time-of-Check to Time-of-Use (TOCTOU) vulnerability in the file archival process by introducing a safe_move_file function. The use of fs::hard_link with a fallback to an atomic copy operation is a solid approach. The refactoring of the calling function and the addition of a file-system-based test significantly improve the code's correctness and reliability. I have one suggestion to enhance the error handling in the new safe_move_file function to prevent a potential data duplication issue under certain failure conditions.

Comment on lines +14 to +26
fn safe_move_file(src: &Path, dst: &Path) -> io::Result<()> {
match fs::hard_link(src, dst) {
Ok(_) => fs::remove_file(src),
Err(e) if e.kind() == ErrorKind::AlreadyExists => Err(e),
Err(_) => {
// Fall back to atomic file creation and copy if hard link fails (e.g., across filesystems)
let mut src_file = fs::File::open(src)?;
let mut dst_file = OpenOptions::new().write(true).create_new(true).open(dst)?;
io::copy(&mut src_file, &mut dst_file)?;
fs::remove_file(src)
}
}
}

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.

high

The current implementation of safe_move_file can lead to duplicate archived files if removing the source file fails after a successful hard link or copy.

If fs::remove_file(src) returns an error, the entire safe_move_file operation fails. The caller (run_archive) will abort, leaving a successfully created link/copy in the archive directory but also leaving the original file in place. On the next run, the archiver will process the original file again, see a conflict with the existing archived file, and create a new version with a -2 suffix, resulting in duplicates.

To make the archival process more robust, the operation should be considered successful once the file is present at the destination. A failure to clean up the source file should not cause the entire move to fail. Ideally, this cleanup failure would be logged.

fn safe_move_file(src: &Path, dst: &Path) -> io::Result<()> {
    match fs::hard_link(src, dst) {
        Ok(_) => {
            // The link is created, so the move is logically complete.
            // If removing the source fails, we proceed anyway to prevent duplicates on subsequent runs.
            // Ideally, this failure would be logged.
            let _ = fs::remove_file(src);
            Ok(())
        }
        Err(e) if e.kind() == ErrorKind::AlreadyExists => Err(e),
        Err(_) => {
            // Fall back to atomic file creation and copy if hard link fails (e.g., across filesystems)
            let mut src_file = fs::File::open(src)?;
            let mut dst_file = OpenOptions::new().write(true).create_new(true).open(dst)?;
            io::copy(&mut src_file, &mut dst_file)?;

            // The file is now copied. We can ignore a failure to remove the source.
            let _ = fs::remove_file(src);
            Ok(())
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant