Index: /trunk/src/wp-admin/includes/class-plugin-upgrader.php
===================================================================
--- /trunk/src/wp-admin/includes/class-plugin-upgrader.php	(revision 51814)
+++ /trunk/src/wp-admin/includes/class-plugin-upgrader.php	(revision 51815)
@@ -227,7 +227,12 @@
 				'clear_working'     => true,
 				'hook_extra'        => array(
-					'plugin' => $plugin,
-					'type'   => 'plugin',
-					'action' => 'update',
+					'plugin'      => $plugin,
+					'type'        => 'plugin',
+					'action'      => 'update',
+					'temp_backup' => array(
+						'slug' => dirname( $plugin ),
+						'src'  => WP_PLUGIN_DIR,
+						'dir'  => 'plugins',
+					),
 				),
 			)
@@ -343,5 +348,10 @@
 					'is_multi'          => true,
 					'hook_extra'        => array(
-						'plugin' => $plugin,
+						'plugin'      => $plugin,
+						'temp_backup' => array(
+							'slug' => dirname( $plugin ),
+							'src'  => WP_PLUGIN_DIR,
+							'dir'  => 'plugins',
+						),
 					),
 				)
Index: /trunk/src/wp-admin/includes/class-theme-upgrader.php
===================================================================
--- /trunk/src/wp-admin/includes/class-theme-upgrader.php	(revision 51814)
+++ /trunk/src/wp-admin/includes/class-theme-upgrader.php	(revision 51815)
@@ -329,7 +329,12 @@
 				'clear_working'     => true,
 				'hook_extra'        => array(
-					'theme'  => $theme,
-					'type'   => 'theme',
-					'action' => 'update',
+					'theme'       => $theme,
+					'type'        => 'theme',
+					'action'      => 'update',
+					'temp_backup' => array(
+						'slug' => $theme,
+						'src'  => get_theme_root( $theme ),
+						'dir'  => 'themes',
+					),
 				),
 			)
@@ -444,5 +449,10 @@
 					'is_multi'          => true,
 					'hook_extra'        => array(
-						'theme' => $theme,
+						'theme'       => $theme,
+						'temp_backup' => array(
+							'slug' => $theme,
+							'src'  => get_theme_root( $theme ),
+							'dir'  => 'themes',
+						),
 					),
 				)
Index: /trunk/src/wp-admin/includes/class-wp-site-health.php
===================================================================
--- /trunk/src/wp-admin/includes/class-wp-site-health.php	(revision 51814)
+++ /trunk/src/wp-admin/includes/class-wp-site-health.php	(revision 51815)
@@ -1884,4 +1884,194 @@
 
 	/**
+	 * Test available disk space for updates.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array The test results.
+	 */
+	public function get_test_available_updates_disk_space() {
+		$available_space       = function_exists( 'disk_free_space' ) ? (int) @disk_free_space( WP_CONTENT_DIR . '/upgrade/' ) : false;
+		$available_space_in_mb = $available_space / MB_IN_BYTES;
+
+		$result = array(
+			'label'       => __( 'Disk space available to safely perform updates' ),
+			'status'      => 'good',
+			'badge'       => array(
+				'label' => __( 'Security' ),
+				'color' => 'blue',
+			),
+			'description' => sprintf(
+				/* translators: %s: Available disk space in MB or GB. */
+				'<p>' . __( '%s available disk space was detected, update routines can be performed safely.' ),
+				size_format( $available_space )
+			),
+			'actions'     => '',
+			'test'        => 'available_updates_disk_space',
+		);
+
+		if ( $available_space_in_mb < 100 ) {
+			$result['description'] = __( 'Available disk space is low, less than 100 MB available.' );
+			$result['status']      = 'recommended';
+		}
+
+		if ( $available_space_in_mb < 20 ) {
+			$result['description'] = __( 'Available disk space is critically low, less than 20 MB available. Proceed with caution, updates may fail.' );
+			$result['status']      = 'critical';
+		}
+
+		if ( ! $available_space ) {
+			$result['description'] = __( 'Could not determine available disk space for updates.' );
+			$result['status']      = 'recommended';
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Test if plugin and theme updates temp-backup directories are writable or can be created.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+	 *
+	 * @return array The test results.
+	 */
+	public function get_test_update_temp_backup_writable() {
+		global $wp_filesystem;
+
+		$result = array(
+			'label'       => sprintf(
+				/* translators: %s: temp-backup */
+				__( 'Plugin and theme update %s directory is writable' ),
+				'temp-backup'
+			),
+			'status'      => 'good',
+			'badge'       => array(
+				'label' => __( 'Security' ),
+				'color' => 'blue',
+			),
+			'description' => sprintf(
+				/* translators: %s: wp-content/upgrade/temp-backup */
+				'<p>' . __( 'The %s directory used to improve the stability of plugin and theme updates is writable.' ),
+				'<code>wp-content/upgrade/temp-backup</code>'
+			),
+			'actions'     => '',
+			'test'        => 'update_temp_backup_writable',
+		);
+
+		if ( ! $wp_filesystem ) {
+			if ( ! function_exists( 'WP_Filesystem' ) ) {
+				require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
+			}
+			WP_Filesystem();
+		}
+		$wp_content = $wp_filesystem->wp_content_dir();
+
+		$upgrade_dir_exists      = $wp_filesystem->is_dir( "$wp_content/upgrade" );
+		$upgrade_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade" );
+		$backup_dir_exists       = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup" );
+		$backup_dir_is_writable  = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup" );
+
+		$plugins_dir_exists      = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup/plugins" );
+		$plugins_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup/plugins" );
+		$themes_dir_exists       = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup/themes" );
+		$themes_dir_is_writable  = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup/themes" );
+
+		if ( $plugins_dir_exists && ! $plugins_dir_is_writable && $themes_dir_exists && ! $themes_dir_is_writable ) {
+			$result['status']      = 'critical';
+			$result['label']       = sprintf(
+				/* translators: %s: temp-backup */
+				__( 'Plugins and themes %s directories exist but are not writable' ),
+				'temp-backup'
+			);
+			$result['description'] = sprintf(
+				/* translators: 1: wp-content/upgrade/temp-backup/plugins, 2: wp-content/upgrade/temp-backup/themes. */
+				'<p>' . __( 'The %1$s and %2$s directories exist but are not writable. These directories are used to improve the stability of plugin updates. Please make sure the server has write permissions to these directories.' ) . '</p>',
+				'<code>wp-content/upgrade/temp-backup/plugins</code>',
+				'<code>wp-content/upgrade/temp-backup/themes</code>'
+			);
+			return $result;
+		}
+
+		if ( $plugins_dir_exists && ! $plugins_dir_is_writable ) {
+			$result['status']      = 'critical';
+			$result['label']       = sprintf(
+				/* translators: %s: temp-backup */
+				__( 'Plugins %s directory exists but is not writable' ),
+				'temp-backup'
+			);
+			$result['description'] = sprintf(
+				/* translators: %s: wp-content/upgrade/temp-backup/plugins */
+				'<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of plugin updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
+				'<code>wp-content/upgrade/temp-backup/plugins</code>'
+			);
+			return $result;
+		}
+
+		if ( $themes_dir_exists && ! $themes_dir_is_writable ) {
+			$result['status']      = 'critical';
+			$result['label']       = sprintf(
+				/* translators: %s: temp-backup */
+				__( 'Themes %s directory exists but is not writable' ),
+				'temp-backup'
+			);
+			$result['description'] = sprintf(
+				/* translators: %s: wp-content/upgrade/temp-backup/themes */
+				'<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
+				'<code>wp-content/upgrade/temp-backup/themes</code>'
+			);
+			return $result;
+		}
+
+		if ( ( ! $plugins_dir_exists || ! $themes_dir_exists ) && $backup_dir_exists && ! $backup_dir_is_writable ) {
+			$result['status']      = 'critical';
+			$result['label']       = sprintf(
+				/* translators: %s: temp-backup */
+				__( 'The %s directory exists but is not writable' ),
+				'temp-backup'
+			);
+			$result['description'] = sprintf(
+				/* translators: %s: wp-content/upgrade/temp-backup */
+				'<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of plugin and theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
+				'<code>wp-content/upgrade/temp-backup</code>'
+			);
+			return $result;
+		}
+
+		if ( ! $backup_dir_exists && $upgrade_dir_exists && ! $upgrade_dir_is_writable ) {
+			$result['status']      = 'critical';
+			$result['label']       = sprintf(
+				/* translators: %s: upgrade */
+				__( 'The %s directory exists but is not writable' ),
+				'upgrade'
+			);
+			$result['description'] = sprintf(
+				/* translators: %s: wp-content/upgrade */
+				'<p>' . __( 'The %s directory exists but is not writable. This directory is used for plugin and theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
+				'<code>wp-content/upgrade</code>'
+			);
+			return $result;
+		}
+
+		if ( ! $upgrade_dir_exists && ! $wp_filesystem->is_writable( $wp_content ) ) {
+			$result['status']      = 'critical';
+			$result['label']       = sprintf(
+				/* translators: %s: upgrade */
+				__( 'The %s directory cannot be created' ),
+				'upgrade'
+			);
+			$result['description'] = sprintf(
+				/* translators: 1: wp-content/upgrade, 2: wp-content. */
+				'<p>' . __( 'The %1$s directory does not exist, and the server does not have write permissions in %2$s to create it. This directory is used for plugin and theme updates. Please make sure the server has write permissions in %2$s.' ) . '</p>',
+				'<code>wp-content/upgrade</code>',
+				'<code>wp-content</code>'
+			);
+			return $result;
+		}
+
+		return $result;
+	}
+
+	/**
 	 * Test if loopbacks work as expected.
 	 *
@@ -2267,68 +2457,77 @@
 		$tests = array(
 			'direct' => array(
-				'wordpress_version'         => array(
+				'wordpress_version'            => array(
 					'label' => __( 'WordPress Version' ),
 					'test'  => 'wordpress_version',
 				),
-				'plugin_version'            => array(
+				'plugin_version'               => array(
 					'label' => __( 'Plugin Versions' ),
 					'test'  => 'plugin_version',
 				),
-				'theme_version'             => array(
+				'theme_version'                => array(
 					'label' => __( 'Theme Versions' ),
 					'test'  => 'theme_version',
 				),
-				'php_version'               => array(
+				'php_version'                  => array(
 					'label' => __( 'PHP Version' ),
 					'test'  => 'php_version',
 				),
-				'php_extensions'            => array(
+				'php_extensions'               => array(
 					'label' => __( 'PHP Extensions' ),
 					'test'  => 'php_extensions',
 				),
-				'php_default_timezone'      => array(
+				'php_default_timezone'         => array(
 					'label' => __( 'PHP Default Timezone' ),
 					'test'  => 'php_default_timezone',
 				),
-				'php_sessions'              => array(
+				'php_sessions'                 => array(
 					'label' => __( 'PHP Sessions' ),
 					'test'  => 'php_sessions',
 				),
-				'sql_server'                => array(
+				'sql_server'                   => array(
 					'label' => __( 'Database Server version' ),
 					'test'  => 'sql_server',
 				),
-				'utf8mb4_support'           => array(
+				'utf8mb4_support'              => array(
 					'label' => __( 'MySQL utf8mb4 support' ),
 					'test'  => 'utf8mb4_support',
 				),
-				'ssl_support'               => array(
+				'ssl_support'                  => array(
 					'label' => __( 'Secure communication' ),
 					'test'  => 'ssl_support',
 				),
-				'scheduled_events'          => array(
+				'scheduled_events'             => array(
 					'label' => __( 'Scheduled events' ),
 					'test'  => 'scheduled_events',
 				),
-				'http_requests'             => array(
+				'http_requests'                => array(
 					'label' => __( 'HTTP Requests' ),
 					'test'  => 'http_requests',
 				),
-				'rest_availability'         => array(
+				'rest_availability'            => array(
 					'label'     => __( 'REST API availability' ),
 					'test'      => 'rest_availability',
 					'skip_cron' => true,
 				),
-				'debug_enabled'             => array(
+				'debug_enabled'                => array(
 					'label' => __( 'Debugging enabled' ),
 					'test'  => 'is_in_debug_mode',
 				),
-				'file_uploads'              => array(
+				'file_uploads'                 => array(
 					'label' => __( 'File uploads' ),
 					'test'  => 'file_uploads',
 				),
-				'plugin_theme_auto_updates' => array(
+				'plugin_theme_auto_updates'    => array(
 					'label' => __( 'Plugin and theme auto-updates' ),
 					'test'  => 'plugin_theme_auto_updates',
+				),
+				'update_temp_backup_writable'  => array(
+					/* translators: %s: temp-backup */
+					'label' => sprintf( __( 'Updates %s directory access' ), 'temp-backup' ),
+					'test'  => 'update_temp_backup_writable',
+				),
+				'available_updates_disk_space' => array(
+					'label' => __( 'Available disk space' ),
+					'test'  => 'available_updates_disk_space',
 				),
 			),
Index: /trunk/src/wp-admin/includes/class-wp-upgrader.php
===================================================================
--- /trunk/src/wp-admin/includes/class-wp-upgrader.php	(revision 51814)
+++ /trunk/src/wp-admin/includes/class-wp-upgrader.php	(revision 51815)
@@ -134,9 +134,23 @@
 	 * and also add the generic strings to `WP_Upgrader::$strings`.
 	 *
+	 * Additionally, it will schedule a weekly task to clean up the temp-backup directory.
+	 *
 	 * @since 2.8.0
+	 * @since 5.9.0 Added the `schedule_temp_backup_cleanup()` task.
 	 */
 	public function init() {
 		$this->skin->set_upgrader( $this );
 		$this->generic_strings();
+		$this->schedule_temp_backup_cleanup();
+	}
+
+	/**
+	 * Schedule cleanup of the temp-backup directory.
+	 *
+	 * @since 5.9.0
+	 */
+	protected function schedule_temp_backup_cleanup() {
+		wp_schedule_event( time(), 'weekly', 'delete_temp_updater_backups' );
+		add_action( 'delete_temp_updater_backups', array( $this, 'delete_all_temp_backups' ) );
 	}
 
@@ -167,4 +181,11 @@
 		$this->strings['maintenance_start'] = __( 'Enabling Maintenance mode&#8230;' );
 		$this->strings['maintenance_end']   = __( 'Disabling Maintenance mode&#8230;' );
+
+		/* translators: %s: temp-backup */
+		$this->strings['temp_backup_mkdir_failed'] = sprintf( __( 'Could not create the %s directory.' ), 'temp-backup' );
+		/* translators: %s: temp-backup */
+		$this->strings['temp_backup_move_failed']    = sprintf( __( 'Could not move old version to the %s directory.' ), 'temp-backup' );
+		$this->strings['temp_backup_restore_failed'] = __( 'Could not restore original version.' );
+
 	}
 
@@ -314,4 +335,7 @@
 		if ( ! empty( $upgrade_files ) ) {
 			foreach ( $upgrade_files as $file ) {
+				if ( 'temp-backup' === $file['name'] ) {
+					continue;
+				}
 				$wp_filesystem->delete( $upgrade_folder . $file['name'], true );
 			}
@@ -492,4 +516,11 @@
 		if ( is_wp_error( $res ) ) {
 			return $res;
+		}
+
+		if ( ! empty( $args['hook_extra']['temp_backup'] ) ) {
+			$temp_backup = $this->move_to_temp_backup_dir( $args['hook_extra']['temp_backup'] );
+			if ( is_wp_error( $temp_backup ) ) {
+				return $temp_backup;
+			}
 		}
 
@@ -812,4 +843,7 @@
 		$this->skin->set_result( $result );
 		if ( is_wp_error( $result ) ) {
+			if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
+				$this->restore_temp_backup( $options['hook_extra']['temp_backup'] );
+			}
 			$this->skin->error( $result );
 
@@ -823,4 +857,9 @@
 
 		$this->skin->after();
+
+		// Clean up the backup kept in the temp-backup directory.
+		if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
+			$this->delete_temp_backup( $options['hook_extra']['temp_backup'] );
+		}
 
 		if ( ! $options['is_multi'] ) {
@@ -949,4 +988,152 @@
 	}
 
+	/**
+	 * Moves the plugin/theme being updated into a temp-backup directory.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+	 *
+	 * @param array $args Array of data for the temp-backup. Must include a slug, the source, and directory.
+	 * @return bool|WP_Error
+	 */
+	public function move_to_temp_backup_dir( $args ) {
+		global $wp_filesystem;
+
+		if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
+			return false;
+		}
+
+		$dest_dir = $wp_filesystem->wp_content_dir() . 'upgrade/temp-backup/';
+		// Create the temp-backup directory if it doesn't exist.
+		if ( (
+				! $wp_filesystem->is_dir( $dest_dir )
+				&& ! $wp_filesystem->mkdir( $dest_dir )
+			) || (
+				! $wp_filesystem->is_dir( $dest_dir . $args['dir'] . '/' )
+				&& ! $wp_filesystem->mkdir( $dest_dir . $args['dir'] . '/' )
+			)
+		) {
+			return new WP_Error( 'fs_temp_backup_mkdir', $this->strings['temp_backup_mkdir_failed'] );
+		}
+
+		$src  = trailingslashit( $args['src'] ) . $args['slug'];
+		$dest = $dest_dir . $args['dir'] . '/' . $args['slug'];
+
+		// Delete the temp-backup directory if it already exists.
+		if ( $wp_filesystem->is_dir( $dest ) ) {
+			$wp_filesystem->delete( $dest, true );
+		}
+
+		// Move to the temp-backup directory.
+		if ( ! $wp_filesystem->move( $src, $dest, true ) ) {
+			return new WP_Error( 'fs_temp_backup_move', $this->strings['temp_backup_move_failed'] );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Restores the plugin/theme from the temp-backup directory.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+	 *
+	 * @param array $args Array of data for the temp-backup. Must include a slug, the source, and directory.
+	 * @return bool|WP_Error
+	 */
+	public function restore_temp_backup( $args ) {
+		global $wp_filesystem;
+
+		if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
+			return false;
+		}
+
+		$src  = $wp_filesystem->wp_content_dir() . 'upgrade/temp-backup/' . $args['dir'] . '/' . $args['slug'];
+		$dest = trailingslashit( $args['src'] ) . $args['slug'];
+
+		if ( $wp_filesystem->is_dir( $src ) ) {
+			// Cleanup.
+			if ( $wp_filesystem->is_dir( $dest ) && ! $wp_filesystem->delete( $dest, true ) ) {
+				return new WP_Error( 'fs_temp_backup_delete', $this->strings['temp_backup_restore_failed'] );
+			}
+
+			// Move it.
+			if ( ! $wp_filesystem->move( $src, $dest, true ) ) {
+				return new WP_Error( 'fs_temp_backup_delete', $this->strings['temp_backup_restore_failed'] );
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Deletes a temp-backup.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+	 *
+	 * @param array $args Array of data for the temp-backup. Must include a slug, the source, and directory.
+	 * @return bool
+	 */
+	public function delete_temp_backup( $args ) {
+		global $wp_filesystem;
+
+		if ( empty( $args['slug'] ) || empty( $args['dir'] ) ) {
+			return false;
+		}
+
+		return $wp_filesystem->delete(
+			$wp_filesystem->wp_content_dir() . "upgrade/temp-backup/{$args['dir']}/{$args['slug']}",
+			true
+		);
+	}
+
+	/**
+	 * Deletes all contents of the temp-backup directory.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+	 */
+	public function delete_all_temp_backups() {
+		/*
+		 * Check if there's a lock, or if currently performing an Ajax request,
+		 * in which case there's a chance we're doing an update.
+		 * Reschedule for an hour from now and exit early.
+		 */
+		if ( get_option( 'core_updater.lock' ) || get_option( 'auto_updater.lock' ) || wp_doing_ajax() ) {
+			wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'delete_temp_updater_backups' );
+			return;
+		}
+
+		add_action(
+			'shutdown',
+			/*
+			 * This action runs on shutdown to make sure there's no plugin updates currently running.
+			 * Using a closure in this case is OK since the action can be removed by removing the parent hook.
+			 */
+			function() {
+				global $wp_filesystem;
+
+				if ( ! $wp_filesystem ) {
+					include_once ABSPATH . '/wp-admin/includes/file.php';
+					WP_Filesystem();
+				}
+
+				$dirlist = $wp_filesystem->dirlist( $wp_filesystem->wp_content_dir() . 'upgrade/temp-backup/' );
+
+				foreach ( array_keys( $dirlist ) as $dir ) {
+					if ( '.' === $dir || '..' === $dir ) {
+						continue;
+					}
+
+					$wp_filesystem->delete( $wp_filesystem->wp_content_dir() . 'upgrade/temp-backup/' . $dir, true );
+				}
+			}
+		);
+	}
 }
 
