Make WordPress Core

Opened 16 hours ago

#65942 new defect (bug)

WP_Upgrader::maintenance_mode() trusts an unconnected $wp_filesystem, causing a fatal TypeError during automatic updates

Reported by: hatterops Owned by:
Priority: normal Milestone: Awaiting Review
Component: Upgrade/Install Version: 7.0.4
Severity: normal Keywords:
Cc: Focuses:

Description

WP_Upgrader::maintenance_mode() only initializes the filesystem if $wp_filesystem isn't already set:

if ( ! $wp_filesystem ) {
    WP_Filesystem();
}

This assumes that if the global is already an object, it must be properly connected. That assumption doesn't hold: if something earlier in the same request leaves a WP_Filesystem_FTPext instance in that global without connect() ever having been called on it (or after a connection that's since gone stale), maintenance_mode() uses it anyway. The next filesystem call that needs a live connection — in our case WP_Filesystem_Base::find_folder() → WP_Filesystem_FTPext::exists() — passes the unset $this->link straight into ftp_nlist(), which fatals under PHP 8's strict internal-function typing:

PHP Fatal error: Uncaught TypeError: ftp_nlist(): Argument #1 ($ftp) must be of type FTP\Connection, null given in wp-admin/includes/class-wp-filesystem-ftpext.php:438
Stack trace:
#0 wp-admin/includes/class-wp-filesystem-ftpext.php(438): ftp_nlist()
#1 wp-admin/includes/class-wp-filesystem-base.php(231): WP_Filesystem_FTPext->exists()
#2 wp-admin/includes/class-wp-filesystem-base.php(58): WP_Filesystem_Base->find_folder()
#3 wp-admin/includes/class-wp-upgrader.php(1025): WP_Filesystem_Base->abspath()
#4 wp-admin/includes/class-wp-automatic-updater.php(474): WP_Upgrader->maintenance_mode()
#5 wp-admin/includes/class-wp-automatic-updater.php(676): WP_Automatic_Updater->update()
#6 wp-includes/update.php(890): WP_Automatic_Updater->run()
#7 (via wp_maybe_auto_update → wp_version_check, triggered under real WP-Cron)

thrown in wp-admin/includes/class-wp-filesystem-ftpext.php on line 438

Because maintenance_mode() is called once per WP_Automatic_Updater::update() invocation — not per attempted file operation — this single uncaught fatal aborts the entire automatic-update run for every item scheduled in that cycle (core, every opted-in plugin, every opted-in theme), not just the one that happened to trigger it.

Confirmed NOT the cause (ruled out during investigation, worth noting so triage doesn't re-tread this): FTP credentials (verified valid via direct FTP login with the exact stored values), disable_functions, the ftp extension not being loaded, and DNS/IPv6 resolution ambiguity on the configured FTP host (php_network_connect_socket_to_host() in main/network.c already iterates and falls back across resolved addresses correctly).

A parallel, manually-constructed WP_Filesystem_FTPext object using the site's real credentials connects successfully every time it's tested — including one second before an observed real crash in the same request — confirming the defect is specifically about whether connect() gets called on the object that ends up being used, not whether a connection can succeed.

What's not yet isolated: the exact condition that leaves the earlier $wp_filesystem unconnected in the first place (it's intermittent, not deterministic). request_filesystem_credentials() (called from WP_Upgrader::fs_connect(), which does always call WP_Filesystem()) seems like the most likely place to look, but this hasn't been confirmed.

Suggested fix

maintenance_mode()'s check should verify the existing $wp_filesystem is actually usable, not just non-null — e.g. checking connection state where the concrete class exposes one, or simply always calling WP_Filesystem() here rather than conditionally skipping it (since a fresh connect is cheap relative to the cost of silently aborting the whole update run).

Workaround in use (mu-plugin, not a core patch):

add_action('wp_maybe_auto_update', function () {
    global $wp_filesystem;
    if ($wp_filesystem instanceof WP_Filesystem_FTPext) {
        $needs_reconnect = empty($wp_filesystem->link) || @ftp_pwd($wp_filesystem->link) === false;
        if ($needs_reconnect) {
            $wp_filesystem = null; // forces maintenance_mode()'s check to do a fresh, real connect
        }
    }
}, 1);

This has been running on the previously failing site and confirmed to allow automatic updates to complete successfully on a previously-reliably-crashing site.

Change History (0)

Note: See TracTickets for help on using tickets.