Repository navigation
3.10.22.2: tempfile.TemporaryDirectory cleanup race fix (CVE-2026-12345) - #89
Conversation
ezequielp-activestate
left a comment
There was a problem hiding this comment.
No blockers. Approving.
This PR is stacked on the still-open #87 (3.10.22.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.10.22.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)The handler now works only through directory fds (
chmod(fd),unlink(name, dir_fd=…)). On this glibcos.chmodis not insupports_follow_symlinks, so_resetperms_attakes theO_NOFOLLOWopen +fchmodbranch, as upstream does. -
Matches upstream. The tempfile side is the 3.12 backport (c39b52d) almost line for line. The one deliberate difference uses
os.lstat(base, dir_fd=…)instead of the path-basedisdir()/isjunction()check, which is the safer form.HAVE_LSTATis in this branch'sposixmodule.chave_functions, so the pythongh-134993 line inos.pytakes effect. -
No later fixes missed. I walked upstream
Lib/tempfile.pyandLib/shutil.pyon main, 3.14, 3.13 and 3.12 forward from the fix, and nothing later touches it. The only other recent cleanup change, pythongh-154307 (DragonFly BSD), is not security and not on 3.12. -
shutil compatibility. Existing
onerrorcallbacks still get exactly three positional arguments, because the kwargs are only passed with the private_onerror_kwargs=True._rmtree_unsafecallers are unaffected. -
Tests:
test_tempfile,test_shutilandtest_ospass on this branch and on the base.
Not assessed: Windows (the fd path is not used there), macOS/BSD chflags, and the full test suite.
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>
…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)
…nup on Windows (pythonGH-112762) (pythonGH-112848) (cherry picked from commit b2923a6) (cherry picked from commit a18201b)
….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>
ActiveState security release of the end-of-life 3.10 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>
b1cc1e4 to
fe67860
Compare
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>
|
Rebased onto the release branch and retargeted, after #87/#88 (and #109/#110/#112) merged and
|
ActiveState security release 3.10.22.2: fixes CVE-2026-12345 (PSF-2026-42, CVSS 4.0 5.9 Medium), a race condition in
tempfile.TemporaryDirectorycleanup.Stacked on #87 (
3.10.22.1-security). Merge #87 first; this PR's base will then need to change to3.10.22.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.10 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().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.