-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
gh-96821: Fix undefined behaviour in _ctypes/cfield.c
#96925
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
base: main
Are you sure you want to change the base?
gh-96821: Fix undefined behaviour in _ctypes/cfield.c
#96925
Conversation
@kumaraditya303 @mdickinson Would you please have a look? Thanks! |
cfield.c
_ctypes/cfield.c
I'm not super-keen on this fix - I agree that it would be good to fix the undefined behaviour, but I wonder whether there's a better way. The multiplication bugs me on both conceptual grounds and technical grounds: conceptually, the intent really is just to shift bits around (unlike the similar changes in It feels as though the right solution is to ensure that we always do the bitwise operations (or at the very least, the shifts) on unsigned types rather than signed types, so that everything's well-defined, and then do sign-extension as necessary. So for static PyObject *
h_get(void *ptr, Py_ssize_t size)
{
short val;
memcpy(&val, ptr, sizeof(val));
GET_BITFIELD(val, size);
return PyLong_FromLong((long)val);
} Could we arrange for the static PyObject *
h_get(void *ptr, Py_ssize_t size)
{
unsigned short val, sign_bit;
memcpy(&val, ptr, sizeof(val));
if (NUM_BITS(size)) {
// Assumes that 0 < NUM_BITS(size) <= width of short
assert(0 < NUM_BITS(size));
assert(NUM_BITS(size) <= 16);
sign_bit = (unsigned short)1 << NUM_BITS(size) - 1;
// Assumes that 0 <= LOW_BIT(size) < width of short
assert(0 <= LOW_BIT(size));
// assert(LOW_BIT(size) < 16);
val = val >> LOW_BIT(size) & (sign_bit | sign_bit - 1U);
val = (val ^ sign_bit) - sign_bit;
}
return PyLong_FromLong((long)(short)val);
} N.B. The |
Thanks for having a look. I actually had a similar idea at first: work in unsigned for the left shift, then convert to signed for the right shift. But I had some bugs in my implementation and things were getting complicated. So instead of thinking too hard, I went with the multiplication instead, which was much simpler for me to wrap my head around. Your code looks good. Probably better than what I was trying to do. If it's similar enough to other fix, then our conpilers can probably see through it just as well, and compile it to the same code we used to have. Btw, I am running benchmarks on main vs a version with -fstrict-overflow enabled, and so far it seems like I'm getting about 1% speedup on (geometric) average across the board for all benchmarks that have a 'significant' difference. It's nothing too exciting, but considering that this is a fairly trivial compiler flag change, I guess we shouldn't complain. (I'm rerunning the benchmarks with slightly different settings, I don't really have much experience with pyperformance.) |
Drive-by comment: there's a use of the cpython/Lib/test/test_ctypes/test_bitfields.py Lines 43 to 44 in c8c0afc
|
I'm thinking something along the lines of this: static PyObject *
h_get(void *ptr, Py_ssize_t size)
{
unsigned short val;
memcpy(&val, ptr, sizeof(val));
GET_SIGNED_BITFIELD(unsigned short, val, size);
return PyLong_FromLong((long)(short)val);
} where: #define GET_SIGNED_BITFIELD(unsigned_type, v, size) \
if (NUM_BITS(size)) { \
unsigned_type sign_bit = (unsigned_type)1 << NUM_BITS(size) - 1; \
v = v >> LOW_BIT(size) & (sign_bit | sign_bit - 1U); \
v = (v ^ sign_bit) - sign_bit; \
} |
I'm a bit worried that there's a deeper bug here. The relevant test case arises from testing this structure:
It seems possible that we're trying to cram the bitfield for In any case, if there is a bug there it would be orthogonal to the fixes in this PR. |
Many bugs. #73939, #84039, #86098, #59324 (which apparently I opened, a long time ago), and more ... In the case of that
and none of those make much sense. But discussion should be deferred to the other issues mentioned above. @matthiasgoergens I don't suppose you'd be interested in becoming a ctypes bitfield expert / champion? There's a definite maintenance hole here. (I'm ruling myself out, I'm afraid: I do have a small portion of the necessary expertise, but unfortunately close to zero of the necessary energy and time.) |
@mdickinson I have a bit of time until mid October, as I am waiting out a non-compete before starting my next job. I'll have a look at the bitfield bugs already mentioned here, let's see how much I can learn. |
This comment was marked as outdated.
This comment was marked as outdated.
Throwing an error may be a backwards incompatible change. Having assignments to bit fields wrap modulo the appropriate power of two wouldn't be unreasonable behaviour; after all, that's what most compilers would do for direct assignments to an integer type that's too small (and is defined behaviour in the case of an unsigned type, and implementation-defined but typical behaviour for signed types). But one of the things that makes ctypes development hard is that we don't just need something reasonable in a self-contained sense; we need something that also matches the behaviour of commonly-used C compilers for the relevant system. So it may be worth a test at C level: if you assign |
Here's what happens with Clang 14.0.0. Using this test code: #include <stdio.h>
struct bitfields {
unsigned int some_val: 2;
signed int other_val: 2;
};
int main(void) {
struct bitfields x;
x.some_val = 7;
x.other_val = 7;
printf("%u %d\n", x.some_val, x.other_val);
return 0;
} when I compile and run on my laptop, I get |
@mdickinson Yes, the modulo behaviour is exactly what you get on the C level. (Not sure if that's what the standard says, but it's definitely what GCC and clang give you.) I figured that out after I wrote that comment, that's why I hid it afterwards. However, I found some other problems. See the new issue and failing-test PR that I made. |
Left-shifting negative numbers is undefined behaviour.
Fortunately, multiplication works just as well, is (implementation) defined behaviour, and gets compiled to the same machine code as before by optimizing compilers.
-fstrict-overflow
#96821