From 19de10d3d8605a290492e4fb3871e12638b0f7bb Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 26 Jan 2026 15:45:17 +0000 Subject: [PATCH] gh-140557: Force alignment of empty `bytearray` and `array.array` buffers (GH-140559) 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 | 35 +++++++++++++++++++ Misc/ACKS | 1 + ...-10-24-17-30-51.gh-issue-140557.X2GETk.rst | 2 ++ ...-01-07-11-57-59.gh-issue-140557.3P6-nW.rst | 2 ++ Modules/_testcapi/buffer.c | 24 +++++++++++++ Modules/arraymodule.c | 2 +- 6 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst create mode 100644 Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst diff --git a/Lib/test/test_buffer.py b/Lib/test/test_buffer.py index 19582e757161..ab65a44bda6e 100644 --- a/Lib/test/test_buffer.py +++ b/Lib/test/test_buffer.py @@ -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 diff --git a/Misc/ACKS b/Misc/ACKS index feb16a62792e..3fe9dcfcc9e7 100644 --- 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 index 000000000000..d584279a0901 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst @@ -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 index 000000000000..997ad592bbaa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst @@ -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. diff --git a/Modules/_testcapi/buffer.c b/Modules/_testcapi/buffer.c index e63d41798245..48393a3dd530 100644 --- a/Modules/_testcapi/buffer.c +++ b/Modules/_testcapi/buffer.c @@ -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; } diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 5769a796b189..22ec3c31fb3e 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -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 -- 2.47.3