Skip to content

Commit 3e346a8

Browse files
committed
pythongh-46376: add (failing) tests for ctypes/pointer/cast/set_contents
Previous attempt to fix pythongh-46376 was incomplete and overall it didn't succeed, and was reverted in 3.11. However, we have discovered some dangerous issues with ctypes, that aren't fixed or documented anywhere. This commit adds tests (@expectedfailure) so at least developers are aware of situations where memory might be corrupted, leaked or when changing a pointer value might have no effect. Hopefully, we should be able to remove @expectedfailure in the future, with new shiny ctypes implementation or at least a bugfix.
1 parent fecb9fa commit 3e346a8

File tree

6 files changed

+349
-46
lines changed

6 files changed

+349
-46
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ Tools/unicode/data/
124124
/config.status.lineno
125125
# hendrikmuhs/ccache-action@v1
126126
/.ccache
127+
# clangd and it's debugger
128+
/.cache
127129
/platform
128130
/profile-clean-stamp
129131
/profile-run-stamp

Lib/test/test_ctypes/test_arrays.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import gc
12
import ctypes
23
import sys
34
import unittest
45
import warnings
5-
from ctypes import (Structure, Array, sizeof, addressof,
6+
import weakref
7+
from ctypes import (Structure, Array, sizeof, addressof, POINTER, pointer,
68
create_string_buffer, create_unicode_buffer,
79
c_char, c_wchar, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint,
810
c_long, c_ulonglong, c_float, c_double, c_longdouble)
@@ -249,6 +251,32 @@ def test_deprecation(self):
249251
with self.assertWarns(DeprecationWarning):
250252
CharArray = ctypes.ARRAY(c_char, 3)
251253

254+
def test_ptr_reuse(self):
255+
w = weakref.WeakValueDictionary()
256+
arr = 3 * POINTER(c_short)
257+
258+
vals = arr(
259+
pointer(w.setdefault(10, c_short(10))),
260+
pointer(w.setdefault(11, c_short(11))),
261+
None,
262+
)
263+
gc.collect()
264+
265+
self.assertEqual(vals[0].contents.value, 10)
266+
self.assertEqual(vals[1].contents.value, 11)
267+
268+
vals[2] = vals[0]
269+
270+
self.assertEqual(vals[2].contents.value, 10)
271+
272+
vals[2][0] = w.setdefault(12, c_short(12))
273+
vals[2].contents = w.setdefault(13, c_short(13))
274+
275+
gc.collect()
276+
277+
self.assertEqual(vals[2].contents.value, 13)
278+
self.assertEqual(vals[0].contents.value, 12)
279+
252280

253281
if __name__ == '__main__':
254282
unittest.main()

Lib/test/test_ctypes/test_cast.py

Lines changed: 303 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import gc
12
import sys
23
import unittest
3-
from ctypes import (Structure, Union, POINTER, cast, sizeof, addressof,
4-
c_void_p, c_char_p, c_wchar_p,
5-
c_byte, c_short, c_int)
4+
from ctypes import (Structure, Union, pointer, POINTER, sizeof, addressof,
5+
c_void_p, c_char_p, c_wchar_p, cast,
6+
c_byte, c_short, c_int, c_int16,
7+
_pointer_type_cache)
8+
import weakref
69

710

811
class Test(unittest.TestCase):
@@ -95,6 +98,303 @@ class MyUnion(Union):
9598
_fields_ = [("a", c_int)]
9699
self.assertRaises(TypeError, cast, array, MyUnion)
97100

101+
@unittest.expectedFailure # gh-46376, gh-107131, gh-107940, gh-108222
102+
def test_cast_detached_structure(self):
103+
class Struct(Structure):
104+
_fields_ = [("a", POINTER(c_int))]
105+
106+
x = Struct(a=pointer(c_int(23)))
107+
self.assertEqual(x.a.contents.value, 23)
108+
109+
p = cast(x.a, POINTER(c_int)) # intentinally, cast to same
110+
p.contents = c_int(32)
111+
112+
self.assertEqual(x.a.contents.value, 32)
113+
114+
@unittest.expectedFailure # gh-46376, gh-107131, gh-107940, gh-108222
115+
def test_casted_structure(self):
116+
class Struct(Structure):
117+
_fields_ = [("a", POINTER(c_int))]
118+
119+
x = Struct(a=pointer(c_int(23)))
120+
self.assertEqual(x.a.contents.value, 23)
121+
122+
p = cast(pointer(x), POINTER(POINTER(c_int))) # intentinally, cast to same
123+
p.contents = pointer(c_int(32))
124+
125+
self.assertEqual(x.a.contents.value, 32)
126+
127+
# to avoid leaking when tests are run several times
128+
# clean up the types left in the cache.
129+
del _pointer_type_cache[Struct]
130+
131+
@unittest.expectedFailure # gh-46376, gh-107131, gh-107940, gh-108222
132+
def test_pointer_array(self):
133+
w = weakref.WeakValueDictionary()
134+
arr = 3 * POINTER(c_short)
135+
136+
vals = arr(
137+
w.setdefault("10", pointer(c_short(10))),
138+
w.setdefault("11", pointer(c_short(11))),
139+
None,
140+
)
141+
142+
self.assertEqual(vals[0].contents.value, 10)
143+
self.assertEqual(vals[1].contents.value, 11)
144+
145+
class Struct(Structure):
146+
_fields_ = [('a', c_int16)]
147+
148+
v = cast(pointer(vals), POINTER(Struct * 3)).contents
149+
v[2] = v[0]
150+
151+
self.assertTrue(vals[2])
152+
self.assertEqual(vals[2].contents.value, 10)
153+
154+
vals[2].contents = w.setdefault("12", pointer(c_short(12)))
155+
156+
self.assertEqual(vals[2].contents.value, 12)
157+
self.assertEqual(vals[0].contents.value, 12)
158+
159+
gc.collect()
160+
self.assertNotIn("10", w)
161+
162+
def test_pointer_identity1(self):
163+
class Struct(Structure):
164+
_fields_ = [('a', c_int16)]
165+
166+
Struct3 = 3 * Struct
167+
c_array = (2 * Struct3)(
168+
Struct3(Struct(a=1), Struct(a=2), Struct(a=3)),
169+
Struct3(Struct(a=4), Struct(a=5), Struct(a=6))
170+
)
171+
self.assertEqual(c_array[0][0].a, 1)
172+
self.assertEqual(c_array[0][1].a, 2)
173+
self.assertEqual(c_array[0][2].a, 3)
174+
self.assertEqual(c_array[1][0].a, 4)
175+
self.assertEqual(c_array[1][1].a, 5)
176+
self.assertEqual(c_array[1][2].a, 6)
177+
178+
p_obj = cast(pointer(c_array), POINTER(pointer(c_array)._type_))
179+
obj = p_obj.contents
180+
self.assertEqual(obj[0][0].a, 1)
181+
self.assertEqual(obj[0][1].a, 2)
182+
self.assertEqual(obj[0][2].a, 3)
183+
self.assertEqual(obj[1][0].a, 4)
184+
self.assertEqual(obj[1][1].a, 5)
185+
self.assertEqual(obj[1][2].a, 6)
186+
187+
p_obj = cast(pointer(c_array[0]), POINTER(pointer(c_array)._type_))
188+
obj = p_obj.contents
189+
190+
self.assertEqual(obj[0][0].a, 1)
191+
self.assertEqual(obj[0][1].a, 2)
192+
self.assertEqual(obj[0][2].a, 3)
193+
self.assertEqual(obj[1][0].a, 4)
194+
self.assertEqual(obj[1][1].a, 5)
195+
self.assertEqual(obj[1][2].a, 6)
196+
197+
@unittest.expectedFailure # gh-46376, gh-107131, gh-107940, gh-108222
198+
def test_cast_structure_array(self):
199+
class Struct(Structure):
200+
_fields_ = [('a', c_int16)]
201+
202+
Struct3 = 3 * Struct
203+
c_array = (2 * Struct3)(
204+
Struct3(Struct(a=1), Struct(a=2), Struct(a=3)),
205+
Struct3(Struct(a=4), Struct(a=5), Struct(a=6))
206+
)
207+
obj = cast(pointer(c_array), POINTER(pointer(c_array)._type_)).contents
208+
209+
c_array[0][0] = Struct(a=100)
210+
self.assertEqual(c_array[0][0].a, 100)
211+
self.assertEqual(obj[0][0].a, 100)
212+
213+
cast(pointer(c_array), POINTER(c_short)).contents = c_short(200)
214+
self.assertEqual(c_array[0][0].a, 200)
215+
self.assertEqual(obj[0][0].a, 200)
216+
217+
def test_pointer_identity2(self):
218+
class Struct(Structure):
219+
_fields_ = [('a', c_int16)]
220+
221+
StructPointer = POINTER(Struct)
222+
s1 = Struct(a=10)
223+
s2 = Struct(a=20)
224+
s3 = Struct(a=30)
225+
pointer_array = (3 * StructPointer)(pointer(s1), pointer(s2), pointer(s3))
226+
self.assertEqual(pointer_array[0][0].a, 10)
227+
self.assertEqual(pointer_array[1][0].a, 20)
228+
self.assertEqual(pointer_array[2][0].a, 30)
229+
self.assertEqual(pointer_array[0].contents.a, 10)
230+
self.assertEqual(pointer_array[1].contents.a, 20)
231+
self.assertEqual(pointer_array[2].contents.a, 30)
232+
233+
p_obj = cast(pointer(pointer_array[0]), POINTER(pointer(pointer_array)._type_))
234+
obj = p_obj.contents
235+
self.assertEqual(obj[0][0].a, 10)
236+
self.assertEqual(obj[1][0].a, 20)
237+
self.assertEqual(obj[2][0].a, 30)
238+
self.assertEqual(obj[0].contents.a, 10)
239+
self.assertEqual(obj[1].contents.a, 20)
240+
self.assertEqual(obj[2].contents.a, 30)
241+
242+
@unittest.expectedFailure # gh-46376, gh-107131, gh-107940, gh-108222
243+
def test_cast_structure_pointer_array(self):
244+
class Struct(Structure):
245+
_fields_ = [('a', c_int16)]
246+
247+
StructPointer = POINTER(Struct)
248+
s1 = Struct(a=10)
249+
s2 = Struct(a=20)
250+
s3 = Struct(a=30)
251+
pointer_array = (3 * StructPointer)(pointer(s1), pointer(s2), pointer(s3))
252+
obj = cast(pointer(pointer_array[0]),
253+
POINTER(pointer(pointer_array)._type_)).contents
254+
255+
pointer_array[0][0].a = 50
256+
self.assertEqual(obj[0][0].a, 50)
257+
258+
c = cast(pointer(pointer_array[0]), POINTER(POINTER(c_short)))
259+
c.contents = pointer(c_short(100))
260+
self.assertEqual(pointer_array[0].contents.a, 100)
261+
self.assertEqual(obj[0][0].a, 100)
262+
263+
@unittest.expectedFailure # gh-46376, gh-107131, gh-107940, gh-108222
264+
def test_pointer_identity3(self):
265+
class Struct(Structure):
266+
_fields_ = [('a', c_int16)]
267+
268+
class StructWithPointers(Structure):
269+
_fields_ = [("s1", POINTER(Struct)), ("s2", POINTER(Struct))]
270+
271+
s1 = Struct(a=10)
272+
s2 = Struct(a=20)
273+
s3 = Struct(a=30)
274+
275+
StructPointer = POINTER(Struct)
276+
277+
pointer_array = (3 * StructPointer)(pointer(s1), pointer(s2), pointer(s3))
278+
279+
struct = StructWithPointers(s1=pointer(s1), s2=pointer(s2))
280+
p_obj = pointer(struct)
281+
obj = p_obj.contents
282+
self.assertEqual(addressof(obj), addressof(struct)) # assertIs?
283+
284+
self.assertEqual(obj.s1[0].a, 10)
285+
self.assertEqual(obj.s2[0].a, 20)
286+
self.assertEqual(obj.s1.contents.a, 10)
287+
self.assertEqual(obj.s2.contents.a, 20)
288+
289+
p_obj = cast(pointer(struct), POINTER(pointer(pointer_array)._type_))
290+
obj = p_obj.contents
291+
self.assertEqual(addressof(obj), addressof(struct))
292+
self.assertEqual(addressof(obj[0]), addressof(struct))
293+
self.assertEqual(addressof(obj[0]), addressof(struct.s1))
294+
self.assertEqual(addressof(obj[1]), addressof(struct.s2))
295+
self.assertEqual(addressof(obj[0].contents), addressof(struct.s1.contents))
296+
self.assertEqual(addressof(obj[0][0]), addressof(pointer_array[0].contents))
297+
self.assertEqual(addressof(obj[1][0]), addressof(pointer_array[1].contents))
298+
299+
self.assertEqual(obj[0][0].a, 10)
300+
self.assertEqual(obj[1][0].a, 20)
301+
self.assertEqual(obj[0].contents.a, 10)
302+
self.assertEqual(obj[1].contents.a, 20)
303+
304+
obj[0][0].a = 23
305+
self.assertEqual(pointer_array[0].contents.a, 23)
306+
307+
obj[0].contents.a = 32
308+
self.assertEqual(pointer_array[0].contents.a, 32)
309+
310+
obj[1].contents = Struct(a=42)
311+
self.assertEqual(pointer_array[1].contents.a, 42)
312+
313+
@unittest.expectedFailure # gh-46376, gh-107131, gh-107940, gh-108222
314+
def test_pointer_set_contents(self):
315+
class Struct(Structure):
316+
_fields_ = [('a', c_int16)]
317+
318+
w = weakref.WeakValueDictionary()
319+
p = pointer(w.setdefault(23, Struct(a=23)))
320+
self.assertIs(p._type_, Struct)
321+
self.assertEqual(cast(p, POINTER(c_int16)).contents._type_, 'h')
322+
323+
pp = pointer(p)
324+
self.assertIs(pp._type_, POINTER(Struct))
325+
326+
cast(pp, POINTER(POINTER(c_int16))).contents.contents = w.setdefault(
327+
"k32",
328+
c_int16(32)
329+
)
330+
331+
self.assertEqual(addressof(p.contents), addressof(pp.contents.contents))
332+
self.assertEqual(pp[0].contents.a, 32)
333+
self.assertEqual(pp.contents[0].a, 32)
334+
self.assertEqual(pp.contents.contents.a, 32)
335+
del pp
336+
gc.collect()
337+
338+
# if c_int16(32) was destroyed both of these two asserts should fail
339+
self.assertIn("k32", w) # but fail here first, so we don't use-after-fee
340+
self.assertEqual(p[0].a, 32)
341+
342+
self.assertEqual(p.contents.a, 32)
343+
self.assertEqual(cast(p, POINTER(c_int16)).contents.value, 32)
344+
345+
@unittest.expectedFailure # gh-46376, gh-107131, gh-107940, gh-108222
346+
def test_p2p_set_contents(self):
347+
class StructA(Structure):
348+
_fields_ = [('a', POINTER(c_int))]
349+
class StructB(Structure):
350+
_fields_ = [('b', POINTER(c_int))]
351+
352+
p = pointer(StructA(a=pointer(c_int(23))))
353+
self.assertEqual(p.contents.a.contents.value, 23)
354+
355+
p.contents.a.contents.value = 32
356+
self.assertEqual(p.contents.a.contents.value, 32)
357+
358+
w = weakref.WeakValueDictionary()
359+
p[0].a = pointer(w.setdefault("1", c_int(100)))
360+
361+
self.assertEqual(p.contents.a.contents.value, 100)
362+
self.assertEqual(p[0].a.contents.value, 100)
363+
364+
pp = cast(p, POINTER(POINTER(c_int)))
365+
pp.contents.contents = w.setdefault("2", c_int(200))
366+
gc.collect()
367+
368+
self.assertEqual(p.contents.a.contents.value, 200)
369+
self.assertEqual(cast(pp, POINTER(StructA)).contents.a.contents.value, 200)
370+
self.assertEqual(cast(pp, POINTER(StructA))[0].a.contents.value, 200)
371+
372+
del pp
373+
gc.collect()
374+
375+
self.assertEqual(p.contents.a.contents.value, 200)
376+
self.assertEqual(p[0].a.contents.value, 200)
377+
378+
x = StructA(a=pointer(c_int(23)))
379+
r = cast(pointer(x), POINTER(POINTER(c_int)))
380+
r.contents.contents = w.setdefault("3", c_int(300))
381+
del r
382+
gc.collect()
383+
# if c_int(300) was destroyed both of these two asserts should fail
384+
self.assertIn("3", w) # but fail here first, so we don't use-after-fee
385+
self.assertEqual(x.a[0], 300)
386+
387+
cast(pointer(x), POINTER(StructB)).contents.b = pointer(
388+
w.setdefault("4", c_int(400))
389+
)
390+
gc.collect()
391+
self.assertEqual(x.a[0], 400)
392+
393+
# to avoid leaking when tests are run several times
394+
# clean up the types left in the cache.
395+
del _pointer_type_cache[StructA]
396+
del _pointer_type_cache[StructB]
397+
98398

99399
if __name__ == "__main__":
100400
unittest.main()

0 commit comments

Comments
 (0)