Back to Blog
medium SEVERITY8 min read

TOCTOU Symlink Attack Fixed: How Race Conditions Threaten Lock Files

A medium-severity TOCTOU (Time-of-Check to Time-of-Use) race condition vulnerability was discovered and fixed in a Rust application's lock file creation logic, where an attacker could exploit the window between a file existence check and its creation to redirect writes to an attacker-controlled path via a symlink. The fix applies the `O_NOFOLLOW` flag on Unix systems, ensuring the OS refuses to follow symlinks at the lock file path and fails loudly instead of silently writing to an attacker-cont

O
By Orbis AppSec
Published May 18, 2026Reviewed June 3, 2026

Answer Summary

A TOCTOU (Time-of-Check to Time-of-Use) race condition vulnerability in Rust's lock file creation allowed attackers to exploit a timing window between checking if a file exists and creating it, using symlinks to redirect writes to arbitrary paths. The fix applies the `O_NOFOLLOW` flag when opening files on Unix systems, instructing the OS to refuse following symlinks and fail loudly rather than silently writing to attacker-controlled destinations.

Vulnerability at a Glance

cweCWE-367 (Time-of-check Time-of-use (TOCTOU) Race Condition)
fixApply O_NOFOLLOW flag to refuse symlink following and fail safely on Unix systems
riskAttackers can redirect file writes to arbitrary locations via symlink exploitation during the race condition window
languageRust
root causeFile existence check and creation are not atomic; attacker can create symlink between check and write
vulnerabilityTOCTOU (Time-of-Check to Time-of-Use) Symlink Attack

TOCTOU Symlink Attack Fixed: How Race Conditions Threaten Lock Files in Rust

Introduction

At first glance, creating a lock file seems trivial — just open a file, write some data, and you're done. But hidden inside that simple operation is a subtle, dangerous assumption: that the path you checked is still the path you're writing to.

This assumption is exactly what a TOCTOU (Time-of-Check to Time-of-Use) race condition exploits. A recently patched vulnerability in src/peers/lock.rs demonstrated precisely this problem. The lock file creation logic used .create(true) when opening the file, which is functionally equivalent to POSIX O_CREAT — but critically, without O_EXCL. This left a small but exploitable window where a local attacker could slip a symlink into place and redirect the application's file writes to an attacker-controlled path.

If you write Rust (or any systems-level code) that deals with file creation, lock files, or temporary files — especially in shared or world-writable directories — this post is for you.


The Vulnerability Explained

What Is a TOCTOU Race Condition?

A TOCTOU vulnerability occurs when a program:

  1. Checks a condition (e.g., "does this file exist?")
  2. Uses a resource based on that check (e.g., "great, I'll create it")

...but between steps 1 and 2, the state of the world changes. An attacker who can observe or predict what the program is about to do can manipulate the filesystem in that tiny window to cause the program to do something unintended.

How the Vulnerable Code Worked

The original lock file creation in src/peers/lock.rs looked something like this:

// VULNERABLE: Before the fix
use std::fs::OpenOptions;

fn create_lock_file(path: &Path) -> std::io::Result<File> {
    OpenOptions::new()
        .write(true)
        .create(true)   // Creates if not exists — but NOT exclusively
        .open(path)
}

The .create(true) flag maps to the POSIX O_CREAT flag. This tells the OS: "open the file, and create it if it doesn't exist." Sounds reasonable — but it has a critical flaw: it will happily follow symlinks.

This means if the path /var/run/myapp/app.lock is actually a symlink pointing to /etc/cron.d/backdoor, the application will dutifully open and write to /etc/cron.d/backdoor without any complaint.

The Attack Scenario

Here's how a local attacker could exploit this step by step:

1. Attacker observes (or predicts) the lock file path:
   /var/run/myapp/app.lock

2. Attacker removes the lock file (if it exists) or waits for a restart.

3. Attacker creates a symlink:
   $ ln -s /etc/cron.d/backdoor /var/run/myapp/app.lock

4. Application starts, calls create_lock_file("/var/run/myapp/app.lock")

5. OS follows the symlink transparently.

6. Application writes its lock data to /etc/cron.d/backdoor instead.

7. Attacker now has a cron job (or worse) installed with the application's privileges.

The attack window is small — but on a loaded system or with a slow startup sequence, it's absolutely exploitable. And in automated environments (containers, CI/CD pipelines, systemd restarts), the timing becomes even more predictable.

Real-World Impact

Depending on what the application writes to the lock file and what privileges it runs with, this vulnerability could enable:

  • Arbitrary file write — overwriting sensitive system files
  • Privilege escalation — if the app runs as root or a privileged service account
  • Denial of service — corrupting files the application depends on
  • Persistence — writing attacker-controlled content to startup scripts or cron directories

This is rated medium severity, but in the right context (e.g., a daemon running as root), it can escalate quickly to critical impact.


The Fix

What Changed

The fix adds the O_NOFOLLOW flag when opening the lock file on Unix systems. This is a well-known, battle-tested defense against symlink-based TOCTOU attacks.

// FIXED: After the patch
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;

fn create_lock_file(path: &Path) -> std::io::Result<File> {
    #[cfg(unix)]
    {
        OpenOptions::new()
            .write(true)
            .create(true)
            .custom_flags(libc::O_NOFOLLOW)  // ← The critical addition
            .open(path)
    }

    #[cfg(not(unix))]
    {
        // Windows doesn't support O_NOFOLLOW; preserve original behavior
        OpenOptions::new()
            .write(true)
            .create(true)
            .open(path)
    }
}

How O_NOFOLLOW Solves the Problem

The O_NOFOLLOW flag instructs the operating system: "If the final component of this path is a symlink, do not follow it — fail instead."

When an attacker places a symlink at the lock file path and the application tries to open it with O_NOFOLLOW:

  • The OS detects the symlink at the final path component
  • Instead of following it, the open() syscall returns -1
  • errno is set to ELOOP
  • The Rust code receives an Err(...) and can handle it gracefully
Without O_NOFOLLOW:
  open("/var/run/myapp/app.lock")  follows symlink  writes to attacker target 

With O_NOFOLLOW:
  open("/var/run/myapp/app.lock")  detects symlink  returns ELOOP error 

The attack is stopped cold. The application fails safely rather than silently doing something dangerous.

Why the #[cfg(unix)] Guard?

The fix is carefully wrapped in platform-specific compilation guards:

#[cfg(unix)]
// ... use O_NOFOLLOW via libc

#[cfg(not(unix))]
// ... use original behavior

This is important for two reasons:

  1. O_NOFOLLOW is a POSIX/Unix concept — Windows handles symlinks differently and doesn't have this flag.
  2. libc::O_NOFOLLOW won't compile on Windows — the guard prevents build failures on non-Unix targets.

This is idiomatic, safe Rust: apply the strongest available protection on platforms that support it, while maintaining compatibility elsewhere.


Prevention & Best Practices

1. Always Use O_EXCL with O_CREAT for Exclusive Creation

If your intent is to create a file that must not already exist, use both flags together:

// Atomic exclusive creation — fails if file already exists
OpenOptions::new()
    .write(true)
    .create_new(true)  // Rust's equivalent of O_CREAT | O_EXCL
    .open(path)?;

create_new(true) in Rust maps to O_CREAT | O_EXCL, which is atomic — the OS guarantees the check and creation happen as one indivisible operation. No race window exists.

2. Use O_NOFOLLOW for Sensitive File Operations

Whenever you open files in paths that could be influenced by untrusted users (world-writable directories, user-controlled paths, temp directories), add O_NOFOLLOW:

#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;

OpenOptions::new()
    .write(true)
    .create(true)
    .custom_flags(libc::O_NOFOLLOW)
    .open(path)?;

3. Avoid Predictable Lock File Paths in Shared Directories

Lock files in /tmp or other world-writable directories are particularly dangerous. Prefer:

  • Application-specific directories with restricted permissions (chmod 700)
  • Runtime directories managed by systemd (RuntimeDirectory=) or similar
  • Paths that unprivileged users cannot create files in

4. Validate Symlinks Before Use (Defense in Depth)

For extra caution, you can explicitly check whether a path is a symlink before opening it:

use std::fs;

fn open_safe(path: &Path) -> std::io::Result<File> {
    // Explicit symlink check (defense in depth — not a replacement for O_NOFOLLOW)
    let metadata = fs::symlink_metadata(path)?;
    if metadata.file_type().is_symlink() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "Refusing to open symlink at lock file path",
        ));
    }
    // Proceed with O_NOFOLLOW as well
    OpenOptions::new()
        .write(true)
        .create(true)
        .custom_flags(libc::O_NOFOLLOW)
        .open(path)
}

⚠️ Note: A standalone symlink check is still subject to TOCTOU by itself — always combine it with O_NOFOLLOW rather than relying on the check alone.

5. Use Established Lock File Libraries

For production code, consider using well-audited crates that handle these edge cases for you:

  • fd-lock — Cross-platform file locking using file descriptors
  • fslock — Simple, safe file locking
  • lockfile — Lock file creation with proper exclusivity

6. Run Security Linters and Audits

  • cargo audit — Checks your dependency tree for known vulnerabilities
  • cargo-geiger — Identifies unsafe code blocks that may need extra scrutiny
  • Clippy — While it won't catch all security issues, it flags many unsafe patterns
  • Manual code review — File I/O code, especially involving paths and permissions, deserves careful human review

Relevant Security Standards

Standard Reference
CWE-362 Concurrent Execution Using Shared Resource with Improper Synchronization (Race Condition)
CWE-61 UNIX Symbolic Link (Symlink) Following
CWE-377 Insecure Temporary File
OWASP File Upload and Path Traversal
POSIX open(2) man page — O_NOFOLLOW, O_EXCL flags

Conclusion

This vulnerability is a perfect illustration of how a single missing flagO_NOFOLLOW — can be the difference between secure and exploitable code. The original code wasn't carelessly written; .create(true) is the natural, obvious way to create a file in Rust. But in security-sensitive contexts, "obvious" and "safe" aren't always the same thing.

Key Takeaways

  • TOCTOU race conditions are real and exploitable, even with small time windows
  • O_NOFOLLOW is your friend — use it whenever opening files in potentially attacker-influenced paths
  • create_new(true) in Rust (i.e., O_CREAT | O_EXCL) provides atomic exclusive file creation
  • Platform guards (#[cfg(unix)]) let you apply the strongest protection available without breaking cross-platform compatibility
  • Defense in depth — combine multiple mitigations rather than relying on any single one

Security vulnerabilities in file handling are among the oldest and most well-understood class of bugs — yet they continue to appear in modern codebases. The fix here is clean, minimal, and idiomatic. It's a great reminder that security improvements don't always require architectural changes; sometimes, one well-placed flag is all it takes.

Stay curious, audit your file I/O code, and keep your lock files safe. 🔒


This post is part of our ongoing series on security vulnerabilities discovered and fixed in open-source Rust projects. If you found this helpful, consider sharing it with your team or checking out our other posts on secure systems programming.

Frequently Asked Questions

What is a TOCTOU symlink attack?

It's a race condition where an attacker exploits the gap between checking if a file exists and creating it by placing a symlink at that path, causing the application to write to an attacker-controlled location instead.

How do you prevent TOCTOU vulnerabilities in Rust?

Use atomic operations that combine the check and creation in a single system call (like `O_CREAT | O_EXCL`), apply `O_NOFOLLOW` to refuse symlink following, and ensure proper error handling for race conditions.

What CWE is TOCTOU symlink attacks?

CWE-367 (Time-of-check Time-of-use Race Condition) and CWE-59 (Improper Link Resolution Before File Access - 'Link Following').

Is checking file permissions enough to prevent TOCTOU attacks?

No. Permission checks happen before the race window opens. An attacker can still create a symlink between your check and creation. You need atomic operations and `O_NOFOLLOW`.

Can static analysis detect TOCTOU vulnerabilities?

Yes, but with limitations. Static analysis can flag patterns like separate exists() checks followed by file creation, but distinguishing between safe and unsafe patterns requires semantic understanding of the code flow.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

high

How cryptographic binding vulnerabilities happen in Rust OpenSSL and how to fix it

CVE-2026-41676 is a high-severity vulnerability in the rust-openssl crate that could allow attackers to exploit cryptographic operations. The fix involves upgrading from version 0.10.63 to 0.10.81, removing unsafe dependency chains, and ensuring proper OpenSSL binding integrity. This vulnerability demonstrates why keeping cryptographic libraries current is critical for production Rust applications.

high

How a named pipe I/O race condition happens in Rust mio and how to fix it

CVE-2024-27308 is a high-severity vulnerability in the Rust `mio` crate (versions prior to 0.8.11) that exposes a race condition in named pipe I/O event handling on Windows. The fix upgrades `mio` from version 0.8.10 to 0.8.11, closing the window for potential exploitation in applications like `rpm-ostree` that depend on async I/O. Because `mio` sits at the foundation of the Tokio async runtime, this flaw has wide blast radius across the Rust ecosystem.

high

CVE-2026-41676: OpenSSL Bindings Vulnerability Fixed in Rust SDK Cargo.lock

A high-severity vulnerability (CVE-2026-41676) was discovered in the `rust-openssl` crate (version 0.10.73) used in the `apps/rust-sdk` component, as flagged by the Trivy scanner in `Cargo.lock`. The fix upgrades the `openssl` crate from `0.10.73` to `0.10.80` and `openssl-sys` from `0.9.109` to `0.9.116`, closing an exploitable attack surface in production code that handles user-influenced input. Because the Rust SDK sits in the production codebase, any attacker able to reach the OpenSSL code p

critical

Critical Memory Safety Bug: Free of Uninitialized Memory in Rust Telemetry (CVE-2021-29937)

CVE-2021-29937 is a critical memory safety vulnerability in the Rust `telemetry` crate (versions prior to 0.1.3) that allows freeing uninitialized memory, leading to undefined behavior, potential crashes, and possible code execution. The fix involves upgrading the crate from version 0.1.0 to 0.1.3, which patches the unsafe memory handling at the root cause. Despite Rust's reputation for memory safety, this vulnerability demonstrates that `unsafe` code blocks can still introduce serious bugs that

high

CVE-2026-41676: Fixing a High-Severity rust-openssl Vulnerability by Upgrading to 0.10.78

CVE-2026-41676 is a high-severity vulnerability in the rust-openssl crate, which provides OpenSSL bindings for Rust applications. The fix involves upgrading the dependency from version 0.10.75 to 0.10.78 in the project's Cargo.lock file, closing a security gap that could expose applications to adversarial exploitation. Keeping cryptographic dependencies current is one of the most impactful and straightforward security practices any Rust team can adopt.

high

CVE-2026-41676: Fixing a High-Severity OpenSSL Vulnerability in Rust Applications

CVE-2026-41676 is a high-severity vulnerability discovered in the rust-openssl crate, which provides OpenSSL bindings for Rust applications. Left unpatched, this flaw could expose backend services to cryptographic or memory-safety attacks through the underlying OpenSSL layer. The fix involved upgrading the rust-openssl dependency from version 0.10.75 to 0.10.78 in the project's Cargo.toml and Cargo.lock files.