https://github.com/python/cpython/commit/969af80daf09fcd2ce8f358e5f9d54e75f8ef330
commit: 969af80daf09fcd2ce8f358e5f9d54e75f8ef330
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-29T11:31:54+02:00
summary:

gh-158219: Fix bytearray constructor to not use hashed object (#158329)

* Add _PyBytes_GET_CACHED_HASH() static inline function.
* _PyBytes_IsMutable() makes sure that the hash value is not cached
  yet.
* bytearray_reinit_from_bytes() checks that the bytes object is
  mutable.

Co-authored-by: Cody Maloney <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-11-52-49.gh-issue-158219.tNjBVV.rst
M Include/internal/pycore_bytesobject.h
M Lib/test/test_bytes.py
M Objects/bytearrayobject.c
M Objects/bytesobject.c

diff --git a/Include/internal/pycore_bytesobject.h 
b/Include/internal/pycore_bytesobject.h
index 8f764f0fa6d6e1..e709940ad98431 100644
--- a/Include/internal/pycore_bytesobject.h
+++ b/Include/internal/pycore_bytesobject.h
@@ -88,6 +88,22 @@ extern void _PyBytes_CheckOverflow(
     const char *type_name);
 #endif
 
+
+// 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 419bee5583de47..ad5091b54d7911 100644
--- a/Lib/test/test_bytes.py
+++ b/Lib/test/test_bytes.py
@@ -5,14 +5,15 @@
 """
 
 import array
+import codecs
 import contextlib
+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
@@ -1706,6 +1707,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 16c38403547818..caf8bad88a280d 100644
--- a/Objects/bytearrayobject.c
+++ b/Objects/bytearrayobject.c
@@ -58,8 +58,13 @@ bytearray_reinit_from_bytes(PyByteArrayObject *self, 
Py_ssize_t size)
     Py_ssize_t alloc = PyBytes_GET_SIZE(self->ob_bytes_object);
     assert(0 <= size && size <= alloc);
 
-    /* Only the empty bytes may be immortal. */
-    assert((alloc == 0) == _Py_IsImmortal(self->ob_bytes_object));
+    if (alloc != 0) {
+        assert(_PyBytes_IsMutable(self->ob_bytes_object));
+    }
+    else {
+        // Use the empty bytes string singleton for an empty bytearray
+        assert(_Py_IsImmortal(self->ob_bytes_object));
+    }
 
     self->ob_bytes = self->ob_start = PyBytes_AS_STRING(self->ob_bytes_object);
     Py_SET_SIZE(self, size);
@@ -1030,7 +1035,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 = PyBytes_GET_SIZE(encoded);
             self->ob_bytes_object = encoded;
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index 91cbfa23e30b20..683306fe724a5b 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -63,18 +63,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)
 
 
 /*
@@ -3346,6 +3335,12 @@ _PyBytes_IsMutable(PyObject *self)
         unsigned char ch = PyBytes_AS_STRING(self)[0];
         assert(self != (PyObject*)CHARACTER(ch));
     }
+
+    // gh-158219: The hash value must not be cached yet. Otherwise, it means
+    // that the bytes object was already used in Python somehow (ex: as a
+    // dictionary key).
+    assert(get_ob_shash((PyBytesObject *)self) == -1);
+
     return 1;
 }
 #endif

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to