Commit 5c3f240f14b for woocommerce

commit 5c3f240f14bf563660053871811c1bf6f9de421c
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date:   Mon Sep 14 18:29:22 2026 +0100

    Fix reentrant feature definition initialization (#68417)

diff --git a/plugins/woocommerce/changelog/fix-features-controller-reentrancy b/plugins/woocommerce/changelog/fix-features-controller-reentrancy
new file mode 100644
index 00000000000..de5f765067b
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-features-controller-reentrancy
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent recursive feature definition initialization when hooks check WooCommerce features during translation or option callbacks.
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index 63320e87469..959d45f541b 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -124,6 +124,13 @@ class FeaturesController {
 	 */
 	private bool $registered_additional_features_via_class_calls = false;

+	/**
+	 * Flag indicating if hardcoded feature definitions are currently being initialized.
+	 *
+	 * @var bool
+	 */
+	private bool $initializing_feature_definitions = false;
+
 	/**
 	 * Flag indicating if we are currently delaying plugin normalization.
 	 *
@@ -260,9 +267,7 @@ class FeaturesController {
 	 * @return array[]
 	 */
 	private function get_feature_definitions() {
-		if ( empty( $this->features ) ) {
-			$this->init_feature_definitions();
-		}
+		$this->maybe_init_feature_definitions();

 		if ( ! $this->registered_additional_features_via_class_calls ) {
 			// This needs to be set to true *before* additional feature definition calls are made,
@@ -282,6 +287,23 @@ class FeaturesController {
 		return $this->features;
 	}

+	/**
+	 * Initialize hardcoded feature definitions if they have not been initialized yet.
+	 */
+	private function maybe_init_feature_definitions(): void {
+		if ( ! empty( $this->features ) || $this->initializing_feature_definitions ) {
+			return;
+		}
+
+		$this->initializing_feature_definitions = true;
+
+		try {
+			$this->init_feature_definitions();
+		} finally {
+			$this->initializing_feature_definitions = false;
+		}
+	}
+
 	/**
 	 * Initialize the hardcoded feature definitions array.
 	 * This doesn't include:
@@ -783,9 +805,7 @@ class FeaturesController {
 			return;
 		}

-		if ( empty( $this->features ) ) {
-			$this->init_feature_definitions();
-		}
+		$this->maybe_init_feature_definitions();

 		/**
 		 * The action for registering features.
diff --git a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
index 6fbc87c4d5a..dae69d05401 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
@@ -282,6 +282,48 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
 		$this->assertEquals( $expected_to_be_enabled, $this->sut->feature_is_enabled( $feature_id ) );
 	}

+	/**
+	 * @testdox Feature definition initialization is safe when a translation callback checks a feature.
+	 */
+	public function test_feature_definition_initialization_is_reentrant_safe() {
+		$reflection_class = new \ReflectionClass( $this->sut );
+
+		$features_property = $reflection_class->getProperty( 'features' );
+		$features_property->setAccessible( true );
+		$features_property->setValue( $this->sut, array() );
+
+		$compat_property = $reflection_class->getProperty( 'compatibility_info_by_feature' );
+		$compat_property->setAccessible( true );
+		$compat_property->setValue( $this->sut, array() );
+
+		$reentrant_feature_enabled = null;
+		$translation_count         = 0;
+
+		$gettext_filter = function ( $translation, $text, $domain ) use ( &$reentrant_feature_enabled, &$translation_count ) {
+			unset( $text, $domain ); // Avoid parameter not used PHPCS errors.
+
+			++$translation_count;
+
+			if ( null === $reentrant_feature_enabled ) {
+				$reentrant_feature_enabled = $this->sut->feature_is_enabled( 'order_withdrawal' );
+			}
+
+			return $translation;
+		};
+
+		add_filter( 'gettext', $gettext_filter, 10, 3 );
+
+		try {
+			$this->sut->get_features( true, false );
+		} finally {
+			remove_filter( 'gettext', $gettext_filter, 10 );
+		}
+
+		$this->assertFalse( $reentrant_feature_enabled );
+		$this->assertGreaterThan( 0, $translation_count );
+		$this->assertNotNull( $this->sut->get_feature_definition( 'order_withdrawal' ) );
+	}
+
 	/**
 	 * @testdox 'change_feature_enable' does nothing and returns false for an invalid feature id.
 	 */