-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
gh-130115: fix thread identifiers for 32-bit musl #130391
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
Conversation
CPython's pthread-based thread identifier relies on pthread_t being able to be represented as an unsigned integer type. This is true in most Linux libc implementations where it's defined as an unsigned long, however musl typedefs it as a struct *. If the pointer has the high bit set and is cast to PyThread_ident_t, the resultant value can be sign-extended [0]. This can cause issues when comparing against threading._MainThread's identifier. The main thread's identifier value is retrieved via _get_main_thread_ident which is backed by an unsigned long which truncates sign extended bits. >>> hex(threading.main_thread().ident) '0xb6f33f3c' >>> hex(threading.current_thread().ident) '0xffffffffb6f33f3c' Work around this by conditionally compiling in some code for non-glibc based Linux platforms that are at risk of sign-extension to return a PyLong based on the main thread's unsigned long thread identifier if the current thread is the main thread. [0]: https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Arrays-and-pointers-implementation.html Signed-off-by: Vincent Fazio <[email protected]>
Signed-off-by: Vincent Fazio <[email protected]>
Drafting this. After doing some initial testing, changing only
I plan on testing on actual hardware (RPi on 32bit Alpine) at some point, but I've emulated this environment in Docker using arm32v7/alpine & the qemu user space emulator in the mean time. I'll rework the patch and then validate on hardware as well |
Move this to a static function so all callers have consistent behavior. Signed-off-by: Vincent Fazio <[email protected]>
Signed-off-by: Vincent Fazio <[email protected]>
I built python
I then edited the ident function to always set the upper bit to force the broken behavior I was seeing in #130115 and re-ran the tests
|
I still want to test on physical hardware and not on an emulated stack, but I have more confidence in this solution. |
forgot to revert the initial commit. Test still pass on a
|
tested on hardware:
|
repeated with high bit set:
|
@mpage It's been a while since we discussed #130115 so I'm interested in your thoughts here. This PR addresses the primary issue, can be backported, and, as far as I can tell, doesn't introduce any regressions. I took a peek at some other changes last night and I'm curious if you think it would be worthwhile to reuse a similar data structure as the min heap for thread identifiers long term. It would recycle them faster, but so long as callers aren't assuming a specific lifetime maybe that's OK. I guess other assumptions are that thread IDs are tied to a specific interpreter instance and can't be passed around between them since they're no longer system level identifiers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks mostly ok to me, but the use of _pthread_t_to_ident
in PyThread_get_thread_ident_ex()
makes me a little nervous when SIZEOF_PTHREAD_T > SIZEOF_LONG
(see inline comment). I would probably take the approach of adding a second field to PyRuntime
(even though it's gross) or potentially reverting to the previous behavior of using set_ident
for the main thread. I'd like to see what others think. I've added a couple of folks who have worked in this area in the past as reviewers.
@@ -357,8 +371,7 @@ PyThread_get_thread_ident_ex(void) { | |||
if (!initialized) | |||
PyThread_init_thread(); | |||
threadid = pthread_self(); | |||
assert(threadid == (pthread_t) (PyThread_ident_t) threadid); | |||
return (PyThread_ident_t) threadid; | |||
return _pthread_t_to_ident(threadid); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is potentially lossy compared to the previous version if SIZEOF_PTHREAD_T > SIZEOF_LONG
. Previously we would cast directly to a PyThread_ident_t
(an unsigned long long
), whereas we now cast through an unsigned long
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand your concern.
This was an edge case in PyThread_start_new_thread
, my guess is that it was there for the situations where pthread_t
was not a ulong and was, instead, a pointer or struct.
#if SIZEOF_PTHREAD_T <= SIZEOF_LONG
return (unsigned long) th;
#else
return (unsigned long) *(unsigned long *) &th;
It's probably fine to drop the condition in _pthread_t_to_ident
and let the caller here truncate it since this API is specifically returning a ULONG and not a PyThread_ident_t
.
Looking at the history:
The cast through ulong* was for Alpha OSF which was dropped from PEP 11 a few years ago (CPython 3.3) https://bugs.python.org/issue8606. My guess is it was also defined as a pointer or struct type and this was a way to work around it by returning at least long
bytes.
I think if we find other platforms where we need to support this workaround, they can be chained to the MUSL #ifdef
I think. The only time it would maybe be a problem is if sizeof(uintptr_t)
< sizeof(ulong)
.
If others agree, I can drop the condition so the function looks like so:
static PyThread_ident_t
_pthread_t_to_ident(pthread_t value) {
PyThread_ident_t ident;
#if defined(__linux__) && !defined(__GLIBC__)
ident = (PyThread_ident_t) (uintptr_t) value;
assert(pthread_equal(value, (pthread_t) (uintptr_t) ident));
#else
ident = (PyThread_ident_t) value;
assert(pthread_equal(value, (pthread_t) ident));
#endif
return ident;
}
I do not suggest this as a long term solution; I do think we need to work towards making this opaque. I'm just trying to find something that is a stop-gap that can be ported back with relative ease that doesn't cause a regression.
Yes, something like that could make sense.
I think that is already true now (you can't make assumptions about the lifetime of pthread identifiers).
Yeah, depending on how we chose to manage thread IDs that could be true. We could choose to manage the the pool of thread IDs per runtime, which would preserve the existing namespacing. |
Python/thread_pthread.h
Outdated
static PyThread_ident_t | ||
_pthread_t_to_ident(pthread_t value) { | ||
#if SIZEOF_PTHREAD_T > SIZEOF_LONG | ||
return (PyThread_ident_t) *(unsigned long *) &value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why go through a pointer? This is cryptic and also uncomfortably fragile (what about big-endian platforms?).
I would suggest perhaps something like:
static PyThread_ident_t
_pthread_t_to_ident(pthread_t value) {
// Avoid sign-extension when converting to a larger int type
#if SIZEOF_PTHREAD_T == SIZEOF_VOID_P
return (uintptr_t) value;
#elif SIZEOF_PTHREAD_T == SIZEOF_LONG
return (unsigned long) value;
#elif SIZEOF_PTHREAD_T == SIZEOF_INT
return (unsigned int) value;
#elif SIZEOF_PTHREAD_T == SIZEOF_LONG_LONG
return (unsigned long long) value;
#else
#error "Unsupported SIZEOF_PTHREAD_T value"
#endif
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was legacy code that I can certainly drop (as mentioned above) . This is definitely simpler, I'll give it a spin and push it if tests pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the branch to this implementation. The unit tests continue to pass AFAICT.
Testing on musl + armv7 on RPi passes with the implementation suggested with and without the high bit set
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, LGTM, but we should run some extended CI checks on this
🤖 New build scheduled with the buildbot fleet by @pitrou for commit 4ae6d7e 🤖 Results will be shown at: https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F130391%2Fmerge If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again. |
@gpshead Do you think this is a candidate for backporting? |
I'm secretly hoping so, I plan on porting the patch for Buildroot, but backporting to 3.13 would allow BR to drop it once we hit 3.13.3 hopefully? |
Sorry, @vfazio and @pitrou, I could not cleanly backport this to
|
) CPython's pthread-based thread identifier relies on pthread_t being able to be represented as an unsigned integer type. This is true in most Linux libc implementations where it's defined as an unsigned long, however musl typedefs it as a struct *. If the pointer has the high bit set and is cast to PyThread_ident_t, the resultant value can be sign-extended [0]. This can cause issues when comparing against threading._MainThread's identifier. The main thread's identifier value is retrieved via _get_main_thread_ident which is backed by an unsigned long which truncates sign extended bits. >>> hex(threading.main_thread().ident) '0xb6f33f3c' >>> hex(threading.current_thread().ident) '0xffffffffb6f33f3c' Work around this by conditionally compiling in some code for non-glibc based Linux platforms that are at risk of sign-extension to return a PyLong based on the main thread's unsigned long thread identifier if the current thread is the main thread. [0]: https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Arrays-and-pointers-implementation.html --------- (cherry picked from commit 7212306) Co-authored-by: Vincent Fazio <[email protected]> Signed-off-by: Vincent Fazio <[email protected]>
GH-132089 is a backport of this pull request to the 3.13 branch. |
The threading code base has probably diverged too much from 3.12 to make an automatic backport feasible. You may want to backport an equivalent fix manually @vfazio , or we can limit ourselves to 3.13 if you prefer. |
Thanks for porting this back! I think back to 3.13 is fine since that's when we first noticed the problem. It was probably an artifact of the change from UL -> ULL ( |
…H-132089) CPython's pthread-based thread identifier relies on pthread_t being able to be represented as an unsigned integer type. This is true in most Linux libc implementations where it's defined as an unsigned long, however musl typedefs it as a struct *. If the pointer has the high bit set and is cast to PyThread_ident_t, the resultant value can be sign-extended [0]. This can cause issues when comparing against threading._MainThread's identifier. The main thread's identifier value is retrieved via _get_main_thread_ident which is backed by an unsigned long which truncates sign extended bits. >>> hex(threading.main_thread().ident) '0xb6f33f3c' >>> hex(threading.current_thread().ident) '0xffffffffb6f33f3c' Work around this by conditionally compiling in some code for non-glibc based Linux platforms that are at risk of sign-extension to return a PyLong based on the main thread's unsigned long thread identifier if the current thread is the main thread. [0]: https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Arrays-and-pointers-implementation.html --------- (cherry picked from commit 7212306) Signed-off-by: Vincent Fazio <[email protected]> Co-authored-by: Vincent Fazio <[email protected]>
CPython's pthread-based thread identifier relies on pthread_t being able to be represented as an unsigned integer type. This is true in most Linux libc implementations where it's defined as an unsigned long, however musl typedefs it as a struct *. If the pointer has the high bit set and is cast to PyThread_ident_t, the resultant value can be sign-extended [0]. This can cause issues when comparing against threading._MainThread's identifier. The main thread's identifier value is retrieved via _get_main_thread_ident which is backed by an unsigned long which truncates sign extended bits. >>> hex(threading.main_thread().ident) '0xb6f33f3c' >>> hex(threading.current_thread().ident) '0xffffffffb6f33f3c' Work around this by conditionally compiling in some code for non-glibc based Linux platforms that are at risk of sign-extension to return a PyLong based on the main thread's unsigned long thread identifier if the current thread is the main thread. [0]: https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Arrays-and-pointers-implementation.html --------- Signed-off-by: Vincent Fazio <[email protected]>
CPython's pthread-based thread identifier relies on pthread_t being able to be represented as an unsigned integer type.
This is true in most Linux libc implementations where it's defined as an unsigned long, however musl typedefs it as a struct *.
If the pointer has the high bit set and is cast to PyThread_ident_t, the resultant value can be sign-extended (https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Arrays-and-pointers-implementation.html). This can cause issues when comparing against
threading._MainThread
's identifier. The main thread's identifier value is retrieved via_get_main_thread_ident
which is backed by an unsigned long which truncates sign extended bits.Work around this by making a new function which translates a
pthread_t
toPyThread_ident_t
by casting through an integer type of the appropriate size to avoid sign extension. Factoring this out allows us to replace the internal logic in the future with some min-heap or other data structure to managepthread_t
objects in a more opaque fashion.Note musl isn't "officially" supported in PEP 11, however platform detection was added in c163d7f and similar PRs have been merged in the past which target it 5633c4f
This PR is intended to be a "minimum" to get this working. Longer term there should maybe be work to keep
pthread_t
opaque and not make assumptions about its type.