{"record":{"id":"d01c0c2d9c824590","repo":"pytest-dev/pytest","slug":"the-temporary-directory-rootdir-is-a-symbolic-li","errorCode":null,"errorMessage":"The temporary directory {rootdir} is a symbolic link. Fix this and try again.","messagePattern":"The temporary directory (.+?) is a symbolic link\\. Fix this and try again\\.","errorType":"exception","errorClass":"OSError","httpStatus":null,"severity":"error","filePath":"src/_pytest/tmpdir.py","lineNumber":189,"sourceCode":"            # temproot is usually shared).\n            # Also, to keep things private, fixup any world-readable temp\n            # rootdir's permissions. Historically 0o755 was used, so we can't\n            # just error out on this, at least for a while.\n            # Don't follow symlinks, otherwise we're open to symlink-swapping\n            # TOCTOU vulnerability.\n            # This check makes us vulnerable to a DoS - a user can `mkdir\n            # /tmp/pytest-of-otheruser` and then `otheruser` will fail this\n            # check. For now we don't consider it a real problem. otheruser can\n            # change their TMPDIR or --basetemp, and maybe give the prankster a\n            # good scolding.\n            uid = get_user_id()\n            if uid is not None:\n                stat_follow_symlinks = (\n                    False if os.stat in os.supports_follow_symlinks else True\n                )\n                rootdir_stat = rootdir.stat(follow_symlinks=stat_follow_symlinks)\n                if stat.S_ISLNK(rootdir_stat.st_mode):\n                    raise OSError(\n                        f\"The temporary directory {rootdir} is a symbolic link. \"\n                        \"Fix this and try again.\"\n                    )\n                if rootdir_stat.st_uid != uid:\n                    raise OSError(\n                        f\"The temporary directory {rootdir} is not owned by the current user. \"\n                        \"Fix this and try again.\"\n                    )\n                if (rootdir_stat.st_mode & 0o077) != 0:\n                    chmod_follow_symlinks = (\n                        False if os.chmod in os.supports_follow_symlinks else True\n                    )\n                    rootdir.chmod(\n                        rootdir_stat.st_mode & ~0o077,\n                        follow_symlinks=chmod_follow_symlinks,\n                    )\n            keep = self._retention_count\n            if self._retention_policy == \"none\":","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/pytest-dev/pytest/blob/0d6fbdeffa57c796123f62f81f7dd370d9b7ecdc/src/_pytest/tmpdir.py#L171-L207","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# shell fix (no code change needed)\n# before: /tmp/pytest-of-alice -> /mnt/scratch/alice (symlink)\nrm /tmp/pytest-of-alice\n# after: pytest recreates it as a real directory on next run","handlingStrategy":"validation","validationCode":"import os, stat, tempfile, getpass\n\ndef ensure_real_temproot() -> str:\n    \"\"\"Pick a TMPDIR whose pytest-of-<user> is not a symlink.\"\"\"\n    root = os.path.join(tempfile.gettempdir(), f\"pytest-of-{getpass.getuser()}\")\n    if os.path.islink(root):\n        raise RuntimeError(f\"{root} is a symlink; remove it or set TMPDIR\")\n    return tempfile.gettempdir()","typeGuard":"import os, stat\n\ndef basetemp_is_safe(path: str) -> bool:\n    if not os.path.exists(path):\n        return True  # pytest will create it\n    st = os.stat(path, follow_symlinks=False) if hasattr(os, \"stat\") else os.lstat(path)\n    return not stat.S_ISLNK(st.st_mode)","tryCatchPattern":"# pytest raises OSError before tests run; catch in a wrapper script.\nimport subprocess, os, sys\ndef run_pytest_safe():\n    for cand in (os.environ.get(\"TMPDIR\"), \"/var/tmp\", \"/tmp\"):\n        if cand:\n            os.environ[\"TMPDIR\"] = cand\n            rc = subprocess.call([sys.executable, \"-m\", \"pytest\"])\n            if rc == 0 or rc != 1:\n                return rc\n    return rc","preventionTips":["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."],"tags":["security","tmp-path","symlink","filesystem","toctou"],"backgroundTag":null,"analyzedSha":"0d6fbdeffa57c796123f62f81f7dd370d9b7ecdc","analyzedAt":"2026-08-11T20:52:36.969Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}