python3-setuptools: Fix CVE-2026-59890

This patch applies the upstream fix as referenced in [2],
using the commit shown in [1].

[1] dd9f436a36
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-59890

(From OE-Core rev: 0c89d54002ed0411ea34a926ccb80c4b6e4d858c)

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
Signed-off-by: Yoann Congal <yoann.congal@smile.fr>
Signed-off-by: Paul Barker <paul@pbarker.dev>
This commit is contained in:
Darsh Kelaiya
2026-07-23 01:40:51 -07:00
committed by Paul Barker
parent 9b68c3fb71
commit 7a6a4aa3b1
2 changed files with 195 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
From d54dff79a9568c25551092711c7ebc28422006a4 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" <jaraco@jaraco.com>
Date: Sat, 27 Jun 2026 10:46:34 -0400
Subject: [PATCH] Normalize Unicode form when matching MANIFEST.in patterns
FileList matched MANIFEST.in patterns against on-disk names byte-for-byte
with no Unicode normalization. On macOS APFS/HFS+, a file stored NFD and a
pattern authored NFC denote the same file but differ byte-for-byte, so an
exclude/global-exclude/recursive-exclude/prune rule could silently fail to
drop a non-ASCII-named file, publishing it in the sdist despite the rule.
Normalize both the pattern and the candidate path to NFC before matching,
via a new unicode_utils.normalize() helper and a _NormalizedMatcher wrapper
around the compiled pattern in translate_pattern.
Fixes GHSA-h35f-9h28-mq5c.
CVE: CVE-2026-59890
Upstream-Status: Backport [https://github.com/pypa/setuptools/commit/dd9f436a36486b4cb8a4c70a2321548b0be09b8f]
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit dd9f436a36486b4cb8a4c70a2321548b0be09b8f)
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst | 7 +++
setuptools/command/egg_info.py | 28 +++++++++++-
setuptools/tests/test_manifest.py | 45 +++++++++++++++++++
setuptools/unicode_utils.py | 14 ++++++
4 files changed, 93 insertions(+), 1 deletion(-)
create mode 100644 newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst
diff --git a/newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst b/newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst
new file mode 100644
index 000000000..42d6c4cfe
--- /dev/null
+++ b/newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst
@@ -0,0 +1,7 @@
+``MANIFEST.in`` matching (via ``FileList``) is now insensitive to Unicode
+normalization form. A pattern authored in one form (e.g. NFC, as typically
+saved by editors) now matches a file whose name is stored on disk in another
+(e.g. NFD, as produced by macOS APFS/HFS+). Previously an ``exclude``,
+``global-exclude``, ``recursive-exclude``, or ``prune`` rule could silently
+fail to drop a non-ASCII-named file from the source distribution, publishing
+it despite the exclusion -- see GHSA-h35f-9h28-mq5c.
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
index 62d2feea9..e858708ee 100644
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -34,6 +34,27 @@ from ..warnings import SetuptoolsDeprecationWarning
PY_MAJOR = '{}.{}'.format(*sys.version_info)
+class _NormalizedMatcher:
+ """
+ Wrap a compiled pattern so that matching is insensitive to Unicode
+ normalization form.
+
+ File names walked from disk (NFD on macOS APFS/HFS+) and patterns from
+ ``MANIFEST.in`` (typically NFC) can denote the same file while differing
+ byte-for-byte. Normalizing both sides before matching keeps an exclusion
+ (or inclusion) from silently failing. See GHSA-h35f-9h28-mq5c.
+ """
+
+ def __init__(self, pattern: re.Pattern) -> None:
+ self._pattern = pattern
+
+ def match(self, path):
+ return self._pattern.match(unicode_utils.normalize(path))
+
+ def search(self, path):
+ return self._pattern.search(unicode_utils.normalize(path))
+
+
def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
"""
Translate a file path glob like '*.txt' in to a regular expression.
@@ -43,6 +64,11 @@ def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
"""
pat = ''
+ # Normalize the pattern so it matches paths regardless of the Unicode
+ # normalization form used on disk (GHSA-h35f-9h28-mq5c). Candidate paths
+ # are normalized to the same form by ``_NormalizedMatcher``.
+ glob = unicode_utils.normalize(glob)
+
# This will split on '/' within [character classes]. This is deliberate.
chunks = glob.split(os.path.sep)
@@ -114,7 +140,7 @@ def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
pat += sep
pat += r'\Z'
- return re.compile(pat, flags=re.MULTILINE | re.DOTALL)
+ return _NormalizedMatcher(re.compile(pat, flags=re.MULTILINE | re.DOTALL))
class InfoCommon:
diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py
index fbd21b197..1a7441fc3 100644
--- a/setuptools/tests/test_manifest.py
+++ b/setuptools/tests/test_manifest.py
@@ -10,6 +10,7 @@ import io
import logging
from distutils import log
from distutils.errors import DistutilsTemplateError
+import unicodedata
from setuptools.command.egg_info import FileList, egg_info, translate_pattern
from setuptools.dist import Distribution
@@ -158,6 +159,21 @@ def test_translated_pattern_mismatch(pattern_mismatch):
assert not translate_pattern(pattern).match(target)
+def test_translate_pattern_unicode_normalization():
+ """
+ Matching is insensitive to Unicode normalization form: a pattern authored
+ in one form matches a path stored on disk in another (and vice versa), so
+ that an exclusion cannot be bypassed by an NFC/NFD mismatch.
+
+ Regression test for GHSA-h35f-9h28-mq5c.
+ """
+ nfc = unicodedata.normalize('NFC', 'café.txt') # 'café.txt' composed
+ nfd = unicodedata.normalize('NFD', 'café.txt') # 'café.txt' decomposed
+ assert nfc != nfd # the two byte forms genuinely differ
+ assert translate_pattern(nfc).match(nfd)
+ assert translate_pattern(nfd).match(nfc)
+
+
class TempDirTestCase:
def setup_method(self, method):
self.temp_dir = tempfile.mkdtemp()
@@ -331,6 +347,35 @@ class TestManifestTest(TempDirTestCase):
files = default_files | set([ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')])
assert files == self.get_files()
+ def test_global_exclude_unicode_normalization(self):
+ """
+ A ``global-exclude`` authored NFC must drop a file whose on-disk name
+ is NFD: on macOS APFS/HFS+ the two are the same file, and even on
+ case/normalization-exact filesystems the decomposed name can be
+ committed and reach the build. Otherwise the file is published in the
+ sdist despite the exclusion.
+
+ Regression test for GHSA-h35f-9h28-mq5c.
+ """
+ nfc_name = unicodedata.normalize('NFC', 'café.txt')
+ nfd_name = unicodedata.normalize('NFD', 'café.txt')
+ assert nfc_name != nfd_name
+ # write the file under its decomposed (NFD) name ...
+ touch(os.path.join(self.temp_dir, 'app', nfd_name))
+ # ... and exclude it with the composed (NFC) form.
+ self.make_manifest(
+ f"""
+ global-include *.txt
+ global-exclude {nfc_name}
+ """
+ )
+ leaked = {
+ f
+ for f in self.get_files()
+ if unicodedata.normalize('NFC', os.path.basename(f)) == nfc_name
+ }
+ assert not leaked, f"excluded file leaked into manifest: {leaked}"
+
class TestFileListTest(TempDirTestCase):
"""
diff --git a/setuptools/unicode_utils.py b/setuptools/unicode_utils.py
index d43dcc11f..311d4075a 100644
--- a/setuptools/unicode_utils.py
+++ b/setuptools/unicode_utils.py
@@ -15,6 +15,20 @@ def decompose(path):
return path
+def normalize(text):
+ """
+ Return *text* in a canonical Unicode form (NFC) so that names which are
+ visually identical but encoded differently compare equal.
+
+ macOS APFS/HFS+ store file names in decomposed form (NFD), while patterns
+ in ``MANIFEST.in`` are typically authored composed (NFC). The two denote
+ the same file but differ byte-for-byte, so matching them directly lets an
+ exclusion silently fail. Normalizing both the walked path and the pattern
+ to a single form before matching avoids that (GHSA-h35f-9h28-mq5c).
+ """
+ return unicodedata.normalize('NFC', text) if isinstance(text, str) else text
+
+
def filesys_decode(path):
"""
Ensure that the given path is decoded,
--
2.44.4

View File

@@ -15,6 +15,7 @@ SRC_URI += " \
file://CVE-2024-6345.patch \
file://CVE-2025-47273-pre1.patch \
file://CVE-2025-47273.patch \
file://CVE-2026-59890.patch \
"
SRC_URI[sha256sum] = "5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"