Skip to content

fix(site): stop following symlinks from a site's php config volume in restore, update --php and migrations - #522

Open
mrrobot47 wants to merge 37 commits into
EasyEngine:developfrom
mrrobot47:fix/safe-container-config-writes
Open

mrrobot47 wants to merge 37 commits into
EasyEngine:developfrom
mrrobot47:fix/safe-container-config-writes

Conversation

@mrrobot47

@mrrobot47 mrrobot47 commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Stacked on #519 and #520, merge after them. This branch is #520's head plus a merge of #519's head, so the diff below also shows their commits until they're merged; the commits of this PR are the four after the merge commit.

What

Host-root code that reads or writes a site's php config volume no longer follows anything the site's php container put there: ee site restore, ee site update --php, ee site enable and #519's php.ini migration all go through one set of checked helpers.

Why

A site's config/php is a link to its config_php volume, which the php container mounts at /usr/local/etc (as root, and as www-data after a PHP switch, which chowns the volume's top directory to www-data). The container can therefore replace php/, php/conf.d, php-fpm.d or any file in them with a symlink, FIFO or device node. EasyEngine then followed it as root:

  • restore_php_conf() ran rsync -a into config/php/php-fpm.d/, config/php/php/php.ini and config/php/php/conf.d/custom.ini, so a swapped directory sent the backed-up files into whatever host directory it pointed at.
  • update_php() removed everything in the volume and mirrored the new version's config into it while the new php container was already running, so a swap during that window redirected deletes and writes. A symlinked custom.ini or easyengine.conf was copied into the backup as a link and then read back through it into the new config, so the contents of a host file ended up in the site's config.
  • fix(templates): drop deprecated session ID ini settings from the PHP 8.4 and 8.5 configs #519's open_ini() checked only php.ini itself, not the directories above it.
  • enable() recursively chowned config/php/php/conf.d while the container may be running.

PHP has no O_NOFOLLOW, openat() or O_PATH, and it resolves symlinks in user space before open(), so a path check alone is always racy. #520 added open_container_file(), which opens once and checks the handle (the kernel's path of the open fd from /proc/self/fd, fstat() against lstat(), one hard link for writes, non-blocking against FIFOs). This PR builds on it.

How

  • site-utils.php:
    • open_container_file() now lstat()s the path first and refuses a missing file, a symlink, FIFO, device or directory without opening it, and the opened inode must be the one it saw.
    • get_container_file_contents() and put_container_file_contents() are the path-level read and in-place write (owner, mode and inode kept; compared read-only first, so an unchanged file isn't opened for writing; an optional expected content refuses a file that changed since it was checked; a short write puts the old content back). Neither creates a file.
    • check_plain_tree() refuses a tree that holds anything but directories and regular files; chown_container_tree() chowns directories and regular files only, with lchown(), re-checking each directory's real path.
    • site_php_conf_dir() takes the directory below config/php (default php/conf.d, as before).
  • Restore: restore_php_conf_files() replaces the three rsync calls. Every backed-up file (each regular file at the top of php-fpm.d/, php/php.ini, php/conf.d/custom.ini) must be a regular file in the backup, since the backup came from the same volume. It is written over the site's existing regular file through a checked handle; a file the site lacks (e.g. a pool file added by hand, restored into a fresh site) is created from inside the running php container (docker exec -i -u root … cat >), where a symlink the container made can't point at the host. A file that can't be restored (a swapped directory, not regular, or missing while php isn't running) is warned about and kept under .backup/<site>-php-conf-not-restored-<time>/. On macOS, where config/php isn't mounted, a missing file is skipped, which is what rsync's copy amounted to there. A backup whose php directory is itself a link (some 7-Zip versions store config/php that way) is refused. keep_session_upload_progress_off() (fix(restore): keep session upload progress off after restoring an older backup #520) uses the same helpers.
  • update --php: the new image is pulled first (docker-compose pull php), while the site still runs. Then the php container, the only one that mounts the volume, is stopped; a running container that still mounts the volume (e.g. from a custom compose file) stops the update; the volume must hold only directories and regular files, or the update stops with "Nothing was changed" and the container is started again. The backup of the old config is a fresh copy (an older backup of the same version could hold a link that the mirror would write through; it is kept aside with a timestamp). The new version's config is put in place before the site starts on the new PHP version, so the new PHP never runs with the old version's config. A failure after the pull is rolled back: php is stopped again if enable() had started it, the old config is copied back from the backup (only while no other container mounts the volume), the old compose file is written and php is started on the old image (enable() is now called so that it throws instead of exiting).
  • enable(): conf.d is given to www-data with chown_container_tree(); a failure is a warning, as the site can run without it.
  • fix(templates): drop deprecated session ID ini settings from the PHP 8.4 and 8.5 configs #519's migration reads and rewrites php.ini through the helpers (capped at 1 MB); a swapped php.ini or php/ directory is now a per-site warning instead of a silent skip, a backup it no longer needs is removed, and a CR-only php.ini is split into lines (it used to be one line, so a match emptied the file).

Behaviour changes

  • ee site restore overwrites existing files' content in place, creates missing ones as root from inside the php container, and doesn't restore subdirectories of php-fpm.d or the files' owner, mode and mtime.
  • ee site update --php stops the php container for the duration of the config switch (after pulling the image) and refuses a config volume that holds symlinks, FIFOs or devices. The image's /usr/local/etc and the template archives have none.
  • The old chown -R www-data: config/php only ever changed the volume's top directory (chown doesn't descend through a symlink named on the command line); that's unchanged.

What is left

  • chown_container_tree() (used by enable(), where the container may be running) re-checks each directory's real path before lchown(), but a directory swapped between that check and the lchown() can still redirect it; there is no fchownat() in PHP.
  • Between the lstat() and the open() inside open_container_file() (microseconds), a symlink, FIFO or device renamed into place is still opened, non-blocking, and closed again before any read or write; opening some devices has side effects. Only an open with O_NOFOLLOW/openat(), which PHP lacks, or a write from inside a container that mounts only the volume avoids that. On macOS config/php is a host directory and only custom.ini is bind-mounted, so the container can't swap anything there.
  • Not covered here: the backup side. 7z u conf.zip php follows symlinks inside the volume (it archives the target's content) and blocks on a FIFO; the restore refuses links but can't tell such content from a real file.

Testing

  • Helper checks on PHP 7.4 and 8.5 (CI, as root): regular file read/write/inode kept/size cap/expected-content mismatch, missing file (nothing created), symlinked file, symlinked parent directory, FIFO (no stall), device node, hard link, check_plain_tree() and chown_container_tree() (symlink target untouched).
  • Race in a throwaway process pair, 20000 rewrites while another process keeps swapping conf.d with a symlink to another directory: the old file-only check plus path write wrote into the other directory 286 (7.4) / 361 (8.5) times; the helpers 0 times.
  • fix(templates): drop deprecated session ID ini settings from the PHP 8.4 and 8.5 configs #519's migration against throwaway site dirs: regular php.ini changed and rolled back; symlinked php/ and symlinked php.ini refused with a warning, the target untouched. fix(templates): drop deprecated session ID ini settings from the PHP 8.4 and 8.5 configs #519's head writes through both.
  • The re-check migration matrix from fix(restore): keep session upload progress off after restoring an older backup #520 (733 checks) still passes on 7.4 and 8.5.
  • On a runner with core develop: with develop's site-command, a restore with a swapped conf.d writes into the host directory and update --php copies a host file into the site's custom.ini through a symlinked custom.ini; with this branch the host directory is untouched, the restore warns, update --php refuses with the php container running again on the old version, a clean restore brings back the backed-up custom.ini with upload progress Off, a restore recreates an easyengine.conf missing in the site from inside the container, an update --php that fails after the switch began (template archive removed) is rolled back to the old config, compose file and PHP version, and a clean update --php serves the new PHP version with custom.ini carried over and the old config backed up. Behat passes.

Release order

site-type-wp and site-type-php use get_container_file_contents()/put_container_file_contents() from this PR when they exist and fall back to a path check otherwise, so they work with an older site-command, but they get the race-safe behaviour only together with this one.

…8.4 and 8.5 configs

session.sid_length and session.sid_bits_per_character are deprecated as of PHP 8.4: setting either one raises an E_DEPRECATED at startup whenever error_reporting includes E_DEPRECATED. PHP's own php.ini-production for 8.4 and later no longer sets them.

Remove both directives and their quick-reference entries from php/php.ini in php84.zip and php85.zip. These archives are what `ee site update --php=<version>` installs, so a site switched to 8.4 or 8.5 no longer gets the deprecated settings. Only that entry changes; every other entry is byte-identical.

Newly created session IDs fall back to PHP's defaults, 32 characters of 0-9a-f instead of 26 characters of 0-9a-v. IDs issued before the change are still accepted.
…d 8.5 sites

A site's php.ini is copied from the PHP image when the site is created and from the version's archive when its PHP version is switched, so existing PHP 8.4 and 8.5 sites keep session.sid_length and session.sid_bits_per_character even with the fixed images and archives. This container migration gives them the same php.ini a new site gets.

It handles php and wp sites on PHP 8.4, 8.5 or `latest` (which runs the php8.4 image), enabled or not. In config/php/php/php.ini it removes every active session.sid_length and session.sid_bits_per_character line, plus the comments shipped right above them and the matching quick reference entry, but only where those comments match the shipped text. Every other line, custom.ini and the other config files stay as they are. A php.ini EasyEngine shipped ends up byte-identical to the one in the archives, and a file that has neither directive is left alone, so running it again changes nothing.

Values other than the shipped 26 and 5 are removed too, with a warning naming the site, the removed line and the backup. Both settings are deprecated as of PHP 8.4 and go away in PHP 9, and php.ini is replaced wholesale on every PHP version switch anyway, so custom.ini is the only place where overrides survive. Keeping an edited value would leave the site with the deprecation and out of step with a switched site.

The previous file is saved as .backup/<site>/php.ini.with-session-sid-settings, and the new file keeps its mode and owner and only replaces the old one once fully written. PHP-FPM reads php.ini only when it starts, so the php container of an enabled site gets the same graceful reload `ee site reload --php` uses (USR2 to FPM) if it's running; stopped or disabled sites pick the file up when they start, and no container is recreated. down() puts the saved files back and reloads FPM again, so a failure later in the upgrade restores the old state. A site that can't be migrated only gets a warning, so it doesn't block the upgrade of the others.
The PHP config archive is picked by the version a site is switched to (`php<version without dots>.zip`), and `ee site update --php` only accepts 5.6 to 8.5, so `latest` never selects it. A site created with `--php=latest` runs the php8.4 image and gets its config from that image, and switching such a site to another version uses the target version's archive. Nothing else in site-command, core, site-type-wp or site-type-php refers to it.

It also still carried the session ID settings deprecated as of PHP 8.4.
…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.
The site's php container can write to the directory that holds php.ini (www-data owns it after a PHP version switch), so it could plant a symlink at the predictable php.ini.ee-tmp path and have the upgrade write, chmod and chown a host file through it.

The file is now opened once, checked through the open handle to be the same regular, singly linked file as the path (no symlink swapped in after the check, no hard link to another file), and read, written and truncated through that handle. No other path is created or followed, and mode and owner stay as they are. The removal only shortens the file, so writing over it needs no new space; if the write fails anyway, the old content is written back. The backup is written from the content that was read, and down() restores through the same checks.
The directory that holds php.ini is root-owned by default: a PHP version switch makes only conf.d owned by www-data, not the php directory itself. It is still mounted into the site's php container, which is why the rewrite trusts no path in it.
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.
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.
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.
…5 configs

Add `session.upload_progress.enabled = Off` to php/php.ini in php81.zip through php85.zip, right after the commented-out default, the same two lines EasyEngine/dockerfiles#374 adds to the 8.1 to 8.5 images. These archives are what `ee site update --php=<version>` installs, so a site switched to one of these versions gets the same php.ini as a site created on it. A site's custom.ini is read after php.ini and still wins, so `session.upload_progress.enabled = On` there turns it back on.

The 5.6 to 8.0 archives stay as they are: their images are not rebuilt and keep PHP's default, and the archives mirror the image's php.ini. Every site's custom.ini already sets Off since v4.13.3.

Only the php/php.ini entry changes; every other entry is byte-identical.
…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.
… 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.
A site's php container can replace custom.ini with a FIFO after the caller's is_file() check, and a blocking fopen() for reading then waits for a writer before the regular-file check runs, which stalls the upgrade or the restore. open_container_file() now opens with O_NONBLOCK (fopen mode 'n'), rejects the FIFO as before, and switches a regular file's handle back to blocking.
On pull requests and develop the suite installed site-command's develop branch, so it never ran the changes under test. A path repository now installs this checkout. A local rclone alias named easyengine (EasyEngine's default rclone-path) lets the suite back up and restore.
The re-check migration turns upload progress off for a site whose only setting is in a CRLF [PATH=] section, puts the line at the top, keeps an explicit global On byte for byte and changes nothing on a second run. Restoring a backup without the line adds it again. A missing custom.ini is warned about and not created, by the migration and by the restore.
… ran again

The upgrade, backup and restore steps refuse to run outside CI unless EE_BEHAT_DESTRUCTIVE=1, so a local run doesn't re-run the migrations on a real host or back up to its real remote. The second migration run is now shown by its warning for the site without custom.ini, the restored site's check waits for the php-fpm reload like the others, the helpers' exit codes are checked, and the saved copies are removed at the end. The workflow sets RCLONE_CONFIG once for the job and installs only rclone; EasyEngine installs the archive tools itself.
A failure from opening or reading custom.ini (a hard-linked or oversized file, a swapped conf.d) reached the per-site catch, whose warning didn't say how to apply the setting by hand. It now ends with the same manual fix as the other warnings.
The before-upgrade and before-restore states relied on the site's php.ini leaving the directive at PHP's default On. Newer php.ini templates and images set it Off, which would hide the gap the scenarios exercise, so the fixtures now set it On in php.ini explicitly, as an older host could have, and the restore site gets its own before check.
…read, write and chown helpers

open_container_file() now refuses a symlink, FIFO, device or missing file before it opens anything, and checks that the opened file is the inode it saw. New get_container_file_contents(), put_container_file_contents(), check_plain_tree() and chown_container_tree() let every caller that touches a site's php config volume use the same checks. site_php_conf_dir() takes the directory below config/php.
… instead of rsync

rsync followed a php-fpm.d, php or conf.d directory that the site's php container had swapped for a symlink, so host root wrote the backed-up files wherever it pointed. Each backed-up file must now be a regular file, and is written over the site's existing regular file through a checked handle. Nothing is created; a file that can't be restored is kept under the backup dir with the manual fix.
…helpers

open_ini() checked only php.ini itself, so a symlinked php/ directory in the config volume redirected the rewrite. Reads are now capped at 1 MB.
update --php removed and re-created everything in the site's config volume while the new php container was running, so the container could redirect those deletes and writes through a symlink. The php container (the only one that mounts the volume) is now stopped first, the volume must hold only directories and regular files, the backup is a fresh copy, and the new config is in place before the site starts with the new PHP version. enable() gives www-data conf.d without following symlinks.
put_container_file_contents() reads first and opens the file for writing only when its content has to change, so an unchanged hard-linked or immutable file is no longer an error.
…php container

Restore used to create a backed-up file that the site lacked (e.g. a pool file added by hand). It is now written from inside the running php container, where a symlink the container made can't point at the host; existing files are still written in place through a checked handle. On macOS, where config/php isn't mounted, a missing file is skipped as before had no effect. The kept-file warning no longer suggests copying through config/php by hand.
…led update --php

The new php image is pulled before php stops, so the site is down only while its config is switched, and a failed pull changes nothing. A failure after that puts the old config back (with php stopped again), the old compose file and a running php container; enable() now throws instead of exiting so this also covers it. An older config backup of the same version is kept aside instead of removed. A failed restart after a refused update says so.
…nged

A refused write (php.ini changed meanwhile or hard-linked) left an unused backup behind; it is removed, and a failed write reports where the backup is.
… session ID settings

A CR-only php.ini was one line, so a match removed the whole file. Lines now end after every LF and after a CR not followed by LF.
… before a rollback

enable() can start a custom compose service that also mounts config_php before it fails; the rollback now refuses to touch the volume while one runs, as the update itself does.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved rollback volume-access and migration line-splitting issues can permit unsafe host writes or erase unrelated configuration.

2 open issues
What changed in this PR

This PR hardens EasyEngine’s PHP configuration handling against container-controlled symlinks during restore, PHP version updates, enablement, and migrations. It also includes the stacked session-configuration fixes from #519 and #520.

Changes:

  • Adds checked file-access helpers and safer restore and ownership operations.
  • Stops PHP during configuration switches and adds rollback handling.
  • Adds session-setting migrations and restore regression coverage with CI backup storage.
File Description
src/​helper/​site-utils.php Adds checked filesystem helpers and session-setting parsing.
src/​helper/​Site_Backup_Restore.php Hardens PHP config restoration and preserves upload-progress protection.
src/​helper/​class-ee-site.php Adds stopped-container config switching, rollback, and checked ownership changes.
migrations/​container/​20261023233000_site-command_recheck_session_upload_progress.php Rechecks upload-progress settings with backups and rollback support.
migrations/​container/​20261023193000_site-command_drop_deprecated_session_sid_settings.php Removes deprecated session ID settings through checked file access.
features/​upload-progress.sh Provides guarded helpers for integration scenarios.
features/​upload-progress.feature Covers migration and restore behavior for upload-progress settings.
.github/​workflows/​test_and_build.yml Configures local backup storage and tests the current checkout.

Review effort: Balanced

Comment thread src/helper/class-ee-site.php

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The session-ID migration can corrupt valid multiline INI entries and leave php.ini unable to parse.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Handle multiline quoted INI values when stripping settings

migrations/​container/​20261023193000_site-command_drop_deprecated_session_sid_settings.php:219

strip_settings() treats physical lines as complete INI entries without tracking quoted values. A valid multiline entry such as session.sid_length = "26\n" loses only its first line, leaving a dangling quote and making the rewritten php.ini fail to parse; a directive-looking line inside another multiline value is also deleted even though it is not a setting. Please identify complete INI entries, preserving quoted-value contents and removing the entire deprecated entry, before overwriting the live configuration.

2 resolved since last review

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants