diff mbox series

[kirkstone,1/1] python3: fix CVE-2025-6075

Message ID 20251121112642.3379117-1-praveen.kumar@windriver.com
State New
Headers show
Series [kirkstone,1/1] python3: fix CVE-2025-6075 | expand

Commit Message

pkumar7 Nov. 21, 2025, 11:26 a.m. UTC
From: Praveen Kumar <praveen.kumar@windriver.com>

If the value passed to os.path.expandvars() is user-controlled a
performance degradation is possible when expanding environment variables.

Reference:
https://nvd.nist.gov/vuln/detail/CVE-2025-6075

Upstream-patch:
https://github.com/python/cpython/commit/892747b4cf0f95ba8beb51c0d0658bfaa381ebca

Signed-off-by: Praveen Kumar <praveen.kumar@windriver.com>
---
 .../python/python3/CVE-2025-6075.patch        | 364 ++++++++++++++++++
 .../python/python3_3.10.19.bb                 |   1 +
 2 files changed, 365 insertions(+)
 create mode 100644 meta/recipes-devtools/python/python3/CVE-2025-6075.patch
diff mbox series

Patch

diff --git a/meta/recipes-devtools/python/python3/CVE-2025-6075.patch b/meta/recipes-devtools/python/python3/CVE-2025-6075.patch
new file mode 100644
index 0000000000..eab5a882a0
--- /dev/null
+++ b/meta/recipes-devtools/python/python3/CVE-2025-6075.patch
@@ -0,0 +1,364 @@ 
+From 892747b4cf0f95ba8beb51c0d0658bfaa381ebca Mon Sep 17 00:00:00 2001
+From: Ɓukasz Langa <lukasz@langa.pl>
+Date: Fri, 31 Oct 2025 17:51:32 +0100
+Subject: [PATCH] gh-136065: Fix quadratic complexity in os.path.expandvars()
+ (GH-134952) (GH-140851)
+
+(cherry picked from commit f029e8db626ddc6e3a3beea4eff511a71aaceb5c)
+
+Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
+
+CVE: CVE-2025-6075
+
+Upstream-Status: Backport [https://github.com/python/cpython/commit/892747b4cf0f95ba8beb51c0d0658bfaa381ebca]
+
+Signed-off-by: Praveen Kumar <praveen.kumar@windriver.com>
+---
+ Lib/ntpath.py                                 | 126 ++++++------------
+ Lib/posixpath.py                              |  43 +++---
+ Lib/test/test_genericpath.py                  |  14 ++
+ Lib/test/test_ntpath.py                       |  20 ++-
+ ...-05-30-22-33-27.gh-issue-136065.bu337o.rst |   1 +
+ 5 files changed, 93 insertions(+), 111 deletions(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
+
+diff --git a/Lib/ntpath.py b/Lib/ntpath.py
+index 9b0cca4..bd2b4e2 100644
+--- a/Lib/ntpath.py
++++ b/Lib/ntpath.py
+@@ -374,17 +374,23 @@ def expanduser(path):
+ # XXX With COMMAND.COM you can use any characters in a variable name,
+ # XXX except '^|<>='.
+
++_varpattern = r"'[^']*'?|%(%|[^%]*%?)|\$(\$|[-\w]+|\{[^}]*\}?)"
++_varsub = None
++_varsubb = None
++
+ def expandvars(path):
+     """Expand shell variables of the forms $var, ${var} and %var%.
+
+     Unknown variables are left unchanged."""
+     path = os.fspath(path)
++    global _varsub, _varsubb
+     if isinstance(path, bytes):
+         if b'$' not in path and b'%' not in path:
+             return path
+-        import string
+-        varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
+-        quote = b'\''
++        if not _varsubb:
++            import re
++            _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
++        sub = _varsubb
+         percent = b'%'
+         brace = b'{'
+         rbrace = b'}'
+@@ -393,94 +399,44 @@ def expandvars(path):
+     else:
+         if '$' not in path and '%' not in path:
+             return path
+-        import string
+-        varchars = string.ascii_letters + string.digits + '_-'
+-        quote = '\''
++        if not _varsub:
++            import re
++            _varsub = re.compile(_varpattern, re.ASCII).sub
++        sub = _varsub
+         percent = '%'
+         brace = '{'
+         rbrace = '}'
+         dollar = '$'
+         environ = os.environ
+-    res = path[:0]
+-    index = 0
+-    pathlen = len(path)
+-    while index < pathlen:
+-        c = path[index:index+1]
+-        if c == quote:   # no expansion within single quotes
+-            path = path[index + 1:]
+-            pathlen = len(path)
+-            try:
+-                index = path.index(c)
+-                res += c + path[:index + 1]
+-            except ValueError:
+-                res += c + path
+-                index = pathlen - 1
+-        elif c == percent:  # variable or '%'
+-            if path[index + 1:index + 2] == percent:
+-                res += c
+-                index += 1
+-            else:
+-                path = path[index+1:]
+-                pathlen = len(path)
+-                try:
+-                    index = path.index(percent)
+-                except ValueError:
+-                    res += percent + path
+-                    index = pathlen - 1
+-                else:
+-                    var = path[:index]
+-                    try:
+-                        if environ is None:
+-                            value = os.fsencode(os.environ[os.fsdecode(var)])
+-                        else:
+-                            value = environ[var]
+-                    except KeyError:
+-                        value = percent + var + percent
+-                    res += value
+-        elif c == dollar:  # variable or '$$'
+-            if path[index + 1:index + 2] == dollar:
+-                res += c
+-                index += 1
+-            elif path[index + 1:index + 2] == brace:
+-                path = path[index+2:]
+-                pathlen = len(path)
+-                try:
+-                    index = path.index(rbrace)
+-                except ValueError:
+-                    res += dollar + brace + path
+-                    index = pathlen - 1
+-                else:
+-                    var = path[:index]
+-                    try:
+-                        if environ is None:
+-                            value = os.fsencode(os.environ[os.fsdecode(var)])
+-                        else:
+-                            value = environ[var]
+-                    except KeyError:
+-                        value = dollar + brace + var + rbrace
+-                    res += value
+-            else:
+-                var = path[:0]
+-                index += 1
+-                c = path[index:index + 1]
+-                while c and c in varchars:
+-                    var += c
+-                    index += 1
+-                    c = path[index:index + 1]
+-                try:
+-                    if environ is None:
+-                        value = os.fsencode(os.environ[os.fsdecode(var)])
+-                    else:
+-                        value = environ[var]
+-                except KeyError:
+-                    value = dollar + var
+-                res += value
+-                if c:
+-                    index -= 1
++
++    def repl(m):
++        lastindex = m.lastindex
++        if lastindex is None:
++            return m[0]
++        name = m[lastindex]
++        if lastindex == 1:
++            if name == percent:
++                return name
++            if not name.endswith(percent):
++                return m[0]
++            name = name[:-1]
+         else:
+-            res += c
+-        index += 1
+-    return res
++            if name == dollar:
++                return name
++            if name.startswith(brace):
++                if not name.endswith(rbrace):
++                    return m[0]
++                name = name[1:-1]
++
++        try:
++            if environ is None:
++                return os.fsencode(os.environ[os.fsdecode(name)])
++            else:
++                return environ[name]
++        except KeyError:
++            return m[0]
++
++    return sub(repl, path)
+
+
+ # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
+diff --git a/Lib/posixpath.py b/Lib/posixpath.py
+index b8dd563..75020ee 100644
+--- a/Lib/posixpath.py
++++ b/Lib/posixpath.py
+@@ -279,42 +279,41 @@ def expanduser(path):
+ # This expands the forms $variable and ${variable} only.
+ # Non-existent variables are left unchanged.
+
+-_varprog = None
+-_varprogb = None
++_varpattern = r'\$(\w+|\{[^}]*\}?)'
++_varsub = None
++_varsubb = None
+
+ def expandvars(path):
+     """Expand shell variables of form $var and ${var}.  Unknown variables
+     are left unchanged."""
+     path = os.fspath(path)
+-    global _varprog, _varprogb
++    global _varsub, _varsubb
+     if isinstance(path, bytes):
+         if b'$' not in path:
+             return path
+-        if not _varprogb:
++        if not _varsubb:
+             import re
+-            _varprogb = re.compile(br'\$(\w+|\{[^}]*\})', re.ASCII)
+-        search = _varprogb.search
++            _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
++        sub = _varsubb
+         start = b'{'
+         end = b'}'
+         environ = getattr(os, 'environb', None)
+     else:
+         if '$' not in path:
+             return path
+-        if not _varprog:
++        if not _varsub:
+             import re
+-            _varprog = re.compile(r'\$(\w+|\{[^}]*\})', re.ASCII)
+-        search = _varprog.search
++            _varsub = re.compile(_varpattern, re.ASCII).sub
++        sub = _varsub
+         start = '{'
+         end = '}'
+         environ = os.environ
+-    i = 0
+-    while True:
+-        m = search(path, i)
+-        if not m:
+-            break
+-        i, j = m.span(0)
+-        name = m.group(1)
+-        if name.startswith(start) and name.endswith(end):
++
++    def repl(m):
++        name = m[1]
++        if name.startswith(start):
++            if not name.endswith(end):
++                return m[0]
+             name = name[1:-1]
+         try:
+             if environ is None:
+@@ -322,13 +321,11 @@ def expandvars(path):
+             else:
+                 value = environ[name]
+         except KeyError:
+-            i = j
++            return m[0]
+         else:
+-            tail = path[j:]
+-            path = path[:i] + value
+-            i = len(path)
+-            path += tail
+-    return path
++            return value
++
++    return sub(repl, path)
+
+
+ # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B.
+diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py
+index 1ff7f75..b0a1326 100644
+--- a/Lib/test/test_genericpath.py
++++ b/Lib/test/test_genericpath.py
+@@ -7,6 +7,7 @@ import os
+ import sys
+ import unittest
+ import warnings
++from test import support
+ from test.support import os_helper
+ from test.support import warnings_helper
+ from test.support.script_helper import assert_python_ok
+@@ -430,6 +431,19 @@ class CommonTest(GenericTest):
+                   os.fsencode('$bar%s bar' % nonascii))
+             check(b'$spam}bar', os.fsencode('%s}bar' % nonascii))
+
++    @support.requires_resource('cpu')
++    def test_expandvars_large(self):
++        expandvars = self.pathmodule.expandvars
++        with os_helper.EnvironmentVarGuard() as env:
++            env.clear()
++            env["A"] = "B"
++            n = 100_000
++            self.assertEqual(expandvars('$A'*n), 'B'*n)
++            self.assertEqual(expandvars('${A}'*n), 'B'*n)
++            self.assertEqual(expandvars('$A!'*n), 'B!'*n)
++            self.assertEqual(expandvars('${A}A'*n), 'BA'*n)
++            self.assertEqual(expandvars('${'*10*n), '${'*10*n)
++
+     def test_abspath(self):
+         self.assertIn("foo", self.pathmodule.abspath("foo"))
+         with warnings.catch_warnings():
+diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
+index f790f77..161e57d 100644
+--- a/Lib/test/test_ntpath.py
++++ b/Lib/test/test_ntpath.py
+@@ -5,8 +5,8 @@ import sys
+ import unittest
+ import warnings
+ from ntpath import ALLOW_MISSING
++from test import support
+ from test.support import os_helper
+-from test.support import TestFailed
+ from test.support.os_helper import FakePath
+ from test import test_genericpath
+ from tempfile import TemporaryFile
+@@ -56,7 +56,7 @@ def tester(fn, wantResult):
+     fn = fn.replace("\\", "\\\\")
+     gotResult = eval(fn)
+     if wantResult != gotResult and _norm(wantResult) != _norm(gotResult):
+-        raise TestFailed("%s should return: %s but returned: %s" \
++        raise support.TestFailed("%s should return: %s but returned: %s" \
+               %(str(fn), str(wantResult), str(gotResult)))
+
+     # then with bytes
+@@ -72,7 +72,7 @@ def tester(fn, wantResult):
+         warnings.simplefilter("ignore", DeprecationWarning)
+         gotResult = eval(fn)
+     if _norm(wantResult) != _norm(gotResult):
+-        raise TestFailed("%s should return: %s but returned: %s" \
++        raise support.TestFailed("%s should return: %s but returned: %s" \
+               %(str(fn), str(wantResult), repr(gotResult)))
+
+
+@@ -689,6 +689,19 @@ class TestNtpath(NtpathTestCase):
+             check('%spam%bar', '%sbar' % nonascii)
+             check('%{}%bar'.format(nonascii), 'ham%sbar' % nonascii)
+
++    @support.requires_resource('cpu')
++    def test_expandvars_large(self):
++        expandvars = ntpath.expandvars
++        with os_helper.EnvironmentVarGuard() as env:
++            env.clear()
++            env["A"] = "B"
++            n = 100_000
++            self.assertEqual(expandvars('%A%'*n), 'B'*n)
++            self.assertEqual(expandvars('%A%A'*n), 'BA'*n)
++            self.assertEqual(expandvars("''"*n + '%%'), "''"*n + '%')
++            self.assertEqual(expandvars("%%"*n), "%"*n)
++            self.assertEqual(expandvars("$$"*n), "$"*n)
++
+     def test_expanduser(self):
+         tester('ntpath.expanduser("test")', 'test')
+
+@@ -923,6 +936,7 @@ class TestNtpath(NtpathTestCase):
+             self.assertIsInstance(b_final_path, bytes)
+             self.assertGreater(len(b_final_path), 0)
+
++
+ class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
+     pathmodule = ntpath
+     attributes = ['relpath']
+diff --git a/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
+new file mode 100644
+index 0000000..1d152bb
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
+@@ -0,0 +1 @@
++Fix quadratic complexity in :func:`os.path.expandvars`.
+--
+2.40.0
diff --git a/meta/recipes-devtools/python/python3_3.10.19.bb b/meta/recipes-devtools/python/python3_3.10.19.bb
index 8680c13893..6f23d258c1 100644
--- a/meta/recipes-devtools/python/python3_3.10.19.bb
+++ b/meta/recipes-devtools/python/python3_3.10.19.bb
@@ -37,6 +37,7 @@  SRC_URI = "http://www.python.org/ftp/python/${PV}/Python-${PV}.tar.xz \
            file://0001-Avoid-shebang-overflow-on-python-config.py.patch \
            file://0001-test_storlines-skip-due-to-load-variability.patch \
            file://0001-gh-107811-tarfile-treat-overflow-in-UID-GID-as-failu.patch \
+           file://CVE-2025-6075.patch \
            "
 
 SRC_URI:append:class-native = " \