You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(restore): keep session upload progress off after restoring an older backup - #520
After ee site restore restores a site's PHP config, add session.upload_progress.enabled = Off back to the top of its custom.ini unless the restored file already sets the directive.
Also add a one-off container migration that checks every WordPress and PHP site's custom.ini again with the same parser and adds the line where the v4.13.3 migrations missed it.
Why
v4.13.3 turned session upload progress off by adding session.upload_progress.enabled = Off to every site's custom.ini (EasyEngine/site-type-wp#246, EasyEngine/site-type-php#120). EasyEngine/dockerfiles#374 makes Off the default in the images' php.ini too, which covers new sites but not a restore, since a restore also brings back the backup's php.ini. PHP's informational advisory of 2026-10-23 for CVE-2026-115560 asks to keep it off until the next regular bugfix release.
restore_php_conf() copies custom.ini and php.ini from the backup over the site's files. A backup taken before v4.13.3 has neither the line in custom.ini nor a php.ini that sets it, so the restored site runs with upload progress On again, PHP's default. Shown on a runner with develop: a WordPress site with a pre-v4.13.3 custom.ini was backed up, upgraded to the v4.13.3 custom.ini (FPM reports Off), then restored: the line was gone and FPM reported On.
How
New helpers in site-utils.php: add_session_upload_progress_off() returns the content with the setting added, or null when the content already sets it outside a [PATH=]/[HOST=] section; ini_sets_globally() and ini_global_part() decide that. The rule is the v4.13.3 migrations' rule: an effective global line, On or Off, is the user's choice and is kept; a commented-out line, another spelling, or a setting only inside a [PATH=]/[HOST=] section doesn't count. The lines go at the very top, so neither such a section nor a multi-line quoted value can capture them; CRLF, a UTF-8 BOM and the rest of the file are kept.
ini_global_part() lets PHP's ini parser decide where each section header starts, ends and what it is called: a [ counts only where the parser would take a new entry, and the header is named by parsing it alone. The migrations' version only recognised a header that was unquoted and alone on an LF-terminated line, so in a CRLF or CR-only file, with a quoted name like ["PATH=/x"], or with [PATH=/x][Session] on one line, a setting inside the section counted as global and the line wasn't added. The part before such a section is read without sections, so an array entry keyed by the directive doesn't count either.
restore_php_conf() calls the new keep_session_upload_progress_off() after copying the files. It writes custom.ini in place (owner, mode and inode kept) through a checked handle (see below), puts the restored file back if a write fails part way, warns with the manual fix when the file is missing or can't be read or written, and logs when it adds the line. The site reload at the end of the restore applies it.
Re-check migration
The v4.13.3 migrations (EasyEngine/site-type-wp#246, EasyEngine/site-type-php#120) used the older header check, so they skipped every custom.ini whose only session.upload_progress.enabled line is inside a [PATH=]/[HOST=] section with a quoted header, two headers on one line, a header named like an earlier key, or CRLF/CR line endings, and every file with an array entry keyed by the directive. Those sites still run with upload progress On. Hosts that upgrade from v4.13.2 run those migrations first, so the new one is ordered after them.
Goes through every wp and php site, enabled or disabled, and adds the same two lines at the top of custom.ini where the directive isn't set outside a [PATH=]/[HOST=] section. A missing custom.ini gets a warning with the manual fix instead of being created; the v4.13.3 migrations already created it wherever conf.d existed. A global On or Off is kept; a global On is logged, naming the site. A file that already has the line is left alone, so a second run changes nothing.
Each changed file is backed up under .backup/session-upload-progress-recheck-<time>/<site>/custom.ini and rewritten in place, keeping owner, mode and inode. down() restores the files byte for byte, so core reverts it when a later upgrade step fails.
Running php containers of enabled sites get the graceful FPM reload the v4.13.3 migrations use, after the image migration and only when the container wasn't recreated. Disabled and stopped sites read the file when they start.
A failure on one site is a warning with the manual fix, and that site's file is restored; the other sites go on.
custom.ini and the directories above it, up to config/php, are in the site's config volume, which its php container can change. New helpers in site-utils.php, used by both the restore and the migration, open the file once and check that the handle is the regular, singly linked file in that site's conf.d before anything is read or written through it: on Linux by the kernel's path of the open file (/proc/self/fd), which a directory swapped for a symlink and back around the open can't fake; elsewhere by resolving conf.d before and after the open. Reads stop at 64 KB. So a symlink, hard link or swapped directory doesn't send a write anywhere else on the host. Nothing is created in that directory, because PHP's fopen() and link() expand the path to an absolute one, which a swapped directory could redirect before any check. A file is opened for writing only when the line has to be added, so a read-only file that already sets the directive is fine. Files over 64 KB are skipped with a warning.
The helpers also gained ini_global_value(), so the migration can tell an explicit On from an Off. A bare [PATH] or [HOST] header (nothing after the word) doesn't scope anything in PHP, and the helpers now treat entries under it as global too.
Testing
Case matrix for the helpers on PHP 7.4 and 8.5 (CI) and 8.5 locally: empty or missing file, the v4.13.2 and v4.13.3 templates (LF, CRLF, BOM, the 5.6 template), explicit On/Off/1/"0", commented-out (; and #), wrong case, other session.upload_progress.* keys, a setting only in [PATH=], ["PATH=..."], ['PATH=...'], [path=...], [PATH = ...], [HOST=...] and ["HOST=..."] (LF, CRLF and CR-only), [PATH=...][Session] on one line, quoted PATH names containing a newline or ], a CR before a header, an array entry keyed by the directive (custom_flags[session.upload_progress.enabled]), a PATH header named like an earlier global key or array, PATH-like text in comments, values and array keys, an ordinary section after a PATH one, [ PATH=...] (global for PHP), a PATH-like line inside a multi-line quoted value or a comment, a [ inside a quoted value before a real header, and settings before and after a syntax error. Each added result is a no-op on a second run, sets the directive globally, keeps line endings, BOM and the original lines, and parses as Off with real PHP. The migrations' version of the check gets 15 of these cases wrong.
The global/scoped verdict of every case was checked against real PHP-FPM (easyengine/php8.4), requesting a script inside and outside the section: all match.
The real restore_php_conf() with stubs: pre-v4.13.3 backup, v4.13.3 backup, explicit On, CRLF [PATH=] On, ["PATH=..."] On, backup without custom.ini; develop leaves three of them On, this branch none.
Re-check migration, in isolation on PHP 7.4 and 8.5 against the real v4.13.3 migration: the case matrix above (56 entries), starting both from the state the v4.13.3 migration leaves and from the untouched files, on enabled and disabled wp and php sites, plus a missing custom.ini, a missing conf.d, a symlinked or hard-linked custom.ini, a hard-linked file that already sets Off, a symlinked conf.d and php/ pointing out of the site, a file over 64 KB, and a site whose container lookup throws. It changes exactly the files that lack a global setting (15 of them after v4.13.3, 34 untouched), keeps everything else, the backups equal the old files, a second run and its down() change nothing, down() restores every file byte for byte, and only running enabled sites whose container wasn't recreated get reloaded. Every end state was served by real PHP-FPM (easyengine/php8.4): the global value is Off for all of them except the seven that set a global On or "0" themselves, which keep it; before the migration the same requests show On for those 15 and 34 files.
The file helpers under a race: conf.d flipped between the real directory and a symlink to an outside directory about 44,000 times while open-and-write ran in a loop; the outside file was never written (with only the before/after path checks, it was).
Re-check migration end to end on a fresh runner: sites with the 15 layouts (two of them WordPress, one disabled), an explicit global On and an Off control, created with core develop, which then re-ran the v4.13.3 migrations: they left all 15 files unchanged and FPM still reported On. Then this branch: an upgrade that fails in the image migration reverts the migration, and every file, its mode, owner and inode, the containers and FPM's values are as before; the next upgrade turns all 15 Off in FPM without restarting a container, keeps the values inside the [PATH=] sections, keeps the explicit On (logged), leaves the control alone, and the disabled site reports Off once enabled; a second run changes nothing and reloads nothing.
On a fresh runner with core develop: a WordPress site with a pre-v4.13.3 custom.ini restored with this branch gets the line back at the top and FPM reports Off; a PHP site whose backup sets session.upload_progress.enabled = On (changed to Off after the backup) is restored with On kept and FPM reports On. The site-command Behat suite passes, also with three extra scenarios for the migration (a CRLF [PATH=]-only file gets Off, a second run changes nothing, an explicit On is kept).
…er backup
A restore copies custom.ini and php.ini from the backup over the site's files. A backup taken before v4.13.3 has neither the session.upload_progress.enabled = Off line that v4.13.3 added to custom.ini nor a php.ini that sets it, so the restored site ran with upload progress back On, PHP's default.
After restoring the PHP config, add the line again at the top of custom.ini unless the restored file already sets the directive outside a [PATH=] or [HOST=] section, so an explicit On or Off is kept, as in the v4.13.3 migrations. The site reload at the end of the restore applies it.
The check that finds those sections now lets PHP's ini parser decide what each header line is. Before, a header was only recognised when it was unquoted and LF-terminated, so in a CRLF file, or with a quoted name like ["PATH=/x"], a setting inside the section was taken as a global one and the line was not added.
This PR restores the session upload-progress mitigation when older backups overwrite a site's PHP configuration, while preserving explicit global settings.
Changes:
Add INI helpers to distinguish global directives from PATH/HOST-scoped settings.
Check restored custom.ini, prepend Off when needed, and warn when the file cannot be updated.
File
Description
src/helper/site-utils.php
Adds INI inspection and upload-progress mitigation helpers.
src/helper/Site_Backup_Restore.php
Applies the mitigation after restoring PHP configuration.
Find the start of a [PATH=]/[HOST=] section by letting PHP's ini parser read up to every '[' and ']' instead of only at the start of an LF-separated line. A header after a CR-only line ending, a second header on the same line as in [PATH=/x][Session], and a quoted name that contains a newline or ']' are now found, so a setting inside such a section no longer counts as global.
Read the part before such a section without sections, so an array entry like custom_flags[session.upload_progress.enabled] = Off isn't taken for the directive. A CR-only file gets CR line endings for the added lines.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The security-sensitive INI parser has an unresolved scope-detection edge case and needs final human validation across supported PHP versions.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Handle scoped headers colliding with existing global keys
src/helper/site-utils.php:1704
A scoped header is missed when its name already exists as a global key. For example:
PATHprivate = ignored
[Session][PATHprivate]session.upload_progress.enabled = Off
Parsing [PATHprivate] replaces the earlier PATHprivate entry without moving it after Session, so end($ini) still sees Session. The helper then treats the scoped directive as global and returns null. If the restored php.ini leaves upload progress enabled, restore skips the mitigation and it stays enabled globally. Identify the header just parsed independently of the result array's insertion order, and add this collision case to the regression checks.
A section that reuses the name of an earlier global key replaces that key in place, so the last entry of the parsed array isn't the header just read. Name the header by parsing it alone, and count it only when it adds that section, which still leaves header-like text in a comment alone.
The header-name collision case (PATHprivate = ignored, [Session], [PATHprivate]) is fixed in b1792c3: each header is now named by parsing it alone and only counts when it adds that section, so the scoped directive is no longer taken as global. The case is in the regression matrix and matches php-fpm.
Decide whether a '[' starts a section header by checking that the ini parser takes an entry at that point, instead of comparing the parsed array before and after the header. An earlier global array or key with the same name as a [PATH=] section no longer hides it, and header-like text in a comment, a value or an array key still doesn't count.
…heck missed it
The v4.13.3 migrations of site-type-wp and site-type-php added `session.upload_progress.enabled = Off` to every custom.ini that didn't already set it globally, but their check took a setting inside a [PATH=] or [HOST=] section for a global one when the section header was quoted, shared its line with another header, was named like an earlier key, or ended with CRLF or CR, and it also counted an array entry keyed by the directive. Those sites still run with PHP's default On.
Add a container migration, ordered after those, that checks the custom.ini of every WordPress and PHP site again with the parser from the restore fix and adds the same lines at the top where the directive still isn't set globally, creating the file when it is missing. A global On or Off is kept; a global On is logged. A site that already has the line is left alone, so a second run changes nothing.
Each changed file is backed up under .backup/session-upload-progress-recheck-<time>/ and written in place, keeping owner, mode and inode. down() restores the files byte for byte. Running php containers of enabled sites get the same graceful FPM reload as in the v4.13.3 migrations, after the image migration and only when the container wasn't recreated. A failure on one site is a warning and its file is restored; the other sites go on.
ini_global_value() returns the value next to ini_sets_globally(), so the migration can tell an explicit On from an Off.
…lobal
PHP scopes entries only to a section whose name has something after PATH or HOST. Under a header named just [PATH] or [HOST], in any case, php-fpm applies them globally, but ini_global_part() cut the global part there, so a global On under such a header was taken as unset and the Off line was added in front of it for nothing.
…ration
custom.ini lives in a directory the site's php container can write to, so a symlink or hard link can appear there between the checks and the write, and the migration runs as root on the host. Read, back up, rewrite and restore the file through one handle, checked after opening to be the regular, singly linked file at that path, the same way the session ID migration handles php.ini. A missing custom.ini is created under an unguessable name and hard-linked into place, because fopen() follows a dangling symlink even in 'x' mode; its owner is set with lchown().
A failed restore inside the per-site error path now only warns instead of aborting the upgrade, the log names the backup directory only when a file was backed up, and the explicit-On check follows PHP's atoi() fallback for values like "1e-5".
…k migration
Only config/php is EasyEngine's own link to the site's config volume; php/ and conf.d below it belong to the container, which can swap either for a symlink to a host directory. Resolve conf.d before and after each open and refuse anything outside the site's config volume, so the migration never creates, rewrites, restores or removes a custom.ini elsewhere on the host. A created file is removed on rollback only while it is still the same file in that directory.
Read custom.ini read-only first and reopen it for writing only when the line has to be added, so a read-only or hard-linked file that already sets the directive is left alone without a warning; a file with other hard links is still never written. Skip files over 64 KB with a warning, since the check re-parses the text before every '['. A write that fails and can't be undone is left for down() but no longer counted or reloaded as done, and a failed backup cleanup no longer reads as a failed restore.
The restored custom.ini sits in the site's config volume, which its php container can change: between the checks and the write it could swap the file for a symlink, or conf.d for a symlink to a host directory, and file_put_contents() as root would follow it. A short write also left only part of the file behind.
Add helpers that open such a file once and verify the handle is the regular, singly linked file in the site's conf.d before anything is read or written through it: on Linux by the kernel's path of the open file from /proc/self/fd, which a directory swapped around the open can't fake, elsewhere by checking the path before and after. A missing file is created under an unguessable name and hard-linked into place, since fopen() follows a dangling symlink even in 'x' mode. The restore now reads read-only first, writes through the handle, and on a failed write puts the restored file back or removes the new one.
Its own handle checks compared paths before and after opening, which a container that swaps conf.d for a host symlink and back around the open can beat; the shared helpers also check the kernel's path of the open file. Clear PHP's realpath cache along with the stat cache before each check, and remove a custom.ini the migration created when writing it fails.
The fstat() check does not enforce MAX_SIZE on this read. The site's PHP container can grow the same file after the check, allowing stream_get_contents() to allocate oversized content and pass it to the repeatedly reparsing helpers. Read at most MAX_SIZE + 1 bytes and reject an oversized result before parsing. Also bound the pre-write comparison at line 237 to MAX_SIZE + 1 bytes so growth during parsing cannot exhaust memory there instead.
Unbounded restore parsing can exhaust memory or cause quadratic work
src/helper/Site_Backup_Restore.php:1741
Unlike the migration, restore reads the entire custom.ini before invoking ini_global_part(), which reparses the preceding content for every [. A large restored file with many array entries can exhaust CLI memory or stall restore with quadratic parsing work. Bound the initial read to 65537 bytes and warn/skip when it exceeds 64 KB. Bound the pre-write comparison read too, so a file that grows during parsing cannot cause an unbounded allocation there.
… directory
remove_container_file() and the owner change of a newly created custom.ini went by path after their checks, so a container that swapped conf.d or php/ for a host symlink in between could have a custom.ini outside the site removed or chowned. Both now enter the directory, check with getcwd() that it is the site's conf.d, and work on the bare file name, which keeps reaching that directory whatever its path points to later.
Reads of the file are capped at 64 KB (read_container_file()), so a file the container keeps growing can't exhaust memory or stall the ini check; the restore warns and leaves a larger custom.ini as it is.
…h the helpers
The size check used fstat() before reading, but the container can grow the file after it; read at most 64 KB plus one byte instead, also when comparing before the write and when restoring. Set the owner of a created custom.ini with chown_container_file().
Creating the file can't be confined to the site's conf.d from PHP: fopen() and link() expand the path to an absolute one, so a conf.d swapped for a host symlink at the wrong moment gets the new file outside, before any check of the open handle can catch it. A site without custom.ini now gets the warning with the manual fix instead; the restore only copies custom.ini from a backup that has one, so the site keeps its own. With nothing created, the helpers that removed and chowned created files are gone.
The read-back after a write stops one byte past the new content, so data the container appends in the meantime can't make it unbounded.
A missing custom.ini is now skipped with a warning and the manual fix, like a symlinked one, instead of being created, for the same reason as in the restore: PHP can't create it safely in a directory the site's container can swap. The v4.13.3 migrations already created it wherever conf.d existed, so this only leaves the sites they warned about. Every changed file now has a backup.
throw new \Exception( "a directory above $name is a symlink" );
}
$handle = @fopen( $file, $mode );
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
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.
What
After
ee site restorerestores a site's PHP config, addsession.upload_progress.enabled = Offback to the top of itscustom.iniunless the restored file already sets the directive.Also add a one-off container migration that checks every WordPress and PHP site's
custom.iniagain with the same parser and adds the line where the v4.13.3 migrations missed it.Why
v4.13.3 turned session upload progress off by adding
session.upload_progress.enabled = Offto every site'scustom.ini(EasyEngine/site-type-wp#246, EasyEngine/site-type-php#120). EasyEngine/dockerfiles#374 makes Off the default in the images'php.initoo, which covers new sites but not a restore, since a restore also brings back the backup'sphp.ini. PHP's informational advisory of 2026-10-23 for CVE-2026-115560 asks to keep it off until the next regular bugfix release.restore_php_conf()copiescustom.iniandphp.inifrom the backup over the site's files. A backup taken before v4.13.3 has neither the line incustom.ininor aphp.inithat sets it, so the restored site runs with upload progress On again, PHP's default. Shown on a runner withdevelop: a WordPress site with a pre-v4.13.3custom.iniwas backed up, upgraded to the v4.13.3custom.ini(FPM reports Off), then restored: the line was gone and FPM reported On.How
site-utils.php:add_session_upload_progress_off()returns the content with the setting added, or null when the content already sets it outside a[PATH=]/[HOST=]section;ini_sets_globally()andini_global_part()decide that. The rule is the v4.13.3 migrations' rule: an effective global line, On or Off, is the user's choice and is kept; a commented-out line, another spelling, or a setting only inside a[PATH=]/[HOST=]section doesn't count. The lines go at the very top, so neither such a section nor a multi-line quoted value can capture them; CRLF, a UTF-8 BOM and the rest of the file are kept.ini_global_part()lets PHP's ini parser decide where each section header starts, ends and what it is called: a[counts only where the parser would take a new entry, and the header is named by parsing it alone. The migrations' version only recognised a header that was unquoted and alone on an LF-terminated line, so in a CRLF or CR-only file, with a quoted name like["PATH=/x"], or with[PATH=/x][Session]on one line, a setting inside the section counted as global and the line wasn't added. The part before such a section is read without sections, so an array entry keyed by the directive doesn't count either.restore_php_conf()calls the newkeep_session_upload_progress_off()after copying the files. It writescustom.iniin place (owner, mode and inode kept) through a checked handle (see below), puts the restored file back if a write fails part way, warns with the manual fix when the file is missing or can't be read or written, and logs when it adds the line. The site reload at the end of the restore applies it.Re-check migration
The v4.13.3 migrations (EasyEngine/site-type-wp#246, EasyEngine/site-type-php#120) used the older header check, so they skipped every
custom.iniwhose onlysession.upload_progress.enabledline is inside a[PATH=]/[HOST=]section with a quoted header, two headers on one line, a header named like an earlier key, or CRLF/CR line endings, and every file with an array entry keyed by the directive. Those sites still run with upload progress On. Hosts that upgrade from v4.13.2 run those migrations first, so the new one is ordered after them.migrations/container/20261023233000_site-command_recheck_session_upload_progress.php:wpandphpsite, enabled or disabled, and adds the same two lines at the top ofcustom.iniwhere the directive isn't set outside a[PATH=]/[HOST=]section. A missingcustom.inigets a warning with the manual fix instead of being created; the v4.13.3 migrations already created it whereverconf.dexisted. A global On or Off is kept; a global On is logged, naming the site. A file that already has the line is left alone, so a second run changes nothing..backup/session-upload-progress-recheck-<time>/<site>/custom.iniand rewritten in place, keeping owner, mode and inode.down()restores the files byte for byte, so core reverts it when a later upgrade step fails.custom.iniand the directories above it, up toconfig/php, are in the site's config volume, which its php container can change. New helpers insite-utils.php, used by both the restore and the migration, open the file once and check that the handle is the regular, singly linked file in that site'sconf.dbefore anything is read or written through it: on Linux by the kernel's path of the open file (/proc/self/fd), which a directory swapped for a symlink and back around the open can't fake; elsewhere by resolvingconf.dbefore and after the open. Reads stop at 64 KB. So a symlink, hard link or swapped directory doesn't send a write anywhere else on the host. Nothing is created in that directory, because PHP'sfopen()andlink()expand the path to an absolute one, which a swapped directory could redirect before any check. A file is opened for writing only when the line has to be added, so a read-only file that already sets the directive is fine. Files over 64 KB are skipped with a warning.The helpers also gained
ini_global_value(), so the migration can tell an explicit On from an Off. A bare[PATH]or[HOST]header (nothing after the word) doesn't scope anything in PHP, and the helpers now treat entries under it as global too.Testing
1/"0", commented-out (;and#), wrong case, othersession.upload_progress.*keys, a setting only in[PATH=],["PATH=..."],['PATH=...'],[path=...],[PATH = ...],[HOST=...]and["HOST=..."](LF, CRLF and CR-only),[PATH=...][Session]on one line, quoted PATH names containing a newline or], a CR before a header, an array entry keyed by the directive (custom_flags[session.upload_progress.enabled]), a PATH header named like an earlier global key or array, PATH-like text in comments, values and array keys, an ordinary section after a PATH one,[ PATH=...](global for PHP), a PATH-like line inside a multi-line quoted value or a comment, a[inside a quoted value before a real header, and settings before and after a syntax error. Each added result is a no-op on a second run, sets the directive globally, keeps line endings, BOM and the original lines, and parses as Off with real PHP. The migrations' version of the check gets 15 of these cases wrong.easyengine/php8.4), requesting a script inside and outside the section: all match.restore_php_conf()with stubs: pre-v4.13.3 backup, v4.13.3 backup, explicit On, CRLF[PATH=]On,["PATH=..."]On, backup withoutcustom.ini;developleaves three of them On, this branch none.wpandphpsites, plus a missingcustom.ini, a missingconf.d, a symlinked or hard-linkedcustom.ini, a hard-linked file that already sets Off, a symlinkedconf.dandphp/pointing out of the site, a file over 64 KB, and a site whose container lookup throws. It changes exactly the files that lack a global setting (15 of them after v4.13.3, 34 untouched), keeps everything else, the backups equal the old files, a second run and itsdown()change nothing,down()restores every file byte for byte, and only running enabled sites whose container wasn't recreated get reloaded. Every end state was served by real PHP-FPM (easyengine/php8.4): the global value is Off for all of them except the seven that set a global On or"0"themselves, which keep it; before the migration the same requests show On for those 15 and 34 files.conf.dflipped between the real directory and a symlink to an outside directory about 44,000 times while open-and-write ran in a loop; the outside file was never written (with only the before/after path checks, it was).develop, which then re-ran the v4.13.3 migrations: they left all 15 files unchanged and FPM still reported On. Then this branch: an upgrade that fails in the image migration reverts the migration, and every file, its mode, owner and inode, the containers and FPM's values are as before; the next upgrade turns all 15 Off in FPM without restarting a container, keeps the values inside the[PATH=]sections, keeps the explicit On (logged), leaves the control alone, and the disabled site reports Off once enabled; a second run changes nothing and reloads nothing.develop: a WordPress site with a pre-v4.13.3custom.inirestored with this branch gets the line back at the top and FPM reports Off; a PHP site whose backup setssession.upload_progress.enabled = On(changed to Off after the backup) is restored with On kept and FPM reports On. The site-command Behat suite passes, also with three extra scenarios for the migration (a CRLF[PATH=]-only file gets Off, a second run changes nothing, an explicit On is kept).