[meta-python][scarthgap][PATCH] python3-simpleeval: Fix CVE-2026-32640

"Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco)" <[email protected]>
Newsgroups org.openembedded.lists.openembedded-devel
Message-ID <[email protected]>
From: Hetvi Thakar <[email protected]>

Manually backport the three upstream security fixes for
CVE-2026-32640 to the Scarthgap simpleeval 0.9.13 recipe [1][2][3].

Include the required unhashable-container correction [4], which
prevents the recursive security checks from raising TypeError on
legitimate list and tuple values.

Harden the recursive callback-argument validation to inspect sets,
frozensets, and dictionary keys, and safely handle cyclic containers.
Add regression coverage for each of these cases.

Do not include the separate generator/coroutine hardening or the
optional performance follow-up. Omit the new ModuleWrapper API so this
stable-branch fix adds no unrelated public feature.

[1] https://github.com/danthedeckie/simpleeval/commit/9cb4a7b99498
[2] https://github.com/danthedeckie/simpleeval/commit/1654cbf02193
[3] https://github.com/danthedeckie/simpleeval/commit/cffa9f68cee5
[4] https://github.com/danthedeckie/simpleeval/commit/d1e4569db678

Signed-off-by: Hetvi Thakar <[email protected]>
---
 .../CVE-2026-32640_p1.patch                   |  55 ++
 .../CVE-2026-32640_p2.patch                   | 187 +++++++
 .../CVE-2026-32640_p3.patch                   | 485 ++++++++++++++++++
 .../CVE-2026-32640_p4.patch                   |  90 ++++
 .../python/python3-simpleeval_0.9.13.bb       |   8 +-
 5 files changed, 824 insertions(+), 1 deletion(-)
 create mode 100644 meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p1.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p2.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p3.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p4.patch

diff --git a/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p1.patch b/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p1.patch
new file mode 100644
index 0000000000..29d7c4bcdd
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p1.patch
@@ -0,0 +1,55 @@
+From 1f59b001d140f3c35b813150de48509bcc61713d Mon Sep 17 00:00:00 2001
+From: Daniel Fairhead <[email protected]>
+Date: Thu, 12 Mar 2026 09:33:17 +0000
+Subject: [PATCH 1/4] Add a few additional DISALLOW_FUNCTIONS
+
+CVE: CVE-2026-32640
+Upstream-Status: Backport [https://github.com/danthedeckie/simpleeval/commit/9cb4a7b99498c173263bd90f77bc185e160fb6b8]
+
+Backport Changes:
+- Add exec while expanding DISALLOW_FUNCTIONS because simpleeval 0.9.13 does not contain the earlier generator-hardening change that added it.
+
+(cherry picked from commit 9cb4a7b99498c173263bd90f77bc185e160fb6b8)
+Signed-off-by: Hetvi Thakar <[email protected]>
+---
+ simpleeval.py | 17 ++++++++++++++++-
+ 1 file changed, 16 insertions(+), 1 deletion(-)
+
+diff --git a/simpleeval.py b/simpleeval.py
+index f6a3ed6..91ed582 100644
+--- a/simpleeval.py
++++ b/simpleeval.py
+@@ -98,6 +98,7 @@ well:
+ 
+ import ast
+ import operator as op
++import os
+ import sys
+ import warnings
+ from random import random
+@@ -123,7 +124,21 @@ DISALLOW_METHODS = ["format", "format_map", "mro"]
+ # their functionality is required, then please wrap them up in a safe container.  And think
+ # very hard about it first.  And don't say I didn't warn you.
+ # builtins is a dict in python >3.6 but a module before
+-DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open}
++DISALLOW_FUNCTIONS = {
++    type,
++    isinstance,
++    eval,
++    getattr,
++    setattr,
++    repr,
++    compile,
++    open,
++    exec,
++    globals,
++    locals,
++    os.popen,
++    os.system,
++}
+ if hasattr(__builtins__, "help") or (
+     hasattr(__builtins__, "__contains__") and "help" in __builtins__  # type: ignore
+ ):
+-- 
+2.35.6
+
diff --git a/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p2.patch b/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p2.patch
new file mode 100644
index 0000000000..3d3d2eae2b
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p2.patch
@@ -0,0 +1,187 @@
+From bf7d5c05015cdbf8af0d298f286e76e9a6abad7c Mon Sep 17 00:00:00 2001
+From: Daniel Fairhead <[email protected]>
+Date: Thu, 12 Mar 2026 09:37:22 +0000
+Subject: [PATCH 2/4] Disallow module access & disallowed function access via
+ attributes.
+
+CVE: CVE-2026-32640
+Upstream-Status: Backport [https://github.com/danthedeckie/simpleeval/commit/1654cbf0219345f707c79664b8657be6b8d23e33]
+
+Backport Changes:
+- Import only Hashable from typing because simpleeval 0.9.13 does not contain the later allowed_attrs type API and its Type, Dict, Set, and Union imports.
+- Adapt TestDisallowedFunctions for 0.9.13's Python-2-compatible DISALLOWED setup: remove its two PYTHON3/dynamic-exec blocks; the upstream baseline already listed exec directly.
+
+(cherry picked from commit 1654cbf0219345f707c79664b8657be6b8d23e33)
+Signed-off-by: Hetvi Thakar <[email protected]>
+---
+ simpleeval.py      | 31 +++++++++++++------
+ test_simpleeval.py | 77 ++++++++++++++++++++++++++++++----------------
+ 2 files changed, 73 insertions(+), 35 deletions(-)
+
+diff --git a/simpleeval.py b/simpleeval.py
+index 91ed582..69aed83 100644
+--- a/simpleeval.py
++++ b/simpleeval.py
+@@ -100,8 +100,10 @@ import ast
+ import operator as op
+ import os
+ import sys
++import types
+ import warnings
+ from random import random
++from typing import Hashable
+ 
+ PYTHON3 = sys.version_info[0] == 3
+ PYTHON35 = PYTHON3 and sys.version_info > (3, 5)
+@@ -240,6 +242,10 @@ class MultipleExpressions(UserWarning):
+     pass
+ 
+ 
++# Sentinal used during attr access
++_ATTR_NOT_FOUND = object()
++
++
+ ########################################
+ # Default simple functions to include:
+ 
+@@ -603,18 +609,25 @@ class SimpleEval(object):  # pylint: disable=too-few-public-methods
+         # eval node
+         node_evaluated = self._eval(node.value)
+ 
++        item = _ATTR_NOT_FOUND
++
+         # Maybe the base object is an actual object, not just a dict
+         try:
+-            return getattr(node_evaluated, node.attr)
++            item = getattr(node_evaluated, node.attr)
+         except (AttributeError, TypeError):
+-            pass
+-
+-        # TODO: is this a good idea?  Try and look for [x] if .x doesn't work?
+-        if self.ATTR_INDEX_FALLBACK:
+-            try:
+-                return node_evaluated[node.attr]
+-            except (KeyError, TypeError):
+-                pass
++            # TODO: is this a good idea?  Try and look for [x] if .x doesn't work?
++            if self.ATTR_INDEX_FALLBACK:
++                try:
++                    item = node_evaluated[node.attr]
++                except (KeyError, TypeError):
++                    pass
++
++        if item is not _ATTR_NOT_FOUND:
++            if isinstance(item, types.ModuleType):
++                raise FeatureNotAvailable("Sorry, modules are not allowed in attribute access")
++            if isinstance(item, Hashable) and item in DISALLOW_FUNCTIONS:
++                raise FeatureNotAvailable("This function is forbidden")
++            return item
+ 
+         # If it is neither, raise an exception
+         raise AttributeDoesNotExist(node.attr, self.expr)
+diff --git a/test_simpleeval.py b/test_simpleeval.py
+index bc83d50..c60ec51 100644
+--- a/test_simpleeval.py
++++ b/test_simpleeval.py
+@@ -590,6 +590,34 @@ class TestTryingToBreakOut(DRYTest):
+ 
+             simpleeval.DISALLOW_PREFIXES = dis
+ 
++    def test_breakout_via_module_access(self):
++        import os.path
++
++        s = SimpleEval(names={"path": os.path})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("path.os.popen('id').read()")
++
++    def test_breakout_via_module_access_attr(self):
++        import os.path
++
++        class Foo:
++            p = os.path
++
++        s = SimpleEval(names={"thing": Foo()})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("thing.p.os.popen('id').read()")
++
++    def test_breakout_via_disallowed_functions_as_attrs(self):
++        class Foo:
++            p = exec
++
++        s = SimpleEval(names={"thing": Foo()})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("thing.p('exit')")
++
+ 
+ class TestCompoundTypes(DRYTest):
+     """Test the compound-types edition of the library"""
+@@ -1199,40 +1227,37 @@ class TestShortCircuiting(DRYTest):
+ 
+ 
+ class TestDisallowedFunctions(DRYTest):
+-    def test_functions_are_disallowed_at_init(self):
+-        DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open]
+-        if simpleeval.PYTHON3:
+-            # pylint: disable=exec-used
+-            exec("DISALLOWED.append(exec)")  # exec is not a function in Python2...
+-
+-        for f in simpleeval.DISALLOW_FUNCTIONS:
+-            assert f in DISALLOWED
++    def test_functions_in_disallowed_functions_list(self):
++        # a bit of double-entry testing. probably pointless.
++        assert simpleeval.DISALLOW_FUNCTIONS.issuperset(
++            {
++                type,
++                isinstance,
++                eval,
++                getattr,
++                setattr,
++                help,
++                repr,
++                compile,
++                open,
++                exec,
++                os.popen,
++                os.system,
++            }
++        )
+ 
+-        for x in DISALLOWED:
++    def test_functions_are_disallowed_at_init(self):
++        for dangerous_function in simpleeval.DISALLOW_FUNCTIONS:
+             with self.assertRaises(FeatureNotAvailable):
+-                SimpleEval(functions={"foo": x})
++                SimpleEval(functions={"foo": dangerous_function})
+ 
+     def test_functions_are_disallowed_in_expressions(self):
+-        DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open]
+-
+-        if simpleeval.PYTHON3:
+-            # pylint: disable=exec-used
+-            exec("DISALLOWED.append(exec)")  # exec is not a function in Python2...
+-
+-        for f in simpleeval.DISALLOW_FUNCTIONS:
+-            assert f in DISALLOWED
+-
+-        DF = simpleeval.DEFAULT_FUNCTIONS.copy()
+-
+-        for x in DISALLOWED:
+-            simpleeval.DEFAULT_FUNCTIONS = DF.copy()
++        for dangerous_function in simpleeval.DISALLOW_FUNCTIONS:
+             with self.assertRaises(FeatureNotAvailable):
+                 s = SimpleEval()
+-                s.functions["foo"] = x
++                s.functions["foo"] = dangerous_function
+                 s.eval("foo(42)")
+ 
+-        simpleeval.DEFAULT_FUNCTIONS = DF.copy()
+-
+ 
+ @unittest.skipIf(simpleeval.PYTHON3 is not True, "Python2 fails - but it's not supported anyway.")
+ @unittest.skipIf(platform.python_implementation() == "PyPy", "GC set_debug not available in PyPy")
+-- 
+2.35.6
+
diff --git a/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p3.patch b/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p3.patch
new file mode 100644
index 0000000000..aa5d2dce0e
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p3.patch
@@ -0,0 +1,485 @@
+From 030eb59004303dd4b88fedd6d80b7cb3e9d261a7 Mon Sep 17 00:00:00 2001
+From: Daniel Fairhead <[email protected]>
+Date: Fri, 13 Mar 2026 06:35:27 +0000
+Subject: [PATCH 3/4] Much stricter lockdown via _check_disallowed_items plus
+ adding ModuleWrapper
+
+CVE: CVE-2026-32640
+Upstream-Status: Backport [https://github.com/danthedeckie/simpleeval/commit/cffa9f68cee54404a2ef43d949a8ae8a3311c503]
+
+Backport Changes:
+- Omit the new ModuleWrapper API and its tests and documentation so the Scarthgap backport does not add an unrelated public feature; raw modules remain fully blocked.
+- Retain only the module-access prohibition documentation because simpleeval 0.9.13 does not contain the later allowed_attrs API or its documentation context.
+- Extend _check_disallowed_items to traverse sets, frozensets, and dictionary keys, with regression coverage, so dangerous callback arguments cannot bypass the recursive check through those containers.
+- Track visited container identities during each recursive check and add cyclic list and dictionary tests to prevent unbounded recursion without weakening forbidden-item detection.
+
+(cherry picked from commit cffa9f68cee54404a2ef43d949a8ae8a3311c503)
+Signed-off-by: Hetvi Thakar <[email protected]>
+---
+ README.rst         |   1 +
+ simpleeval.py      |  34 +++-
+ test_simpleeval.py | 384 +++++++++++++++++++++++++++++++++++++++++++++
+ 3 files changed, 418 insertions(+), 1 deletion(-)
+
+diff --git a/README.rst b/README.rst
+index ed7c655..6ac327b 100644
+--- a/README.rst
++++ b/README.rst
+@@ -416,6 +416,7 @@ A few builtin functions are listed in ``simpleeval.DISALLOW_FUNCTIONS``.  ``type
+ If you need to give access to this kind of functionality to your expressions, then be very
+ careful.  You'd be better wrapping the functions in your own safe wrappers.
+ 
++Accessing modules as attributes is disallowed too.
+ The initial idea came from J.F. Sebastian on Stack Overflow
+ ( http://stackoverflow.com/a/9558001/1973500 ) with modifications and many improvements,
+ see the head of the main file for contributors list.
+diff --git a/simpleeval.py b/simpleeval.py
+index 69aed83..9f3003e 100644
+--- a/simpleeval.py
++++ b/simpleeval.py
+@@ -424,6 +424,36 @@ class SimpleEval(object):  # pylint: disable=too-few-public-methods
+     def __del__(self):
+         self.nodes = None
+ 
++    def _check_disallowed_items(self, item, visited=None):
++        """Check if item contains disallowed functions or modules.
++        Recursively checks containers (dict, list, tuple, set, frozenset).
++        Raises FeatureNotAvailable if forbidden content found.
++        """
++        if isinstance(item, types.ModuleType):
++            raise FeatureNotAvailable("Sorry, modules are not allowed")
++        if isinstance(item, Hashable) and item in DISALLOW_FUNCTIONS:
++            raise FeatureNotAvailable("This function is forbidden")
++
++        if not isinstance(item, (dict, list, tuple, set, frozenset)):
++            return
++
++        if visited is None:
++            visited = set()
++
++        item_id = id(item)
++        if item_id in visited:
++            return
++        visited.add(item_id)
++
++        if isinstance(item, dict):
++            for key in item.keys():
++                self._check_disallowed_items(key, visited)
++            for value in item.values():
++                self._check_disallowed_items(value, visited)
++        else:
++            for element in item:
++                self._check_disallowed_items(element, visited)
++
+     @staticmethod
+     def parse(expr):
+         """parse an expression into a node tree"""
+@@ -458,7 +488,9 @@ class SimpleEval(object):  # pylint: disable=too-few-public-methods
+                 "Sorry, {0} is not available in this " "evaluator".format(type(node).__name__)
+             )
+ 
+-        return handler(node)
++        result = handler(node)
++        self._check_disallowed_items(result)
++        return result
+ 
+     def _eval_expr(self, node):
+         return self._eval(node.value)
+diff --git a/test_simpleeval.py b/test_simpleeval.py
+index c60ec51..f3c9a60 100644
+--- a/test_simpleeval.py
++++ b/test_simpleeval.py
+@@ -618,6 +618,390 @@ class TestTryingToBreakOut(DRYTest):
+         with self.assertRaises(FeatureNotAvailable):
+             s.eval("thing.p('exit')")
+ 
++    def test_breakout_forbidden_function_in_list(self):
++        """Disallowed functions in lists should be blocked"""
++        s = SimpleEval(names={"funcs": [exec, eval]})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("funcs[0]('exit')")
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("funcs[1]('1+1')")
++
++    def test_breakout_module_in_list(self):
++        """Modules in lists should be blocked"""
++        import os.path
++
++        s = SimpleEval(names={"things": [os.path, os.system]})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("things[0].os.popen('id').read()")
++
++    def test_breakout_forbidden_function_in_dict_value(self):
++        """Disallowed functions as dict values should be blocked"""
++        s = SimpleEval(names={"funcs": {"bad": exec, "evil": eval}})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("funcs['bad']('exit')")
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("funcs['evil']('1+1')")
++
++    def test_breakout_module_in_dict_value(self):
++        """Modules as dict values should be blocked"""
++        import os.path
++
++        s = SimpleEval(names={"things": {"p": os.path, "s": os.system}})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("things['p'].os.popen('id').read()")
++
++    def test_breakout_forbidden_function_in_set(self):
++        """Disallowed functions in sets should be blocked"""
++
++        def invoke(items):
++            return next(iter(items))("40+2")
++
++        s = SimpleEval(names={"funcs": {eval}}, functions={"invoke": invoke})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("invoke(funcs)")
++
++    def test_breakout_forbidden_function_in_frozenset(self):
++        """Disallowed functions in frozensets should be blocked"""
++
++        def invoke(items):
++            return next(iter(items))("40+2")
++
++        s = SimpleEval(names={"funcs": frozenset({eval})}, functions={"invoke": invoke})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("invoke(funcs)")
++
++    def test_breakout_forbidden_function_in_dict_key(self):
++        """Disallowed functions as dict keys should be blocked"""
++
++        def invoke(items):
++            return next(iter(items))("40+2")
++
++        s = SimpleEval(names={"funcs": {eval: "value"}}, functions={"invoke": invoke})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("invoke(funcs)")
++
++    def test_cyclic_list(self):
++        """Cyclic lists should be checked without unbounded recursion"""
++        value = []
++        value.append(value)
++
++        self.assertIs(SimpleEval(names={"value": value}).eval("value"), value)
++
++    def test_cyclic_dict(self):
++        """Cyclic dicts should be checked without unbounded recursion"""
++        value = {}
++        value["self"] = value
++
++        self.assertIs(SimpleEval(names={"value": value}).eval("value"), value)
++
++    def test_cyclic_container_with_forbidden_function(self):
++        """A cycle should not hide other forbidden container members"""
++        value = [eval]
++        value.append(value)
++
++        with self.assertRaises(FeatureNotAvailable):
++            SimpleEval(names={"value": value}).eval("value")
++
++    def test_breakout_function_returning_forbidden_function(self):
++        """Functions returning disallowed functions should be blocked"""
++
++        def get_evil():
++            return exec
++
++        s = SimpleEval(names={}, functions={"get_evil": get_evil})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("get_evil()('exit')")
++
++    def test_breakout_function_returning_module(self):
++        """Functions returning modules should be blocked"""
++        import os.path
++
++        def get_module():
++            return os.path
++
++        s = SimpleEval(names={}, functions={"get_module": get_module})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("get_module().os.popen('id').read()")
++
++    def test_dunder_all_in_module(self):
++        """__all__ should be blocked (starts with _)"""
++        import os
++
++        s = SimpleEval(names={"os": os})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("os.__all__")
++
++    def test_dunder_dict_in_module(self):
++        """__dict__ should be blocked (starts with _)"""
++        import os
++
++        s = SimpleEval(names={"os": os})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("os.__dict__")
++
++    def test_forbidden_method_in_tuple(self):
++        """Disallowed functions in tuples should be blocked"""
++        s = SimpleEval(names={"funcs": (exec, eval)})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("funcs[0]('exit')")
++
++    def test_module_in_tuple(self):
++        """Modules in tuples should be blocked"""
++        import os
++
++        s = SimpleEval(names={"mods": (os.path, os.system)})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("mods[0].os.popen('id').read()")
++
++    def test_breakout_via_nested_container_forbidden_func(self):
++        """Disallowed functions nested in containers should be blocked"""
++        s = SimpleEval(names={"data": {"nested": {"funcs": [exec]}}})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("data['nested']['funcs'][0]('exit')")
++
++    def test_breakout_via_nested_container_module(self):
++        """Modules nested in containers should be blocked"""
++        import os
++
++        s = SimpleEval(names={"data": {"mods": {"p": os.path}}})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("data['mods']['p'].os.popen('id').read()")
++
++    def test_forbidden_methods_on_allowed_attrs(self):
++        """Disallowed methods listed in DISALLOW_METHODS should be
++        blocked"""
++        s = SimpleEval()
++
++        # format and format_map are in DISALLOW_METHODS
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("'test {0}'.format")
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("'test'.format_map({0: 'x'})")
++
++        # __mro__ is in DISALLOW_METHODS
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("'test'.mro")
++
++    def test_function_returning_forbidden_method(self):
++        """Functions returning disallowed methods should be blocked"""
++
++        def get_exec_module():
++            import os
++
++            return os
++
++        s = SimpleEval(names={}, functions={"get_os": get_exec_module})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("get_os().__name__")
++
++    def test_compound_module_submodule_access(self):
++        """Accessing submodules of a passed module should be blocked"""
++        import os.path
++
++        s = SimpleEval(names={"path": os.path})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("path.os")
++
++    def test_forbidden_func_via_class_method(self):
++        """Accessing forbidden functions via class methods should be
++        blocked"""
++
++        class Container:
++            @staticmethod
++            def get_exec():
++                return exec
++
++        s = SimpleEval(names={"c": Container()})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("c.get_exec()('exit')")
++
++    def test_module_via_class_method(self):
++        """Accessing modules via class methods should be blocked"""
++        import os
++
++        class Container:
++            @staticmethod
++            def get_os():
++                return os
++
++        s = SimpleEval(names={"c": Container()})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("c.get_os().popen('id').read()")
++
++    def test_forbidden_func_via_property(self):
++        """Accessing forbidden functions via properties should be
++        blocked"""
++
++        class Container:
++            @property
++            def evil(self):
++                return exec
++
++        s = SimpleEval(names={"c": Container()})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("c.evil('exit')")
++
++    def test_module_via_property(self):
++        """Accessing modules via properties should be blocked"""
++        import os
++
++        class Container:
++            @property
++            def mod(self):
++                return os
++
++        s = SimpleEval(names={"c": Container()})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("c.mod.popen('id').read()")
++
++    def test_forbidden_function_direct_from_names(self):
++        """Forbidden functions passed directly in names should
++        be blocked when accessed"""
++        s = SimpleEval(names={"evil": exec})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("evil")
++
++    def test_module_direct_from_names(self):
++        """Modules passed directly in names should be blocked
++        when accessed"""
++        import os
++
++        s = SimpleEval(names={"m": os})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("m")
++
++    def test_forbidden_function_via_callable_name_handler(self):
++        """Forbidden functions from callable name handlers should
++        be blocked"""
++
++        def name_handler(node):
++            if node.id == "evil":
++                return exec
++            raise simpleeval.NameNotDefined(node.id, "")
++
++        s = SimpleEval(names=name_handler)
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("evil")
++
++    def test_module_via_callable_name_handler(self):
++        """Modules from callable name handlers should be blocked"""
++        import os
++
++        def name_handler(node):
++            if node.id == "m":
++                return os
++            raise simpleeval.NameNotDefined(node.id, "")
++
++        s = SimpleEval(names=name_handler)
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("m")
++
++    def test_forbidden_function_passed_to_custom_function(self):
++        """Passing forbidden functions to custom functions should be
++        blocked - they can be executed by the custom function"""
++
++        def evil_caller(func):
++            return func("print('pwned')")
++
++        s = SimpleEval(names={"evil": exec}, functions={"evil_caller": evil_caller})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("evil_caller(evil)")
++
++    def test_module_passed_to_custom_function(self):
++        """Passing modules to custom functions should be blocked - they
++        can be used by the custom function"""
++        import os
++
++        def os_caller(mod):
++            return mod.system("id")
++
++        s = SimpleEval(names={"m": os}, functions={"os_caller": os_caller})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("os_caller(m)")
++
++    def test_forbidden_function_in_list_passed_to_custom_function(self):
++        """Forbidden functions in containers passed to custom functions
++        should be blocked"""
++
++        def extract_and_call(items):
++            return items[0]("print('pwned')")
++
++        s = SimpleEval(names={"funcs": [exec, eval]}, functions={"extract": extract_and_call})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("extract(funcs)")
++
++    def test_module_in_list_passed_to_custom_function(self):
++        """Modules in containers passed to custom functions should be
++        blocked"""
++        import os
++
++        def extract_and_use(items):
++            return items[0].system("id")
++
++        s = SimpleEval(names={"mods": [os.path, os]}, functions={"extract": extract_and_use})
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("extract(mods)")
++
++    def test_forbidden_function_in_dict_passed_to_custom_function(self):
++        """Forbidden functions in dicts passed to custom functions should
++        be blocked"""
++
++        def extract_and_call(d):
++            return d["bad"]("print('pwned')")
++
++        s = SimpleEval(
++            names={"funcs": {"bad": exec, "good": print}}, functions={"extract": extract_and_call}
++        )
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("extract(funcs)")
++
++    def test_module_in_dict_passed_to_custom_function(self):
++        """Modules in dicts passed to custom functions should be blocked"""
++        import os
++
++        def extract_and_use(d):
++            return d["m"].system("id")
++
++        s = SimpleEval(
++            names={"mods": {"m": os, "p": os.path}}, functions={"extract": extract_and_use}
++        )
++
++        with self.assertRaises(FeatureNotAvailable):
++            s.eval("extract(mods)")
++
+ 
+ class TestCompoundTypes(DRYTest):
+     """Test the compound-types edition of the library"""
+-- 
+2.35.6
+
diff --git a/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p4.patch b/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p4.patch
new file mode 100644
index 0000000000..d03a5cf4fa
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p4.patch
@@ -0,0 +1,90 @@
+From 42483617f1fd47c81bbf66c71c2ce5cbe2d3df29 Mon Sep 17 00:00:00 2001
+From: Daniel Fairhead <[email protected]>
+Date: Fri, 13 Mar 2026 13:27:47 +0000
+Subject: [PATCH 4/4] Fix unhashable items inside tuples bug.
+
+CVE: CVE-2026-32640
+Upstream-Status: Backport [https://github.com/danthedeckie/simpleeval/commit/d1e4569db678a3cb42b779f404aa203665d52ab0]
+
+Backport Changes:
+- Omit the upstream 1.0.6 version metadata change and retain the 0.9.13 setuptools build configuration.
+- Remove the standalone Hashable import added by the preceding 0.9.13 backport while retaining the target Python compatibility flags.
+
+(cherry picked from commit d1e4569db678a3cb42b779f404aa203665d52ab0)
+Signed-off-by: Hetvi Thakar <[email protected]>
+---
+ simpleeval.py      | 16 +++++++++++++---
+ test_simpleeval.py |  9 +++++++++
+ 2 files changed, 22 insertions(+), 3 deletions(-)
+
+diff --git a/simpleeval.py b/simpleeval.py
+index 9f3003e..a6745db 100644
+--- a/simpleeval.py
++++ b/simpleeval.py
+@@ -103,7 +103,6 @@ import sys
+ import types
+ import warnings
+ from random import random
+-from typing import Hashable
+ 
+ PYTHON3 = sys.version_info[0] == 3
+ PYTHON35 = PYTHON3 and sys.version_info > (3, 5)
+@@ -119,6 +118,17 @@ MAX_SHIFT_BASE = int(sys.float_info.max)  # highest on left side of << or >>
+ DISALLOW_PREFIXES = ["_", "func_"]
+ DISALLOW_METHODS = ["format", "format_map", "mro"]
+ 
++########################################
++# Tiny helpers:
++
++
++def is_hashable(value):
++    try:
++        return hash(value)
++    except TypeError:
++        return False
++
++
+ # Disallow functions:
+ # This, strictly speaking, is not necessary.  These /should/ never be accessable anyway,
+ # if DISALLOW_PREFIXES and DISALLOW_METHODS are all right.  This is here to try and help
+@@ -431,7 +441,7 @@ class SimpleEval(object):  # pylint: disable=too-few-public-methods
+         """
+         if isinstance(item, types.ModuleType):
+             raise FeatureNotAvailable("Sorry, modules are not allowed")
+-        if isinstance(item, Hashable) and item in DISALLOW_FUNCTIONS:
++        if is_hashable(item) and item in DISALLOW_FUNCTIONS:
+             raise FeatureNotAvailable("This function is forbidden")
+ 
+         if not isinstance(item, (dict, list, tuple, set, frozenset)):
+@@ -657,7 +667,7 @@ class SimpleEval(object):  # pylint: disable=too-few-public-methods
+         if item is not _ATTR_NOT_FOUND:
+             if isinstance(item, types.ModuleType):
+                 raise FeatureNotAvailable("Sorry, modules are not allowed in attribute access")
+-            if isinstance(item, Hashable) and item in DISALLOW_FUNCTIONS:
++            if is_hashable(item) and item in DISALLOW_FUNCTIONS:
+                 raise FeatureNotAvailable("This function is forbidden")
+             return item
+ 
+diff --git a/test_simpleeval.py b/test_simpleeval.py
+index f3c9a60..a654f6d 100644
+--- a/test_simpleeval.py
++++ b/test_simpleeval.py
+@@ -347,6 +347,15 @@ class TestFunctions(DRYTest):
+         self.t("foo(mult=2, to_return=4)", 8)
+         self.t("foo(2, 10)", 20)
+ 
++    def test_function_with_list_args(self):
++        # Regression test, makes sure we can pass lists (non-hashable) items as
++        # kwargs to functions.
++
++        def func(*args, **kwargs):
++            return 42
++
++        simple_eval("test(boo=x)", functions={"test": func}, names={"x": [1, 2]})
++
+ 
+ class TestOperators(DRYTest):
+     """Test adding in new operators, removing them, make sure it works."""
+-- 
+2.35.6
+
diff --git a/meta-python/recipes-devtools/python/python3-simpleeval_0.9.13.bb b/meta-python/recipes-devtools/python/python3-simpleeval_0.9.13.bb
index af2c4a1e2e..a8ff053a1e 100644
--- a/meta-python/recipes-devtools/python/python3-simpleeval_0.9.13.bb
+++ b/meta-python/recipes-devtools/python/python3-simpleeval_0.9.13.bb
@@ -9,7 +9,13 @@ inherit pypi python_setuptools_build_meta ptest
 
 BBCLASSEXTEND = "native nativesdk"
 
-SRC_URI += "file://run-ptest"
+SRC_URI += " \
+    file://CVE-2026-32640_p1.patch \
+    file://CVE-2026-32640_p2.patch \
+    file://CVE-2026-32640_p3.patch \
+    file://CVE-2026-32640_p4.patch \
+    file://run-ptest \
+"
 
 RDEPENDS:${PN} += " \
 	python3-math \
-- 
2.35.6
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.