Repository navigation
fix: make setlocale() thread-local - #2831
Open
felixbuenemann wants to merge 1 commit into
Open
felixbuenemann wants to merge 1 commit into
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.
felixbuenemann
force-pushed
the
fix/thread-local-setlocale
branch
from
November 10, 2026 16:59
ea7d3ee to
1b532a6
Compare
Member
|
Shouldn't this be changed in PHP directly? |
Author
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. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.