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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 97 additions & 3 deletions Lib/test/test_ctypes/test_cast.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import sys
import unittest
from ctypes import (Structure, Union, POINTER, cast, sizeof, addressof,
c_void_p, c_char_p, c_wchar_p,
c_byte, c_short, c_int)
from ctypes import (Structure, Union, pointer, POINTER, sizeof, addressof,
c_void_p, c_char_p, c_wchar_p, cast,
c_byte, c_short, c_int, c_int16)


class Test(unittest.TestCase):
Expand Down Expand Up @@ -95,6 +95,100 @@ class MyUnion(Union):
_fields_ = [("a", c_int)]
self.assertRaises(TypeError, cast, array, MyUnion)

def test_pointer_identity(self):
class Struct(Structure):
_fields_ = [('a', c_int16)]
Struct3 = 3 * Struct
c_array = (2 * Struct3)(
Struct3(Struct(a=1), Struct(a=2), Struct(a=3)),
Struct3(Struct(a=4), Struct(a=5), Struct(a=6))
)
self.assertEqual(c_array[0][0].a, 1)
self.assertEqual(c_array[0][1].a, 2)
self.assertEqual(c_array[0][2].a, 3)
self.assertEqual(c_array[1][0].a, 4)
self.assertEqual(c_array[1][1].a, 5)
self.assertEqual(c_array[1][2].a, 6)
p_obj = cast(pointer(c_array), POINTER(pointer(c_array)._type_))
obj = p_obj.contents
self.assertEqual(obj[0][0].a, 1)
self.assertEqual(obj[0][1].a, 2)
self.assertEqual(obj[0][2].a, 3)
self.assertEqual(obj[1][0].a, 4)
self.assertEqual(obj[1][1].a, 5)
self.assertEqual(obj[1][2].a, 6)
p_obj = cast(pointer(c_array[0]), POINTER(pointer(c_array)._type_))
obj = p_obj.contents
self.assertEqual(obj[0][0].a, 1)
self.assertEqual(obj[0][1].a, 2)
self.assertEqual(obj[0][2].a, 3)
self.assertEqual(obj[1][0].a, 4)
self.assertEqual(obj[1][1].a, 5)
self.assertEqual(obj[1][2].a, 6)
StructPointer = POINTER(Struct)
s1 = Struct(a=10)
s2 = Struct(a=20)
s3 = Struct(a=30)
pointer_array = (3 * StructPointer)(pointer(s1), pointer(s2), pointer(s3))
self.assertEqual(pointer_array[0][0].a, 10)
self.assertEqual(pointer_array[1][0].a, 20)
self.assertEqual(pointer_array[2][0].a, 30)
self.assertEqual(pointer_array[0].contents.a, 10)
self.assertEqual(pointer_array[1].contents.a, 20)
self.assertEqual(pointer_array[2].contents.a, 30)
p_obj = cast(pointer(pointer_array[0]), POINTER(pointer(pointer_array)._type_))
obj = p_obj.contents
self.assertEqual(obj[0][0].a, 10)
self.assertEqual(obj[1][0].a, 20)
self.assertEqual(obj[2][0].a, 30)
self.assertEqual(obj[0].contents.a, 10)
self.assertEqual(obj[1].contents.a, 20)
self.assertEqual(obj[2].contents.a, 30)
class StructWithPointers(Structure):
_fields_ = [("s1", POINTER(Struct)), ("s2", POINTER(Struct))]
struct = StructWithPointers(s1=pointer(s1), s2=pointer(s2))
p_obj = pointer(struct)
obj = p_obj.contents
self.assertEqual(obj.s1[0].a, 10)
self.assertEqual(obj.s2[0].a, 20)
self.assertEqual(obj.s1.contents.a, 10)
self.assertEqual(obj.s2.contents.a, 20)
p_obj = cast(pointer(struct), POINTER(pointer(pointer_array)._type_))
obj = p_obj.contents
self.assertEqual(obj[0][0].a, 10)
self.assertEqual(obj[1][0].a, 20)
self.assertEqual(obj[0].contents.a, 10)
self.assertEqual(obj[1].contents.a, 20)

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

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

from code import interact; interact(local=locals())

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(p.contents.a, 32) # fails, wat, holds 23
self.assertEqual(pp.contents.contents.a, 32) # fails, wat, holds 23


if __name__ == "__main__":
unittest.main()
60 changes: 32 additions & 28 deletions Modules/_ctypes/_ctypes.c
Original file line number Diff line number Diff line change
Expand Up @@ -5139,6 +5139,8 @@ static PyObject *
Pointer_get_contents(CDataObject *self, void *closure)
{
StgDictObject *stgdict;
PyObject *ptr2ptr;
CDataObject *p2p;

if (*(void **)self->b_ptr == NULL) {
PyErr_SetString(PyExc_ValueError,
Expand All @@ -5148,38 +5150,40 @@ Pointer_get_contents(CDataObject *self, void *closure)

stgdict = PyObject_stgdict((PyObject *)self);
assert(stgdict); /* Cannot be NULL for pointer instances */
assert(stgdict->proto);

PyObject *keep = GetKeepedObjects(self);
if (keep != NULL) {
// check if it's a pointer to a pointer:
// pointers will have '0' key in the _objects
int ptr_probe = PyDict_ContainsString(keep, "0");
if (ptr_probe < 0) {
if (self->b_objects != NULL && PyDict_CheckExact(self->b_objects)) {
// Pointer_set_contents uses KeepRef(self, 1, value); we retrieve that
ptr2ptr = PyDict_GetItemString(self->b_objects, "1");
if (ptr2ptr == NULL) {
PyErr_SetString(PyExc_ValueError,
"Unexpected NULL pointer in _objects");
return NULL;
}
if (ptr_probe) {
PyObject *item;
if (PyDict_GetItemStringRef(keep, "1", &item) < 0) {
return NULL;
}
if (item == NULL) {
PyErr_SetString(PyExc_ValueError,
"Unexpected NULL pointer in _objects");
return NULL;
}
#ifndef NDEBUG
CDataObject *ptr2ptr = (CDataObject *)item;
// Don't construct a new object,
// return existing one instead to preserve refcount.
// Double-check that we are returning the same thing.
// if our base pointer is cast from another type,
// its `_type_` proto will be incompatible with the
// type of the object stored in `b_objects["1"]` because
// `_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

}
if (res) {
// It's not a cast: don't construct a new object,
// return existing one instead to preserve refcount
p2p = (CDataObject*) ptr2ptr;
printf("self->b_ptr=%lu\n", *(void**) self->b_ptr);
printf("p2p->b_ptr=%lu\n", *(void**) p2p->b_ptr);
printf("self->b_value.c=%lu\n", *(void**) self->b_value.c);
printf("p2p->b_value.c=%lu\n", *(void**) p2p->b_value.c);
assert(
*(void**) self->b_ptr == ptr2ptr->b_ptr ||
*(void**) self->b_value.c == ptr2ptr->b_ptr ||
*(void**) self->b_ptr == ptr2ptr->b_value.c ||
*(void**) self->b_value.c == ptr2ptr->b_value.c
);
#endif
return item;
*(void**) self->b_ptr == *(void**) p2p->b_ptr ||
*(void**) self->b_value.c == *(void**) p2p->b_ptr ||
*(void**) self->b_ptr == *(void**) p2p->b_value.c ||
*(void**) self->b_value.c == *(void**) p2p->b_value.c
); // double-check that we are returning the same thing
Py_INCREF(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

Expand Down