Repository navigation
fix: make setlocale() thread-local - #2831
felixbuenemann wants to merge 1 commit into
Conversation
setlocale() changes the locale of the whole process, while FrankenPHP runs requests in threads of one process: a request uses the locale set by requests running at the same time, the end of a request resets the locale of all others to "C", and concurrent calls crash the process (setlocale() isn't thread-safe; on macOS it aborts or segfaults). Like putenv() and getenv(), setlocale() is now replaced with a version that gives each thread its own locale with the POSIX newlocale() and uselocale() functions, which the locale-dependent C functions follow. Each combination of category locales is created once, under a mutex, and shared by all threads, so switching locales no longer reloads the locale data. Queries and composite LC_ALL names return the same names as the C library, and the arguments are checked like setlocale() does in each PHP version. A script now starts with the process locale, and in worker mode each request starts with the locale the worker script set before handling requests. Windows builds of PHP already keep the locale per thread, so there only the locale of the worker script is restored after each request. A thread-local setlocale() was proposed for PHP itself (php/php-src#20531), but closed in favour of a broader ZTS rework.
ea7d3ee to
1b532a6
Compare
|
Shouldn't this be changed in PHP directly? |
Ideally it would, but work started on it before was abandoned and it could land earliest in php 8.7 which is a year a way. What would make sense is trying to get the php-specific bits into 8.7 in a parallel PR. However this code provides a solution for 8.2+ when used with frankenphp. |
|
I think if classified as a bug, this might also get merged into older PHP versions. Would make the implementation a bit simpler as you could just mirror the windows implementation. |
|
It's not a bug, it's documented behaviour, so I'm not sure the RMs would agree to backport this into <8.7. That being said, this 100% does not belong in FrankenPHP whatsoever. |
OK, closing this PR. |
|
Perhaps noteworthy, I'm planning to tackle this in a more thorough fashion for 8.7 with the intent to drop NTS builds entirely. If you would like to try for a real rework of locale handling to uselocale, please feel more than welcome to (and aim for code that doesn't need. |
|
@henderkes I already opened php/php-src#25420 before I read your last comment. I would be grateful for your review! |
setlocale()changes the locale of the whole process, so in FrankenPHP:C;setlocale()isn't thread-safe.This PR replaces
setlocale()with a version that gives each thread its own locale, using the POSIXnewlocale()anduselocale()functions. It's the same approach as thegetenv()/putenv()sandbox. PHP code doesn't need to change.Related: #1941 (gettext mixing languages, where this approach was first discussed), #1737 (setlocale() named among the MT-unsafe functions WordPress calls). This also updates the worker-mode note added in #2779.
Reproduction
The new tests fail on
main. With the PHP 8.5 ZTS build on macOS, all three fail (localeconv()['decimal_point']returning another request's,,Cinstead ofde_DE.UTF-8, etc.), and the test binary aborted (SIGABRT) in 2 of 3 runs.In a real application (TYPO3 patched for worker-mode, alternating German and English pages, 8 concurrent connections),
mainrendered 171 of 4000 pages with the wrong locale, then aborted. With this PR, all 4000 pages were correct.How it works
setlocale()is replaced inMINITthroughCG(function_table), likegetenv()andputenv(). Everything is in the newthreadlocale.c, and the changes tofrankenphp.care small.newlocale(), under a mutex, and shared by all threads. Threads only switch between these objects withuselocale(), and the locale-dependent C functions (printf(),localeconv(),strftime(),strcoll()…) follow it.freelocale()crashes while another thread callsnewlocale().setlocale(LC_ALL, '0')) return the same names as the C library, including compositeLC_ALLnames in each platform's own format, which differs between glibc, musl and macOS/BSD. Sosetlocale(LC_ALL, $saved)still restores all categories.ext/standard/string.c, gated byPHP_VERSION_IDso it matches each version:RSHUTDOWN, the thread switches back to the process locale. PHP's own reset of the process locale toCno longer runs.frankenphp_handle_request().setlocale()isn't replaced. In worker mode, the locale the worker script set is restored after each request, as on other platforms.Limitation: C code that calls the C library's
setlocale()directly, in a PHP extension for instance, still changes the locale of the whole process.Tests
TestSetlocale,TestSetlocale_workerandTestSetlocaleBeforeHandlingRequests_workerrun concurrent requests that switch between locales. They pass with-race.'0','',null, arrays,LC_ALLcomposites, wrong types, strict_types, and NUL bytes. Platforms:uselocale();Performance
setlocale(), which reloads the locale data on every call. With this PR it takes 0.15 µs, because the locale objects are reused.Relation to php-src
The same change was proposed for PHP itself in php/php-src#20531. Reviewers there found the
uselocale()approach reasonable, but the PR was closed in August 2026 in favour of a broader ZTS rework. Until PHP has thread-local locales, this fixes the problem for FrankenPHP users on all supported PHP versions. Once PHP has them, the override can be gated byPHP_VERSION_IDand removed later.