pytest-dev/pytest · error · OSError

The temporary directory

Error message

The temporary directory {rootdir} is a symbolic link. Fix this and try again.

What it means

When pytest computes the default basetemp root (pytest-of-<user> under TMPDIR), it stats the directory without following symlinks where the platform allows and rejects it if the mode is a symlink. This blocks a symlink-swapping TOCTOU attack where an attacker could redirect the predictable temp path to an arbitrary target between pytest's stat and its writes. The check is deliberately conservative: any symlink at that path is fatal.

Solutions

  1. Remove the offending symlink so pytest recreates a real directory: `rm /tmp/pytest-of-<user>` (or wherever the message points).
  2. Point pytest elsewhere with `--basetemp=/path/to/real/dir` or by exporting TMPDIR to a non-symlinked location.
  3. Audit whoever created the symlink — it may be unintentional or hostile in shared environments.

Example fix

# shell fix (no code change needed)
# before: /tmp/pytest-of-alice -> /mnt/scratch/alice (symlink)
rm /tmp/pytest-of-alice
# after: pytest recreates it as a real directory on next run
Defensive patterns

Strategy: validation

Validate before calling

import os, stat, tempfile, getpass

def ensure_real_temproot() -> str:
    """Pick a TMPDIR whose pytest-of-<user> is not a symlink."""
    root = os.path.join(tempfile.gettempdir(), f"pytest-of-{getpass.getuser()}")
    if os.path.islink(root):
        raise RuntimeError(f"{root} is a symlink; remove it or set TMPDIR")
    return tempfile.gettempdir()

Type guard

import os, stat

def basetemp_is_safe(path: str) -> bool:
    if not os.path.exists(path):
        return True  # pytest will create it
    st = os.stat(path, follow_symlinks=False) if hasattr(os, "stat") else os.lstat(path)
    return not stat.S_ISLNK(st.st_mode)

Try / catch

# pytest raises OSError before tests run; catch in a wrapper script.
import subprocess, os, sys
def run_pytest_safe():
    for cand in (os.environ.get("TMPDIR"), "/var/tmp", "/tmp"):
        if cand:
            os.environ["TMPDIR"] = cand
            rc = subprocess.call([sys.executable, "-m", "pytest"])
            if rc == 0 or rc != 1:
                return rc
    return rc

Prevention

When it happens

Trigger: An earlier process or another user symlinked /tmp/pytest-of-<user> (or the platform temp root) to elsewhere; TMPDIR itself resolves through a chain where pytest-of-<user> ends up a symlink; a container/CI image that symlinks the temp area for space reasons.

Common situations: Shared CI runners where one job left a symlink behind; misconfigured TMPDIR pointing at a symlinked scratch volume; an admin 'helpfully' redirecting the dir to a bigger disk; the documented prank/DoS scenario from the source comment.

Related errors


AI-assisted analysis of pytest-dev/pytest@0d6fbdeffa (2026-08-11). Data as JSON: /api/errors/d01c0c2d9c824590. Report an issue: GitHub.

Appendix: source

Thrown at src/_pytest/tmpdir.py:189

            # temproot is usually shared).
            # Also, to keep things private, fixup any world-readable temp
            # rootdir's permissions. Historically 0o755 was used, so we can't
            # just error out on this, at least for a while.
            # Don't follow symlinks, otherwise we're open to symlink-swapping
            # TOCTOU vulnerability.
            # This check makes us vulnerable to a DoS - a user can `mkdir
            # /tmp/pytest-of-otheruser` and then `otheruser` will fail this
            # check. For now we don't consider it a real problem. otheruser can
            # change their TMPDIR or --basetemp, and maybe give the prankster a
            # good scolding.
            uid = get_user_id()
            if uid is not None:
                stat_follow_symlinks = (
                    False if os.stat in os.supports_follow_symlinks else True
                )
                rootdir_stat = rootdir.stat(follow_symlinks=stat_follow_symlinks)
                if stat.S_ISLNK(rootdir_stat.st_mode):
                    raise OSError(
                        f"The temporary directory {rootdir} is a symbolic link. "
                        "Fix this and try again."
                    )
                if rootdir_stat.st_uid != uid:
                    raise OSError(
                        f"The temporary directory {rootdir} is not owned by the current user. "
                        "Fix this and try again."
                    )
                if (rootdir_stat.st_mode & 0o077) != 0:
                    chmod_follow_symlinks = (
                        False if os.chmod in os.supports_follow_symlinks else True
                    )
                    rootdir.chmod(
                        rootdir_stat.st_mode & ~0o077,
                        follow_symlinks=chmod_follow_symlinks,
                    )
            keep = self._retention_count
            if self._retention_policy == "none":

View on GitHub (pinned to 0d6fbdeffa)