Commit 4554e1861e for woocommerce

commit 4554e1861e3198fdc8eca582d62902e9e022bcf2
Author: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date:   Thu Feb 5 09:40:23 2026 -0800

    Prevent Concurrent Installations (#63100)

    Due to the way that transients were being used, WC_Install::install()
    was vulnerable to time-of-check to time-of-use race conditions.
    This commit replaces the transient lock with an atomic one
    that relies on database semantics.

diff --git a/plugins/woocommerce/changelog/fix-wc-install-atomicity b/plugins/woocommerce/changelog/fix-wc-install-atomicity
new file mode 100644
index 0000000000..dfa855b96f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wc-install-atomicity
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Use atomic locking to prevent WC_Install::install() from running concurrently.
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 411e92a8b6..5971a15c29 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -586,19 +586,16 @@ class WC_Install {
 			return;
 		}

-		// Check if we are not already running this routine.
-		if ( self::is_installing() ) {
+		// Create a lock to prevent multiple installs from running simultaneously.
+		if ( ! self::create_lock() ) {
 			return;
 		}

-		// If we made it till here nothing is running yet, lets set the transient now.
-		set_transient( 'wc_installing', 'yes', MINUTE_IN_SECONDS * 10 );
-		wc_maybe_define_constant( 'WC_INSTALLING', true );
-
 		try {
+			wc_maybe_define_constant( 'WC_INSTALLING', true );
 			self::install_core();
 		} finally {
-			delete_transient( 'wc_installing' );
+			self::release_lock();
 		}

 		// Use add_option() here to avoid overwriting this value with each
@@ -661,12 +658,50 @@ class WC_Install {
 	}

 	/**
-	 * Returns true if we're installing.
+	 * Attempts to acquire an installation lock.
 	 *
-	 * @return bool
+	 * @return bool True if a lock was acquired, otherwise false.
+	 */
+	private static function create_lock(): bool {
+		global $wpdb;
+
+		// Insert will fail if it already exists so this functions as a mutex.
+		$created_lock = $wpdb->query(
+			$wpdb->prepare(
+				"INSERT INTO {$wpdb->options} (option_name, option_value, autoload) VALUES ('wc_installing', %d, 'no')",
+				time()
+			)
+		);
+
+		// Take over the lock if it's stale (older than 10 minutes).
+		if ( ! $created_lock ) {
+			$created_lock = $wpdb->query(
+				$wpdb->prepare(
+					"UPDATE {$wpdb->options} SET option_value = %d WHERE option_name = 'wc_installing' AND option_value < %d",
+					time(),
+					time() - ( MINUTE_IN_SECONDS * 10 )
+				)
+			);
+		}
+
+		if ( $created_lock ) {
+			// Set the transient for backward compatibility in case others are relying on it to signal an ongoing install.
+			set_transient( 'wc_installing', 'yes', MINUTE_IN_SECONDS * 10 );
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Releases the installation lock.
 	 */
-	private static function is_installing() {
-		return 'yes' === get_transient( 'wc_installing' );
+	private static function release_lock(): void {
+		// Delete the transient BEFORE the option to avoid races that might result in an active lock with an empty transient.
+		delete_transient( 'wc_installing' );
+
+		global $wpdb;
+		$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name = 'wc_installing'" );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/util/install.php b/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
index 7bf4bd0da2..70c15e4f32 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
@@ -190,5 +190,6 @@ class WC_Tests_Install extends WC_Unit_Test_Case {

 		include dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) . '/uninstall.php';
 		delete_transient( 'wc_installing' );
+		delete_option( 'wc_installing' );
 	}
 }
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/category-lookup.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/category-lookup.php
index 90da372105..8b605ea98d 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/category-lookup.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/category-lookup.php
@@ -26,7 +26,9 @@ class WC_Admin_Tests_Category_Lookup extends WP_UnitTestCase {
 	 */

 	public function setUp(): void {
-		delete_transient('wc_installing');
+		delete_transient( 'wc_installing' );
+		delete_option( 'wc_installing' );
+
 		parent::setUp();
 		$parent                = wp_insert_term( 'test_parent', 'product_cat' );
 		$parent2               = wp_insert_term( 'test_parent_2', 'product_cat' );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php
index 37d75aa9af..104f320e13 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php
@@ -139,6 +139,8 @@ class WC_Admin_Tests_Install extends WP_UnitTestCase {
 	 */
 	public function test_options_are_set() {
 		delete_transient( 'wc_installing' );
+		delete_option( 'wc_installing' );
+
 		WC_Install::install();

 		$options = array(
@@ -157,6 +159,8 @@ class WC_Admin_Tests_Install extends WP_UnitTestCase {
 	 */
 	public function test_woocommerce_admin_installed_action() {
 		delete_transient( 'wc_installing' );
+		delete_option( 'wc_installing' );
+
 		WC_Install::install();
 		$this->assertTrue( did_action( 'woocommerce_admin_installed' ) > 0 );
 	}
@@ -229,6 +233,8 @@ class WC_Admin_Tests_Install extends WP_UnitTestCase {
 	 */
 	public function test_migrate_options() {
 		delete_transient( 'wc_installing' );
+		delete_option( 'wc_installing' );
+
 		WC_Install::install();
 		$this->assertTrue( defined( 'WC_ADMIN_MIGRATING_OPTIONS' ) );
 		$migrated_options = array(