diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 678615ea172aef..43c2a552ee90b7 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -663,6 +663,24 @@ 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_symlinked_directory_error(self): + """ + Test that venv creation fails when a directory inside the target + directory is a symlink. + """ + outside = tempfile.mkdtemp() + self.addCleanup(rmtree, outside) + for n in range(1, len(self.lib) + 1): + subdirs = self.lib[:n] + with self.subTest(subdirs=subdirs): + rmtree(self.env_dir) + os.makedirs(os.path.join(self.env_dir, *subdirs[:-1])) + os.symlink(outside, os.path.join(self.env_dir, *subdirs), + target_is_directory=True) + self.assertRaises(ValueError, venv.create, self.env_dir) + self.assertEqual(os.listdir(outside), []) + def test_upgrade(self): """ Test upgrading an existing environment directory. diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index 620168be16862e..60fe070b1f9f4b 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -141,6 +141,12 @@ def ensure_directories(self, env_dir): """ def create_if_needed(d): + path = env_dir + for part in os.path.relpath(d, env_dir).split(os.sep)[:-1]: + path = os.path.join(path, part) + if os.path.islink(path) or os.path.isjunction(path): + raise ValueError(f'Refusing to create a venv in {env_dir} ' + f'because {path} is a link.') if not os.path.exists(d): os.makedirs(d) elif os.path.islink(d) or os.path.isfile(d): diff --git a/Misc/NEWS.d/next/Library/2026-10-30-10-36-30.gh-issue-160782.n6JCUU.rst b/Misc/NEWS.d/next/Library/2026-10-30-10-36-30.gh-issue-160782.n6JCUU.rst new file mode 100644 index 00000000000000..d4da24bdca0b5e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-30-10-36-30.gh-issue-160782.n6JCUU.rst @@ -0,0 +1,2 @@ +:mod:`venv`: a symbolic link or junction in place of a directory inside the +environment now raises :exc:`ValueError` instead of being followed.