Repository navigation
3.9.25.2: tempfile.TemporaryDirectory cleanup race fix (CVE-2026-12345) - #90
Conversation
ezequielp-activestate
left a comment
There was a problem hiding this comment.
No blockers. Approving, with one small test nit.
This PR is stacked on the still-open #88 (3.9.25.1-security), so it can only land after that one.
What I checked (CPython built from this branch, Linux x86_64, Debian glibc 2.28, tests run as a non-root user):
-
The fix works. I wrote an independent repro: a subdirectory of a
TemporaryDirectoryis made unwritable so cleanup takes thePermissionErrorpath, and the subdirectory is swapped for a symlink to an outside directory at the handler's firstchmodor at its retryunlink.base ( 3.9.25.1-security)this PR swap at the handler's chmodoutside file deleted untouched swap at the retry unlinkoutside file deleted untouched (cleanup raises NotADirectoryErrorfor the symlink) -
Matches upstream. The
shutil.py/tempfile.py/os.pychanges are the same as the 3.10 port (#89), apart from the privateignore_errorson_rmtree()that 3.9 needs. That port matches the upstream 3.12 backport (c39b52d), with the saferos.lstat(base, dir_fd=…)in place of the path-basedisdir()/isjunction().HAVE_LSTATis in this branch'sposixmodule.c, so theos.pyline takes effect. -
No later fixes missed. I walked upstream
Lib/tempfile.pyandLib/shutil.pyforward on main, 3.14, 3.13 and 3.12, and nothing later touches it. -
shutil compatibility. Existing
onerrorcallbacks still get three positional arguments. -
Tests:
test_tempfile,test_shutilandtest_ospass.
⚪ Nit: test_cleanup_with_symlink_race leaks a temporary directory on 3.9. After the test, $TMPDIR holds a leftover <random>/dir1 (mode 0700). Run with /tmp as the working directory, regrtest reports test_tempfile failed (env changed). The cause is 3.9's TemporaryDirectory.cleanup(), which only runs _rmtree() if self._finalizer.detach(). The first d1.cleanup() detaches the finalizer even though it raises, so the second d1.cleanup() in the test's finally does nothing. 3.10's cleanup() also retries if … or _os.path.exists(self.name), which is why the same test is clean in #89. Calling d1._rmtree(d1.name) (or shutil.rmtree) in the finally fixes it.
Not assessed: Windows (the fd path is not used there), macOS/BSD chflags, and the full test suite.
|
Fixed the test leak in 63a77ee. The
|
ezequielp-activestate
left a comment
There was a problem hiding this comment.
Re-review of 63a77ee: still approved. The only change is the test fix for the temp-directory leak I noted: test_cleanup_with_symlink_race now removes the directory with d1._rmtree(d1.name), since 3.9's cleanup() does nothing once the failed attempt has detached the finalizer. On Python 3.9 with this branch's Lib/, python -m test --fail-env-changed test_tempfile passes, and running the race test from /tmp leaves nothing behind.
…or` during tempdirectory cleanup (pythonGH-29940) (pythonGH-112754) (cherry picked from commit 8cdfee1) Co-authored-by: Ken Jin <kenjin@python.org> Co-authored-by: andrei kulakov <andrei.avk@gmail.com> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> (cherry picked from commit 23920a0) (cherry picked from commit e3888c0) 3.9.25.x: 3.9's TemporaryDirectory has no ignore_cleanup_errors (added in 3.10), so _rmtree() gains a private ignore_errors parameter that is always False here; it keeps the code in the 3.10/3.11 shape for the follow-up fixes. The ignore_cleanup_errors test is not ported.
…nup on Windows (pythonGH-112762) (pythonGH-112848) (cherry picked from commit b2923a6) (cherry picked from commit a18201b) (cherry picked from commit ffb9ae4) 3.9.25.x: test uses support.change_cwd (3.9 has no test.support.os_helper).
…TemporaryDirectory (CVE-2026-12345) When TemporaryDirectory cleanup recovers from a PermissionError it resets permissions and retries removal. It did that by resolving paths again, so an attacker who could modify the tree during cleanup could replace a directory with a symlink and make cleanup delete files, or reset their permissions and flags, outside of the temporary directory. Files are now handled relative to the open directory file descriptors that the fd-based shutil.rmtree() walk already holds. Port of the upstream fix (pythonGH-157580, pythonGH-158430), based on the 3.12 backport (python#159074), plus pythongh-134993 (os.lstat in os.supports_dir_fd). 3.10.22.x: the 3.12 fix relies on 3.12's shutil.rmtree() API (onexc, a public dir_fd parameter, and the _onexc_kwargs switch that passes direntry/dir_fd to the callback). 3.10 only has onerror and a recursive _rmtree_safe_fd(), so this adds the private keyword arguments _dir_fd and _onerror_kwargs to rmtree() instead and threads the parent directory fd and os.DirEntry through _rmtree_safe_fd(). With _onerror_kwargs left false, existing onerror callbacks get the same three positional arguments as before. TemporaryDirectory._rmtree() is ported onto the 3.11 shape of the code (bpo-43153 and pythongh-79325 are picked first), using os.lstat(base, dir_fd=...) where 3.12 uses os.path.isdir()/isjunction() on the full path. Tests are adapted: onerror instead of onexc, local root and platform skips instead of support.requires_non_root_user and PY_SUPPORT_TIER. (cherry picked from commit c39b52d) (cherry picked from commit 5c20517) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 776b466) 3.9.25.x: tests use test.support instead of test.support.os_helper, and test_parent_mode_preserved only covers the default (no ignore_errors) case because 3.9's TemporaryDirectory has no ignore_cleanup_errors.
ActiveState security release of the end-of-life 3.9 line: the pythongh-157579 tempfile.TemporaryDirectory cleanup race fix (CVE-2026-12345), ported from 3.12, with its prerequisites bpo-43153 and pythongh-79325. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up. 3.9's TemporaryDirectory.cleanup() only removes the tree if self._finalizer.detach() succeeds. The test's first cleanup() detaches the finalizer even though it raises, so the second cleanup() in the finally block did nothing and left <tmp>/dir1 behind; regrtest reported test_tempfile as "env changed". Remove the tree with d1._rmtree() instead. (3.10's cleanup() also retries while the directory still exists, so #89 is unaffected.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63a77ee to
89e9acb
Compare
|
Rebased onto the release branch and retargeted, after #87/#88 (and #109/#110/#112) merged and
|
ActiveState security release 3.9.25.2: fixes CVE-2026-12345 (PSF-2026-42, CVSS 4.0 5.9 Medium), a race condition in
tempfile.TemporaryDirectorycleanup.Stacked on #88 (
3.9.25.1-security). Merge #88 first; this PR's base will then need to change to3.9.25.1's merged base branch.The vulnerability
When cleanup hits a
PermissionError, it resets permissions and retries the removal by resolving the path again. An attacker who can modify the tree during cleanup can swap a directory for a symlink. Cleanup then deletes files, or resets their permissions and flags, outside the temporary directory, with the privileges of the process doing the cleanup. Upstream issue: python#157579. The fix is in main and in 3.12 through 3.15; it was not backported to 3.11.Commits
PermissionErrorbehindNotADirectoryErrorduring cleanup. Prerequisite.repeatedguard). Prerequisite.tempfile.TemporaryDirectorypython/cpython#157579: the security fix, ported from the 3.12 backport ([3.12] gh-157579: Fix race condition in the cleanup oftempfile.TemporaryDirectory(GH-157580) (GH-158430) python/cpython#159074), plus os.lstat() supports dir_fd but is not in os.supports_dir_fd python/cpython#134993 (os.lstatinos.supports_dir_fd). Files are reset and removed relative to the directory fds that the fd-basedshutil.rmtree()walk already holds, instead of by path.How the port differs from 3.12
3.12's fix depends on 3.12's
shutil.rmtree()(onexc, a publicdir_fdparameter, and the_onexc_kwargsswitch). 3.9 only hasonerrorand a recursive_rmtree_safe_fd(), so:rmtree()gains private keyword arguments_dir_fdand_onerror_kwargs._rmtree_safe_fd()now passes the parent directory fd and theos.DirEntryalong to the error callback._onerror_kwargsleft false (every existing caller),onerrorcallbacks still get the same three positional arguments.TemporaryDirectory._rmtree()usesos.lstat(base, dir_fd=...)where 3.12 checks the full path withisdir()/isjunction().TemporaryDirectoryhas noignore_cleanup_errors(it arrived in 3.10)._rmtree()gets a privateignore_errorsparameter that is always False, so the code matches the 3.10/3.11 shape. Tests usetest.supportinstead ofos_helper.The adaptations are described in the commit message bodies.
Testing (Linux x86_64, local build)
python -m testsuite passes.test_cleanup_with_symlink_race,test_parent_mode_preserved,test_cleanup_safe(tempfile) andtest_on_error_kwargs(shutil).test_cleanup_with_symlink_racefails on the base branch: the file outside the temp directory gets deleted. It passes with this change.