From ca88569040755554deba07dd6cfc9c01e739c67f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 4 Jul 2026 13:26:29 +0100 Subject: [PATCH 1/2] [mypyc] Make list get item memory safe on free-threaded builds --- mypyc/lib-rt/CPy.h | 38 ++++++++++++- mypyc/lib-rt/list_ops.c | 97 ++++++++++++++++++++-------------- mypyc/lib-rt/mypyc_util.h | 4 ++ mypyc/test-data/run-lists.test | 41 ++++++++++++++ 4 files changed, 139 insertions(+), 41 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index c22c4162669bd..7abfeb9b90cdc 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -664,16 +664,50 @@ PyObject *CPyObject_GetSlice(PyObject *obj, CPyTagged start, CPyTagged end); // List operations -PyObject *CPyList_Build(Py_ssize_t len, ...); +#ifndef Py_GIL_DISABLED + PyObject *CPyList_GetItem(PyObject *list, CPyTagged index); PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index); +PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index); + +#else + +PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index); + +static inline PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) { + if (likely(CPyTagged_CheckShort(index) && !CPyTagged_IsNegative(index))) { + // Inlined fast path + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + return PyList_GetItemRef(list, n); + } else { + return CPyList_GetItem_(list, index); + } +} + +static inline PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + if (n < 0) { + n += PyList_GET_SIZE(list); + } + return PyList_GetItemRef(list, n); +} + +static inline PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) { + if (index < 0) { + index += PyList_GET_SIZE(list); + } + return PyList_GetItemRef(list, index); +} + +#endif + PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index); PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index); -PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index); PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index); bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value); void CPyList_SetItemUnsafe(PyObject *list, Py_ssize_t index, PyObject *value); bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value); +PyObject *CPyList_Build(Py_ssize_t len, ...); PyObject *CPyList_PopLast(PyObject *obj); PyObject *CPyList_Pop(PyObject *obj, CPyTagged index); CPyTagged CPyList_Count(PyObject *obj, PyObject *value); diff --git a/mypyc/lib-rt/list_ops.c b/mypyc/lib-rt/list_ops.c index 8b7eaa1acf3f0..641a7fc912c69 100644 --- a/mypyc/lib-rt/list_ops.c +++ b/mypyc/lib-rt/list_ops.c @@ -49,6 +49,8 @@ PyObject *CPyList_Copy(PyObject *list) { return PyObject_CallMethodNoArgs(list, mypyc_interned_str.copy); } +#ifndef Py_GIL_DISABLED + PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); Py_ssize_t size = PyList_GET_SIZE(list); @@ -69,24 +71,6 @@ PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index) { return result; } -PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index) { - Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); - Py_ssize_t size = PyList_GET_SIZE(list); - if (n >= 0) { - if (n >= size) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - } else { - n += size; - if (n < 0) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - } - return PyList_GET_ITEM(list, n); -} - PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) { if (CPyTagged_CheckShort(index)) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); @@ -112,6 +96,62 @@ PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) { } } +PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) { + size_t size = PyList_GET_SIZE(list); + if (likely((uint64_t)index < size)) { + PyObject *result = PyList_GET_ITEM(list, index); + Py_INCREF(result); + return result; + } + if (index >= 0) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + index += size; + if (index < 0) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + PyObject *result = PyList_GET_ITEM(list, index); + Py_INCREF(result); + return result; +} + +#else /* Py_GIL_DISABLED */ + +PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index) { + if (CPyTagged_CheckShort(index)) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + if (n < 0) { + n += PyList_GET_SIZE(list); + } + return PyList_GetItemRef(list, n); + } else { + PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG); + return NULL; + } +} + +#endif + +PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + Py_ssize_t size = PyList_GET_SIZE(list); + if (n >= 0) { + if (n >= size) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + } else { + n += size; + if (n < 0) { + PyErr_SetString(PyExc_IndexError, "list index out of range"); + return NULL; + } + } + return PyList_GET_ITEM(list, n); +} + PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index) { if (CPyTagged_CheckShort(index)) { Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); @@ -135,27 +175,6 @@ PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index) { } } -PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) { - size_t size = PyList_GET_SIZE(list); - if (likely((uint64_t)index < size)) { - PyObject *result = PyList_GET_ITEM(list, index); - Py_INCREF(result); - return result; - } - if (index >= 0) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - index += size; - if (index < 0) { - PyErr_SetString(PyExc_IndexError, "list index out of range"); - return NULL; - } - PyObject *result = PyList_GET_ITEM(list, index); - Py_INCREF(result); - return result; -} - PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index) { size_t size = PyList_GET_SIZE(list); if (likely((uint64_t)index < size)) { diff --git a/mypyc/lib-rt/mypyc_util.h b/mypyc/lib-rt/mypyc_util.h index 4a4552d059b8c..d7a3eb3214bed 100644 --- a/mypyc/lib-rt/mypyc_util.h +++ b/mypyc/lib-rt/mypyc_util.h @@ -156,6 +156,10 @@ static inline CPyTagged CPyTagged_ShortFromSsize_t(Py_ssize_t x) { return x << 1; } +static inline int CPyTagged_IsNegative(CPyTagged x) { + return ((Py_ssize_t)x) < 0; +} + // Are we targeting Python 3.X or newer? #define CPY_3_11_FEATURES (PY_VERSION_HEX >= 0x030b0000) #define CPY_3_12_FEATURES (PY_VERSION_HEX >= 0x030c0000) diff --git a/mypyc/test-data/run-lists.test b/mypyc/test-data/run-lists.test index 40ca1b6e005f9..fece24ae5d9d3 100644 --- a/mypyc/test-data/run-lists.test +++ b/mypyc/test-data/run-lists.test @@ -171,6 +171,47 @@ def test_list_build() -> None: l3.append('a') assert l3 == ['a'] +class C: + def __init__(self, s: str) -> None: + self.s = s + +a = [C("a"), C("b"), C("c")] + +def test_list_index() -> None: + zero = int() + assert a[zero].s == "a" + assert a[zero + 1].s == "b" + assert a[zero + 2].s == "c" + assert a[zero - 1].s == "c" + assert a[zero - 2].s == "b" + assert a[zero - 3].s == "a" + with assertRaises(IndexError): + a[zero + 3] + with assertRaises(IndexError): + a[zero - 4] + # TODO: Raise IndexError? + with assertRaises(OverflowError): + a[zero + 2**90] + with assertRaises(OverflowError): + a[zero - 2**90] + +def test_list_index_literal() -> None: + assert a[0].s == "a" + assert a[1].s == "b" + assert a[2].s == "c" + assert a[-1].s == "c" + assert a[-2].s == "b" + assert a[-3].s == "a" + with assertRaises(IndexError): + a[3] + with assertRaises(IndexError): + a[-4] + # TODO: Raise IndexError? + with assertRaises(OverflowError): + a[2**90] + with assertRaises(OverflowError): + a[-2**90] + def test_append() -> None: l = [1, 2] l.append(10) From d30ba7c700b646c9825e4eec038a04a65e74faa4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 4 Jul 2026 13:40:36 +0100 Subject: [PATCH 2/2] [mypyc] Make list set item memory safe on free-threaded builds --- mypyc/lib-rt/CPy.h | 22 ++++++- mypyc/lib-rt/list_ops.c | 108 +++++++++++++++++++-------------- mypyc/test-data/run-lists.test | 25 +++++++- 3 files changed, 104 insertions(+), 51 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 7abfeb9b90cdc..458db90efd530 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -669,10 +669,13 @@ PyObject *CPyObject_GetSlice(PyObject *obj, CPyTagged start, CPyTagged end); PyObject *CPyList_GetItem(PyObject *list, CPyTagged index); PyObject *CPyList_GetItemShort(PyObject *list, CPyTagged index); PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index); +bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value); +bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value); #else PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index); +bool CPyList_SetItem_(PyObject *list, CPyTagged index, PyObject *value); static inline PyObject *CPyList_GetItem(PyObject *list, CPyTagged index) { if (likely(CPyTagged_CheckShort(index) && !CPyTagged_IsNegative(index))) { @@ -699,14 +702,29 @@ static inline PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) { return PyList_GetItemRef(list, index); } +static inline bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value) { + if (likely(CPyTagged_CheckShort(index) && !CPyTagged_IsNegative(index))) { + // Inlined fast path + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + return PyList_SetItem(list, n, value) >= 0; + } else { + return CPyList_SetItem_(list, index, value); + } +} + +static inline bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value) { + if (index < 0) { + index += PyList_GET_SIZE(list); + } + return PyList_SetItem(list, index, value) >= 0; +} + #endif PyObject *CPyList_GetItemBorrow(PyObject *list, CPyTagged index); PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index); PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index); -bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value); void CPyList_SetItemUnsafe(PyObject *list, Py_ssize_t index, PyObject *value); -bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value); PyObject *CPyList_Build(Py_ssize_t len, ...); PyObject *CPyList_PopLast(PyObject *obj); PyObject *CPyList_Pop(PyObject *obj, CPyTagged index); diff --git a/mypyc/lib-rt/list_ops.c b/mypyc/lib-rt/list_ops.c index 641a7fc912c69..e8fd061ada687 100644 --- a/mypyc/lib-rt/list_ops.c +++ b/mypyc/lib-rt/list_ops.c @@ -117,6 +117,53 @@ PyObject *CPyList_GetItemInt64(PyObject *list, int64_t index) { return result; } +bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value) { + if (CPyTagged_CheckShort(index)) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + Py_ssize_t size = PyList_GET_SIZE(list); + if (n >= 0) { + if (n >= size) { + PyErr_SetString(PyExc_IndexError, "list assignment index out of range"); + return false; + } + } else { + n += size; + if (n < 0) { + PyErr_SetString(PyExc_IndexError, "list assignment index out of range"); + return false; + } + } + // PyList_SET_ITEM doesn't decref the old element, so we do + Py_DECREF(PyList_GET_ITEM(list, n)); + // N.B: Steals reference + PyList_SET_ITEM(list, n, value); + return true; + } else { + PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG); + return false; + } +} + +bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value) { + size_t size = PyList_GET_SIZE(list); + if (unlikely((uint64_t)index >= size)) { + if (index > 0) { + PyErr_SetString(PyExc_IndexError, "list assignment index out of range"); + return false; + } + index += size; + if (index < 0) { + PyErr_SetString(PyExc_IndexError, "list assignment index out of range"); + return false; + } + } + // PyList_SET_ITEM doesn't decref the old element, so we do + Py_DECREF(PyList_GET_ITEM(list, index)); + // N.B: Steals reference + PyList_SET_ITEM(list, index, value); + return true; +} + #else /* Py_GIL_DISABLED */ PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index) { @@ -132,6 +179,20 @@ PyObject *CPyList_GetItem_(PyObject *list, CPyTagged index) { } } +bool CPyList_SetItem_(PyObject *list, CPyTagged index, PyObject *value) { + if (CPyTagged_CheckShort(index)) { + Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); + Py_ssize_t size = PyList_GET_SIZE(list); + if (n < 0) { + n += size; + } + return PyList_SetItem(list, n, value) >= 0; + } else { + PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG); + return false; + } +} + #endif PyObject *CPyList_GetItemShortBorrow(PyObject *list, CPyTagged index) { @@ -192,53 +253,6 @@ PyObject *CPyList_GetItemInt64Borrow(PyObject *list, int64_t index) { return PyList_GET_ITEM(list, index); } -bool CPyList_SetItem(PyObject *list, CPyTagged index, PyObject *value) { - if (CPyTagged_CheckShort(index)) { - Py_ssize_t n = CPyTagged_ShortAsSsize_t(index); - Py_ssize_t size = PyList_GET_SIZE(list); - if (n >= 0) { - if (n >= size) { - PyErr_SetString(PyExc_IndexError, "list assignment index out of range"); - return false; - } - } else { - n += size; - if (n < 0) { - PyErr_SetString(PyExc_IndexError, "list assignment index out of range"); - return false; - } - } - // PyList_SET_ITEM doesn't decref the old element, so we do - Py_DECREF(PyList_GET_ITEM(list, n)); - // N.B: Steals reference - PyList_SET_ITEM(list, n, value); - return true; - } else { - PyErr_SetString(PyExc_OverflowError, CPYTHON_LARGE_INT_ERRMSG); - return false; - } -} - -bool CPyList_SetItemInt64(PyObject *list, int64_t index, PyObject *value) { - size_t size = PyList_GET_SIZE(list); - if (unlikely((uint64_t)index >= size)) { - if (index > 0) { - PyErr_SetString(PyExc_IndexError, "list assignment index out of range"); - return false; - } - index += size; - if (index < 0) { - PyErr_SetString(PyExc_IndexError, "list assignment index out of range"); - return false; - } - } - // PyList_SET_ITEM doesn't decref the old element, so we do - Py_DECREF(PyList_GET_ITEM(list, index)); - // N.B: Steals reference - PyList_SET_ITEM(list, index, value); - return true; -} - // This function should only be used to fill in brand new lists. void CPyList_SetItemUnsafe(PyObject *list, Py_ssize_t index, PyObject *value) { PyList_SET_ITEM(list, index, value); diff --git a/mypyc/test-data/run-lists.test b/mypyc/test-data/run-lists.test index fece24ae5d9d3..d06fc9ce10b0b 100644 --- a/mypyc/test-data/run-lists.test +++ b/mypyc/test-data/run-lists.test @@ -177,7 +177,7 @@ class C: a = [C("a"), C("b"), C("c")] -def test_list_index() -> None: +def test_get_item() -> None: zero = int() assert a[zero].s == "a" assert a[zero + 1].s == "b" @@ -195,7 +195,7 @@ def test_list_index() -> None: with assertRaises(OverflowError): a[zero - 2**90] -def test_list_index_literal() -> None: +def test_get_item_literal() -> None: assert a[0].s == "a" assert a[1].s == "b" assert a[2].s == "c" @@ -212,6 +212,27 @@ def test_list_index_literal() -> None: with assertRaises(OverflowError): a[-2**90] +def test_set_item() -> None: + l = a.copy() + zero = int() + zero2 = int() + l[zero] = C("x") + assert l[zero2].s == "x" + l[zero + 2] = C("y") + assert l[zero2 + 2].s == "y" + l[zero - 1] = C("t") + assert l[zero2 - 1].s == "t" + l[zero - 3] = C("u") + assert l[zero2 - 3].s == "u" + with assertRaises(IndexError): + a[zero + 3] = C("z") + with assertRaises(IndexError): + a[zero - 4] = C("z") + with assertRaises(OverflowError): + a[zero + 2**90] = C("z") + with assertRaises(OverflowError): + a[zero - 2**90] = C("z") + def test_append() -> None: l = [1, 2] l.append(10)