Why file descriptor leaks matter

A file descriptor is a handle to an operating system resource. In practice, that resource may be:

  • a regular file
  • a TCP connection
  • a Unix domain socket
  • a pipe
  • an event source such as epoll, kqueue, or inotify

If descriptors accumulate, the process may hit limits such as ulimit -n. Once that happens, new connections and file opens begin to fail, often in ways that are hard to diagnose. In security-sensitive services, descriptor exhaustion can become a denial-of-service vector.

Rust usually closes descriptors automatically when values go out of scope. The problem is that “usually” is not enough in systems code. You need to understand where automatic cleanup can be bypassed and how to structure code so cleanup remains reliable under failure.

How Rust normally closes resources

Most standard library types that own OS resources implement Drop. When the value leaves scope, Rust runs the destructor and closes the underlying descriptor.

use std::fs::File;

fn open_and_use() -> std::io::Result<()> {
    let file = File::open("/etc/hosts")?;
    // use file here
    Ok(()) // file is closed here automatically
}

This is the default and preferred behavior. In well-structured code, you should rely on RAII rather than manual cleanup.

However, there are important exceptions.

Common ways leaks happen

PatternRiskTypical cause
mem::forget(value)Destructor never runsIntentional leak or misuse
ManuallyDrop<T>Cleanup becomes manualUnsafe code or FFI wrappers
Rc/Arc cyclesResources stay alive indefinitelyGraph-like ownership mistakes
std::process::Command with inherited FDsChild process inherits unintended descriptorsMissing close-on-exec discipline
FFI ownership transferDouble-close or leakUnclear ownership contract
panic = abort pathsCleanup skipped on abortCrash-only failure mode

The rest of this article focuses on preventing these cases in practical Rust code.

Prefer RAII wrappers over raw descriptors

When working with low-level APIs, avoid storing raw RawFd values as your primary type. Wrap them in a type that owns the descriptor and closes it in Drop.

The standard library already provides useful wrappers:

  • std::fs::File
  • std::net::TcpStream
  • std::os::unix::net::UnixStream
  • std::os::fd::OwnedFd on Unix-like platforms

If you need your own abstraction, model it after these types.

use std::os::fd::{AsRawFd, OwnedFd, RawFd};

pub struct SocketHandle {
    fd: OwnedFd,
}

impl SocketHandle {
    pub fn raw_fd(&self) -> RawFd {
        self.fd.as_raw_fd()
    }
}

This design makes ownership explicit. The handle is closed automatically when SocketHandle is dropped, and callers cannot accidentally duplicate cleanup responsibility.

Avoid mem::forget unless you are intentionally leaking

mem::forget prevents destructors from running. That means any descriptor owned by the forgotten value remains open until process exit.

use std::fs::File;
use std::mem;

fn bad() -> std::io::Result<()> {
    let file = File::open("/etc/hosts")?;
    mem::forget(file); // leaks the file descriptor
    Ok(())
}

In security-sensitive code, this is almost always a bug. If you see mem::forget, ask whether the leak is intentional and documented. If not, replace it with a design that transfers ownership cleanly.

Better alternatives

  • Let the value drop normally.
  • Move ownership into another struct.
  • Use Option<T> and take() when you need conditional release.
  • Use std::mem::drop(value) only when you want to close early, not suppress cleanup.

Be careful with ManuallyDrop

ManuallyDrop<T> disables automatic destruction. It is useful in low-level containers and FFI bindings, but it shifts responsibility to you.

use std::mem::ManuallyDrop;
use std::fs::File;

struct Wrapper {
    file: ManuallyDrop<File>,
}

This can be correct, but only if you implement a complete and audited cleanup path. Any early return, panic, or ownership transfer bug can leak the descriptor.

Guidance for ManuallyDrop

  • Use it only in small, well-reviewed unsafe code.
  • Keep the unsafe surface area minimal.
  • Document exactly who closes the resource.
  • Prefer OwnedFd or a safe wrapper whenever possible.

If you are not writing a container or FFI boundary, ManuallyDrop is usually unnecessary.

Design for early returns and error propagation

Rust’s ? operator makes error handling concise, but cleanup only works if ownership is structured correctly. The key is to ensure the descriptor is owned by a value with a destructor at every exit path.

use std::fs::File;
use std::io::{self, Read};

fn read_config() -> io::Result<String> {
    let mut file = File::open("/etc/myapp/config.toml")?;
    let mut buf = String::new();
    file.read_to_string(&mut buf)?;
    Ok(buf)
}

Even if read_to_string fails, file is dropped when the function returns. No explicit cleanup is needed.

Avoid splitting ownership across unrelated variables

This pattern is risky:

let fd = open_resource()?;
let raw = fd.as_raw_fd();
// later, raw is used independently

Once you extract a raw descriptor, you may accidentally duplicate it, store it beyond the owner’s lifetime, or close it twice. Keep the owning type in scope for as long as possible.

Use OwnedFd and BorrowedFd for explicit ownership

Rust’s modern fd API helps distinguish ownership from temporary access.

  • OwnedFd owns the descriptor and closes it on drop.
  • BorrowedFd is a non-owning view tied to a lifetime.
  • AsFd exposes borrowed access without transferring ownership.

This distinction is valuable because it makes leaks and double-closes harder to express.

use std::os::fd::{AsFd, BorrowedFd, OwnedFd};

fn inspect_fd(fd: BorrowedFd<'_>) {
    let _raw = fd.as_fd();
}

fn takes_ownership(fd: OwnedFd) {
    // closes automatically when function ends
}

Rule of thumb

TypeMeaningSafe default use
OwnedFdOwns and closes the descriptorStore in structs
BorrowedFd<'_>Temporary non-owning referenceFunction parameters
RawFdUnchecked integer handleInterop only

Prefer OwnedFd in application code. Use RawFd only at the boundary where an API requires it.

Prevent leaks across FFI boundaries

FFI is one of the most common places for descriptor leaks. A C library may expect you to free a resource with a specific function, or it may transfer ownership in a way that is not obvious from the Rust side.

Establish a clear ownership contract

Before wrapping an FFI resource, answer these questions:

  • Who allocates the descriptor?
  • Who closes it?
  • Can ownership be transferred?
  • What happens on error?
  • Is the close function idempotent?

If the contract is unclear, wrap the resource in a Rust type that encodes the rules explicitly.

pub struct FfiHandle {
    fd: std::os::fd::OwnedFd,
}

impl FfiHandle {
    pub fn new(fd: std::os::fd::OwnedFd) -> Self {
        Self { fd }
    }
}

If a C API returns a raw descriptor, convert it into OwnedFd as soon as possible. That gives you automatic cleanup and reduces the chance of leaks.

Watch for descriptor inheritance in child processes

A descriptor leak is not always about memory or scope. A process can also leak descriptors into child processes if they are inherited across exec.

This matters for security because a child process may gain access to files or sockets it should not see. It also matters operationally because the parent may think a descriptor is closed while the child still holds it open.

Best practices

  • Ensure descriptors are created with close-on-exec semantics when possible.
  • Use APIs that set FD_CLOEXEC by default.
  • Audit any code that spawns subprocesses.
  • Avoid passing raw descriptors unless the child explicitly needs them.

On Unix, many modern Rust APIs and libraries already set close-on-exec for you, but you should verify this behavior for low-level or platform-specific code.

Handle cycles and shared ownership carefully

Rc and Arc are useful, but they can keep resources alive longer than expected if you create cycles. A cycle involving a resource-owning type can prevent Drop from ever running.

use std::sync::{Arc, Weak};

struct Session {
    // owns a socket or file handle
}

struct Node {
    parent: Option<Weak<Node>>,
    session: Arc<Session>,
}

Use Weak for back-references and avoid storing Arc in both directions unless you have a deliberate lifecycle strategy. For resource-owning objects, a tree or acyclic graph is usually safer than a general graph.

Test for leaks, not just correctness

Leak prevention should be part of your test strategy. Unit tests can verify behavior, but they rarely catch resource exhaustion under load.

Useful testing approaches

  • Run integration tests under low descriptor limits.
  • Repeatedly open and close resources in loops.
  • Use tools like lsof, procfs, or platform-specific counters to observe descriptor growth.
  • Stress error paths, not just success paths.

A simple pattern is to create and drop many handles in a loop and assert that the process remains healthy. For services, load testing is especially important because leaks often appear only after thousands of requests.

Practical checklist

Use this checklist when reviewing Rust code that touches files, sockets, or other descriptors:

  • Prefer owning wrapper types such as File, TcpStream, or OwnedFd.
  • Avoid mem::forget unless the leak is intentional and documented.
  • Keep ManuallyDrop confined to small unsafe modules.
  • Convert raw descriptors into owned types immediately.
  • Use BorrowedFd for temporary access and OwnedFd for ownership.
  • Audit FFI ownership rules carefully.
  • Set close-on-exec behavior for subprocess safety.
  • Avoid Rc/Arc cycles around resource owners.
  • Test under load and under low descriptor limits.

A robust pattern for resource-owning types

A good resource wrapper should be simple, explicit, and hard to misuse.

use std::os::fd::{AsRawFd, OwnedFd, RawFd};

pub struct Connection {
    fd: OwnedFd,
}

impl Connection {
    pub fn new(fd: OwnedFd) -> Self {
        Self { fd }
    }

    pub fn raw_fd(&self) -> RawFd {
        self.fd.as_raw_fd()
    }
}

This pattern keeps ownership in one place, exposes only borrowed access, and lets Rust close the descriptor automatically. If you later add logging, metrics, or protocol state, the cleanup behavior remains unchanged.

Conclusion

File descriptor leaks are not just a performance issue. In server applications, they can become a reliability and security problem that leads to denial of service or unintended resource exposure. Rust gives you strong tools to prevent leaks, but you still need disciplined ownership design, careful FFI boundaries, and attention to low-level escape hatches.

The safest approach is to keep descriptors inside owning types, avoid suppressing destructors, and treat raw handles as an implementation detail rather than a primary abstraction.

Learn more with useful resources