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 orbisai0security
May 18, 2026

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.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

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.

high

ReDoS in Nushell's TUI: When Search Input Freezes Your Terminal

A high-severity Regular Expression Denial of Service (ReDoS) vulnerability was discovered and patched in Nushell's interactive TUI explorer, where unvalidated user keystrokes could be passed directly into regex compilation, allowing adversarial inputs to consume 100% CPU and freeze the interface. This fix adds proper input validation and length limits to the search input handler, preventing catastrophic backtracking attacks. Understanding this vulnerability is essential for any developer buildin

medium

Resource Exhaustion via Unchecked File Imports: How Missing Limits Create DoS Vulnerabilities

A medium-severity vulnerability in a file transfer receiver allowed attackers to exhaust server resources by sending maliciously crafted import files with no size limits, no JSON depth restrictions, and millions of entries loaded directly into memory. The fix introduces explicit input validation guards that reject unauthenticated or malformed requests before any disk or network operations begin. Understanding this class of vulnerability is essential for any developer building file ingestion pipe

medium

Mass Assignment Vulnerability: Why Your Rails Models Need attr_accessible

A medium-severity mass assignment vulnerability was identified in a Ruby on Rails model that lacked proper attribute whitelisting via `attr_accessible` or strong parameters. Without this protection, attackers can manipulate any model attribute through crafted HTTP requests, potentially escalating privileges or corrupting data. The fix enforces explicit attribute allowlisting, closing the door on unauthorized mass assignment exploitation.