From 0488f676a565a2a91ab2c46cf7dd66ae4781e930 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Thu, 24 Sep 2026 13:43:51 +0200 Subject: [PATCH] gh-158068: venv: do not clear env_dir if it's a symlink or a file Previously `venv --clear` called clear_directory() on env_dir whenever it existed. If env_dir was a symlink to a directory, this followed the link and wiped the contents of the target directory -- possible data loss. If it was a regular file it failed with an unclear error. Now creating a venv over an existing symlink or file raises ValueError before anything is removed, matching the check already done for the non-clear case. Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_venv.py | 28 +++++++++++++++++++ Lib/venv/__init__.py | 7 +++-- ...-09-24-14-00-00.gh-issue-158068.Xf2ogr.rst | 4 +++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-24-14-00-00.gh-issue-158068.Xf2ogr.rst diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 2f30d3108021dc..ad11b7c5f854cc 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -663,6 +663,34 @@ def test_unoverwritable_fails(self): self.assertRaises((ValueError, OSError), venv.create, self.env_dir) self.clear_directory(self.env_dir) + @unittest.skipUnless(can_symlink(), 'Needs symlinks') + def test_clear_symlink_not_followed(self): + # gh-158068: venv --clear must not wipe the contents of a directory + # that env_dir merely points to via a symlink. + target = os.path.join(self.env_dir, 'target') + os.mkdir(target) + keep = os.path.join(target, 'keep') + with open(keep, 'wb') as f: + f.write(b'Still here?') + link = os.path.join(self.env_dir, 'link') + os.symlink(target, link) + + with self.assertRaises(ValueError): + venv.create(link, clear=True) + # The symlink target and its content are left untouched. + self.assertTrue(os.path.islink(link)) + self.assertTrue(os.path.exists(keep)) + + def test_clear_file_not_removed(self): + # gh-158068: venv --clear must not treat a regular file as env_dir. + target = os.path.join(self.env_dir, 'afile') + with open(target, 'wb') as f: + f.write(b'Still here?') + + with self.assertRaises(ValueError): + venv.create(target, clear=True) + self.assertTrue(os.path.isfile(target)) + def test_upgrade(self): """ Test upgrading an existing environment directory. diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index 38e1bfe0c5fdb9..0759d572f995ed 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -149,8 +149,11 @@ def create_if_needed(d): if os.pathsep in os.fspath(env_dir): raise ValueError(f'Refusing to create a venv in {env_dir} because ' f'it contains the PATH separator {os.pathsep}.') - if os.path.exists(env_dir) and self.clear: - self.clear_directory(env_dir) + if self.clear: + if os.path.islink(env_dir) or os.path.isfile(env_dir): + raise ValueError('Unable to create directory %r' % env_dir) + if os.path.exists(env_dir): + self.clear_directory(env_dir) context = types.SimpleNamespace() context.env_dir = env_dir context.env_name = os.path.split(env_dir)[1] diff --git a/Misc/NEWS.d/next/Library/2026-09-24-14-00-00.gh-issue-158068.Xf2ogr.rst b/Misc/NEWS.d/next/Library/2026-09-24-14-00-00.gh-issue-158068.Xf2ogr.rst new file mode 100644 index 00000000000000..29b2f4ef11ec93 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-24-14-00-00.gh-issue-158068.Xf2ogr.rst @@ -0,0 +1,4 @@ +:mod:`venv` no longer clears the target directory when it is a symlink or a +regular file. Previously ``venv --clear`` followed a symlink and could delete +the contents of the linked directory. It now raises :exc:`ValueError` before +removing anything.