Skip to content

gh-107940: Only reuse the pointer object when it matches the _type_ of the container #108222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from

Conversation

ambv
Copy link
Contributor

@ambv ambv commented Aug 21, 2023

Closes #107940. Also, solves a related yet undiscovered issue where an array of pointers reuses the array's memory for the pointer objects.

@ambv
Copy link
Contributor Author

ambv commented Aug 21, 2023

Skipping news as this is a fix for a yet unreleased regression.

@ambv ambv added needs backport to 3.11 only security fixes needs backport to 3.12 only security fixes labels Aug 21, 2023
@ambv
Copy link
Contributor Author

ambv commented Aug 21, 2023

cc @code-of-kpp

@code-of-kpp
Copy link
Contributor

Yep, there's a bug for sure, I had an intuition that if one would come up with some interesting way to cast things, we can break things, just couldn't come up with an example back then.

// `_objects` is shared between casts and the original.
int res = PyObject_IsInstance(ptr2ptr, stgdict->proto);
if (res == -1) {
return NULL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it might be useful to call PyErr_SetString here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did what other code in _ctypes.c did for this usage, which wasn't to set the error string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes, PyObject_IsInstance will do it if there's an error

); // double-check that we are returning the same thing
Py_INCREF(ptr2ptr);
return (PyObject *) ptr2ptr;
return ptr2ptr;
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, we will fall down here if types are different, meaning that if someone will try to set contents of the contents, we'll have the same bug as we had originally - garbage in the memory. Let me check that/play with that a bit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cast(void *ptr, PyObject *src, *ctype does this:

result = (CDataObject *)_PyObject_CallNoArgs(ctype);
index = PyLong_FromVoidPtr((void *)src);
rc = PyDict_SetItem(result->b_objects, index, src);
memcpy(result->b_ptr, &ptr, sizeof(void *));
return (PyObject *)result;

e.g., given how id currently works, in the test above:

cast(pp, POINTER(POINTER(c_int16)))._objects[id(pp)] is pp

so it feels like the safe solution would be to call cast() in the
when if (res) { is false, the proper one would be to extact "unsafe"
part from cast() and call it directly

@code-of-kpp
Copy link
Contributor

code-of-kpp commented Aug 21, 2023

I definitely got something weird, that I can't quickly figure out:

    def test_pointer_set_contents(self):
        class Struct(Structure):
            _fields_ = [('a', c_int16)]
        p = pointer(Struct(a=23))
        self.assertIs(p._type_, Struct)
        self.assertEqual(cast(p, POINTER(c_int16)).contents._type_, 'h')

        pp = pointer(p)
        self.assertIs(pp._type_, POINTER(Struct))

        cast(pp, POINTER(POINTER(c_int16))).contents.contents = c_int16(32)

        self.assertIs(p.contents, pp.contents.contents)

        self.assertEqual(cast(p, POINTER(c_int16)).contents.value, 32)
        self.assertEqual(p[0].a, 32)  # works
        self.assertEqual(pp[0].contents.a, 32)  # works
        self.assertEqual(pp.contents[0].a, 32)  # works

        self.assertEqual(pp.contents.contents.a, 32)  # fails, wat, holds 23
        self.assertEqual(p.contents.a, 32)  # fails, wat, holds 23

@ambv
Copy link
Contributor Author

ambv commented Aug 23, 2023

I can't repro your result. For me pp.contents.contents itself leads to the assertion failure in Pointer_get_contents. I'm looking into further.

@code-of-kpp
Copy link
Contributor

Very interesting. If you're OK with this, I can spend some time this week investigating too.

@ambv
Copy link
Contributor Author

ambv commented Aug 23, 2023

Sure. We might need to back out your original fix out of 3.11 until those rough edges are addressed as the next release is going to likely go out either today or tomorrow.

@code-of-kpp
Copy link
Contributor

I have a partial solution:

            ptr2ptr = (CDataObject*) PyDict_GetItemString(keep, "1");
            if (ptr2ptr ==  NULL) {
                PyErr_SetString(PyExc_ValueError,
                "Unexpected NULL pointer in _objects");
                return NULL;
            }

            // if we need to return a pointer, an array or a structure,
            // Pointer_item will do the right thing
            if (
                PyCStructTypeObject_Check(stgdict->proto) || 
                PyCArrayTypeObject_Check(stgdict->proto) ||
                PyCPointerTypeObject_Check(stgdict->proto)
            ) {
                return Pointer_item(self, 0);
            } else if ( PyUnicode_Check(stgdict->proto)
                && (strchr("sPzUZXO", PyUnicode_AsUTF8(stgdict->proto)[0]))) {
                // simple pointer types, c_void_p, c_wchar_p, BSTR, ... */
                return Pointer_item(self, 0);
            }

So, basically, if we are about to return Pointer, Structure or an Array, back off to the same thing as [0] will do.

It messes up a bit the garbage collection: replaced values are gonna be saved longer than needed. Still, I think it's a better solution, than what we have now in 3.11, where a memory leak can happen.
All other tests pass.
What do you think?

ambv added 2 commits August 24, 2023 23:23
…tainer

Closes python#107940. Also, solves a related yet undiscovered issue where an array of
pointers reuses the array's memory for the pointer objects.
@ambv ambv marked this pull request as draft August 24, 2023 21:26
@serhiy-storchaka serhiy-storchaka added needs backport to 3.13 bugs and security fixes and removed needs backport to 3.11 only security fixes labels May 9, 2024
@ambv ambv closed this Oct 7, 2024
@ambv ambv deleted the gh-107940 branch October 7, 2024 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs backport to 3.12 only security fixes needs backport to 3.13 bugs and security fixes skip news
Projects
None yet
Development

Successfully merging this pull request may close these issues.

incompatible behaviour between 3.12.0rc1 and previous versions in ctypes
4 participants