Skip to content

gh-143005: prevent incompatible __class__ reassignment for ctypes arrays#143907

Open
VanshAgarwal24036 wants to merge 12 commits into
python:mainfrom
VanshAgarwal24036:gh-143005-ctypes-class-safety
Open

gh-143005: prevent incompatible __class__ reassignment for ctypes arrays#143907
VanshAgarwal24036 wants to merge 12 commits into
python:mainfrom
VanshAgarwal24036:gh-143005-ctypes-class-safety

Conversation

@VanshAgarwal24036
Copy link
Copy Markdown
Contributor

@VanshAgarwal24036 VanshAgarwal24036 commented Jan 16, 2026

This PR fixes a heap buffer overflow in ctypes arrays caused by assigning
class to an incompatible array type.

The change rejects class reassignment when the new array type differs
in length, size, or element type, preventing out-of-bounds memory access.
A regression test is included.

@VanshAgarwal24036 VanshAgarwal24036 force-pushed the gh-143005-ctypes-class-safety branch from 59929a5 to d17521c Compare January 16, 2026 14:02
@VanshAgarwal24036
Copy link
Copy Markdown
Contributor Author

@serhiy-storchaka
I’ve opened a PR fixing the ctypes array class reassignment issue discussed here.
CI is green now; no rush, just sharing in case you have time to take a look.

Copy link
Copy Markdown
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

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

Thank you for your PR, @VanshAgarwal24036. I did not know what to do with this issue. It seems that adding check in tp_setattro is the way to go. But I suspect we need more tests -- each condition should be covered by alternate tests,

Comment thread Modules/_ctypes/_ctypes.c Outdated
Comment thread Modules/_ctypes/_ctypes.c Outdated
Comment thread Modules/_ctypes/_ctypes.c Outdated
Comment thread Lib/test/test_ctypes/test_arrays.py
Comment thread Modules/_ctypes/_ctypes.c
Comment thread Modules/_ctypes/_ctypes.c
Comment thread Modules/_ctypes/_ctypes.c
@picnixz
Copy link
Copy Markdown
Member

picnixz commented Jan 20, 2026

What if we simply expose __class__ as a getset? isn't it possible?

@serhiy-storchaka
Copy link
Copy Markdown
Member

serhiy-storchaka commented Jan 20, 2026

There are problems with such special names. If we expose __class__ as a getset, then c_long.__class__ will be not a type of c_long (the same as type(c_long)), but a descriptor. This will confuse much code.

@picnixz
Copy link
Copy Markdown
Member

picnixz commented Jan 20, 2026

Arf yes you are right. It just feels to me that it is an overkill but I do not think we have a better alternative, do we?

@VanshAgarwal24036
Copy link
Copy Markdown
Contributor Author

Thanks for the discussion

I agree exposing __class__ as getset would be problematic since it would turn
type(obj).__class__ into descriptor and break expectations.

Given that validating reassignment in tp_setattro seems like least disruptive
approach. I will follow up with additional tests to cover the remaining edge cases
(abstract targets, non-array targets, zero-length arrays, etc.) if you suggest.

@VanshAgarwal24036
Copy link
Copy Markdown
Contributor Author

@serhiy-storchaka Do i add more test case as we discuss or how to proceed?

@serhiy-storchaka
Copy link
Copy Markdown
Member

Are there tests for old_info == NULL, new_info == NULL, old_info->length < 0, new_info->length < 0? Add tests for successful __class__ assignments. For example, c_int, c_long and c_longlong should be compatible if they have the same size (and ssize_t should be compatible with one of them). Are c_long and c_ulong compatible? What about arrays of structures? Arrays of pointers of different type?

Since there are changes in from_param(), tests for from_param() are also needed.

@VanshAgarwal24036
Copy link
Copy Markdown
Contributor Author

@serhiy-storchaka Please review it

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

This PR is stale because it has been open for 30 days with no activity.

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label May 5, 2026
Copy link
Copy Markdown
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

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

Please add a test for deletion of the __class__ attribute (it crashes now) and fix it.

The following cases are not covered by tests:

  • old_info == NULL
  • old_info->length < 0
  • new_info->length < 0

Is this possible to test? Are these cases possible in theory?

Also, all tests with different lengths have also different sizes, and all tests with different sizes have different lengths. We need tests for different sizes and same length and for different lengths with same size.

Comment on lines +143 to +144
A = c_long * 3
B = c_ulonglong * 3
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

c_long and c_ulonglong can also have different size. Should not we test c_long with c_longlong.

Comment on lines +135 to +136
A = c_int * 3
B = c_double * 3
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

c_int and c_double most likely have different size. We need to test different types with the same size. I suggest to unite multiple tests in the same method and use a loop with an array of type pairs:

for A, B in [
    (c_int * 3, c_double * 3),
    (c_long * 3, c_double * 3),
    (c_longlong * 3, c_double * 3),
    ...
]:
    with self.subTest(A=A, B=B):
        a = A()
        with self.assertRaises(TypeError):
            a.__class__ = B

You can also use test.support.subTests.

a.__class__ = AbstractArray

def test_ctypes_array_class_assignment_same_size_ints(self):
if sizeof(c_int) != sizeof(c_long):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If sizeof(c_int) != sizeof(c_long), try other pair, for example c_long and c_longlong.

a.__class__ = B

def test_ctypes_array_class_assignment_structs(self):
class S1(Structure):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does it work for different structs with the same definition?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review stale Stale PR or inactive for long period of time.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants