Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,16 @@ FrankenPHP sandboxes environment variables per-thread:
4. `frankenphp_getenv()` looks a variable up in thread-local layers, the first match wins: `sandboxed_env` (`putenv()` writes and unsets), `prepared_env` (the `env` of the current request, released after each worker request), `worker_env` (the `env` of the worker script, kept for all its requests), then `main_thread_env`. `putenv()` writes land only in `sandboxed_env` and **never** mutate the process-global OS environment, so a value set by one request is not visible to the Go/Caddy side, to child processes (`proc_open`, `exec`, ...), or to other requests. This also avoids races on the global C environment.
5. `reset_sandboxed_environment()` releases `sandboxed_env` after each PHP script execution. In regular mode that's per request; in worker mode it only runs when the worker script itself exits, so `putenv()` writes are visible to subsequent worker requests on the same thread until the script restarts.

## Per-thread locale

The C library's `setlocale()` changes the locale of the whole process and isn't thread-safe: a request would use the locale of the requests running at the same time, and concurrent calls can crash the process. FrankenPHP replaces PHP's `setlocale()` with `frankenphp_setlocale()` (`threadlocale.c`), which gives each thread its own locale with the POSIX `newlocale()` and `uselocale()` functions:

1. Each combination of category locales is created once with `newlocale()`, under a mutex, and kept until the process exits. Threads only switch between these locale objects with `uselocale()`, which the locale-dependent C functions (`strftime()`, `strcoll()`, `localeconv()`, `printf()`...) follow.
2. The locale objects store the name of each category, so `setlocale($category, '0')` and composite `LC_ALL` names (to restore all categories at once) behave as with the C library.
3. When a script ends (`RSHUTDOWN`), the thread switches back to the process locale. In worker mode, it also switches back after each request to the locale the worker script set before handling requests. PHP's own reset of the process locale to `C` never runs, because `BG(locale_changed)` stays unset.

Windows builds of PHP already keep the locale per thread, so the override is only installed on other platforms. On Windows, FrankenPHP only restores the locale of the worker script after each request. C code calling the C library's `setlocale()` directly, in a PHP extension for instance, still changes the locale of the whole process.

## Request flow (regular mode)

1. HTTP request arrives at Caddy
Expand Down
2 changes: 1 addition & 1 deletion docs/worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ Everything else is shared between the requests handled by the same thread, as in
- **Engine and extension state**: `error_get_last()`, `libxml_get_errors()` and `libxml_get_last_error()` return the errors an earlier request left behind. `http_get_last_response_headers()` returns the response headers of the last HTTP call made on the thread, even by an earlier request. `strtok()` continues the string an earlier request gave it. After `mt_srand()` or `srand()` with a seed, `mt_rand()`, `rand()`, `shuffle()`, `str_shuffle()` and `array_rand()` continue that seeded sequence. The errors, headers and strings can contain another client's data, and a seeded sequence makes random values predictable: reset them as shown below.
- **Objects in reference cycles**: The garbage collector destroys them when PHP triggers it, possibly while a later request is handled, so their destructors run in that request and their output goes to its client. Call `gc_collect_cycles()` after `frankenphp_handle_request()` returns, as in the example above.
- **Shutdown functions**: Callbacks registered with `register_shutdown_function()` run when the worker script stops, not at the end of the request that registered them, and stay in memory until then. If a request stops the script (`exit()`, a fatal error, a timeout), they run inside that request and their output and headers go to its client. Register them once, outside the handler, and run per-request work at the end of the handler or after `frankenphp_handle_request()` returns.
- **Process-wide settings**: `umask()` and `setlocale()` change the whole FrankenPHP process, not only the current thread. Worker mode doesn't restore them between requests, and in every mode they also affect requests running at the same time. Set them once at startup, or call `chmod()` on created files instead of changing the umask.
- **Process-wide settings**: `umask()` changes the whole FrankenPHP process, not only the current thread. Worker mode doesn't restore it between requests, and in every mode it also affects requests running at the same time. Call `chmod()` on created files instead of changing the umask. `setlocale()`, however, only changes the locale of the current thread, and each request starts with the locale the worker script set before handling requests.

This is by design and is what makes worker mode fast. However, it requires attention to avoid unintended side effects:

Expand Down
24 changes: 18 additions & 6 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@

#include "_cgo_export.h"
#include "frankenphp_arginfo.h"
#include "threadlocale.h"
#ifdef FRANKENPHP_TEST
/* The persistent_zval helpers are only compiled in when a consumer needs
* them. The step that lands the first real caller (background workers)
Expand Down Expand Up @@ -639,6 +640,8 @@ static void frankenphp_worker_request_shutdown() {
prepared_env = NULL;
}

frankenphp_restore_worker_locale();

zend_set_memory_limit(PG(memory_limit));
}

Expand All @@ -650,6 +653,7 @@ bool frankenphp_shutdown_dummy_request(void) {

worker_env = prepared_env;
prepared_env = NULL;
frankenphp_save_worker_locale();
frankenphp_worker_request_shutdown();

return true;
Expand Down Expand Up @@ -1272,6 +1276,8 @@ PHP_MINIT_FUNCTION(frankenphp) {
php_error(E_WARNING, "Failed to find built-in getenv function");
}

frankenphp_override_setlocale();

// Override opcache_reset (may not be available yet if opcache loads as a
// shared extension in PHP 8.4 and below)
frankenphp_override_opcache_reset();
Expand Down Expand Up @@ -1341,15 +1347,21 @@ PHP_MINFO_FUNCTION(frankenphp) {
}
}

static PHP_RSHUTDOWN_FUNCTION(frankenphp) {
frankenphp_reset_thread_locale();

return SUCCESS;
}

static zend_module_entry frankenphp_module = {
STANDARD_MODULE_HEADER,
"frankenphp",
ext_functions, /* function table */
PHP_MINIT(frankenphp), /* initialization */
NULL, /* shutdown */
NULL, /* request initialization */
NULL, /* request shutdown */
PHP_MINFO(frankenphp), /* information */
ext_functions, /* function table */
PHP_MINIT(frankenphp), /* initialization */
NULL, /* shutdown */
NULL, /* request initialization */
PHP_RSHUTDOWN(frankenphp), /* request shutdown */
PHP_MINFO(frankenphp), /* information */
TOSTRING(FRANKENPHP_VERSION),
STANDARD_MODULE_PROPERTIES};

Expand Down
10 changes: 10 additions & 0 deletions testdata/setlocale-worker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

// de-DE is the name on Windows
$workerLocale = setlocale(LC_NUMERIC, ['C.UTF-8', 'de-DE']);

while (frankenphp_handle_request(function () use ($workerLocale) {
echo $workerLocale, '|', setlocale(LC_NUMERIC, '0');
setlocale(LC_NUMERIC, 'C');
})) {
}
29 changes: 29 additions & 0 deletions testdata/setlocale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

require_once __DIR__.'/_executor.php';

return function () {
$initial = setlocale(LC_NUMERIC, '0');
if ($_GET['locale'] === '0') {
echo $initial;

return;
}

// de-DE and fr-FR are the names on Windows
$set = $_GET['locale'] === 'C'
? setlocale(LC_NUMERIC, 'C')
: setlocale(LC_NUMERIC, ['de_DE.UTF-8', 'de_DE.utf8', 'de-DE', 'fr_FR.UTF-8', 'fr_FR.utf8', 'fr-FR', 'C.UTF-8']);
// let concurrent requests change their locale meanwhile
usleep(1000);
$current = setlocale(LC_NUMERIC, '0');
$float = sprintf('%.1f', 1.5);
$decimalPoint = localeconv()['decimal_point'];

// the composite name of all categories restores them
$all = setlocale(LC_ALL, '0');
setlocale(LC_ALL, 'C');
$restored = setlocale(LC_ALL, $all) === $all && setlocale(LC_ALL, '0') === $all;

echo implode('|', [$initial, $set, $current, $float, $decimalPoint, $restored ? 'restored' : 'not restored']);
};
Loading
Loading