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