external/python3/0001-3.9-bpo-43757-Make-pathlib-use-os.path.realpath-to-r.patch.1
 |  537 +++
 
external/python3/0001-3.9-gh-135034-Normalize-link-targets-in-tarfile-add-.patch.1
 | 1771 ++++++++++
 external/python3/UnpackedTarball_python3.mk                                    
    |    2 
 3 files changed, 2310 insertions(+)

New commits:
commit 3ef91e344e89faf6fe0b333d0a8115f1d621594e
Author:     Michael Stahl <[email protected]>
AuthorDate: Wed Oct 1 20:05:16 2025 +0200
Commit:     Michael Stahl <[email protected]>
CommitDate: Tue Oct 14 20:06:16 2025 +0200

    python3: add patch for tarfile issues
    
    CVE-2025-4435 CVE-2024-12718 CVE-2025-4138 CVE-2025-4330 CVE-2025-4517
    
    ... plus some prerequisites to be able to apply it.
    
    This was fixed in 3.9.23 but 3.8 is EOL.
    
    Change-Id: I490ef48fbd95ec9b92ad7ed722f0c3c1cd535955
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/191751
    Tested-by: allotropia jenkins <[email protected]>
    Reviewed-by: Michael Stahl <[email protected]>

diff --git 
a/external/python3/0001-3.9-bpo-43757-Make-pathlib-use-os.path.realpath-to-r.patch.1
 
b/external/python3/0001-3.9-bpo-43757-Make-pathlib-use-os.path.realpath-to-r.patch.1
new file mode 100644
index 000000000000..0b117111c9ee
--- /dev/null
+++ 
b/external/python3/0001-3.9-bpo-43757-Make-pathlib-use-os.path.realpath-to-r.patch.1
@@ -0,0 +1,537 @@
+From 00af9794dd118f7b835dd844b2b609a503ad951e Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?=C5=81ukasz=20Langa?= <[email protected]>
+Date: Mon, 2 Jun 2025 18:28:09 +0200
+Subject: [PATCH] [3.9] bpo-43757: Make pathlib use os.path.realpath() to
+ resolve symlinks in a path (GH-25264) (GH-135035)
+
+Also adds a new "strict" argument to realpath() to avoid changing the default 
behaviour of pathlib while sharing the implementation.
+
+(cherry-picked from commit baecfbd849dbf42360d3a84af6cc13160838f24d)
+
+Co-authored-by: Barney Gale <[email protected]>
+---
+ Doc/library/os.path.rst                       |  18 ++-
+ Lib/ntpath.py                                 |   4 +-
+ Lib/pathlib.py                                | 148 +++++-------------
+ Lib/posixpath.py                              |  26 ++-
+ Lib/test/test_ntpath.py                       |  60 ++++++-
+ Lib/test/test_posixpath.py                    |  57 ++++++-
+ .../2021-04-08-22-11-27.bpo-25264.b33fa0.rst  |   3 +
+ 7 files changed, 192 insertions(+), 124 deletions(-)
+ create mode 100644 
Misc/NEWS.d/next/Library/2021-04-08-22-11-27.bpo-25264.b33fa0.rst
+
+diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst
+index 97bb684a245..19a5ae028a5 100644
+--- a/Doc/library/os.path.rst
++++ b/Doc/library/os.path.rst
+@@ -345,15 +345,24 @@ the :mod:`glob` module.)
+       Accepts a :term:`path-like object`.
+ 
+ 
+-.. function:: realpath(path)
++.. function:: realpath(path, *, strict=False)
+ 
+    Return the canonical path of the specified filename, eliminating any 
symbolic
+    links encountered in the path (if they are supported by the operating
+    system).
+ 
++   If a path doesn't exist or a symlink loop is encountered, and *strict* is
++   ``True``, :exc:`OSError` is raised. If *strict* is ``False``, the path is
++   resolved as far as possible and any remainder is appended without checking
++   whether it exists.
++
+    .. note::
+-      When symbolic link cycles occur, the returned path will be one member of
+-      the cycle, but no guarantee is made about which member that will be.
++      This function emulates the operating system's procedure for making a 
path
++      canonical, which differs slightly between Windows and UNIX with respect
++      to how links and subsequent path components interact.
++
++      Operating system APIs make paths canonical as needed, so it's not
++      normally necessary to call this function.
+ 
+    .. versionchanged:: 3.6
+       Accepts a :term:`path-like object`.
+@@ -361,6 +370,9 @@ the :mod:`glob` module.)
+    .. versionchanged:: 3.8
+       Symbolic links and junctions are now resolved on Windows.
+ 
++   .. versionchanged:: 3.9.23
++      The *strict* parameter was added.
++
+ 
+ .. function:: relpath(path, start=os.curdir)
+ 
+diff --git a/Lib/ntpath.py b/Lib/ntpath.py
+index 6f771773a7d..92b46e20fc8 100644
+--- a/Lib/ntpath.py
++++ b/Lib/ntpath.py
+@@ -622,7 +622,7 @@ def _getfinalpathname_nonstrict(path):
+                 tail = join(name, tail) if tail else name
+         return tail
+ 
+-    def realpath(path):
++    def realpath(path, *, strict=False):
+         path = normpath(path)
+         if isinstance(path, bytes):
+             prefix = b'\\?\'
+@@ -647,6 +647,8 @@ def realpath(path):
+             path = _getfinalpathname(path)
+             initial_winerror = 0
+         except OSError as ex:
++            if strict:
++                raise
+             initial_winerror = ex.winerror
+             path = _getfinalpathname_nonstrict(path)
+         # The path returned by _getfinalpathname will always start with \?\ -
+diff --git a/Lib/pathlib.py b/Lib/pathlib.py
+index 7aeda14a141..1185a245739 100644
+--- a/Lib/pathlib.py
++++ b/Lib/pathlib.py
+@@ -14,15 +14,6 @@
+ 
+ 
+ supports_symlinks = True
+-if os.name == 'nt':
+-    import nt
+-    if sys.getwindowsversion()[:2] >= (6, 0):
+-        from nt import _getfinalpathname
+-    else:
+-        supports_symlinks = False
+-        _getfinalpathname = None
+-else:
+-    nt = None
+ 
+ 
+ __all__ = [
+@@ -34,14 +25,17 @@
+ # Internals
+ #
+ 
++_WINERROR_NOT_READY = 21  # drive exists but is not accessible
++_WINERROR_INVALID_NAME = 123  # fix for bpo-35306
++_WINERROR_CANT_RESOLVE_FILENAME = 1921  # broken symlink pointing to itself
++
+ # EBADF - guard against macOS `stat` throwing EBADF
+ _IGNORED_ERROS = (ENOENT, ENOTDIR, EBADF, ELOOP)
+ 
+ _IGNORED_WINERRORS = (
+-    21,  # ERROR_NOT_READY - drive exists but is not accessible
+-    123, # ERROR_INVALID_NAME - fix for bpo-35306
+-    1921,  # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to 
itself
+-)
++    _WINERROR_NOT_READY,
++    _WINERROR_INVALID_NAME,
++    _WINERROR_CANT_RESOLVE_FILENAME)
+ 
+ def _ignore_error(exception):
+     return (getattr(exception, 'errno', None) in _IGNORED_ERROS or
+@@ -200,30 +194,6 @@ def casefold_parts(self, parts):
+     def compile_pattern(self, pattern):
+         return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch
+ 
+-    def resolve(self, path, strict=False):
+-        s = str(path)
+-        if not s:
+-            return os.getcwd()
+-        previous_s = None
+-        if _getfinalpathname is not None:
+-            if strict:
+-                return self._ext_to_normal(_getfinalpathname(s))
+-            else:
+-                tail_parts = []  # End of the path after the first one not 
found
+-                while True:
+-                    try:
+-                        s = self._ext_to_normal(_getfinalpathname(s))
+-                    except FileNotFoundError:
+-                        previous_s = s
+-                        s, tail = os.path.split(s)
+-                        tail_parts.append(tail)
+-                        if previous_s == s:
+-                            return path
+-                    else:
+-                        return os.path.join(s, *reversed(tail_parts))
+-        # Means fallback on absolute
+-        return None
+-
+     def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix):
+         prefix = ''
+         if s.startswith(ext_prefix):
+@@ -234,10 +204,6 @@ def _split_extended_path(self, s, 
ext_prefix=ext_namespace_prefix):
+                 s = '\' + s[3:]
+         return prefix, s
+ 
+-    def _ext_to_normal(self, s):
+-        # Turn back an extended path into a normal DOS-like path
+-        return self._split_extended_path(s)[1]
+-
+     def is_reserved(self, parts):
+         # NOTE: the rules for reserved names seem somewhat complicated
+         # (e.g. r"..\NUL" is reserved but not r"foo\NUL").
+@@ -324,54 +290,6 @@ def casefold_parts(self, parts):
+     def compile_pattern(self, pattern):
+         return re.compile(fnmatch.translate(pattern)).fullmatch
+ 
+-    def resolve(self, path, strict=False):
+-        sep = self.sep
+-        accessor = path._accessor
+-        seen = {}
+-        def _resolve(path, rest):
+-            if rest.startswith(sep):
+-                path = ''
+-
+-            for name in rest.split(sep):
+-                if not name or name == '.':
+-                    # current dir
+-                    continue
+-                if name == '..':
+-                    # parent dir
+-                    path, _, _ = path.rpartition(sep)
+-                    continue
+-                if path.endswith(sep):
+-                    newpath = path + name
+-                else:
+-                    newpath = path + sep + name
+-                if newpath in seen:
+-                    # Already seen this path
+-                    path = seen[newpath]
+-                    if path is not None:
+-                        # use cached value
+-                        continue
+-                    # The symlink is not resolved, so we must have a symlink 
loop.
+-                    raise RuntimeError("Symlink loop from %r" % newpath)
+-                # Resolve the symbolic link
+-                try:
+-                    target = accessor.readlink(newpath)
+-                except OSError as e:
+-                    if e.errno != EINVAL and strict:
+-                        raise
+-                    # Not a symlink, or non-strict mode. We just leave the 
path
+-                    # untouched.
+-                    path = newpath
+-                else:
+-                    seen[newpath] = None # not resolved symlink
+-                    path = _resolve(path, target)
+-                    seen[newpath] = path # resolved symlink
+-
+-            return path
+-        # NOTE: according to POSIX, getcwd() cannot contain path components
+-        # which are symlinks.
+-        base = '' if path.is_absolute() else os.getcwd()
+-        return _resolve(base, str(path)) or sep
+-
+     def is_reserved(self, parts):
+         return False
+ 
+@@ -443,17 +361,11 @@ def link_to(self, target):
+ 
+     replace = os.replace
+ 
+-    if nt:
+-        if supports_symlinks:
+-            symlink = os.symlink
+-        else:
+-            def symlink(a, b, target_is_directory):
+-                raise NotImplementedError("symlink() not available on this 
system")
++    if hasattr(os, "symlink"):
++        symlink = os.symlink
+     else:
+-        # Under POSIX, os.symlink() takes two args
+-        @staticmethod
+-        def symlink(a, b, target_is_directory):
+-            return os.symlink(a, b)
++        def symlink(self, src, dst, target_is_directory=False):
++            raise NotImplementedError("os.symlink() not available on this 
system")
+ 
+     utime = os.utime
+ 
+@@ -475,6 +387,12 @@ def group(self, path):
+     def readlink(self, path):
+         return os.readlink(path)
+ 
++    getcwd = os.getcwd
++
++    expanduser = staticmethod(os.path.expanduser)
++
++    realpath = staticmethod(os.path.realpath)
++
+ 
+ _normal_accessor = _NormalAccessor()
+ 
+@@ -1212,17 +1130,27 @@ def resolve(self, strict=False):
+         """
+         if self._closed:
+             self._raise_closed()
+-        s = self._flavour.resolve(self, strict=strict)
+-        if s is None:
+-            # No symlink resolution => for consistency, raise an error if
+-            # the path doesn't exist or is forbidden
+-            self.stat()
+-            s = str(self.absolute())
+-        # Now we have no symlinks in the path, it's safe to normalize it.
+-        normed = self._flavour.pathmod.normpath(s)
+-        obj = self._from_parts((normed,), init=False)
+-        obj._init(template=self)
+-        return obj
++
++        def check_eloop(e):
++            winerror = getattr(e, 'winerror', 0)
++            if e.errno == ELOOP or winerror == 
_WINERROR_CANT_RESOLVE_FILENAME:
++                raise RuntimeError("Symlink loop from %r" % e.filename)
++
++        try:
++            s = self._accessor.realpath(self, strict=strict)
++        except OSError as e:
++            check_eloop(e)
++            raise
++        p = self._from_parts((s,))
++
++        # In non-strict mode, realpath() doesn't raise on symlink loops.
++        # Ensure we get an exception by calling stat()
++        if not strict:
++            try:
++                p.stat()
++            except OSError as e:
++                check_eloop(e)
++        return p
+ 
+     def stat(self):
+         """
+diff --git a/Lib/posixpath.py b/Lib/posixpath.py
+index af2814bdb05..eb80fb9e9be 100644
+--- a/Lib/posixpath.py
++++ b/Lib/posixpath.py
+@@ -385,16 +385,16 @@ def abspath(path):
+ # Return a canonical path (i.e. the absolute location of a file on the
+ # filesystem).
+ 
+-def realpath(filename):
++def realpath(filename, *, strict=False):
+     """Return the canonical path of the specified filename, eliminating any
+ symbolic links encountered in the path."""
+     filename = os.fspath(filename)
+-    path, ok = _joinrealpath(filename[:0], filename, {})
++    path, ok = _joinrealpath(filename[:0], filename, strict, {})
+     return abspath(path)
+ 
+ # Join two paths, normalizing and eliminating any symbolic links
+ # encountered in the second path.
+-def _joinrealpath(path, rest, seen):
++def _joinrealpath(path, rest, strict, seen):
+     if isinstance(path, bytes):
+         sep = b'/'
+         curdir = b'.'
+@@ -423,7 +423,15 @@ def _joinrealpath(path, rest, seen):
+                 path = pardir
+             continue
+         newpath = join(path, name)
+-        if not islink(newpath):
++        try:
++            st = os.lstat(newpath)
++        except OSError:
++            if strict:
++                raise
++            is_link = False
++        else:
++            is_link = stat.S_ISLNK(st.st_mode)
++        if not is_link:
+             path = newpath
+             continue
+         # Resolve the symbolic link
+@@ -434,10 +442,14 @@ def _joinrealpath(path, rest, seen):
+                 # use cached value
+                 continue
+             # The symlink is not resolved, so we must have a symlink loop.
+-            # Return already resolved part + rest of the path unchanged.
+-            return join(newpath, rest), False
++            if strict:
++                # Raise OSError(errno.ELOOP)
++                os.stat(newpath)
++            else:
++                # Return already resolved part + rest of the path unchanged.
++                return join(newpath, rest), False
+         seen[newpath] = None # not resolved symlink
+-        path, ok = _joinrealpath(path, os.readlink(newpath), seen)
++        path, ok = _joinrealpath(path, os.readlink(newpath), strict, seen)
+         if not ok:
+             return join(path, rest), False
+         seen[newpath] = path # resolved symlink
+diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
+index 6f881f197c4..2d3fa101bc4 100644
+--- a/Lib/test/test_ntpath.py
++++ b/Lib/test/test_ntpath.py
+@@ -267,6 +267,17 @@ def test_realpath_basic(self):
+         self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1")),
+                          os.fsencode(ABSTFN))
+ 
++    @support.skip_unless_symlink
++    @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
++    def test_realpath_strict(self):
++        # Bug #43757: raise FileNotFoundError in strict mode if we encounter
++        # a path that does not exist.
++        ABSTFN = ntpath.abspath(support.TESTFN)
++        os.symlink(ABSTFN + "1", ABSTFN)
++        self.addCleanup(support.unlink, ABSTFN)
++        self.assertRaises(FileNotFoundError, ntpath.realpath, ABSTFN, 
strict=True)
++        self.assertRaises(FileNotFoundError, ntpath.realpath, ABSTFN + "2", 
strict=True)
++
+     @support.skip_unless_symlink
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+     def test_realpath_relative(self):
+@@ -338,8 +349,9 @@ def test_realpath_broken_symlinks(self):
+     @support.skip_unless_symlink
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+     def test_realpath_symlink_loops(self):
+-        # Symlink loops are non-deterministic as to which path is returned, 
but
+-        # it will always be the fully resolved path of one member of the cycle
++        # Symlink loops in non-strict mode are non-deterministic as to which
++        # path is returned, but it will always be the fully resolved path of
++        # one member of the cycle
+         ABSTFN = ntpath.abspath(support.TESTFN)
+         self.addCleanup(support.unlink, ABSTFN)
+         self.addCleanup(support.unlink, ABSTFN + "1")
+@@ -381,6 +393,50 @@ def test_realpath_symlink_loops(self):
+         # Test using relative path as well.
+         self.assertPathEqual(ntpath.realpath(ntpath.basename(ABSTFN)), ABSTFN)
+ 
++    @support.skip_unless_symlink
++    @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
++    def test_realpath_symlink_loops_strict(self):
++        # Symlink loops raise OSError in strict mode
++        ABSTFN = ntpath.abspath(support.TESTFN)
++        self.addCleanup(support.unlink, ABSTFN)
++        self.addCleanup(support.unlink, ABSTFN + "1")
++        self.addCleanup(support.unlink, ABSTFN + "2")
++        self.addCleanup(support.unlink, ABSTFN + "y")
++        self.addCleanup(support.unlink, ABSTFN + "c")
++        self.addCleanup(support.unlink, ABSTFN + "a")
++
++        os.symlink(ABSTFN, ABSTFN)
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=True)
++
++        os.symlink(ABSTFN + "1", ABSTFN + "2")
++        os.symlink(ABSTFN + "2", ABSTFN + "1")
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1", strict=True)
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2", strict=True)
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\x", 
strict=True)
++        # Windows eliminates '..' components before resolving links, so the
++        # following call is not expected to raise.
++        self.assertPathEqual(ntpath.realpath(ABSTFN + "1\..", strict=True),
++                             ntpath.dirname(ABSTFN))
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\..\x", 
strict=True)
++        os.symlink(ABSTFN + "x", ABSTFN + "y")
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\..\"
++                                             + ntpath.basename(ABSTFN) + "y",
++                                             strict=True)
++        self.assertRaises(OSError, ntpath.realpath,
++                          ABSTFN + "1\..\" + ntpath.basename(ABSTFN) + "1",
++                          strict=True)
++
++        os.symlink(ntpath.basename(ABSTFN) + "a\b", ABSTFN + "a")
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a", strict=True)
++
++        os.symlink("..\" + ntpath.basename(ntpath.dirname(ABSTFN))
++                   + "\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c")
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c", strict=True)
++
++        # Test using relative path as well.
++        self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
++                          strict=True)
++
+     @support.skip_unless_symlink
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+     def test_realpath_symlink_prefix(self):
+diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py
+index 18819a5dc1c..aee3cb421fb 100644
+--- a/Lib/test/test_posixpath.py
++++ b/Lib/test/test_posixpath.py
+@@ -350,6 +350,19 @@ def test_realpath_basic(self):
+         finally:
+             support.unlink(ABSTFN)
+ 
++    @unittest.skipUnless(hasattr(os, "symlink"),
++                         "Missing symlink implementation")
++    @skip_if_ABSTFN_contains_backslash
++    def test_realpath_strict(self):
++        # Bug #43757: raise FileNotFoundError in strict mode if we encounter
++        # a path that does not exist.
++        try:
++            os.symlink(ABSTFN+"1", ABSTFN)
++            self.assertRaises(FileNotFoundError, realpath, ABSTFN, 
strict=True)
++            self.assertRaises(FileNotFoundError, realpath, ABSTFN + "2", 
strict=True)
++        finally:
++            support.unlink(ABSTFN)
++
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+@@ -365,7 +378,7 @@ def test_realpath_relative(self):
+     @skip_if_ABSTFN_contains_backslash
+     def test_realpath_symlink_loops(self):
+         # Bug #930024, return the path unchanged if we get into an infinite
+-        # symlink loop.
++        # symlink loop in non-strict mode (default).
+         try:
+             os.symlink(ABSTFN, ABSTFN)
+             self.assertEqual(realpath(ABSTFN), ABSTFN)
+@@ -402,6 +415,48 @@ def test_realpath_symlink_loops(self):
+             support.unlink(ABSTFN+"c")
+             support.unlink(ABSTFN+"a")
+ 
++    @unittest.skipUnless(hasattr(os, "symlink"),
++                         "Missing symlink implementation")
++    @skip_if_ABSTFN_contains_backslash
++    def test_realpath_symlink_loops_strict(self):
++        # Bug #43757, raise OSError if we get into an infinite symlink loop in
++        # strict mode.
++        try:
++            os.symlink(ABSTFN, ABSTFN)
++            self.assertRaises(OSError, realpath, ABSTFN, strict=True)
++
++            os.symlink(ABSTFN+"1", ABSTFN+"2")
++            os.symlink(ABSTFN+"2", ABSTFN+"1")
++            self.assertRaises(OSError, realpath, ABSTFN+"1", strict=True)
++            self.assertRaises(OSError, realpath, ABSTFN+"2", strict=True)
++
++            self.assertRaises(OSError, realpath, ABSTFN+"1/x", strict=True)
++            self.assertRaises(OSError, realpath, ABSTFN+"1/..", strict=True)
++            self.assertRaises(OSError, realpath, ABSTFN+"1/../x", strict=True)
++            os.symlink(ABSTFN+"x", ABSTFN+"y")
++            self.assertRaises(OSError, realpath,
++                              ABSTFN+"1/../" + basename(ABSTFN) + "y", 
strict=True)
++            self.assertRaises(OSError, realpath,
++                              ABSTFN+"1/../" + basename(ABSTFN) + "1", 
strict=True)
++
++            os.symlink(basename(ABSTFN) + "a/b", ABSTFN+"a")
++            self.assertRaises(OSError, realpath, ABSTFN+"a", strict=True)
++
++            os.symlink("../" + basename(dirname(ABSTFN)) + "/" +
++                       basename(ABSTFN) + "c", ABSTFN+"c")
++            self.assertRaises(OSError, realpath, ABSTFN+"c", strict=True)
++
++            # Test using relative path as well.
++            with support.change_cwd(dirname(ABSTFN)):
++                self.assertRaises(OSError, realpath, basename(ABSTFN), 
strict=True)
++        finally:
++            support.unlink(ABSTFN)
++            support.unlink(ABSTFN+"1")
++            support.unlink(ABSTFN+"2")
++            support.unlink(ABSTFN+"y")
++            support.unlink(ABSTFN+"c")
++            support.unlink(ABSTFN+"a")
++
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+diff --git a/Misc/NEWS.d/next/Library/2021-04-08-22-11-27.bpo-25264.b33fa0.rst 
b/Misc/NEWS.d/next/Library/2021-04-08-22-11-27.bpo-25264.b33fa0.rst
+new file mode 100644
+index 00000000000..593846ec15c
+--- /dev/null
++++ b/Misc/NEWS.d/next/Library/2021-04-08-22-11-27.bpo-25264.b33fa0.rst
+@@ -0,0 +1,3 @@
++:func:`os.path.realpath` now accepts a *strict* keyword-only argument.
++When set to ``True``, :exc:`OSError` is raised if a path doesn't exist
++or a symlink loop is encountered.
+-- 
+2.51.0
+
diff --git 
a/external/python3/0001-3.9-gh-135034-Normalize-link-targets-in-tarfile-add-.patch.1
 
b/external/python3/0001-3.9-gh-135034-Normalize-link-targets-in-tarfile-add-.patch.1
new file mode 100644
index 000000000000..9d6c34b1ece9
--- /dev/null
+++ 
b/external/python3/0001-3.9-gh-135034-Normalize-link-targets-in-tarfile-add-.patch.1
@@ -0,0 +1,1771 @@
+From dd8f187d0746da151e0025c51680979ac5b4cfb1 Mon Sep 17 00:00:00 2001
+From: "T. Wouters" <[email protected]>
+Date: Tue, 3 Jun 2025 19:02:50 +0200
+Subject: [PATCH] [3.9] gh-135034: Normalize link targets in tarfile, add
+ `os.path.realpath(strict='allow_missing')` (GH-135037) (GH-135084)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Addresses CVEs 2024-12718, 2025-4138, 2025-4330, and 2025-4517.
+(cherry picked from commit 3612d8f51741b11f36f8fb0494d79086bac9390a)
+
+Co-authored-by: Ɓukasz Langa <[email protected]>
+Co-authored-by: Petr Viktorin <[email protected]>
+Co-authored-by: Seth Michael Larson <[email protected]>
+Co-authored-by: Adam Turner <[email protected]>
+Co-authored-by: Serhiy Storchaka <[email protected]>
+---
+ Doc/library/os.path.rst                       |  33 +-
+ Doc/library/tarfile.rst                       |  20 ++
+ Doc/whatsnew/3.9.rst                          |  34 ++
+ Lib/genericpath.py                            |  11 +-
+ Lib/ntpath.py                                 |  35 +-
+ Lib/posixpath.py                              |  15 +-
+ Lib/tarfile.py                                | 161 +++++++--
+ Lib/test/test_ntpath.py                       | 169 +++++++++-
+ Lib/test/test_posixpath.py                    | 282 +++++++++++++---
+ Lib/test/test_tarfile.py                      | 314 ++++++++++++++++--
+ ...-06-02-11-32-23.gh-issue-135034.RLGjbp.rst |   6 +
+ 11 files changed, 946 insertions(+), 134 deletions(-)
+ create mode 100644 
Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst
+
+diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst
+index 19a5ae028a5..235a4deecfc 100644
+--- a/Doc/library/os.path.rst
++++ b/Doc/library/os.path.rst
+@@ -351,10 +351,26 @@ the :mod:`glob` module.)
+    links encountered in the path (if they are supported by the operating
+    system).
+ 
+-   If a path doesn't exist or a symlink loop is encountered, and *strict* is
+-   ``True``, :exc:`OSError` is raised. If *strict* is ``False``, the path is
+-   resolved as far as possible and any remainder is appended without checking
+-   whether it exists.
++   By default, the path is evaluated up to the first component that does not
++   exist, is a symlink loop, or whose evaluation raises :exc:`OSError`.
++   All such components are appended unchanged to the existing part of the 
path.
++
++   Some errors that are handled this way include "access denied", "not a
++   directory", or "bad argument to internal function". Thus, the
++   resulting path may be missing or inaccessible, may still contain
++   links or loops, and may traverse non-directories.
++
++   This behavior can be modified by keyword arguments:
++
++   If *strict* is ``True``, the first error encountered when evaluating the 
path is
++   re-raised.
++   In particular, :exc:`FileNotFoundError` is raised if *path* does not exist,
++   or another :exc:`OSError` if it is otherwise inaccessible.
++
++   If *strict* is :py:data:`os.path.ALLOW_MISSING`, errors other than
++   :exc:`FileNotFoundError` are re-raised (as with ``strict=True``).
++   Thus, the returned path will not contain any symbolic links, but the named
++   file and some of its parent directories may be missing.
+ 
+    .. note::
+       This function emulates the operating system's procedure for making a 
path
+@@ -373,6 +389,15 @@ the :mod:`glob` module.)
+    .. versionchanged:: 3.9.23
+       The *strict* parameter was added.
+ 
++   .. versionchanged:: next
++      The :py:data:`~os.path.ALLOW_MISSING` value for the *strict* parameter
++      was added.
++
++.. data:: ALLOW_MISSING
++
++   Special value used for the *strict* argument in :func:`realpath`.
++
++   .. versionadded:: next
+ 
+ .. function:: relpath(path, start=os.curdir)
+ 
+diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst
+index 165088529a8..d03f13db5d8 100644
+--- a/Doc/library/tarfile.rst
++++ b/Doc/library/tarfile.rst
+@@ -237,6 +237,15 @@ The :mod:`tarfile` module defines the following 
exceptions:
+    Raised to refuse extracting a symbolic link pointing outside the 
destination
+    directory.
+ 
++.. exception:: LinkFallbackError
++
++   Raised to refuse emulating a link (hard or symbolic) by extracting another
++   archive member, when that member would be rejected by the filter location.
++   The exception that was raised to reject the replacement member is available
++   as :attr:`!BaseException.__context__`.
++
++   .. versionadded:: next
++
+ 
+ The following constants are available at the module level:
+ 
+@@ -954,6 +963,12 @@ reused in custom filters:
+   Implements the ``'data'`` filter.
+   In addition to what ``tar_filter`` does:
+ 
++  - Normalize link targets (:attr:`TarInfo.linkname`) using
++    :func:`os.path.normpath`.
++    Note that this removes internal ``..`` components, which may change the
++    meaning of the link if the path in :attr:`!TarInfo.linkname` traverses
++    symbolic links.
++
+   - :ref:`Refuse <tarfile-extraction-refuse>` to extract links (hard or soft)
+     that link to absolute paths, or ones that link outside the destination.
+ 
+@@ -982,6 +997,10 @@ reused in custom filters:
+ 
+   Return the modified ``TarInfo`` member.
+ 
++  .. versionchanged:: next
++
++     Link targets are now normalized.
++
+ 
+ .. _tarfile-extraction-refuse:
+ 
+@@ -1008,6 +1027,7 @@ Here is an incomplete list of things to consider:
+ * Extract to a :func:`new temporary directory <tempfile.mkdtemp>`
+   to prevent e.g. exploiting pre-existing links, and to make it easier to
+   clean up after a failed extraction.
++* Disallow symbolic links if you do not need the functionality.
+ * When working with untrusted data, use external (e.g. OS-level) limits on
+   disk, memory and CPU usage.
+ * Check filenames against an allow-list of characters
+diff --git a/Lib/genericpath.py b/Lib/genericpath.py
+index ce36451a3af..ad8d47b41d4 100644
+--- a/Lib/genericpath.py
++++ b/Lib/genericpath.py
+@@ -8,7 +8,7 @@
+ 
+ __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime',
+            'getsize', 'isdir', 'isfile', 'samefile', 'sameopenfile',
+-           'samestat']
++           'samestat', 'ALLOW_MISSING']
+ 
+ 
+ # Does a path exist?
+@@ -153,3 +153,12 @@ def _check_arg_types(funcname, *args):
+                             f'os.PathLike object, not 
{s.__class__.__name__!r}') from None
+     if hasstr and hasbytes:
+         raise TypeError("Can't mix strings and bytes in path components") 
from None
++
++# A singleton with a true boolean value.
++@object.__new__
++class ALLOW_MISSING:
++    """Special value for use in realpath()."""
++    def __repr__(self):
++        return 'os.path.ALLOW_MISSING'
++    def __reduce__(self):
++        return self.__class__.__name__
+diff --git a/Lib/ntpath.py b/Lib/ntpath.py
+index 92b46e20fc8..2588ea356fe 100644
+--- a/Lib/ntpath.py
++++ b/Lib/ntpath.py
+@@ -29,7 +29,8 @@
+            "ismount", "expanduser","expandvars","normpath","abspath",
+            "curdir","pardir","sep","pathsep","defpath","altsep",
+            
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
+-           "samefile", "sameopenfile", "samestat", "commonpath"]
++           "samefile", "sameopenfile", "samestat", "commonpath",
++           "ALLOW_MISSING"]
+ 
+ def _get_bothseps(path):
+     if isinstance(path, bytes):
+@@ -532,9 +533,10 @@ def abspath(path):
+     from nt import _getfinalpathname, readlink as _nt_readlink
+ except ImportError:
+     # realpath is a no-op on systems without _getfinalpathname support.
+-    realpath = abspath
++    def realpath(path, *, strict=False):
++        return abspath(path)
+ else:
+-    def _readlink_deep(path):
++    def _readlink_deep(path, ignored_error=OSError):
+         # These error codes indicate that we should stop reading links and
+         # return the path we currently have.
+         # 1: ERROR_INVALID_FUNCTION
+@@ -567,7 +569,7 @@ def _readlink_deep(path):
+                         path = old_path
+                         break
+                     path = normpath(join(dirname(old_path), path))
+-            except OSError as ex:
++            except ignored_error as ex:
+                 if ex.winerror in allowed_winerror:
+                     break
+                 raise
+@@ -576,7 +578,7 @@ def _readlink_deep(path):
+                 break
+         return path
+ 
+-    def _getfinalpathname_nonstrict(path):
++    def _getfinalpathname_nonstrict(path, ignored_error=OSError):
+         # These error codes indicate that we should stop resolving the path
+         # and return the value we currently have.
+         # 1: ERROR_INVALID_FUNCTION
+@@ -600,17 +602,18 @@ def _getfinalpathname_nonstrict(path):
+             try:
+                 path = _getfinalpathname(path)
+                 return join(path, tail) if tail else path
+-            except OSError as ex:
++            except ignored_error as ex:
+                 if ex.winerror not in allowed_winerror:
+                     raise
+                 try:
+                     # The OS could not resolve this path fully, so we attempt
+                     # to follow the link ourselves. If we succeed, join the 
tail
+                     # and return.
+-                    new_path = _readlink_deep(path)
++                    new_path = _readlink_deep(path,
++                                              ignored_error=ignored_error)
+                     if new_path != path:
+                         return join(new_path, tail) if tail else new_path
+-                except OSError:
++                except ignored_error:
+                     # If we fail to readlink(), let's keep traversing
+                     pass
+                 path, name = split(path)
+@@ -641,16 +644,24 @@ def realpath(path, *, strict=False):
+             if normcase(path) == normcase(devnull):
+                 return '\\.\NUL'
+         had_prefix = path.startswith(prefix)
++
++        if strict is ALLOW_MISSING:
++            ignored_error = FileNotFoundError
++            strict = True
++        elif strict:
++            ignored_error = ()
++        else:
++            ignored_error = OSError
++
+         if not had_prefix and not isabs(path):
+             path = join(cwd, path)
+         try:
+             path = _getfinalpathname(path)
+             initial_winerror = 0
+-        except OSError as ex:
+-            if strict:
+-                raise
++        except ignored_error as ex:
+             initial_winerror = ex.winerror
+-            path = _getfinalpathname_nonstrict(path)
++            path = _getfinalpathname_nonstrict(path,
++                                               ignored_error=ignored_error)
+         # The path returned by _getfinalpathname will always start with \?\ -
+         # strip off that prefix unless it was already provided on the original
+         # path.
+diff --git a/Lib/posixpath.py b/Lib/posixpath.py
+index eb80fb9e9be..de2b90c10cc 100644
+--- a/Lib/posixpath.py
++++ b/Lib/posixpath.py
+@@ -35,7 +35,7 @@
+            "samefile","sameopenfile","samestat",
+            "curdir","pardir","sep","pathsep","defpath","altsep","extsep",
+            "devnull","realpath","supports_unicode_filenames","relpath",
+-           "commonpath"]
++           "commonpath", "ALLOW_MISSING"]
+ 
+ 
+ def _get_sep(path):
+@@ -403,6 +403,15 @@ def _joinrealpath(path, rest, strict, seen):
+         sep = '/'
+         curdir = '.'
+         pardir = '..'
++        getcwd = os.getcwd
++    if strict is ALLOW_MISSING:
++        ignored_error = FileNotFoundError
++    elif strict:
++        ignored_error = ()
++    else:
++        ignored_error = OSError
++
++    maxlinks = None
+ 
+     if isabs(rest):
+         rest = rest[1:]
+@@ -425,9 +434,7 @@ def _joinrealpath(path, rest, strict, seen):
+         newpath = join(path, name)
+         try:
+             st = os.lstat(newpath)
+-        except OSError:
+-            if strict:
+-                raise
++        except ignored_error:
+             is_link = False
+         else:
+             is_link = stat.S_ISLNK(st.st_mode)
+diff --git a/Lib/tarfile.py b/Lib/tarfile.py
+index d75ba50b667..7c9027dde84 100755
+--- a/Lib/tarfile.py
++++ b/Lib/tarfile.py
+@@ -749,10 +749,22 @@ def __init__(self, tarinfo, path):
+         super().__init__(f'{tarinfo.name!r} would link to {path!r}, '
+                          + 'which is outside the destination')
+ 
++class LinkFallbackError(FilterError):
++    def __init__(self, tarinfo, path):
++        self.tarinfo = tarinfo
++        self._path = path
++        super().__init__(f'link {tarinfo.name!r} would be extracted as a '
++                         + f'copy of {path!r}, which was rejected')
++
++# Errors caused by filters -- both "fatal" and "non-fatal" -- that
++# we consider to be issues with the argument, rather than a bug in the
++# filter function
++_FILTER_ERRORS = (FilterError, OSError, ExtractError)
++
+ def _get_filtered_attrs(member, dest_path, for_data=True):
+     new_attrs = {}
+     name = member.name
+-    dest_path = os.path.realpath(dest_path)
++    dest_path = os.path.realpath(dest_path, strict=os.path.ALLOW_MISSING)
+     # Strip leading / (tar's directory separator) from filenames.
+     # Include os.sep (target OS directory separator) as well.
+     if name.startswith(('/', os.sep)):
+@@ -762,7 +774,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
+         # For example, 'C:/foo' on Windows.
+         raise AbsolutePathError(member)
+     # Ensure we stay in the destination
+-    target_path = os.path.realpath(os.path.join(dest_path, name))
++    target_path = os.path.realpath(os.path.join(dest_path, name),
++                                   strict=os.path.ALLOW_MISSING)
+     if os.path.commonpath([target_path, dest_path]) != dest_path:
+         raise OutsideDestinationError(member, target_path)
+     # Limit permissions (no high bits, and go-w)
+@@ -800,6 +813,9 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
+         if member.islnk() or member.issym():
+             if os.path.isabs(member.linkname):
+                 raise AbsoluteLinkError(member)
++            normalized = os.path.normpath(member.linkname)
++            if normalized != member.linkname:
++                new_attrs['linkname'] = normalized
+             if member.issym():
+                 target_path = os.path.join(dest_path,
+                                            os.path.dirname(name),
+@@ -807,7 +823,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
+             else:
+                 target_path = os.path.join(dest_path,
+                                            member.linkname)
+-            target_path = os.path.realpath(target_path)
++            target_path = os.path.realpath(target_path,
++                                           strict=os.path.ALLOW_MISSING)
+             if os.path.commonpath([target_path, dest_path]) != dest_path:
+                 raise LinkOutsideDestinationError(member, target_path)
+     return new_attrs
+@@ -2268,30 +2285,58 @@ def extractall(self, path=".", members=None, *, 
numeric_owner=False,
+             members = self
+ 
+         for member in members:
+-            tarinfo = self._get_extract_tarinfo(member, filter_function, path)
++            tarinfo, unfiltered = self._get_extract_tarinfo(
++                member, filter_function, path)
+             if tarinfo is None:
+                 continue
+             if tarinfo.isdir():
+                 # For directories, delay setting attributes until later,
+                 # since permissions can interfere with extraction and
+                 # extracting contents can reset mtime.
+-                directories.append(tarinfo)
++                directories.append(unfiltered)
+             self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(),
+-                              numeric_owner=numeric_owner)
++                              numeric_owner=numeric_owner,
++                              filter_function=filter_function)
+ 
+         # Reverse sort directories.
+         directories.sort(key=lambda a: a.name, reverse=True)
+ 
++
+         # Set correct owner, mtime and filemode on directories.
+-        for tarinfo in directories:
+-            dirpath = os.path.join(path, tarinfo.name)
++        for unfiltered in directories:
+             try:
++                # Need to re-apply any filter, to take the *current* 
filesystem
++                # state into account.
++                try:
++                    tarinfo = filter_function(unfiltered, path)
++                except _FILTER_ERRORS as exc:
++                    self._log_no_directory_fixup(unfiltered, repr(exc))
++                    continue
++                if tarinfo is None:
++                    self._log_no_directory_fixup(unfiltered,
++                                                 'excluded by filter')
++                    continue
++                dirpath = os.path.join(path, tarinfo.name)
++                try:
++                    lstat = os.lstat(dirpath)
++                except FileNotFoundError:
++                    self._log_no_directory_fixup(tarinfo, 'missing')
++                    continue
++                if not stat.S_ISDIR(lstat.st_mode):
++                    # This is no longer a directory; presumably a later
++                    # member overwrote the entry.
++                    self._log_no_directory_fixup(tarinfo, 'not a directory')
++                    continue
+                 self.chown(tarinfo, dirpath, numeric_owner=numeric_owner)
+                 self.utime(tarinfo, dirpath)
+                 self.chmod(tarinfo, dirpath)
+             except ExtractError as e:
+                 self._handle_nonfatal_error(e)
+ 
++    def _log_no_directory_fixup(self, member, reason):
++        self._dbg(2, "tarfile: Not fixing up directory %r (%s)" %
++                  (member.name, reason))
++
+     def extract(self, member, path="", set_attrs=True, *, numeric_owner=False,
+                 filter=None):
+         """Extract a member from the archive to the current working directory,
+@@ -2307,41 +2352,56 @@ def extract(self, member, path="", set_attrs=True, *, 
numeric_owner=False,
+            String names of common filters are accepted.
+         """
+         filter_function = self._get_filter_function(filter)
+-        tarinfo = self._get_extract_tarinfo(member, filter_function, path)
++        tarinfo, unfiltered = self._get_extract_tarinfo(
++            member, filter_function, path)
+         if tarinfo is not None:
+             self._extract_one(tarinfo, path, set_attrs, numeric_owner)
+ 
+     def _get_extract_tarinfo(self, member, filter_function, path):
+-        """Get filtered TarInfo (or None) from member, which might be a str"""
++        """Get (filtered, unfiltered) TarInfos from *member*
++
++        *member* might be a string.
++
++        Return (None, None) if not found.
++        """
++
+         if isinstance(member, str):
+-            tarinfo = self.getmember(member)
++            unfiltered = self.getmember(member)
+         else:
+-            tarinfo = member
++            unfiltered = member
+ 
+-        unfiltered = tarinfo
++        filtered = None
+         try:
+-            tarinfo = filter_function(tarinfo, path)
++            filtered = filter_function(unfiltered, path)
+         except (OSError, FilterError) as e:
+             self._handle_fatal_error(e)
+         except ExtractError as e:
+             self._handle_nonfatal_error(e)
+-        if tarinfo is None:
++        if filtered is None:
+             self._dbg(2, "tarfile: Excluded %r" % unfiltered.name)
+-            return None
++            return None, None
++
+         # Prepare the link target for makelink().
+-        if tarinfo.islnk():
+-            tarinfo = copy.copy(tarinfo)
+-            tarinfo._link_target = os.path.join(path, tarinfo.linkname)
+-        return tarinfo
++        if filtered.islnk():
++            filtered = copy.copy(filtered)
++            filtered._link_target = os.path.join(path, filtered.linkname)
++        return filtered, unfiltered
+ 
+-    def _extract_one(self, tarinfo, path, set_attrs, numeric_owner):
+-        """Extract from filtered tarinfo to disk"""
++    def _extract_one(self, tarinfo, path, set_attrs, numeric_owner,
++                     filter_function=None):
++        """Extract from filtered tarinfo to disk.
++
++           filter_function is only used when extracting a *different*
++           member (e.g. as fallback to creating a symlink)
++        """
+         self._check("r")
+ 
+         try:
+             self._extract_member(tarinfo, os.path.join(path, tarinfo.name),
+                                  set_attrs=set_attrs,
+-                                 numeric_owner=numeric_owner)
++                                 numeric_owner=numeric_owner,
++                                 filter_function=filter_function,
++                                 extraction_root=path)
+         except OSError as e:
+             self._handle_fatal_error(e)
+         except ExtractError as e:
+@@ -2399,9 +2459,13 @@ def extractfile(self, member):
+             return None
+ 
+     def _extract_member(self, tarinfo, targetpath, set_attrs=True,
+-                        numeric_owner=False):
+-        """Extract the TarInfo object tarinfo to a physical
++                        numeric_owner=False, *, filter_function=None,
++                        extraction_root=None):
++        """Extract the filtered TarInfo object tarinfo to a physical
+            file called targetpath.
++
++           filter_function is only used when extracting a *different*
++           member (e.g. as fallback to creating a symlink)
+         """
+         # Fetch the TarInfo object for the given name
+         # and build the destination pathname, replacing
+@@ -2430,7 +2494,10 @@ def _extract_member(self, tarinfo, targetpath, 
set_attrs=True,
+         elif tarinfo.ischr() or tarinfo.isblk():
+             self.makedev(tarinfo, targetpath)
+         elif tarinfo.islnk() or tarinfo.issym():
+-            self.makelink(tarinfo, targetpath)
++            self.makelink_with_filter(
++                tarinfo, targetpath,
++                filter_function=filter_function,
++                extraction_root=extraction_root)
+         elif tarinfo.type not in SUPPORTED_TYPES:
+             self.makeunknown(tarinfo, targetpath)
+         else:
+@@ -2512,10 +2579,18 @@ def makedev(self, tarinfo, targetpath):
+                  os.makedev(tarinfo.devmajor, tarinfo.devminor))
+ 
+     def makelink(self, tarinfo, targetpath):
++        return self.makelink_with_filter(tarinfo, targetpath, None, None)
++
++    def makelink_with_filter(self, tarinfo, targetpath,
++                             filter_function, extraction_root):
+         """Make a (symbolic) link called targetpath. If it cannot be created
+           (platform limitation), we try to make a copy of the referenced file
+           instead of a link.
++
++          filter_function is only used when extracting a *different*
++          member (e.g. as fallback to creating a link).
+         """
++        keyerror_to_extracterror = False
+         try:
+             # For systems that support symbolic and hard links.
+             if tarinfo.issym():
+@@ -2523,18 +2598,38 @@ def makelink(self, tarinfo, targetpath):
+                     # Avoid FileExistsError on following os.symlink.
+                     os.unlink(targetpath)
+                 os.symlink(tarinfo.linkname, targetpath)
++                return
+             else:
+                 if os.path.exists(tarinfo._link_target):
+                     os.link(tarinfo._link_target, targetpath)
+-                else:
+-                    self._extract_member(self._find_link_target(tarinfo),
+-                                         targetpath)
++                    return
+         except symlink_exception:
++            keyerror_to_extracterror = True
++
++        try:
++            unfiltered = self._find_link_target(tarinfo)
++        except KeyError:
++            if keyerror_to_extracterror:
++                raise ExtractError(
++                    "unable to resolve link inside archive")
++            else:
++                raise
++
++        if filter_function is None:
++            filtered = unfiltered
++        else:
++            if extraction_root is None:
++                raise ExtractError(
++                    "makelink_with_filter: if filter_function is not None, "
++                    + "extraction_root must also not be None")
+             try:
+-                self._extract_member(self._find_link_target(tarinfo),
+-                                     targetpath)
+-            except KeyError:
+-                raise ExtractError("unable to resolve link inside archive")
++                filtered = filter_function(unfiltered, extraction_root)
++            except _FILTER_ERRORS as cause:
++                raise LinkFallbackError(tarinfo, unfiltered.name) from cause
++        if filtered is not None:
++            self._extract_member(filtered, targetpath,
++                                 filter_function=filter_function,
++                                 extraction_root=extraction_root)
+ 
+     def chown(self, tarinfo, targetpath, numeric_owner):
+         """Set owner of targetpath according to tarinfo. If numeric_owner
+diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
+index 2d3fa101bc4..8f07d18e134 100644
+--- a/Lib/test/test_ntpath.py
++++ b/Lib/test/test_ntpath.py
+@@ -1,8 +1,10 @@
+ import ntpath
+ import os
++import subprocess
+ import sys
+ import unittest
+ import warnings
++from ntpath import ALLOW_MISSING
+ from test.support import TestFailed, FakePath
+ from test import support, test_genericpath
+ from tempfile import TemporaryFile
+@@ -72,6 +74,27 @@ def tester(fn, wantResult):
+               %(str(fn), str(wantResult), repr(gotResult)))
+ 
+ 
++def _parameterize(*parameters):
++    """Simplistic decorator to parametrize a test
++
++    Runs the decorated test multiple times in subTest, with a value from
++    'parameters' passed as an extra positional argument.
++    Calls doCleanups() after each run.
++
++    Not for general use. Intended to avoid indenting for easier backports.
++
++    See https://discuss.python.org/t/91827 for discussing generalizations.
++    """
++    def _parametrize_decorator(func):
++        def _parameterized(self, *args, **kwargs):
++            for parameter in parameters:
++                with self.subTest(parameter):
++                    func(self, *args, parameter, **kwargs)
++                self.doCleanups()
++        return _parameterized
++    return _parametrize_decorator
++
++
+ class NtpathTestCase(unittest.TestCase):
+     def assertPathEqual(self, path1, path2):
+         if path1 == path2 or _norm(path1) == _norm(path2):
+@@ -242,6 +265,27 @@ def test_realpath_curdir(self):
+         tester("ntpath.realpath('.\.')", expected)
+         tester("ntpath.realpath('\'.join(['.'] * 100))", expected)
+ 
++    def test_realpath_curdir_strict(self):
++        expected = ntpath.normpath(os.getcwd())
++        tester("ntpath.realpath('.', strict=True)", expected)
++        tester("ntpath.realpath('./.', strict=True)", expected)
++        tester("ntpath.realpath('/'.join(['.'] * 100), strict=True)", 
expected)
++        tester("ntpath.realpath('.\.', strict=True)", expected)
++        tester("ntpath.realpath('\'.join(['.'] * 100), strict=True)", 
expected)
++
++    def test_realpath_curdir_missing_ok(self):
++        expected = ntpath.normpath(os.getcwd())
++        tester("ntpath.realpath('.', strict=ALLOW_MISSING)",
++               expected)
++        tester("ntpath.realpath('./.', strict=ALLOW_MISSING)",
++               expected)
++        tester("ntpath.realpath('/'.join(['.'] * 100), strict=ALLOW_MISSING)",
++               expected)
++        tester("ntpath.realpath('.\.', strict=ALLOW_MISSING)",
++               expected)
++        tester("ntpath.realpath('\'.join(['.'] * 100), strict=ALLOW_MISSING)",
++               expected)
++
+     def test_realpath_pardir(self):
+         expected = ntpath.normpath(os.getcwd())
+         tester("ntpath.realpath('..')", ntpath.dirname(expected))
+@@ -254,17 +298,43 @@ def test_realpath_pardir(self):
+         tester("ntpath.realpath('\'.join(['..'] * 50))",
+                ntpath.splitdrive(expected)[0] + '\')
+ 
++    def test_realpath_pardir_strict(self):
++        expected = ntpath.normpath(os.getcwd())
++        tester("ntpath.realpath('..', strict=True)", ntpath.dirname(expected))
++        tester("ntpath.realpath('../..', strict=True)",
++               ntpath.dirname(ntpath.dirname(expected)))
++        tester("ntpath.realpath('/'.join(['..'] * 50), strict=True)",
++               ntpath.splitdrive(expected)[0] + '\')
++        tester("ntpath.realpath('..\..', strict=True)",
++               ntpath.dirname(ntpath.dirname(expected)))
++        tester("ntpath.realpath('\'.join(['..'] * 50), strict=True)",
++               ntpath.splitdrive(expected)[0] + '\')
++
++    def test_realpath_pardir_missing_ok(self):
++        expected = ntpath.normpath(os.getcwd())
++        tester("ntpath.realpath('..', strict=ALLOW_MISSING)",
++               ntpath.dirname(expected))
++        tester("ntpath.realpath('../..', strict=ALLOW_MISSING)",
++               ntpath.dirname(ntpath.dirname(expected)))
++        tester("ntpath.realpath('/'.join(['..'] * 50), strict=ALLOW_MISSING)",
++               ntpath.splitdrive(expected)[0] + '\')
++        tester("ntpath.realpath('..\..', strict=ALLOW_MISSING)",
++               ntpath.dirname(ntpath.dirname(expected)))
++        tester("ntpath.realpath('\'.join(['..'] * 50), strict=ALLOW_MISSING)",
++               ntpath.splitdrive(expected)[0] + '\')
++
+     @support.skip_unless_symlink
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+-    def test_realpath_basic(self):
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_basic(self, kwargs):
+         ABSTFN = ntpath.abspath(support.TESTFN)
+         open(ABSTFN, "wb").close()
+         self.addCleanup(support.unlink, ABSTFN)
+         self.addCleanup(support.unlink, ABSTFN + "1")
+ 
+         os.symlink(ABSTFN, ABSTFN + "1")
+-        self.assertPathEqual(ntpath.realpath(ABSTFN + "1"), ABSTFN)
+-        self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1")),
++        self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN)
++        self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1"), 
**kwargs),
+                          os.fsencode(ABSTFN))
+ 
+     @support.skip_unless_symlink
+@@ -280,14 +350,15 @@ def test_realpath_strict(self):
+ 
+     @support.skip_unless_symlink
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+-    def test_realpath_relative(self):
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_relative(self, kwargs):
+         ABSTFN = ntpath.abspath(support.TESTFN)
+         open(ABSTFN, "wb").close()
+         self.addCleanup(support.unlink, ABSTFN)
+         self.addCleanup(support.unlink, ABSTFN + "1")
+ 
+         os.symlink(ABSTFN, ntpath.relpath(ABSTFN + "1"))
+-        self.assertPathEqual(ntpath.realpath(ABSTFN + "1"), ABSTFN)
++        self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN)
+ 
+     @support.skip_unless_symlink
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+@@ -439,7 +510,62 @@ def test_realpath_symlink_loops_strict(self):
+ 
+     @support.skip_unless_symlink
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+-    def test_realpath_symlink_prefix(self):
++    def test_realpath_symlink_loops_raise(self):
++        # Symlink loops raise OSError in ALLOW_MISSING mode
++        ABSTFN = ntpath.abspath(support.TESTFN)
++        self.addCleanup(support.unlink, ABSTFN)
++        self.addCleanup(support.unlink, ABSTFN + "1")
++        self.addCleanup(support.unlink, ABSTFN + "2")
++        self.addCleanup(support.unlink, ABSTFN + "y")
++        self.addCleanup(support.unlink, ABSTFN + "c")
++        self.addCleanup(support.unlink, ABSTFN + "a")
++        self.addCleanup(support.unlink, ABSTFN + "x")
++
++        os.symlink(ABSTFN, ABSTFN)
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN, 
strict=ALLOW_MISSING)
++
++        os.symlink(ABSTFN + "1", ABSTFN + "2")
++        os.symlink(ABSTFN + "2", ABSTFN + "1")
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1",
++                            strict=ALLOW_MISSING)
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2",
++                            strict=ALLOW_MISSING)
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\x",
++                            strict=ALLOW_MISSING)
++
++        # Windows eliminates '..' components before resolving links;
++        # realpath is not expected to raise if this removes the loop.
++        self.assertPathEqual(ntpath.realpath(ABSTFN + "1\.."),
++                             ntpath.dirname(ABSTFN))
++        self.assertPathEqual(ntpath.realpath(ABSTFN + "1\..\x"),
++                             ntpath.dirname(ABSTFN) + "\x")
++
++        os.symlink(ABSTFN + "x", ABSTFN + "y")
++        self.assertPathEqual(ntpath.realpath(ABSTFN + "1\..\"
++                                             + ntpath.basename(ABSTFN) + "y"),
++                             ABSTFN + "x")
++        self.assertRaises(
++            OSError, ntpath.realpath,
++            ABSTFN + "1\..\" + ntpath.basename(ABSTFN) + "1",
++            strict=ALLOW_MISSING)
++
++        os.symlink(ntpath.basename(ABSTFN) + "a\b", ABSTFN + "a")
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a",
++                            strict=ALLOW_MISSING)
++
++        os.symlink("..\" + ntpath.basename(ntpath.dirname(ABSTFN))
++                + "\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c")
++        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c",
++                            strict=ALLOW_MISSING)
++
++        # Test using relative path as well.
++        self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
++                            strict=ALLOW_MISSING)
++
++    @support.skip_unless_symlink
++    @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_symlink_prefix(self, kwargs):
+         ABSTFN = ntpath.abspath(support.TESTFN)
+         self.addCleanup(support.unlink, ABSTFN + "3")
+         self.addCleanup(support.unlink, "\\?\" + ABSTFN + "3.")
+@@ -454,9 +580,9 @@ def test_realpath_symlink_prefix(self):
+             f.write(b'1')
+         os.symlink("\\?\" + ABSTFN + "3.", ABSTFN + "3.link")
+ 
+-        self.assertPathEqual(ntpath.realpath(ABSTFN + "3link"),
++        self.assertPathEqual(ntpath.realpath(ABSTFN + "3link", **kwargs),
+                              ABSTFN + "3")
+-        self.assertPathEqual(ntpath.realpath(ABSTFN + "3.link"),
++        self.assertPathEqual(ntpath.realpath(ABSTFN + "3.link", **kwargs),
+                              "\\?\" + ABSTFN + "3.")
+ 
+         # Resolved paths should be usable to open target files
+@@ -466,14 +592,17 @@ def test_realpath_symlink_prefix(self):
+             self.assertEqual(f.read(), b'1')
+ 
+         # When the prefix is included, it is not stripped
+-        self.assertPathEqual(ntpath.realpath("\\?\" + ABSTFN + "3link"),
++        self.assertPathEqual(ntpath.realpath("\\?\" + ABSTFN + "3link", 
**kwargs),
+                              "\\?\" + ABSTFN + "3")
+-        self.assertPathEqual(ntpath.realpath("\\?\" + ABSTFN + "3.link"),
++        self.assertPathEqual(ntpath.realpath("\\?\" + ABSTFN + "3.link", 
**kwargs),
+                              "\\?\" + ABSTFN + "3.")
+ 
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+     def test_realpath_nul(self):
+         tester("ntpath.realpath('NUL')", r'\.\NUL')
++        tester("ntpath.realpath('NUL', strict=False)", r'\.\NUL')
++        tester("ntpath.realpath('NUL', strict=True)", r'\.\NUL')
++        tester("ntpath.realpath('NUL', strict=ALLOW_MISSING)", r'\.\NUL')
+ 
+     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+     @unittest.skipUnless(HAVE_GETSHORTPATHNAME, 'need _getshortpathname')
+@@ -497,12 +626,20 @@ def test_realpath_cwd(self):
+ 
+         self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short))
+ 
+-        with support.change_cwd(test_dir_long):
+-            self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
+-        with support.change_cwd(test_dir_long.lower()):
+-            self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
+-        with support.change_cwd(test_dir_short):
+-            self.assertPathEqual(test_file_long, ntpath.realpath("file.txt"))
++        for kwargs in {}, {'strict': True}, {'strict': ALLOW_MISSING}:
++            with self.subTest(**kwargs):
++                with support.change_cwd(test_dir_long):
++                    self.assertPathEqual(
++                        test_file_long,
++                        ntpath.realpath("file.txt", **kwargs))
++                with support.change_cwd(test_dir_long.lower()):
++                    self.assertPathEqual(
++                        test_file_long,
++                        ntpath.realpath("file.txt", **kwargs))
++                with support.change_cwd(test_dir_short):
++                    self.assertPathEqual(
++                        test_file_long,
++                        ntpath.realpath("file.txt", **kwargs))
+ 
+     def test_expandvars(self):
+         with support.EnvironmentVarGuard() as env:
+diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py
+index aee3cb421fb..639cd2d8873 100644
+--- a/Lib/test/test_posixpath.py
++++ b/Lib/test/test_posixpath.py
+@@ -1,7 +1,10 @@
+ import os
++import sys
+ import posixpath
+ import unittest
+-from posixpath import realpath, abspath, dirname, basename
++from functools import partial
++from posixpath import realpath, abspath, dirname, basename, ALLOW_MISSING
++from test import support
+ from test import support, test_genericpath
+ from test.support import FakePath
+ from unittest import mock
+@@ -33,6 +36,26 @@ def safe_rmdir(dirname):
+     except OSError:
+         pass
+ 
++def _parameterize(*parameters):
++    """Simplistic decorator to parametrize a test
++
++    Runs the decorated test multiple times in subTest, with a value from
++    'parameters' passed as an extra positional argument.
++    Does *not* call doCleanups() after each run.
++
++    Not for general use. Intended to avoid indenting for easier backports.
++
++    See https://discuss.python.org/t/91827 for discussing generalizations.
++    """
++    def _parametrize_decorator(func):
++        def _parameterized(self, *args, **kwargs):
++            for parameter in parameters:
++                with self.subTest(parameter):
++                    func(self, *args, parameter, **kwargs)
++        return _parameterized
++    return _parametrize_decorator
++
++
+ class PosixPathTest(unittest.TestCase):
+ 
+     def setUp(self):
+@@ -320,33 +343,36 @@ def test_normpath(self):
+                          b"/foo/bar")
+ 
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_curdir(self):
+-        self.assertEqual(realpath('.'), os.getcwd())
+-        self.assertEqual(realpath('./.'), os.getcwd())
+-        self.assertEqual(realpath('/'.join(['.'] * 100)), os.getcwd())
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_curdir(self, kwargs):
++        self.assertEqual(realpath('.', **kwargs), os.getcwd())
++        self.assertEqual(realpath('./.', **kwargs), os.getcwd())
++        self.assertEqual(realpath('/'.join(['.'] * 100), **kwargs), 
os.getcwd())
+ 
+-        self.assertEqual(realpath(b'.'), os.getcwdb())
+-        self.assertEqual(realpath(b'./.'), os.getcwdb())
+-        self.assertEqual(realpath(b'/'.join([b'.'] * 100)), os.getcwdb())
++        self.assertEqual(realpath(b'.', **kwargs), os.getcwdb())
++        self.assertEqual(realpath(b'./.', **kwargs), os.getcwdb())
++        self.assertEqual(realpath(b'/'.join([b'.'] * 100), **kwargs), 
os.getcwdb())
+ 
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_pardir(self):
+-        self.assertEqual(realpath('..'), dirname(os.getcwd()))
+-        self.assertEqual(realpath('../..'), dirname(dirname(os.getcwd())))
+-        self.assertEqual(realpath('/'.join(['..'] * 100)), '/')
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_pardir(self, kwargs):
++        self.assertEqual(realpath('..', **kwargs), dirname(os.getcwd()))
++        self.assertEqual(realpath('../..', **kwargs), 
dirname(dirname(os.getcwd())))
++        self.assertEqual(realpath('/'.join(['..'] * 100), **kwargs), '/')
+ 
+-        self.assertEqual(realpath(b'..'), dirname(os.getcwdb()))
+-        self.assertEqual(realpath(b'../..'), dirname(dirname(os.getcwdb())))
+-        self.assertEqual(realpath(b'/'.join([b'..'] * 100)), b'/')
++        self.assertEqual(realpath(b'..', **kwargs), dirname(os.getcwdb()))
++        self.assertEqual(realpath(b'../..', **kwargs), 
dirname(dirname(os.getcwdb())))
++        self.assertEqual(realpath(b'/'.join([b'..'] * 100), **kwargs), b'/')
+ 
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_basic(self):
++    @_parameterize({}, {'strict': ALLOW_MISSING})
++    def test_realpath_basic(self, kwargs):
+         # Basic operation.
+         try:
+             os.symlink(ABSTFN+"1", ABSTFN)
+-            self.assertEqual(realpath(ABSTFN), ABSTFN+"1")
++            self.assertEqual(realpath(ABSTFN, **kwargs), ABSTFN+"1")
+         finally:
+             support.unlink(ABSTFN)
+ 
+@@ -363,19 +389,118 @@ def test_realpath_strict(self):
+         finally:
+             support.unlink(ABSTFN)
+ 
++    def test_realpath_invalid_paths(self):
++        path = '/\x00'
++        self.assertRaises(ValueError, realpath, path, strict=False)
++        self.assertRaises(ValueError, realpath, path, strict=True)
++        self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
++        path = b'/\x00'
++        self.assertRaises(ValueError, realpath, path, strict=False)
++        self.assertRaises(ValueError, realpath, path, strict=True)
++        self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
++        path = '/nonexistent/x\x00'
++        self.assertRaises(ValueError, realpath, path, strict=False)
++        self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++        self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
++        path = b'/nonexistent/x\x00'
++        self.assertRaises(ValueError, realpath, path, strict=False)
++        self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++        self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
++        path = '/\x00/..'
++        self.assertRaises(ValueError, realpath, path, strict=False)
++        self.assertRaises(ValueError, realpath, path, strict=True)
++        self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
++        path = b'/\x00/..'
++        self.assertRaises(ValueError, realpath, path, strict=False)
++        self.assertRaises(ValueError, realpath, path, strict=True)
++        self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
++
++        path = '/nonexistent/x\x00/..'
++        self.assertRaises(ValueError, realpath, path, strict=False)
++        self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++        self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
++        path = b'/nonexistent/x\x00/..'
++        self.assertRaises(ValueError, realpath, path, strict=False)
++        self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++        self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
++
++        path = '/\udfff'
++        if sys.platform == 'win32':
++            self.assertEqual(realpath(path, strict=False), path)
++            self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++            self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
++        else:
++            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=False)
++            self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
++            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALLOW_MISSING)
++        path = '/nonexistent/\udfff'
++        if sys.platform == 'win32':
++            self.assertEqual(realpath(path, strict=False), path)
++            self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
++        else:
++            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=False)
++            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALLOW_MISSING)
++        self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++        path = '/\udfff/..'
++        if sys.platform == 'win32':
++            self.assertEqual(realpath(path, strict=False), '/')
++            self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++            self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/')
++        else:
++            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=False)
++            self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
++            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALLOW_MISSING)
++        path = '/nonexistent/\udfff/..'
++        if sys.platform == 'win32':
++            self.assertEqual(realpath(path, strict=False), '/nonexistent')
++            self.assertEqual(realpath(path, strict=ALLOW_MISSING), 
'/nonexistent')
++        else:
++            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=False)
++            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALLOW_MISSING)
++        self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++
++        path = b'/\xff'
++        if sys.platform == 'win32':
++            self.assertRaises(UnicodeDecodeError, realpath, path, 
strict=False)
++            self.assertRaises(UnicodeDecodeError, realpath, path, strict=True)
++            self.assertRaises(UnicodeDecodeError, realpath, path, 
strict=ALLOW_MISSING)
++        else:
++            self.assertEqual(realpath(path, strict=False), path)
++            self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++            self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
++        path = b'/nonexistent/\xff'
++        if sys.platform == 'win32':
++            self.assertRaises(UnicodeDecodeError, realpath, path, 
strict=False)
++            self.assertRaises(UnicodeDecodeError, realpath, path, 
strict=ALLOW_MISSING)
++        else:
++            self.assertEqual(realpath(path, strict=False), path)
++        self.assertRaises(FileNotFoundError, realpath, path, strict=True)
++
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_relative(self):
++    @_parameterize({}, {'strict': ALLOW_MISSING})
++    def test_realpath_relative(self, kwargs):
+         try:
+             os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN)
+-            self.assertEqual(realpath(ABSTFN), ABSTFN+"1")
++            self.assertEqual(realpath(ABSTFN, **kwargs), ABSTFN+"1")
+         finally:
+             support.unlink(ABSTFN)
+ 
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
++    @_parameterize({}, {'strict': ALLOW_MISSING})
++    def test_realpath_missing_pardir(self, kwargs):
++        try:
++            os.symlink(support.TESTFN + "1", support.TESTFN)
++            self.assertEqual(
++                realpath("nonexistent/../" + support.TESTFN, **kwargs), 
ABSTFN + "1")
++        finally:
++            support.unlink(support.TESTFN)
++
++    @support.skip_unless_symlink
++    @skip_if_ABSTFN_contains_backslash
+     def test_realpath_symlink_loops(self):
+         # Bug #930024, return the path unchanged if we get into an infinite
+         # symlink loop in non-strict mode (default).
+@@ -418,37 +543,38 @@ def test_realpath_symlink_loops(self):
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_symlink_loops_strict(self):
++    @_parameterize({'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_symlink_loops_strict(self, kwargs):
+         # Bug #43757, raise OSError if we get into an infinite symlink loop in
+-        # strict mode.
++        # the strict modes.
+         try:
+             os.symlink(ABSTFN, ABSTFN)
+-            self.assertRaises(OSError, realpath, ABSTFN, strict=True)
++            self.assertRaises(OSError, realpath, ABSTFN, **kwargs)
+ 
+             os.symlink(ABSTFN+"1", ABSTFN+"2")
+             os.symlink(ABSTFN+"2", ABSTFN+"1")
+-            self.assertRaises(OSError, realpath, ABSTFN+"1", strict=True)
+-            self.assertRaises(OSError, realpath, ABSTFN+"2", strict=True)
++            self.assertRaises(OSError, realpath, ABSTFN+"1", **kwargs)
++            self.assertRaises(OSError, realpath, ABSTFN+"2", **kwargs)
+ 
+-            self.assertRaises(OSError, realpath, ABSTFN+"1/x", strict=True)
+-            self.assertRaises(OSError, realpath, ABSTFN+"1/..", strict=True)
+-            self.assertRaises(OSError, realpath, ABSTFN+"1/../x", strict=True)
++            self.assertRaises(OSError, realpath, ABSTFN+"1/x", **kwargs)
++            self.assertRaises(OSError, realpath, ABSTFN+"1/..", **kwargs)
++            self.assertRaises(OSError, realpath, ABSTFN+"1/../x", **kwargs)
+             os.symlink(ABSTFN+"x", ABSTFN+"y")
+             self.assertRaises(OSError, realpath,
+-                              ABSTFN+"1/../" + basename(ABSTFN) + "y", 
strict=True)
++                              ABSTFN+"1/../" + basename(ABSTFN) + "y", 
**kwargs)
+             self.assertRaises(OSError, realpath,
+-                              ABSTFN+"1/../" + basename(ABSTFN) + "1", 
strict=True)
++                              ABSTFN+"1/../" + basename(ABSTFN) + "1", 
**kwargs)
+ 
+             os.symlink(basename(ABSTFN) + "a/b", ABSTFN+"a")
+-            self.assertRaises(OSError, realpath, ABSTFN+"a", strict=True)
++            self.assertRaises(OSError, realpath, ABSTFN+"a", **kwargs)
+ 
+             os.symlink("../" + basename(dirname(ABSTFN)) + "/" +
+                        basename(ABSTFN) + "c", ABSTFN+"c")
+-            self.assertRaises(OSError, realpath, ABSTFN+"c", strict=True)
++            self.assertRaises(OSError, realpath, ABSTFN+"c", **kwargs)
+ 
+             # Test using relative path as well.
+             with support.change_cwd(dirname(ABSTFN)):
+-                self.assertRaises(OSError, realpath, basename(ABSTFN), 
strict=True)
++                self.assertRaises(OSError, realpath, basename(ABSTFN), 
**kwargs)
+         finally:
+             support.unlink(ABSTFN)
+             support.unlink(ABSTFN+"1")
+@@ -460,13 +586,14 @@ def test_realpath_symlink_loops_strict(self):
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_repeated_indirect_symlinks(self):
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_repeated_indirect_symlinks(self, kwargs):
+         # Issue #6975.
+         try:
+             os.mkdir(ABSTFN)
+             os.symlink('../' + basename(ABSTFN), ABSTFN + '/self')
+             os.symlink('self/self/self', ABSTFN + '/link')
+-            self.assertEqual(realpath(ABSTFN + '/link'), ABSTFN)
++            self.assertEqual(realpath(ABSTFN + '/link', **kwargs), ABSTFN)
+         finally:
+             support.unlink(ABSTFN + '/self')
+             support.unlink(ABSTFN + '/link')
+@@ -475,14 +602,15 @@ def test_realpath_repeated_indirect_symlinks(self):
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_deep_recursion(self):
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_deep_recursion(self, kwargs):
+         depth = 10
+         try:
+             os.mkdir(ABSTFN)
+             for i in range(depth):
+                 os.symlink('/'.join(['%d' % i] * 10), ABSTFN + '/%d' % (i + 
1))
+             os.symlink('.', ABSTFN + '/0')
+-            self.assertEqual(realpath(ABSTFN + '/%d' % depth), ABSTFN)
++            self.assertEqual(realpath(ABSTFN + '/%d' % depth, **kwargs), 
ABSTFN)
+ 
+             # Test using relative path as well.
+             with support.change_cwd(ABSTFN):
+@@ -495,7 +623,8 @@ def test_realpath_deep_recursion(self):
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_resolve_parents(self):
++    @_parameterize({}, {'strict': ALLOW_MISSING})
++    def test_realpath_resolve_parents(self, kwargs):
+         # We also need to resolve any symlinks in the parents of a relative
+         # path passed to realpath. E.g.: current working directory is
+         # /usr/doc with 'doc' being a symlink to /usr/share/doc. We call
+@@ -506,7 +635,8 @@ def test_realpath_resolve_parents(self):
+             os.symlink(ABSTFN + "/y", ABSTFN + "/k")
+ 
+             with support.change_cwd(ABSTFN + "/k"):
+-                self.assertEqual(realpath("a"), ABSTFN + "/y/a")
++                self.assertEqual(realpath("a", **kwargs),
++                                    ABSTFN + "/y/a")
+         finally:
+             support.unlink(ABSTFN + "/k")
+             safe_rmdir(ABSTFN + "/y")
+@@ -515,7 +645,8 @@ def test_realpath_resolve_parents(self):
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_resolve_before_normalizing(self):
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_resolve_before_normalizing(self, kwargs):
+         # Bug #990669: Symbolic links should be resolved before we
+         # normalize the path. E.g.: if we have directories 'a', 'k' and 'y'
+         # in the following hierarchy:
+@@ -530,10 +661,10 @@ def test_realpath_resolve_before_normalizing(self):
+             os.symlink(ABSTFN + "/k/y", ABSTFN + "/link-y")
+ 
+             # Absolute path.
+-            self.assertEqual(realpath(ABSTFN + "/link-y/.."), ABSTFN + "/k")
++            self.assertEqual(realpath(ABSTFN + "/link-y/..", **kwargs), 
ABSTFN + "/k")
+             # Relative path.
+             with support.change_cwd(dirname(ABSTFN)):
+-                self.assertEqual(realpath(basename(ABSTFN) + "/link-y/.."),
++                self.assertEqual(realpath(basename(ABSTFN) + "/link-y/..", 
**kwargs),
+                                  ABSTFN + "/k")
+         finally:
+             support.unlink(ABSTFN + "/link-y")
+@@ -544,7 +675,8 @@ def test_realpath_resolve_before_normalizing(self):
+     @unittest.skipUnless(hasattr(os, "symlink"),
+                          "Missing symlink implementation")
+     @skip_if_ABSTFN_contains_backslash
+-    def test_realpath_resolve_first(self):
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_resolve_first(self, kwargs):
+         # Bug #1213894: The first component of the path, if not absolute,
+         # must be resolved too.
+ 
+@@ -554,13 +686,70 @@ def test_realpath_resolve_first(self):
+             os.symlink(ABSTFN, ABSTFN + "link")
+             with support.change_cwd(dirname(ABSTFN)):
+                 base = basename(ABSTFN)
+-                self.assertEqual(realpath(base + "link"), ABSTFN)
+-                self.assertEqual(realpath(base + "link/k"), ABSTFN + "/k")
++                self.assertEqual(realpath(base + "link", **kwargs), ABSTFN)
++                self.assertEqual(realpath(base + "link/k", **kwargs), ABSTFN 
+ "/k")
+         finally:
+             support.unlink(ABSTFN + "link")
+             safe_rmdir(ABSTFN + "/k")
+             safe_rmdir(ABSTFN)
+ 
++    @support.skip_unless_symlink
++    @skip_if_ABSTFN_contains_backslash
++    @unittest.skipIf(os.chmod not in os.supports_follow_symlinks, "Can't set 
symlink permissions")
++    @unittest.skipIf(sys.platform != "darwin", "only macOS requires read 
permission to readlink()")
++    @_parameterize({'strict': True}, {'strict': ALLOW_MISSING})
++    def test_realpath_unreadable_symlink_strict(self, kwargs):
++        try:
++            os.symlink(ABSTFN+"1", ABSTFN)
++            os.chmod(ABSTFN, 0o000, follow_symlinks=False)
++            with self.assertRaises(PermissionError):
++                realpath(ABSTFN, **kwargs)
++            with self.assertRaises(PermissionError):
++                realpath(ABSTFN + '/foo', **kwargs),
++            with self.assertRaises(PermissionError):
++                realpath(ABSTFN + '/../foo', **kwargs)
++            with self.assertRaises(PermissionError):
++                realpath(ABSTFN + '/foo/..', **kwargs)
++        finally:
++            os.chmod(ABSTFN, 0o755, follow_symlinks=False)
++            os.unlink(ABSTFN)
++
++    @skip_if_ABSTFN_contains_backslash
++    @support.skip_unless_symlink
++    def test_realpath_unreadable_directory(self):
++        try:
++            os.mkdir(ABSTFN)
++            os.mkdir(ABSTFN + '/k')
++            os.chmod(ABSTFN, 0o000)
++            self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
++            self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
++            self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN)
++
++            try:
++                os.stat(ABSTFN)
++            except PermissionError:
++                pass
++            else:
++                self.skipTest('Cannot block permissions')
++
++            self.assertEqual(realpath(ABSTFN + '/k', strict=False),
++                             ABSTFN + '/k')
++            self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
++                              strict=True)
++            self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
++                              strict=ALLOW_MISSING)
++
++            self.assertEqual(realpath(ABSTFN + '/missing', strict=False),
++                             ABSTFN + '/missing')
++            self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
++                              strict=True)
++            self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
++                              strict=ALLOW_MISSING)
++        finally:
++            os.chmod(ABSTFN, 0o755)
++            safe_rmdir(ABSTFN + '/k')
++            safe_rmdir(ABSTFN)
++
+     def test_relpath(self):
+         (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar")
+         try:
+@@ -737,9 +926,12 @@ def test_path_normpath(self):
+     def test_path_abspath(self):
+         self.assertPathEqual(self.path.abspath)
+ 
+-    def test_path_realpath(self):
++    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
++    def test_path_realpath(self, kwargs):
+         self.assertPathEqual(self.path.realpath)
+ 
++        self.assertPathEqual(partial(self.path.realpath, **kwargs))
++
+     def test_path_relpath(self):
+         self.assertPathEqual(self.path.relpath)
+ 
+diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
+index 57ce58f0709..01d1e5e0ccf 100644
+--- a/Lib/test/test_tarfile.py
++++ b/Lib/test/test_tarfile.py
+@@ -2438,9 +2438,35 @@ def test__all__(self):
+                      'tar_filter', 'FilterError', 'AbsoluteLinkError',
+                      'OutsideDestinationError', 'SpecialFileError',
+                      'AbsolutePathError', 'LinkOutsideDestinationError',
++                     'LinkFallbackError',
+                      }
+         support.check__all__(self, tarfile, blacklist=blacklist)
+ 
++    @unittest.skipUnless(support.can_symlink(), 'requires symlink support')
++    @unittest.skipUnless(hasattr(os, 'chmod'), "missing os.chmod")
++    @unittest.mock.patch('os.chmod')
++    def test_deferred_directory_attributes_update(self, mock_chmod):
++        # Regression test for gh-127987: setting attributes on arbitrary files
++        tempdir = os.path.join(TEMPDIR, 'test127987')
++        def mock_chmod_side_effect(path, mode, **kwargs):
++            target_path = os.path.realpath(path)
++            if os.path.commonpath([target_path, tempdir]) != tempdir:
++                raise Exception("should not try to chmod anything outside the 
destination", target_path)
++        mock_chmod.side_effect = mock_chmod_side_effect
++
++        outside_tree_dir = os.path.join(TEMPDIR, 'outside_tree_dir')
++        with ArchiveMaker() as arc:
++            arc.add('x', symlink_to='.')
++            arc.add('x', type=tarfile.DIRTYPE, mode='?rwsrwsrwt')
++            arc.add('x', symlink_to=outside_tree_dir)
++
++        os.makedirs(outside_tree_dir)
++        try:
++            arc.open().extractall(path=tempdir, filter='tar')
++        finally:
++            support.rmtree(outside_tree_dir)
++            support.rmtree(tempdir)
++
+ 
+ class CommandLineTest(unittest.TestCase):
+ 
+@@ -2986,6 +3012,10 @@ def check_files_present(self, directory):
+         got_paths = set(
+             p.relative_to(directory)
+             for p in pathlib.Path(directory).glob('**/*'))
++        if self.extraction_filter == 'data':
++            # The 'data' filter is expected to reject special files
++            for path in 'ustar/fifotype', 'ustar/blktype', 'ustar/chrtype':
++                got_paths.discard(pathlib.Path(path))
+         self.assertEqual(self.control_paths, got_paths)
+ 
+     @contextmanager
+@@ -3212,12 +3242,28 @@ def __exit__(self, *exc):
+         self.bio = None
+ 
+     def add(self, name, *, type=None, symlink_to=None, hardlink_to=None,
+-            mode=None, size=None, **kwargs):
+-        """Add a member to the test archive. Call within `with`."""
++            mode=None, size=None, content=None, **kwargs):
++        """Add a member to the test archive. Call within `with`.
++
++        Provides many shortcuts:
++        - default `type` is based on symlink_to, hardlink_to, and trailing `/`
++          in name (which is stripped)
++        - size & content defaults are based on each other
++        - content can be str or bytes
++        - mode should be textual ('-rwxrwxrwx')
++
++        (add more! this is unstable internal test-only API)
++        """
+         name = str(name)
+         tarinfo = tarfile.TarInfo(name).replace(**kwargs)
++        if content is not None:
++            if isinstance(content, str):
++                content = content.encode()
++            size = len(content)
+         if size is not None:
+             tarinfo.size = size
++            if content is None:
++                content = bytes(tarinfo.size)
+         if mode:
+             tarinfo.mode = _filemode_to_int(mode)
+         if symlink_to is not None:
+@@ -3231,7 +3277,7 @@ def add(self, name, *, type=None, symlink_to=None, 
hardlink_to=None,
+         if type is not None:
+             tarinfo.type = type
+         if tarinfo.isreg():
+-            fileobj = io.BytesIO(bytes(tarinfo.size))
++            fileobj = io.BytesIO(content)
+         else:
+             fileobj = None
+         self.tar_w.addfile(tarinfo, fileobj)
+@@ -3253,7 +3299,7 @@ class TestExtractionFilters(unittest.TestCase):
+     destdir = outerdir / 'dest'
+ 
+     @contextmanager
+-    def check_context(self, tar, filter):
++    def check_context(self, tar, filter, *, check_flag=True, 
ignored_trees=()):
+         """Extracts `tar` to `self.destdir` and allows checking the result
+ 
+         If an error occurs, it must be checked using `expect_exception`
+@@ -3262,27 +3308,46 @@ def check_context(self, tar, filter):
+         except the destination directory itself and parent directories of
+         other files.
+         When checking directories, do so before their contents.
++
++        A file called 'flag' is made in outerdir (i.e. outside destdir)
++        before extraction; it should not be altered nor should its contents
++        be read/copied.
++
++        *ignored_trees* is a set of directories to remove (including their
++        contents) right after the archive is extracted. It is a workaround
++        for Path.glob() failing to get all files in Python 3.10 and below.
+         """
+         with support.temp_dir(self.outerdir):
++            flag_path = self.outerdir / 'flag'
++            flag_path.write_text('capture me')
+             try:
+                 tar.extractall(self.destdir, filter=filter)
+             except Exception as exc:
+                 self.raised_exception = exc
++                self.reraise_exception = True
+                 self.expected_paths = set()
+             else:
++                for ignored_tree in ignored_trees:
++                    support.rmtree((self.destdir / ignored_tree).resolve())
+                 self.raised_exception = None
++                self.reraise_exception = False
+                 self.expected_paths = set(self.outerdir.glob('**/*'))
+                 self.expected_paths.discard(self.destdir)
++                self.expected_paths.discard(flag_path)
+             try:
+-                yield
++                yield self
+             finally:
+                 tar.close()
+-            if self.raised_exception:
++            if self.reraise_exception:
+                 raise self.raised_exception
+             self.assertEqual(self.expected_paths, set())
++            if check_flag:
++                self.assertEqual(flag_path.read_text(), 'capture me')
++            else:
++                assert filter == 'fully_trusted'
+ 
+     def expect_file(self, name, type=None, symlink_to=None, mode=None,
+-                    size=None):
++                    size=None, content=None):
+         """Check a single file. See check_context."""
+         if self.raised_exception:
+             raise self.raised_exception
+@@ -3292,7 +3357,7 @@ def expect_file(self, name, type=None, symlink_to=None, 
mode=None,
+         self.expected_paths.remove(path)
+ 
+         # When checking mode, ignore Windows (which can only set user read and
+-        # user write bits). Newer versions of Python use 
`os_helper.can_chmod()`
++        # user write bits). Newer versions of Python use `support.can_chmod()`
+         # instead of hardcoding Windows.
+         if mode is not None and sys.platform != 'win32':
+             got = stat.filemode(stat.S_IMODE(path.stat().st_mode))
+@@ -3306,26 +3371,45 @@ def expect_file(self, name, type=None, 
symlink_to=None, mode=None,
+             # The symlink might be the same (textually) as what we expect,
+             # but some systems change the link to an equivalent path, so
+             # we fall back to samefile().
+-            if expected != got:
+-                self.assertTrue(got.samefile(expected))
++            try:
++                if expected != got:
++                    self.assertTrue(got.samefile(expected))
++            except Exception as e:
++                # attach a note, so it's shown even if `samefile` fails
++                e.add_note(f'{expected=}, {got=}')
++                raise
+         elif type == tarfile.REGTYPE or type is None:
+             self.assertTrue(path.is_file())
+         elif type == tarfile.DIRTYPE:
+             self.assertTrue(path.is_dir())
+         elif type == tarfile.FIFOTYPE:
+             self.assertTrue(path.is_fifo())
++        elif type == tarfile.SYMTYPE:
++            self.assertTrue(path.is_symlink())
+         else:
+             raise NotImplementedError(type)
+         if size is not None:
+             self.assertEqual(path.stat().st_size, size)
++        if content is not None:
++            self.assertEqual(path.read_text(), content)
+         for parent in path.parents:
+             self.expected_paths.discard(parent)
+ 
++    def expect_any_tree(self, name):
++        """Check a directory; forget about its contents."""
++        tree_path = (self.destdir / name).resolve()
++        self.expect_file(tree_path, type=tarfile.DIRTYPE)
++        self.expected_paths = {
++            p for p in self.expected_paths
++            if tree_path not in p.parents
++        }
++
+     def expect_exception(self, exc_type, message_re='.'):
+         with self.assertRaisesRegex(exc_type, message_re):
+             if self.raised_exception is not None:
+                 raise self.raised_exception
+-        self.raised_exception = None
++        self.reraise_exception = False
++        return self.raised_exception
+ 
+     def test_benign_file(self):
+         with ArchiveMaker() as arc:
+@@ -3409,6 +3493,78 @@ def test_parent_symlink(self):
+             with self.check_context(arc.open(), 'data'):
+                 self.expect_file('parent/evil')
+ 
++    @support.skip_unless_symlink
++    def test_realpath_limit_attack(self):
++        # (CVE-2025-4517)
++
++        with ArchiveMaker() as arc:
++            # populate the symlinks and dirs that expand in os.path.realpath()
++            # The component length is chosen so that in common cases, the 
unexpanded
++            # path fits in PATH_MAX, but it overflows when the final symlink
++            # is expanded
++            steps = "abcdefghijklmnop"
++            if sys.platform == 'win32':
++                component = 'd' * 25
++            elif 'PC_PATH_MAX' in os.pathconf_names:
++                max_path_len = os.pathconf(self.outerdir.parent, 
"PC_PATH_MAX")
++                path_sep_len = 1
++                dest_len = len(str(self.destdir)) + path_sep_len
++                component_len = (max_path_len - dest_len) // (len(steps) + 
path_sep_len)
++                component = 'd' * component_len
++            else:
++                raise NotImplementedError("Need to guess component length for 
{sys.platform}")
++            path = ""
++            step_path = ""
++            for i in steps:
++                arc.add(os.path.join(path, component), type=tarfile.DIRTYPE,
++                        mode='drwxrwxrwx')
++                arc.add(os.path.join(path, i), symlink_to=component)
++                path = os.path.join(path, component)
++                step_path = os.path.join(step_path, i)
++            # create the final symlink that exceeds PATH_MAX and simply points
++            # to the top dir.
++            # this link will never be expanded by
++            # os.path.realpath(strict=False), nor anything after it.
++            linkpath = os.path.join(*steps, "l"*254)
++            parent_segments = [".."] * len(steps)
++            arc.add(linkpath, symlink_to=os.path.join(*parent_segments))
++            # make a symlink outside to keep the tar command happy
++            arc.add("escape", symlink_to=os.path.join(linkpath, ".."))
++            # use the symlinks above, that are not checked, to create a 
hardlink
++            # to a file outside of the destination path
++            arc.add("flaglink", hardlink_to=os.path.join("escape", "flag"))
++            # now that we have the hardlink we can overwrite the file
++            arc.add("flaglink", content='overwrite')
++            # we can also create new files as well!
++            arc.add("escape/newfile", content='new')
++
++        with (self.subTest('fully_trusted'),
++              self.check_context(arc.open(), filter='fully_trusted',
++                                 check_flag=False, 
ignored_trees={component})):
++            if sys.platform == 'win32':
++                self.expect_exception((FileNotFoundError, FileExistsError))
++            elif self.raised_exception:
++                # Cannot symlink/hardlink: tarfile falls back to getmember()
++                self.expect_exception(KeyError)
++                # Otherwise, this block should never enter.
++            else:
++                self.expect_file('flaglink', content='overwrite')
++                self.expect_file('../newfile', content='new')
++                self.expect_file('escape', type=tarfile.SYMTYPE)
++                self.expect_file('a', symlink_to=component)
++
++        for filter in 'tar', 'data':
++            with self.subTest(filter), self.check_context(arc.open(), 
filter=filter):
++                exc = self.expect_exception((OSError, KeyError))
++                if isinstance(exc, OSError):
++                    if sys.platform == 'win32':
++                        # 3: ERROR_PATH_NOT_FOUND
++                        # 5: ERROR_ACCESS_DENIED
++                        # 206: ERROR_FILENAME_EXCED_RANGE
++                        self.assertIn(exc.winerror, (3, 5, 206))
++                    else:
++                        self.assertEqual(exc.errno, errno.ENAMETOOLONG)
++
+     def test_parent_symlink2(self):
+         # Test interplaying symlinks
+         # Inspired by 'dirsymlink2b' in jwilk/traversal-archives
+@@ -3625,8 +3781,8 @@ def test_chains(self):
+             arc.add('symlink2', symlink_to=os.path.join(
+                 'linkdir', 'hardlink2'))
+             arc.add('targetdir/target', size=3)
+-            arc.add('linkdir/hardlink', hardlink_to='targetdir/target')
+-            arc.add('linkdir/hardlink2', hardlink_to='linkdir/symlink')
++            arc.add('linkdir/hardlink', hardlink_to=os.path.join('targetdir', 
'target'))
++            arc.add('linkdir/hardlink2', hardlink_to=os.path.join('linkdir', 
'symlink'))
+ 
+         for filter in 'tar', 'data', 'fully_trusted':
+             with self.check_context(arc.open(), filter):
+@@ -3642,6 +3798,126 @@ def test_chains(self):
+                     self.expect_file('linkdir/symlink', size=3)
+                     self.expect_file('symlink2', size=3)
+ 
++    def test_sneaky_hardlink_fallback(self):
++        # (CVE-2025-4330)
++        # Test that when hardlink extraction falls back to extracting members
++        # from the archive, the extracted member is (re-)filtered.
++        with ArchiveMaker() as arc:
++            # Create a directory structure so the c/escape symlink stays
++            # inside the path
++            arc.add("a/t/dummy")
++            # Create b/ directory
++            arc.add("b/")
++            # Point "c" to the bottom of the tree in "a"
++            arc.add("c", symlink_to=os.path.join("a", "t"))
++            # link to non-existant location under "a"
++            arc.add("c/escape", symlink_to=os.path.join("..", "..",
++                                                        "link_here"))
++            # Move "c" to point to "b" ("c/escape" no longer exists)
++            arc.add("c", symlink_to="b")
++            # Attempt to create a hard link to "c/escape". Since it doesn't
-e 
... etc. - the rest is truncated

Reply via email to