new file mode 100644
@@ -0,0 +1,148 @@
+From 66c8bc346c0c614edc05535145c0424a14fba213 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington@users.noreply.github.com>
+Date: Mon, 29 Jun 2026 21:11:22 +0200
+Subject: [PATCH] gh-151987: Pass filter_function to `TarFile._extract_one()`
+ during `.extract()` (GH-151988) (#152609)
+
+(cherry picked from commit 7ccdbaba2c54250a70d7f25632152df7655a5e0a)
+
+Co-authored-by: Petr Viktorin <encukou@gmail.com>
+Co-authored-by: Seth Michael Larson <seth@python.org>
+
+CVE: CVE-2026-4360
+Upstream-Status: Backport [https://github.com/python/cpython/commit/5e0ef3f1afe892e4f64eb83368db57ac4c40cba0]
+
+Signed-off-by: Leonid Iziumtsev <leonid.iziumtsev@est.tech>
+---
+ Lib/tarfile.py | 3 +-
+ Lib/test/test_tarfile.py | 92 +++++++++++++++++++
+ ...-06-23-14-19-30.gh-issue-151987.8mNIMf.rst | 2 +
+ 3 files changed, 96 insertions(+), 1 deletion(-)
+ create mode 100644 Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst
+
+diff --git a/Lib/tarfile.py b/Lib/tarfile.py
+index e6734db..2c46179 100644
+--- a/Lib/tarfile.py
++++ b/Lib/tarfile.py
+@@ -2510,7 +2510,8 @@ class TarFile(object):
+ tarinfo, unfiltered = self._get_extract_tarinfo(
+ member, filter_function, path)
+ if tarinfo is not None:
+- self._extract_one(tarinfo, path, set_attrs, numeric_owner)
++ self._extract_one(tarinfo, path, set_attrs, numeric_owner,
++ filter_function=filter_function)
+
+ def _get_extract_tarinfo(self, member, filter_function, path):
+ """Get (filtered, unfiltered) TarInfos from *member*
+diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
+index d974c7d..9a15585 100644
+--- a/Lib/test/test_tarfile.py
++++ b/Lib/test/test_tarfile.py
+@@ -4399,6 +4399,98 @@ class TestExtractionFilters(unittest.TestCase):
+ st_mode = cc.outerdir.stat().st_mode
+ self.assertNotEqual(st_mode & 0o777, 0o777)
+
++ @symlink_test
++ @unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
++ @unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
++ @unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
++ @support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
++ def test_chown_links_on_extract(self, link_type):
++ with ArchiveMaker() as arc:
++ arc.add("test.txt",
++ uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
++ arc.add("link",
++ type=link_type,
++ linkname='test.txt',
++ uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
++
++ with (
++ os_helper.temp_dir() as tmpdir,
++ arc.open() as tar,
++ unittest.mock.patch("os.chown") as mock_chown,
++ unittest.mock.patch("os.lchown") as mock_lchown,
++ unittest.mock.patch("os.geteuid") as mock_geteuid,
++ ):
++ # Set UID to 0 so chown() is attempted.
++ mock_geteuid.return_value = 0
++ tar.extract("link", path=tmpdir, filter='data')
++ extract_path = os.path.join(tmpdir, "link")
++
++ if link_type == tarfile.SYMTYPE:
++ mock_chown.assert_not_called()
++ mock_lchown.assert_called_once_with(extract_path, -1, -1)
++ else:
++ mock_chown.assert_has_calls([
++ unittest.mock.call(extract_path, -1, -1),
++ unittest.mock.call(extract_path, -1, -1)
++ ])
++ mock_lchown.assert_not_called()
++
++ @symlink_test
++ @unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
++ @unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
++ @unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
++ @support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
++ def test_chown_links_on_extractall(self, link_type):
++ with ArchiveMaker() as arc:
++ arc.add("test.txt",
++ uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
++ arc.add("link",
++ type=link_type,
++ linkname='test.txt',
++ uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
++
++ with (
++ os_helper.temp_dir() as tmpdir,
++ arc.open() as tar,
++ unittest.mock.patch("os.chown") as mock_chown,
++ unittest.mock.patch("os.lchown") as mock_lchown,
++ unittest.mock.patch("os.geteuid") as mock_geteuid,
++ ):
++ # Set UID to 0 so chown() is attempted.
++ mock_geteuid.return_value = 0
++ tar.extractall(path=tmpdir, filter='data')
++ extract_link_path = os.path.join(tmpdir, "link")
++ extract_file_path = os.path.join(tmpdir, "test.txt")
++
++ if link_type == tarfile.SYMTYPE:
++ mock_chown.assert_called_once_with(extract_file_path, -1, -1)
++ mock_lchown.assert_called_once_with(extract_link_path, -1, -1)
++ else:
++ mock_chown.assert_has_calls([
++ unittest.mock.call(extract_file_path, -1, -1),
++ unittest.mock.call(extract_link_path, -1, -1)
++ ])
++ mock_lchown.assert_not_called()
++
++ def test_extract_filters_target(self):
++ # Test that when extract() falls back to extracting (rather than
++ # linking) a hardlink target, it filters the target.
++ with ArchiveMaker() as arc:
++ arc.add("target")
++ arc.add("link", hardlink_to="target")
++ def testing_filter(member, path):
++ if member.name == 'target':
++ # target: set read-only
++ return member.replace(mode=stat.S_IRUSR)
++ # link: don't overwrite the mode
++ return member.replace(mode=None)
++ tempdir = pathlib.Path(TEMPDIR) / 'extract'
++ with os_helper.temp_dir(tempdir), arc.open() as tar:
++ tar.extract("link", path=tempdir, filter=testing_filter)
++ path = tempdir / 'link'
++ if os_helper.can_chmod():
++ self.assertFalse(path.stat().st_mode & stat.S_IWUSR)
++
+ def test_link_fallback_normalizes(self):
+ # Make sure hardlink fallbacks work for non-normalized paths for all
+ # filters
+diff --git a/Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst b/Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst
+new file mode 100644
+index 0000000..9eea7b3
+--- /dev/null
++++ b/Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst
+@@ -0,0 +1,2 @@
++The :meth:`tarfile.TarFile.extract` method now applies the given filter when
++it extracts a link target from the archive as a fallback.
@@ -24,6 +24,7 @@ SRC_URI = "http://www.python.org/ftp/python/${PV}/Python-${PV}.tar.xz \
file://0001-prefer-valid-entrypoints.patch \
file://CVE-2026-11940.patch \
file://CVE-2026-11972.patch \
+ file://CVE-2026-4360.patch \
"
SRC_URI:append:class-native = " \
file://0001-Lib-sysconfig.py-use-prefix-value-from-build-configu.patch \
Backport patch to fix CVE-2026-4360. Reference: https://nvd.nist.gov/vuln/detail/CVE-2026-4360 Upstream fix: https://github.com/python/cpython/commit/5e0ef3f1afe892e4f64eb83368db57ac4c40cba0 Signed-off-by: Leonid Iziumtsev <leonid.iziumtsev@est.tech> --- .../python/python3/CVE-2026-4360.patch | 148 ++++++++++++++++++ .../recipes-devtools/python/python3_3.14.6.bb | 1 + 2 files changed, 149 insertions(+) create mode 100644 meta/recipes-devtools/python/python3/CVE-2026-4360.patch