Submitted By:            Joe Locash <jlocash at gmail dot com>
Date:                    2026-06-23
Initial Package Version: 3.14.6
Upstream Status:         Applied
Origin:                  Upstream:
                           https://github.com/python/cpython/pull/151565
                           https://github.com/python/cpython/pull/151998
                           https://github.com/python/cpython/pull/151565
Description:             Upsream fixes for CVE-2026-12003, CVE-2026-11940, and
                         CVE-2026-0864.

From 4d228c411bf23ff71e36db7d285d7e23e875b926 Mon Sep 17 00:00:00 2001
From: Steve Dower <steve.dower@python.org>
Date: Wed, 17 Jun 2026 00:16:06 +0100
Subject: [PATCH 1/4] gh-151544: Fixes CVE-2026-12003 by removing the fallback
 to %VPATH%/Modules/Setup.local for discovering sources in getpath.py
 (GH-151545) (cherry picked from commit
 9e863fab283eddca9c2a8f9d1ee30f4dc243e314)

Co-authored-by: Steve Dower <steve.dower@python.org>
---
 Makefile.pre.in                                   |  2 ++
 ...2026-06-16-14-58-02.gh-issue-151544._bexVy.rst |  4 ++++
 Modules/getpath.py                                | 15 ++++-----------
 3 files changed, 10 insertions(+), 11 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Security/2026-06-16-14-58-02.gh-issue-151544._bexVy.rst

diff --git a/Makefile.pre.in b/Makefile.pre.in
index f86d7363e0900f..75a892e94b0965 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -1679,6 +1679,8 @@ Programs/_bootstrap_python.o: Programs/_bootstrap_python.c $(BOOTSTRAP_HEADERS)
 _bootstrap_python: $(LIBRARY_OBJS_OMIT_FROZEN) Programs/_bootstrap_python.o Modules/getpath.o Modules/Setup.local
 	$(LINKCC) $(PY_LDFLAGS_NOLTO) -o $@ $(LIBRARY_OBJS_OMIT_FROZEN) \
 		Programs/_bootstrap_python.o Modules/getpath.o $(LIBS) $(MODLIBS) $(SYSLIBS)
+	# Dummy pybuilddir.txt  is needed for _bootstrap_python to be runnable
+	@echo "none" > ./pybuilddir.txt
 
 
 ############################################################################
diff --git a/Misc/NEWS.d/next/Security/2026-06-16-14-58-02.gh-issue-151544._bexVy.rst b/Misc/NEWS.d/next/Security/2026-06-16-14-58-02.gh-issue-151544._bexVy.rst
new file mode 100644
index 00000000000000..418e3b4b967794
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-06-16-14-58-02.gh-issue-151544._bexVy.rst
@@ -0,0 +1,4 @@
+:file:`Modules/Setup.local` is no longer used as a landmark to discover
+whether Python is running in a source tree, as it could potentially affect
+actual installs. The :file:`pybuilddir.txt` file is now the sole indicator
+of running in a source tree.
diff --git a/Modules/getpath.py b/Modules/getpath.py
index b89d7427e3febd..0e4f1e87e7342a 100644
--- a/Modules/getpath.py
+++ b/Modules/getpath.py
@@ -129,8 +129,7 @@
 # checked by looking for the BUILDDIR_TXT file, which contains the
 # relative path to the platlib dir. The executable_dir value is
 # derived from joining the VPATH preprocessor variable to the
-# directory containing pybuilddir.txt. If it is not found, the
-# BUILD_LANDMARK file is found, which is part of the source tree.
+# directory containing pybuilddir.txt.
 # prefix is then found by searching up for a file that should only
 # exist in the source tree, and the stdlib dir is set to prefix/Lib.
 
@@ -177,7 +176,6 @@
 
 if os_name == 'posix' or os_name == 'darwin':
     BUILDDIR_TXT = 'pybuilddir.txt'
-    BUILD_LANDMARK = 'Modules/Setup.local'
     DEFAULT_PROGRAM_NAME = f'python{VERSION_MAJOR}'
     STDLIB_SUBDIR = f'{platlibdir}/python{VERSION_MAJOR}.{VERSION_MINOR}{ABI_THREAD}'
     STDLIB_LANDMARKS = [f'{STDLIB_SUBDIR}/os.py', f'{STDLIB_SUBDIR}/os.pyc']
@@ -190,7 +188,6 @@
 
 elif os_name == 'nt':
     BUILDDIR_TXT = 'pybuilddir.txt'
-    BUILD_LANDMARK = f'{VPATH}\\Modules\\Setup.local'
     DEFAULT_PROGRAM_NAME = f'python'
     STDLIB_SUBDIR = 'Lib'
     STDLIB_LANDMARKS = [f'{STDLIB_SUBDIR}\\os.py', f'{STDLIB_SUBDIR}\\os.pyc']
@@ -512,13 +509,9 @@ def search_up(prefix, *landmarks, test=isfile):
         platstdlib_dir = real_executable_dir
         build_prefix = joinpath(real_executable_dir, VPATH)
     except (FileNotFoundError, PermissionError):
-        if isfile(joinpath(real_executable_dir, BUILD_LANDMARK)):
-            build_prefix = joinpath(real_executable_dir, VPATH)
-            if os_name == 'nt':
-                # QUIRK: Windows builds need platstdlib_dir to be the executable
-                # dir. Normally the builddir marker handles this, but in this
-                # case we need to correct manually.
-                platstdlib_dir = real_executable_dir
+        # We used to check for an alternate landmark here, but now we require
+        # BUILDDIR_TXT to exist. (gh-151544; CVE-2026-12003)
+        pass
 
     if build_prefix:
         if os_name == 'nt':

From b76f1f9ba175c511063c086c083614b8bd5cd4bc Mon Sep 17 00:00:00 2001
From: Steve Dower <steve.dower@python.org>
Date: Wed, 17 Jun 2026 19:36:56 +0100
Subject: [PATCH 2/4] Add argv0 setting for Python process in WASM

---
 Tools/wasm/wasi/__main__.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Tools/wasm/wasi/__main__.py b/Tools/wasm/wasi/__main__.py
index b57bcaca924380..25c1829e1295e9 100644
--- a/Tools/wasm/wasi/__main__.py
+++ b/Tools/wasm/wasi/__main__.py
@@ -421,6 +421,8 @@ def main():
         "--wasm max-wasm-stack=16777216 "
         # Enable thread support; causes use of preview1.
         # "--wasm threads=y --wasi threads=y "
+        # Set argv0 to the Python process
+        "--argv0 {PYTHON_WASM} "
         # Map the checkout to / to load the stdlib from /Lib.
         "--dir {HOST_DIR}::{GUEST_DIR} "
         # Set PYTHONPATH to the sysconfig data.

From b002dba22ac4bdec24e7b89cad3e4cc50dbec081 Mon Sep 17 00:00:00 2001
From: Steve Dower <steve.dower@python.org>
Date: Wed, 17 Jun 2026 20:46:53 +0100
Subject: [PATCH 3/4] Add symlink creation for pybuilddir.txt

---
 Tools/wasm/wasi/__main__.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/Tools/wasm/wasi/__main__.py b/Tools/wasm/wasi/__main__.py
index 25c1829e1295e9..3d15b9f62df000 100644
--- a/Tools/wasm/wasi/__main__.py
+++ b/Tools/wasm/wasi/__main__.py
@@ -368,6 +368,10 @@ def configure_wasi_python(context, working_dir):
         file.write(f'#!/bin/sh\nexec {host_runner} {python_wasm} "$@"\n')
     exec_script.chmod(0o755)
     log("🏃", f"Created {exec_script} (--host-runner)... ")
+    pybuilddir_txt = working_dir / "pybuilddir.txt"
+    if not pybuilddir_txt.exists():
+        os.symlink(CHECKOUT / "pybuilddir.txt", pybuilddir_txt)
+        log("📝", f"Symlinked {pybuilddir_txt} to normal location")
     sys.stdout.flush()
 
 
@@ -399,6 +403,11 @@ def clean_contents(context):
         if LOCAL_SETUP.read_bytes() == LOCAL_SETUP_MARKER:
             log("🧹", f"Deleting generated {LOCAL_SETUP} ...")
 
+    pybuilddir_txt = working_dir / "pybuilddir.txt"
+    if pybuilddir_txt.exists():
+        log("🧹", f"Deleting {pybuilddir_txt} ...")
+        pybuilddir_txt.unlink()
+
 
 def build_steps(*steps):
     """Construct a command from other steps."""
@@ -421,8 +430,6 @@ def main():
         "--wasm max-wasm-stack=16777216 "
         # Enable thread support; causes use of preview1.
         # "--wasm threads=y --wasi threads=y "
-        # Set argv0 to the Python process
-        "--argv0 {PYTHON_WASM} "
         # Map the checkout to / to load the stdlib from /Lib.
         "--dir {HOST_DIR}::{GUEST_DIR} "
         # Set PYTHONPATH to the sysconfig data.

From c5125ff1e63a50694f17b712a600b4f971e8d1ff Mon Sep 17 00:00:00 2001
From: Steve Dower <steve.dower@python.org>
Date: Wed, 17 Jun 2026 21:02:34 +0100
Subject: [PATCH 4/4] Remove unnecessary deletion of pybuilddir.txt

---
 Tools/wasm/wasi/__main__.py | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/Tools/wasm/wasi/__main__.py b/Tools/wasm/wasi/__main__.py
index 3d15b9f62df000..abe30fe0b4c77f 100644
--- a/Tools/wasm/wasi/__main__.py
+++ b/Tools/wasm/wasi/__main__.py
@@ -403,11 +403,6 @@ def clean_contents(context):
         if LOCAL_SETUP.read_bytes() == LOCAL_SETUP_MARKER:
             log("🧹", f"Deleting generated {LOCAL_SETUP} ...")
 
-    pybuilddir_txt = working_dir / "pybuilddir.txt"
-    if pybuilddir_txt.exists():
-        log("🧹", f"Deleting {pybuilddir_txt} ...")
-        pybuilddir_txt.unlink()
-
 
 def build_steps(*steps):
     """Construct a command from other steps."""
From 6b66c843887161f1e82c61c9e9489ba7cf5d36cf Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <stan@python.org>
Date: Tue, 23 Jun 2026 14:31:38 +0100
Subject: [PATCH] gh-151558: Fix symlink escape via `tarfile`
 hardlink-extraction fallback (GH-151559) (cherry picked from commit
 27dd970bf6b17ebca7c8ed486a40ab043ed7af8f)

Co-authored-by: Stan Ulbrych <stan@python.org>
---
 Lib/tarfile.py                                |  3 +++
 Lib/test/test_tarfile.py                      | 24 +++++++++++++++++++
 ...-06-10-13-08-19.gh-issue-151558.mL74i2.rst |  3 +++
 3 files changed, 30 insertions(+)
 create mode 100644 Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst

diff --git a/Lib/tarfile.py b/Lib/tarfile.py
index e6734db24f642e8..63f23490e8a149e 100644
--- a/Lib/tarfile.py
+++ b/Lib/tarfile.py
@@ -2782,6 +2782,9 @@ def makelink_with_filter(self, tarinfo, targetpath,
                     "makelink_with_filter: if filter_function is not None, "
                     + "extraction_root must also not be None")
             try:
+                filter_function(
+                    unfiltered.replace(name=tarinfo.name, deep=False),
+                    extraction_root)
                 filtered = filter_function(unfiltered, extraction_root)
             except _FILTER_ERRORS as cause:
                 raise LinkFallbackError(tarinfo, unfiltered.name) from cause
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
index d974c7d46ec18c0..0fc7413be8db28c 100644
--- a/Lib/test/test_tarfile.py
+++ b/Lib/test/test_tarfile.py
@@ -4344,6 +4344,30 @@ def test_sneaky_hardlink_fallback(self):
                     self.expect_file("boom", symlink_to='../../link_here')
                     self.expect_file("c", symlink_to='b')
 
+    @symlink_test
+    def test_sneaky_hardlink_fallback_deep(self):
+        # (CVE-2026-11940)
+        with ArchiveMaker() as arc:
+            arc.add("a/b/s", symlink_to=os.path.join("..", "escape"))
+            arc.add("s", hardlink_to=os.path.join("a", "b", "s"))
+
+        with self.check_context(arc.open(), 'data'):
+            e = self.expect_exception(
+                tarfile.LinkFallbackError,
+                "link 's' would be extracted as a copy of "
+                + "'a/b/s', which was rejected")
+            self.assertIsInstance(e.__cause__,
+                                  tarfile.LinkOutsideDestinationError)
+
+        for filter in 'tar', 'fully_trusted':
+            with self.subTest(filter), self.check_context(arc.open(), filter):
+                if not os_helper.can_symlink():
+                    self.expect_file("a/")
+                    self.expect_file("a/b/")
+                else:
+                    self.expect_file("a/b/s", symlink_to=os.path.join('..', 'escape'))
+                    self.expect_file("s", symlink_to=os.path.join('..', 'escape'))
+
     @symlink_test
     def test_exfiltration_via_symlink(self):
         # (CVE-2025-4138)
diff --git a/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst b/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst
new file mode 100644
index 000000000000000..74459d5680e21a3
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst
@@ -0,0 +1,3 @@
+Fixed an vulnerability in the :mod:`tarfile` ``data`` and ``tar`` extraction
+filters where crafted archives could create a symlink pointing outside the
+destination directory. This was a bypass of :cve:`2025-4330`.
From 314f46b6c51d366460b66292b1aecfe9fb71de94 Mon Sep 17 00:00:00 2001
From: Seth Larson <seth@python.org>
Date: Tue, 23 Jun 2026 08:33:51 -0500
Subject: [PATCH] gh-143927: Normalize all line endings (CR, CRLF, and LF) in
 configparser (GH-143929) (cherry picked from commit
 5858e42c539dac8394636a6e9b30472b8994851f)

Co-authored-by: Seth Larson <seth@python.org>
---
 Lib/configparser.py                                   |  4 +++-
 Lib/test/test_configparser.py                         | 11 +++++++++++
 .../2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst    |  2 ++
 3 files changed, 16 insertions(+), 1 deletion(-)
 create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst

diff --git a/Lib/configparser.py b/Lib/configparser.py
index a53ac87276445ad..3c452afe8ade485 100644
--- a/Lib/configparser.py
+++ b/Lib/configparser.py
@@ -992,7 +992,9 @@ def _write_section(self, fp, section_name, section_items, delimiter, unnamed=Fal
             value = self._interpolation.before_write(self, section_name, key,
                                                      value)
             if value is not None or not self._allow_no_value:
-                value = delimiter + str(value).replace('\n', '\n\t')
+                # Convert all possible line-endings into '\n\t'
+                value = (delimiter + str(value).replace('\r\n', '\n')
+                         .replace('\r', '\n').replace('\n', '\n\t'))
             else:
                 value = ""
             fp.write("{}{}\n".format(key, value))
diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py
index 8d8dd2a2bf27fbf..4783943f71a1092 100644
--- a/Lib/test/test_configparser.py
+++ b/Lib/test/test_configparser.py
@@ -526,6 +526,17 @@ def test_default_case_sensitivity(self):
             cf.get(self.default_section, "Foo"), "Bar",
             "could not locate option, expecting case-insensitive defaults")
 
+    def test_crlf_normalization(self):
+        cf = self.newconfig({"key1": "a\nb","key2": "a\rb", "key3": "a\r\nb", "key4": "a\r\nb"})
+        buf = io.StringIO()
+        cf.write(buf)
+        cf_str = buf.getvalue()
+        self.assertNotIn("\r", cf_str)
+        self.assertNotIn("\r\n", cf_str)
+        self.assertEqual(cf_str.count("\n"), 10)
+        self.assertEqual(cf_str.count("\n\t"), 4)
+        self.assertTrue(cf_str.endswith("\n\n"))
+
     def test_parse_errors(self):
         cf = self.newconfig()
         self.parse_error(cf, configparser.ParsingError,
diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst
new file mode 100644
index 000000000000000..ca554997e5c3963
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst
@@ -0,0 +1,2 @@
+Normalize all line endings (CR, CRLF, and LF) to LF+TAB when writing
+multi-line configparser values.
