Crash fix in case of forks: remove usage of pthread_once#483
Crash fix in case of forks: remove usage of pthread_once#483
Conversation
When compiling on Alpine Linux (musl libc) and running on glibc systems, pthread_once has incompatible internal state machines that cause crashes after fork(). This was observed as SIGSEGV in PHP binaries that use forking when returning from the pthread_once API. Root cause: - musl uses state value 1 during pthread_once initialization - glibc uses state value (__fork_generation | 1), typically 5 - After fork(), glibc sees musl's state=1, treats it as invalid for the current fork generation, and re-initializes - This causes double initialization of pthread keys Solution: Replaced pthread_once with atomic-based initialization in AllocationTracker::get_tl_state(): - Uses std::atomic<int> with three states: kKeyNotInitialized, kKeyInitializing, kKeyInitialized - Hot path optimized with relaxed atomic load - Slow path uses compare-and-swap with proper memory ordering - Added safety timeout to prevent infinite loops - Returns nullptr on timeout
- Cover TLS init with forking
Benchmark results for collatzParameters
SummaryFound 0 performance improvements and 0 performance regressions! Performance is the same for 1 metrics, 0 unstable metrics. See unchanged results
|
Benchmark results for BadBoggleSolver_runParameters
SummaryFound 0 performance improvements and 0 performance regressions! Performance is the same for 1 metrics, 0 unstable metrics. See unchanged results
|
|
Sadly, we will have to rethink this PR. |
|
What could be happening:
A fix could be about securing the init after the fork ? |
| pthread_key_t key = _tl_state_key.load(std::memory_order_relaxed); | ||
|
|
||
| // In debug builds, verify our assumption | ||
| assert(key != kInvalidKey && "pthread key should be initialized before use"); |
There was a problem hiding this comment.
this might trigger with a relaxed order, but at least I will know about it
I might change mem order
5dd2c62 to
dc0d1b6
Compare
- Use the key as synchronization - Avoid init during a get of the thread local state - Add a fork handler to ensure we have the key initialized
dc0d1b6 to
57ba49e
Compare
|
I think we could completely remove |
|
Thanks, This is a super interesting simplification. That would indeed guarantee that the variable is available. Can LD_PRELOAD + |
|
looks like defaults are at 512 bytes in glibc if I'm looking at the correct setting: |
Static TLS size is computed at startup from the requirements of all the initially loaded libs (ie. DT_NEEDED libs + preloaded libs), therefore it cannot fail with glibc allocates some additional ("static surplus") space to static TLS to account for dlopen'ed libraries that use initial-exec TLS model for performance (libgomp, audit libraries?). But since libdd_profiling is not dlopen'ed (at least not the loader, that's why I put the TLS variable in it), static TLS space is not an issue. |
|
Ok, I'm convinced! Thanks for the proposal, I'll close this again to do a new refactor. |
What does this PR do?
Disclaimer: I do not fully understand why we have a crash where the key was not initialized.
However we know that in the case of forks we can be in a state where we try to init in the hot path (during an allocation).
This could be the reason for the crash.
This PR addresses the symptoms.
We could go one step further here, to make sure we wrap the full init sequence (not just thread local state)
Motivation
Avoid a crash.
Additional Notes
NA
How to test the change?
I added a test for forking. Though it does not reproduce the crash.