diff --git a/Include/internal/pycore_bytesobject.h b/Include/internal/pycore_bytesobject.h index 177e6d10134adb..be85d9debd8e96 100644 --- a/Include/internal/pycore_bytesobject.h +++ b/Include/internal/pycore_bytesobject.h @@ -73,6 +73,22 @@ _PyBytes_Repeat(char* dest, Py_ssize_t len_dest, */ #define _PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1) + +// Return the cached hash value, or -1 if not cached yet. +static inline Py_hash_t +_PyBytes_GET_CACHED_HASH(PyBytesObject *self) +{ +_Py_COMP_DIAG_PUSH +_Py_COMP_DIAG_IGNORE_DEPR_DECLS +#ifdef Py_GIL_DISABLED + return _Py_atomic_load_ssize_relaxed(&self->ob_shash); +#else + return self->ob_shash; +#endif +_Py_COMP_DIAG_POP +} + + /* --- PyBytesWriter ------------------------------------------------------ */ struct PyBytesWriter { diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index c714fc2e76e83c..c2163a7908f753 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -5,13 +5,14 @@ """ import array +import codecs +import copy +import functools import operator import os +import pickle import re import sys -import copy -import functools -import pickle import tempfile import textwrap import threading @@ -1645,6 +1646,29 @@ def test_take_bytes_optimization(self): bytes_header_size = sys.getsizeof(b'') self.assertEqual(ba.__alloc__(), 499 + bytes_header_size) + def test_take_bytes_hash(self): + # gh-158219: bytearray constructor must not use a bytes object + # if its hash value is already cached. + + def encode(string, errors='strict'): + encoded = string.encode('utf-8') + hash(encoded) # a codec may hash its own output + return encoded, len(string) + + def hashing_codec(name): + if name != 'test_take_bytes_hash': + return None + return codecs.CodecInfo(encode, None, name=name) + + codecs.register(hashing_codec) + self.addCleanup(codecs.unregister, hashing_codec) + + ba = bytearray('hello', 'test_take_bytes_hash') + ba[0] = ord('H') + taken = ba.take_bytes() + self.assertEqual(taken, b'Hello') + self.assertEqual(hash(taken), hash(b'Hello')) + def test_take_bytes_reentrant_resize(self): # gh-153570: n.__index__() can resize the bytearray, so take_bytes() # must re-read the size afterwards. It cached the size before the diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-11-52-49.gh-issue-158219.tNjBVV.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-11-52-49.gh-issue-158219.tNjBVV.rst new file mode 100644 index 00000000000000..691a588e03c2b2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-11-52-49.gh-issue-158219.tNjBVV.rst @@ -0,0 +1,2 @@ +Fix :class:`bytearray` constructor: do not use a bytes object if its hash +value is already computed. Patch by Cody Maloney and Victor Stinner. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index e5024b1c74682d..e3ec862939a241 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -989,7 +989,8 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg, /* Most encodes return a new unique bytes, just use it as buffer. */ if (_PyObject_IsUniquelyReferenced(encoded) - && PyBytes_CheckExact(encoded)) + && PyBytes_CheckExact(encoded) + && _PyBytes_GET_CACHED_HASH((PyBytesObject*)encoded) == -1) { Py_ssize_t size = Py_SIZE(encoded); self->ob_bytes_object = encoded; diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 1f0b03beed43b1..2931a1402908dd 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -62,18 +62,7 @@ _Py_COMP_DIAG_IGNORE_DEPR_DECLS _Py_COMP_DIAG_POP } -static inline Py_hash_t -get_ob_shash(PyBytesObject *a) -{ -_Py_COMP_DIAG_PUSH -_Py_COMP_DIAG_IGNORE_DEPR_DECLS -#ifdef Py_GIL_DISABLED - return _Py_atomic_load_ssize_relaxed(&a->ob_shash); -#else - return a->ob_shash; -#endif -_Py_COMP_DIAG_POP -} +#define get_ob_shash(op) _PyBytes_GET_CACHED_HASH(op) /*