]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-140557: Force alignment of empty `bytearray` and `array.array` buffers (GH-140559)
authorJake Lishman <jake@binhbar.com>
Mon, 26 Jan 2026 15:45:17 +0000 (15:45 +0000)
committerGitHub <noreply@github.com>
Mon, 26 Jan 2026 15:45:17 +0000 (16:45 +0100)
This ensures the buffers used by the empty `bytearray` and `array.array`
are aligned the same as a pointer returned by the allocator.  This is a
more convenient default for interop with other languages that have
stricter requirements of type-safe buffers (e.g. Rust's `&[T]` type)
even when empty.

Lib/test/test_buffer.py
Misc/ACKS
Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst [new file with mode: 0644]
Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst [new file with mode: 0644]
Modules/_testcapi/buffer.c
Modules/arraymodule.c

index 19582e757161fc1ea0be47f5fefeff1a9ad405c2..ab65a44bda6e7eb0cd3d9b9c650fb5e5785b515b 100644 (file)
@@ -4447,6 +4447,41 @@ class TestBufferProtocol(unittest.TestCase):
         with self.assertRaises(SystemError):
             obj.__buffer__(inspect.BufferFlags.WRITE)
 
+    @support.cpython_only
+    @unittest.skipIf(_testcapi is None, "requires _testcapi")
+    def test_bytearray_alignment(self):
+        # gh-140557: pointer alignment of buffers including empty allocation
+        # should be at least to `size_t`.
+        align = struct.calcsize("N")
+        cases = [
+            bytearray(),
+            bytearray(1),
+            bytearray(b"0123456789abcdef"),
+            bytearray(16),
+        ]
+        ptrs = [_testcapi.buffer_pointer_as_int(array) for array in cases]
+        self.assertEqual([ptr % align for ptr in ptrs], [0]*len(ptrs))
+
+    @support.cpython_only
+    @unittest.skipIf(_testcapi is None, "requires _testcapi")
+    def test_array_alignment(self):
+        # gh-140557: pointer alignment of buffers including empty allocation
+        # should match the maximum array alignment.
+        align = max(struct.calcsize(fmt) for fmt in ARRAY)
+        cases = [array.array(fmt) for fmt in ARRAY]
+        # Empty arrays
+        self.assertEqual(
+            [_testcapi.buffer_pointer_as_int(case) % align for case in cases],
+            [0] * len(cases),
+        )
+        for case in cases:
+            case.append(0)
+        # Allocated arrays
+        self.assertEqual(
+            [_testcapi.buffer_pointer_as_int(case) % align for case in cases],
+            [0] * len(cases),
+        )
+
     @support.cpython_only
     def test_pybuffer_size_from_format(self):
         # basic tests
index feb16a62792e7f9ecffd68ddbde8b444185aef1a..3fe9dcfcc9e7d0bea0a7f4abf10cdf748e6d5c56 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1151,6 +1151,7 @@ Per Lindqvist
 Eric Lindvall
 Gregor Lingl
 Everett Lipman
+Jake Lishman
 Mirko Liss
 Alexander Liu
 Hui Liu
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst
new file mode 100644 (file)
index 0000000..d584279
--- /dev/null
@@ -0,0 +1,2 @@
+:class:`bytearray` buffers now have the same alignment
+when empty as when allocated. Unaligned buffers can still be created by slicing.
diff --git a/Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst b/Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst
new file mode 100644 (file)
index 0000000..997ad59
--- /dev/null
@@ -0,0 +1,2 @@
+:class:`array.array` buffers now have the same alignment when empty as when
+allocated. Unaligned buffers can still be created by slicing.
index e63d4179824529352146f8175d6cf9fbab9f5cb6..48393a3dd530c65cb5d0bd4b4c616c602360d12f 100644 (file)
@@ -98,6 +98,27 @@ static PyTypeObject testBufType = {
     .tp_members = testbuf_members
 };
 
+/* Get the pointer from a buffer-supporting object as a PyLong.
+ *
+ * Used to test alignment properties. */
+static PyObject *
+buffer_pointer_as_int(PyObject *Py_UNUSED(module), PyObject *obj)
+{
+    PyObject *out;
+    Py_buffer view;
+    if (PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE) != 0) {
+        return NULL;
+    }
+    out = PyLong_FromVoidPtr(view.buf);
+    PyBuffer_Release(&view);
+    return out;
+}
+
+static PyMethodDef test_methods[] = {
+    {"buffer_pointer_as_int", buffer_pointer_as_int, METH_O},
+    {NULL},
+};
+
 int
 _PyTestCapi_Init_Buffer(PyObject *m) {
     if (PyType_Ready(&testBufType) < 0) {
@@ -106,6 +127,9 @@ _PyTestCapi_Init_Buffer(PyObject *m) {
     if (PyModule_AddObjectRef(m, "testBuf", (PyObject *)&testBufType)) {
         return -1;
     }
+    if (PyModule_AddFunctions(m, test_methods) < 0) {
+        return -1;
+    }
 
     return 0;
 }
index 5769a796b18902b8e8455e6d23485910ed994c56..22ec3c31fb3ee100a22fc70fa88dd2242626413e 100644 (file)
@@ -2664,7 +2664,7 @@ array_ass_subscr(PyObject *op, PyObject *item, PyObject *value)
     }
 }
 
-static const void *emptybuf = "";
+static const _Py_ALIGNED_DEF(ALIGNOF_MAX_ALIGN_T, char) emptybuf[] = "";
 
 
 static int