diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst
index cb9300f072b9e7..e42bdc06be09ff 100644
--- a/Doc/library/collections.rst
+++ b/Doc/library/collections.rst
@@ -326,7 +326,7 @@ For example::
.. versionadded:: 3.10
The usual dictionary methods are available for :class:`Counter` objects
- except for two which work differently for counters.
+ except for these two which work differently for counters:
.. method:: fromkeys(iterable)
diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
index 769541116993c4..660fec4f1be865 100644
--- a/Lib/http/cookies.py
+++ b/Lib/http/cookies.py
@@ -391,17 +391,21 @@ def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.OutputString())
def js_output(self, attrs=None):
+ import base64
# Print javascript
output_string = self.OutputString(attrs)
if _has_control_character(output_string):
raise CookieError("Control characters are not allowed in cookies")
+ # Base64-encode value to avoid template
+ # injection in cookie values.
+ output_encoded = base64.b64encode(output_string.encode('utf-8')).decode("ascii")
return """
- """ % (output_string.replace('"', r'\"'))
+ """ % (output_encoded,)
def OutputString(self, attrs=None):
# Build up our result
diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
index e2c7551c0b3341..cfcbc17bd6df80 100644
--- a/Lib/test/test_http_cookies.py
+++ b/Lib/test/test_http_cookies.py
@@ -1,5 +1,5 @@
# Simple test suite for http/cookies.py
-
+import base64
import copy
import unittest
import doctest
@@ -175,17 +175,19 @@ def test_load(self):
self.assertEqual(C.output(['path']),
'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
- self.assertEqual(C.js_output(), r"""
+ cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; Path=/acme; Version=1').decode('ascii')
+ self.assertEqual(C.js_output(), fr"""
""")
- self.assertEqual(C.js_output(['path']), r"""
+ cookie_encoded = base64.b64encode(b'Customer="WILE_E_COYOTE"; Path=/acme').decode('ascii')
+ self.assertEqual(C.js_output(['path']), fr"""
""")
@@ -290,17 +292,19 @@ def test_quoted_meta(self):
self.assertEqual(C.output(['path']),
'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme')
- self.assertEqual(C.js_output(), r"""
+ expected_encoded_cookie = base64.b64encode(b'Customer=\"WILE_E_COYOTE\"; Path=/acme; Version=1').decode('ascii')
+ self.assertEqual(C.js_output(), fr"""
""")
- self.assertEqual(C.js_output(['path']), r"""
+ expected_encoded_cookie = base64.b64encode(b'Customer=\"WILE_E_COYOTE\"; Path=/acme').decode('ascii')
+ self.assertEqual(C.js_output(['path']), fr"""
""")
@@ -391,13 +395,16 @@ def test_setter(self):
self.assertEqual(
M.output(),
"Set-Cookie: %s=%s; Path=/foo" % (i, "%s_coded_val" % i))
+ expected_encoded_cookie = base64.b64encode(
+ ("%s=%s; Path=/foo" % (i, "%s_coded_val" % i)).encode("ascii")
+ ).decode('ascii')
expected_js_output = """
- """ % (i, "%s_coded_val" % i)
+ """ % (expected_encoded_cookie,)
self.assertEqual(M.js_output(), expected_js_output)
for i in ["foo bar", "foo@bar"]:
# Try some illegal characters
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-14-36-44.gh-issue-148820.XhOGhA.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-14-36-44.gh-issue-148820.XhOGhA.rst
new file mode 100644
index 00000000000000..392becaffb73cf
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-14-36-44.gh-issue-148820.XhOGhA.rst
@@ -0,0 +1,5 @@
+Fix a race in :c:type:`!_PyRawMutex` on the free-threaded build where a
+``Py_PARK_INTR`` return from ``_PySemaphore_Wait`` could let the waiter
+destroy its semaphore before the unlocking thread's
+``_PySemaphore_Wakeup`` completed, causing a fatal ``ReleaseSemaphore``
+error.
diff --git a/Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst b/Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst
new file mode 100644
index 00000000000000..d7d376737e4ad1
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-04-21-13-46-30.gh-issue-90309.srvj9q.rst
@@ -0,0 +1,3 @@
+Base64-encode values when embedding cookies to JavaScript using the
+:meth:`http.cookies.BaseCookie.js_output` method to avoid injection
+and escaping.
diff --git a/Python/lock.c b/Python/lock.c
index 752a5899e088a5..af136fefd299d3 100644
--- a/Python/lock.c
+++ b/Python/lock.c
@@ -248,7 +248,16 @@ _PyRawMutex_LockSlow(_PyRawMutex *m)
// Wait for us to be woken up. Note that we still have to lock the
// mutex ourselves: it is NOT handed off to us.
- _PySemaphore_Wait(&waiter.sema, -1);
+ //
+ // Loop until we observe an actual wakeup. A return of Py_PARK_INTR
+ // could otherwise let us exit _PySemaphore_Wait and destroy
+ // `waiter.sema` while _PyRawMutex_UnlockSlow's matching
+ // _PySemaphore_Wakeup is still pending, since the unlocker has
+ // already CAS-removed us from the waiter list without any handshake.
+ int res;
+ do {
+ res = _PySemaphore_Wait(&waiter.sema, -1);
+ } while (res != Py_PARK_OK);
}
_PySemaphore_Destroy(&waiter.sema);
diff --git a/Python/parking_lot.c b/Python/parking_lot.c
index 99c1ad848be795..8823d77719cb9a 100644
--- a/Python/parking_lot.c
+++ b/Python/parking_lot.c
@@ -61,7 +61,9 @@ _PySemaphore_Init(_PySemaphore *sema)
NULL // unnamed
);
if (!sema->platform_sem) {
- Py_FatalError("parking_lot: CreateSemaphore failed");
+ _Py_FatalErrorFormat(__func__,
+ "parking_lot: CreateSemaphore failed (error: %u)",
+ GetLastError());
}
#elif defined(_Py_USE_SEMAPHORES)
if (sem_init(&sema->platform_sem, /*pshared=*/0, /*value=*/0) < 0) {
@@ -141,8 +143,8 @@ _PySemaphore_Wait(_PySemaphore *sema, PyTime_t timeout)
}
else {
_Py_FatalErrorFormat(__func__,
- "unexpected error from semaphore: %u (error: %u)",
- wait, GetLastError());
+ "unexpected error from semaphore: %u (error: %u, handle: %p)",
+ wait, GetLastError(), sema->platform_sem);
}
#elif defined(_Py_USE_SEMAPHORES)
int err;
@@ -230,7 +232,9 @@ _PySemaphore_Wakeup(_PySemaphore *sema)
{
#if defined(MS_WINDOWS)
if (!ReleaseSemaphore(sema->platform_sem, 1, NULL)) {
- Py_FatalError("parking_lot: ReleaseSemaphore failed");
+ _Py_FatalErrorFormat(__func__,
+ "parking_lot: ReleaseSemaphore failed (error: %u, handle: %p)",
+ GetLastError(), sema->platform_sem);
}
#elif defined(_Py_USE_SEMAPHORES)
int err = sem_post(&sema->platform_sem);
diff --git a/Tools/pixi-packages/README.md b/Tools/pixi-packages/README.md
index 4b44fd12150752..d818fddaac6a1e 100644
--- a/Tools/pixi-packages/README.md
+++ b/Tools/pixi-packages/README.md
@@ -36,9 +36,8 @@ Each package definition is contained in a subdirectory, but they share the build
- More package variants (such as UBSan)
- Support for Windows
-- Using a single `pixi.toml` and `recipe.yaml` for all package variants is blocked on
- [pixi#5364](https://github.com/prefix-dev/pixi/pull/5364)
- and [pixi#5248](https://github.com/prefix-dev/pixi/issues/5248)
+- Using a single `pixi.toml` for all package variants is blocked on
+ [pixi#5248](https://github.com/prefix-dev/pixi/issues/5248)
## Troubleshooting
@@ -48,7 +47,7 @@ FATAL: ThreadSanitizer: unexpected memory mapping 0x7977bd072000-0x7977bd500000
```
To fix it, try reducing `mmap_rnd_bits`:
-```bash
+```console
$ sudo sysctl vm.mmap_rnd_bits
vm.mmap_rnd_bits = 32 # too high for TSan
$ sudo sysctl vm.mmap_rnd_bits=28 # reduce it
diff --git a/Tools/pixi-packages/asan/pixi.toml b/Tools/pixi-packages/asan/pixi.toml
index e3b5673d962659..bf9841e18677ca 100644
--- a/Tools/pixi-packages/asan/pixi.toml
+++ b/Tools/pixi-packages/asan/pixi.toml
@@ -5,7 +5,11 @@
channels = ["https://prefix.dev/conda-forge"]
platforms = ["linux-64", "linux-aarch64", "osx-64", "osx-arm64"]
preview = ["pixi-build"]
+requires-pixi = ">=0.66.0"
[package.build.backend]
name = "pixi-build-rattler-build"
version = "*"
+
+[package.build.config]
+recipe = "../default/recipe.yaml"
diff --git a/Tools/pixi-packages/asan/recipe.yaml b/Tools/pixi-packages/asan/recipe.yaml
deleted file mode 100644
index 30d0d5a2ed2e04..00000000000000
--- a/Tools/pixi-packages/asan/recipe.yaml
+++ /dev/null
@@ -1,94 +0,0 @@
-# NOTE: Please always only modify default/recipe.yaml and then run clone-recipe.sh to
-# propagate the changes to the other variants.
-
-context:
- # Keep up to date
- freethreading_tag: ${{ "t" if "freethreading" in variant else "" }}
-
-recipe:
- name: python
-
-source:
- - path: ../../..
-
-outputs:
-- package:
- name: python_abi
- version: ${{ version }}
- build:
- string: "0_${{ abi_tag }}"
- requirements:
- run_constraints:
- - python ${{ version }}.* *_${{ abi_tag }}
-
-- package:
- name: python
- version: ${{ version }}
- build:
- string: "0_${{ abi_tag }}"
- files:
- exclude:
- - "*.o"
- script:
- file: ../build.sh
- env:
- PYTHON_VARIANT: ${{ variant }}
- python:
- site_packages_path: "lib/python${{ version }}${{ freethreading_tag }}/site-packages"
-
- # derived from https://github.com/conda-forge/python-feedstock/blob/main/recipe/meta.yaml
- requirements:
- build:
- - ${{ compiler('c') }}
- - ${{ compiler('cxx') }}
- # Note that we are not using stdlib arguments which means the packages
- # are built for the build settings and are not relocatable to a different
- # machine that has a older system version. (eg: macOS/glibc version)
- - make
- - pkg-config
- # configure script looks for llvm-ar for lto
- - if: osx
- then:
- - llvm-tools
-
- host:
- - bzip2
- - sqlite
- - liblzma-devel
- - zlib
- - zstd
- - openssl
- - readline
- - tk
- # These two are just to get the headers needed for tk.h, but is unused
- - xorg-libx11
- - xorg-xorgproto
- - ncurses
- - libffi
- - if: linux
- then:
- - libuuid
- - libmpdec-devel
- - expat
- - if: linux and "san" in variant
- then:
- - libsanitizer
- - if: osx and "san" in variant
- then:
- - libcompiler-rt
-
- ignore_run_exports:
- from_package:
- - xorg-libx11
- - xorg-xorgproto
-
- run_exports:
- noarch:
- - python
- weak:
- - python_abi ${{ version }}.* *_${{ abi_tag }}
-
-about:
- homepage: https://www.python.org/
- license: Python-2.0
- license_file: LICENSE
diff --git a/Tools/pixi-packages/clone-recipe.sh b/Tools/pixi-packages/clone-recipe.sh
index 52b2568837c8e1..25ceaf85c35f56 100755
--- a/Tools/pixi-packages/clone-recipe.sh
+++ b/Tools/pixi-packages/clone-recipe.sh
@@ -6,5 +6,5 @@ set -o errexit
cd "$(dirname "$0")"
for variant in asan freethreading tsan-freethreading; do
- cp -av default/recipe.yaml default/pixi.toml ${variant}/
+ cp -av default/pixi.toml ${variant}/
done
diff --git a/Tools/pixi-packages/default/pixi.toml b/Tools/pixi-packages/default/pixi.toml
index e3b5673d962659..bf9841e18677ca 100644
--- a/Tools/pixi-packages/default/pixi.toml
+++ b/Tools/pixi-packages/default/pixi.toml
@@ -5,7 +5,11 @@
channels = ["https://prefix.dev/conda-forge"]
platforms = ["linux-64", "linux-aarch64", "osx-64", "osx-arm64"]
preview = ["pixi-build"]
+requires-pixi = ">=0.66.0"
[package.build.backend]
name = "pixi-build-rattler-build"
version = "*"
+
+[package.build.config]
+recipe = "../default/recipe.yaml"
diff --git a/Tools/pixi-packages/freethreading/pixi.toml b/Tools/pixi-packages/freethreading/pixi.toml
index e3b5673d962659..bf9841e18677ca 100644
--- a/Tools/pixi-packages/freethreading/pixi.toml
+++ b/Tools/pixi-packages/freethreading/pixi.toml
@@ -5,7 +5,11 @@
channels = ["https://prefix.dev/conda-forge"]
platforms = ["linux-64", "linux-aarch64", "osx-64", "osx-arm64"]
preview = ["pixi-build"]
+requires-pixi = ">=0.66.0"
[package.build.backend]
name = "pixi-build-rattler-build"
version = "*"
+
+[package.build.config]
+recipe = "../default/recipe.yaml"
diff --git a/Tools/pixi-packages/freethreading/recipe.yaml b/Tools/pixi-packages/freethreading/recipe.yaml
deleted file mode 100644
index 30d0d5a2ed2e04..00000000000000
--- a/Tools/pixi-packages/freethreading/recipe.yaml
+++ /dev/null
@@ -1,94 +0,0 @@
-# NOTE: Please always only modify default/recipe.yaml and then run clone-recipe.sh to
-# propagate the changes to the other variants.
-
-context:
- # Keep up to date
- freethreading_tag: ${{ "t" if "freethreading" in variant else "" }}
-
-recipe:
- name: python
-
-source:
- - path: ../../..
-
-outputs:
-- package:
- name: python_abi
- version: ${{ version }}
- build:
- string: "0_${{ abi_tag }}"
- requirements:
- run_constraints:
- - python ${{ version }}.* *_${{ abi_tag }}
-
-- package:
- name: python
- version: ${{ version }}
- build:
- string: "0_${{ abi_tag }}"
- files:
- exclude:
- - "*.o"
- script:
- file: ../build.sh
- env:
- PYTHON_VARIANT: ${{ variant }}
- python:
- site_packages_path: "lib/python${{ version }}${{ freethreading_tag }}/site-packages"
-
- # derived from https://github.com/conda-forge/python-feedstock/blob/main/recipe/meta.yaml
- requirements:
- build:
- - ${{ compiler('c') }}
- - ${{ compiler('cxx') }}
- # Note that we are not using stdlib arguments which means the packages
- # are built for the build settings and are not relocatable to a different
- # machine that has a older system version. (eg: macOS/glibc version)
- - make
- - pkg-config
- # configure script looks for llvm-ar for lto
- - if: osx
- then:
- - llvm-tools
-
- host:
- - bzip2
- - sqlite
- - liblzma-devel
- - zlib
- - zstd
- - openssl
- - readline
- - tk
- # These two are just to get the headers needed for tk.h, but is unused
- - xorg-libx11
- - xorg-xorgproto
- - ncurses
- - libffi
- - if: linux
- then:
- - libuuid
- - libmpdec-devel
- - expat
- - if: linux and "san" in variant
- then:
- - libsanitizer
- - if: osx and "san" in variant
- then:
- - libcompiler-rt
-
- ignore_run_exports:
- from_package:
- - xorg-libx11
- - xorg-xorgproto
-
- run_exports:
- noarch:
- - python
- weak:
- - python_abi ${{ version }}.* *_${{ abi_tag }}
-
-about:
- homepage: https://www.python.org/
- license: Python-2.0
- license_file: LICENSE
diff --git a/Tools/pixi-packages/tsan-freethreading/pixi.toml b/Tools/pixi-packages/tsan-freethreading/pixi.toml
index e3b5673d962659..bf9841e18677ca 100644
--- a/Tools/pixi-packages/tsan-freethreading/pixi.toml
+++ b/Tools/pixi-packages/tsan-freethreading/pixi.toml
@@ -5,7 +5,11 @@
channels = ["https://prefix.dev/conda-forge"]
platforms = ["linux-64", "linux-aarch64", "osx-64", "osx-arm64"]
preview = ["pixi-build"]
+requires-pixi = ">=0.66.0"
[package.build.backend]
name = "pixi-build-rattler-build"
version = "*"
+
+[package.build.config]
+recipe = "../default/recipe.yaml"
diff --git a/Tools/pixi-packages/tsan-freethreading/recipe.yaml b/Tools/pixi-packages/tsan-freethreading/recipe.yaml
deleted file mode 100644
index 30d0d5a2ed2e04..00000000000000
--- a/Tools/pixi-packages/tsan-freethreading/recipe.yaml
+++ /dev/null
@@ -1,94 +0,0 @@
-# NOTE: Please always only modify default/recipe.yaml and then run clone-recipe.sh to
-# propagate the changes to the other variants.
-
-context:
- # Keep up to date
- freethreading_tag: ${{ "t" if "freethreading" in variant else "" }}
-
-recipe:
- name: python
-
-source:
- - path: ../../..
-
-outputs:
-- package:
- name: python_abi
- version: ${{ version }}
- build:
- string: "0_${{ abi_tag }}"
- requirements:
- run_constraints:
- - python ${{ version }}.* *_${{ abi_tag }}
-
-- package:
- name: python
- version: ${{ version }}
- build:
- string: "0_${{ abi_tag }}"
- files:
- exclude:
- - "*.o"
- script:
- file: ../build.sh
- env:
- PYTHON_VARIANT: ${{ variant }}
- python:
- site_packages_path: "lib/python${{ version }}${{ freethreading_tag }}/site-packages"
-
- # derived from https://github.com/conda-forge/python-feedstock/blob/main/recipe/meta.yaml
- requirements:
- build:
- - ${{ compiler('c') }}
- - ${{ compiler('cxx') }}
- # Note that we are not using stdlib arguments which means the packages
- # are built for the build settings and are not relocatable to a different
- # machine that has a older system version. (eg: macOS/glibc version)
- - make
- - pkg-config
- # configure script looks for llvm-ar for lto
- - if: osx
- then:
- - llvm-tools
-
- host:
- - bzip2
- - sqlite
- - liblzma-devel
- - zlib
- - zstd
- - openssl
- - readline
- - tk
- # These two are just to get the headers needed for tk.h, but is unused
- - xorg-libx11
- - xorg-xorgproto
- - ncurses
- - libffi
- - if: linux
- then:
- - libuuid
- - libmpdec-devel
- - expat
- - if: linux and "san" in variant
- then:
- - libsanitizer
- - if: osx and "san" in variant
- then:
- - libcompiler-rt
-
- ignore_run_exports:
- from_package:
- - xorg-libx11
- - xorg-xorgproto
-
- run_exports:
- noarch:
- - python
- weak:
- - python_abi ${{ version }}.* *_${{ abi_tag }}
-
-about:
- homepage: https://www.python.org/
- license: Python-2.0
- license_file: LICENSE