From 1df9661d563b4ea493e45ebc18baa5379064f1ae Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 13 Sep 2026 20:27:49 +0200 Subject: [PATCH 1/2] BUG: detect and ignore cyclic symbolic link chains in sdist Fixes #870. --- mesonpy/__init__.py | 28 ++++++++++++++++++---------- tests/packages/symlinks/ccc.py | 1 + tests/packages/symlinks/cycle | 1 + tests/packages/symlinks/foo | 1 + tests/test_sdist.py | 1 + 5 files changed, 22 insertions(+), 10 deletions(-) create mode 120000 tests/packages/symlinks/ccc.py create mode 120000 tests/packages/symlinks/cycle create mode 120000 tests/packages/symlinks/foo diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index b7d05496a..7e83c63e0 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -1063,24 +1063,31 @@ def sdist(self, directory: Path) -> pathlib.Path: with tarfile.open(meson_dist_path, 'r:gz') as meson_dist, mesonpy._util.create_targz(sdist_path) as sdist: for member in meson_dist.getmembers(): + # Record the original member name. The symbolic link + # resolution loop will make ``member`` point to the link + # target, but it needs to be archived under the original name. + # Symbolic link target resolution must be relative to + # ``member.name``, which therefore cannot be updated in the + # symbolic link resolution loop. + name = member.name + # Recursively resolve symbolic links. The source distribution # archive format specification allows for symbolic links as # long as the target path does not include a '..' component. # This makes symbolic links support unusable in most cases, # therefore include the symbolic link targets as regular files # in all cases. + visited = set() while member.issym(): - name = member.name + # Detect symbolic link chains resulting in a cycle. + if member.name in visited: + warnings.warn( + f'symbolic link resulting in a cycle ignored: {name}', stacklevel=1) + break + visited.add(member.name) target = posixpath.normpath(posixpath.join(posixpath.dirname(member.name), member.linkname)) try: - # This can be implemented using the .replace() method - # in Python 3.12 and later. The .replace() method was - # added as part of PEP 706 and back-ported to Python - # 3.9 and later in patch releases, thus it cannot be - # relied upon until the minimum supported Python - # version is 3.12. - member = copy.copy(meson_dist.getmember(target)) - member.name = name + member = meson_dist.getmember(target) except KeyError: warnings.warn( 'symbolic link with absolute path target, pointing outside the ' @@ -1089,6 +1096,7 @@ def sdist(self, directory: Path) -> pathlib.Path: if member.isdir(): warnings.warn( f'symbolic link pointing to a directory ignored: {name}', stacklevel=1) + break # Copy `member` before starting to modify it member = copy.copy(member) @@ -1113,7 +1121,7 @@ def sdist(self, directory: Path) -> pathlib.Path: member.pax_headers = {} # Rewrite the path to match the sdist distribution name. - stem = member.name.split('/', 1)[1] + stem = name.split('/', 1)[1] member.name = '/'.join((dist_name, stem)) if stem == 'pyproject.toml': diff --git a/tests/packages/symlinks/ccc.py b/tests/packages/symlinks/ccc.py new file mode 120000 index 000000000..caf3b0e3d --- /dev/null +++ b/tests/packages/symlinks/ccc.py @@ -0,0 +1 @@ +submodule/bbb.py \ No newline at end of file diff --git a/tests/packages/symlinks/cycle b/tests/packages/symlinks/cycle new file mode 120000 index 000000000..191028156 --- /dev/null +++ b/tests/packages/symlinks/cycle @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/tests/packages/symlinks/foo b/tests/packages/symlinks/foo new file mode 120000 index 000000000..b9d47ed8c --- /dev/null +++ b/tests/packages/symlinks/foo @@ -0,0 +1 @@ +cycle \ No newline at end of file diff --git a/tests/test_sdist.py b/tests/test_sdist.py index 70a3e357e..9048a4e23 100644 --- a/tests/test_sdist.py +++ b/tests/test_sdist.py @@ -237,6 +237,7 @@ def test_symlinks(tmp_path, sdist_symlinks): 'symlinks-1.0.0/meson.build', 'symlinks-1.0.0/pyproject.toml', 'symlinks-1.0.0/__init__.py', + 'symlinks-1.0.0/ccc.py', 'symlinks-1.0.0/submodule/__init__.py', 'symlinks-1.0.0/submodule/aaa.py', 'symlinks-1.0.0/submodule/bbb.py', From e507abb20ece54de536d7f64a53a092b01909c24 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 13 Sep 2026 20:31:59 +0200 Subject: [PATCH 2/2] MAINT: move code for clarity Avoid coping the object describing the tar file member when it does not need to be modified (directories, spacial files, and symbolic links that have not been resolved). --- mesonpy/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 7e83c63e0..ff4025dd2 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -1098,12 +1098,12 @@ def sdist(self, directory: Path) -> pathlib.Path: f'symbolic link pointing to a directory ignored: {name}', stacklevel=1) break - # Copy `member` before starting to modify it - member = copy.copy(member) - if member.isfile(): file = meson_dist.extractfile(member.name) + # Copy ``member`` before starting to modify it + member = copy.copy(member) + # Reset pax extended header. The tar archive member may be # using pax headers to store some file metadata. The pax # headers are not reset when the metadata is modified and