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
- Remove the offending symlink so pytest recreates a real directory: `rm /tmp/pytest-of-<user>` (or wherever the message points).
- Point pytest elsewhere with `--basetemp=/path/to/real/dir` or by exporting TMPDIR to a non-symlinked location.
- 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
- Do not symlink /tmp/pytest-of-<user>; let pytest create a real directory.
- Point TMPDIR at a non-symlinked scratch volume in containers.
- In shared CI, clean the temp tree between jobs to clear leftover symlinks.
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
- The temporary directory
- is not a normalized and relative path
- could not create numbered dir with prefix
- directory argument cannot contain :: selection parts
- example " " is not found as a file or directory
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)