Repository: numpy/numpy
File: numpy/_core/src/multiarray/ctors.c (C)

Code before:
NPY_NO_EXPORT int
PyArray_AssignFromCache_Recursive(
        PyArrayObject *self, const int ndim, coercion_cache_obj **cache)
{
    /* Consume first cache element by extracting information and freeing it */
    PyObject *obj = (*cache)->arr_or_sequence;
    Py_INCREF(obj);
    PyObject *orig_obj = (*cache)->converted_obj;
    npy_bool sequence = (*cache)->sequence;
    int depth = (*cache)->depth;
    int ret = 0;
    *cache = npy_unlink_coercion_cache(*cache);

    /* The element is either a sequence, or an array */
    if (!sequence) {
        /* Straight forward array assignment */
        assert(PyArray_Check(obj));
        if (PyArray_CopyInto(self, (PyArrayObject *)obj) < 0) {
            goto fail;
        }
    }
    else {
        assert(depth != ndim);
        // this macro thakes *the argument* of PySequence_Fast, which is orig_obj;
        // not the object returned by PySequence_Fast, which is obj
        NPY_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(orig_obj);
        npy_intp length = PySequence_Length(obj);
        if (length != PyArray_DIMS(self)[0]) {
            PyErr_SetString(PyExc_RuntimeError,
                            "Inconsistent object during array creation? "
                            "Content of sequences changed (length inconsistent).");
            ret = -1;
            goto finish_section;
        }
        for (npy_intp i = 0; i < length; i++) {
            PyObject *value = PySequence_Fast_GET_ITEM(obj, i);

            if (ndim == depth + 1) {
                /*
                 * Straight forward assignment of elements.  Note that it is
                 * possible for such an element to be a 0-D array or array-like.
                 * `PyArray_Pack` supports arrays as well as we want: We
                 * support exact NumPy arrays, but at this point ignore others.
                 * (Please see the `PyArray_Pack` function comment if this
                 * rightly confuses you.)
                 */
                char *item;
                item = (PyArray_BYTES(self) + i * PyArray_STRIDES(self)[0]);
                if (PyArray_Pack(PyArray_DESCR(self), item, value) < 0) {
                    ret = -1;
                    goto finish_section;

Review comment (bug):
This can still crash and produce data races if the sequence gets mutated but the size remains same. Inside the loop, the items of sequences are accessed using borrowed references (`PySequence_Fast_GET_ITEM`) so if the critical section gets suspended and other thread sets a new object at current index then it can crash with use-after-free. 

You should be able to reproduce this case with a test which tries to create an `ndarray` from a list whose size is fixed but it getting concurrently modified by other thread with `list.__setitem__`. 

Code after:
NPY_NO_EXPORT int
PyArray_AssignFromCache_Recursive(
        PyArrayObject *self, const int ndim, coercion_cache_obj **cache)
{
    int ret = -1;
    /* Consume first cache element by extracting information and freeing it */
    PyObject *obj = (*cache)->arr_or_sequence;
    Py_INCREF(obj);
    npy_bool is_sequence = (*cache)->sequence;
    /*
      If it is a sequence, this object is the argument to PySequence_Fast, e.g.
      the iterable that the user wants to coerce into an array
    */
    PyObject *orig_seq = (*cache)->converted_obj;
    /* Owned reference to an item in the sequence */
    PyObject *item_pyvalue = NULL;
    int depth = (*cache)->depth;
    *cache = npy_unlink_coercion_cache(*cache);

    /* The element is either a sequence or an array */
    if (!is_sequence) {
        /* Straight forward array assignment */
        assert(PyArray_Check(obj));
        if (PyArray_CopyInto(self, (PyArrayObject *)obj) < 0) {
            goto finish;
        }
    }
    else {
        assert(depth != ndim);
        npy_intp orig_length = PyArray_DIMS(self)[0];
        int err = 1;
        NPY_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(orig_seq);
        for (npy_intp i = 0; i < orig_length; i++) {
            // this macro takes *the argument* of PySequence_Fast, which is orig_seq;
            // not the object returned by PySequence_Fast, which is a proxy object
            // with its own per-object PyMutex lock.
            // We want to lock the list object exposed to users, not the proxy.
            npy_intp length = PySequence_Fast_GET_SIZE(obj);
            if (length != orig_length) {
                PyErr_SetString(PyExc_RuntimeError,
                                "Inconsistent object during array creation? "
                                "Content of sequences changed (length inconsistent).");
                goto finish_critical_section;
            }
            else {
                Py_XSETREF(item_pyvalue, Py_NewRef(PySequence_Fast_GET_ITEM(obj, i)));
            }

            if (ndim == depth + 1) {
                /*
                 * Straight forward assignment of elements.  Note that it is