Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Python POSIX dir_fd and O_NOFOLLOW: reject a final symlink on an owned open

Last updated: 1 Oct 20265 min read
tutorial
IntermediateBy AITrove Editorial

A directory descriptor and O_NOFOLLOW let a POSIX open reject a symlink in its final path component.

Download Python source kit

Operation contract

An owned temporary directory contains one receipt file and a symlink pointing at it. The fixture opens the real file relative to an already-open directory descriptor, reads its exact bytes, and sees an error when it tries to open the symlink with O_NOFOLLOW.

Failure boundary

This is a checked POSIX fixture; Windows requires another implementation. O_NOFOLLOW guards the final component only, and a directory descriptor does not by itself make every later operation race-free. Do not use string-prefix path checks as a substitute for descriptor-based ownership and a clear symlink policy.

Working program

python
import errno
import os
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as directory:
    receipt_path = Path(directory) / "receipt-47.txt"
    receipt_path.write_bytes(b"paid:47")
    (Path(directory) / "current.txt").symlink_to(receipt_path.name)
    directory_fd = os.open(directory, os.O_RDONLY | os.O_DIRECTORY)
    try:
        receipt_fd = os.open("receipt-47.txt", os.O_RDONLY | os.O_NOFOLLOW,
                             dir_fd=directory_fd)
        try:
            print("receipt", os.read(receipt_fd, 7))
        finally:
            os.close(receipt_fd)
        try:
            link_fd = os.open("current.txt", os.O_RDONLY | os.O_NOFOLLOW,
                              dir_fd=directory_fd)
        except OSError as failure:
            print("link_rejected", failure.errno in (errno.ELOOP, errno.EMLINK))
        else:
            os.close(link_fd)
            raise AssertionError("symlink unexpectedly opened")
    finally:
        os.close(directory_fd)

Output

Output
receipt b'paid:47'
link_rejected True

Costs and limits

Descriptor-based opens cost system calls and require explicit close ownership. They avoid trusting an earlier resolved path name, but the full threat model still determines which components and operations need protection.

Common Mistakes

  • O_NOFOLLOW covers the final component, not every ancestor in a path.
  • A prior path check can become stale before a later open.
  • Close both the opened file and directory descriptors on every path.

Connected lessons

Test this contract.

python
nofollow-owned-directory
Storage details