|
Affected versions:
- WPML Multilingual CMS: 4.9.2
- ACFML (Advanced Custom Fields Multilingual): 2.2.1
- ACF Pro: 6.x
- PHP: 8.3.30
Steps to reproduce:
1. Create an ACF Options Page with 50+ fields, including post_object and taxonomy field types
2. Set WPML translation preferences on these fields to "Copy" or "Copy once"
3. Navigate to the Options Page in a non-default language (e.g. ?page=website-settings&lang=en)
Expected: Options page loads normally.
Actual: Fatal error:
Maximum call stack size of 8339456 bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?
Root cause analysis:
There are two compounding issues in ACFML\Options\EditorHooks::preRenderOnTranslatedOptionsPage():
Issue 1 — Excessive stack depth per field:
The acf/load_value closure added at line 103 calls $this->sitepress->get_current_language() (line 117) for every single field. Each call passes
through WPML_Language_Resolution::filter_for_legal_langs() which calls Str::includes() — this goes through the WPML FP currying/pipe/compose
chain consuming ~24 stack frames per invocation. With 90+ fields this adds up significantly but alone doesn't cause overflow.
Issue 2 — True recursion via convertRelationshipField (the actual crash):
For fields with WPML_COPY_CUSTOM_FIELD or WPML_COPY_ONCE_CUSTOM_FIELD preferences, the closure calls convertRelationshipField() which triggers:
EditorHooks closure (acf/load_value filter)
→ convertRelationshipField()
→ WPML_ACF_Worker::convertMetaValue()
→ WPML_ACF_Field::convert_ids()
→ WPML_ACF_Term_Id::convert()
→ get_field_object() ← triggers ACF field loading
→ acf_get_value()
→ apply_filters('acf/load_value')
→ EditorHooks closure ← re-enters the same filter
→ convertRelationshipField()
→ ... (infinite loop)
WPML_ACF_Term_Id::convert() at line 45 calls get_field_object(), which internally calls acf_get_value(), which fires the acf/load_value filter
— re-entering the same ACFML closure. This creates genuine infinite recursion.
Why this only manifests on PHP 8.3+:
PHP 8.3 introduced zend.max_allowed_stack_size which actively detects deep/infinite recursion. On PHP 8.2 and below, the same recursion occurs
but is not detected — the process either survives (if OS stack limit is large enough) or segfaults silently.
Why this only manifests with many fields:
Small Options Pages with few relationship-type fields may not trigger convertRelationshipField or may not nest deeply enough to exceed the
stack limit. The threshold depends on the number of post_object/taxonomy fields with Copy/Copy-once translation preferences.
Suggested fix:
1. Add a re-entrancy guard to the acf/load_value closure in preRenderOnTranslatedOptionsPage() to prevent recursive field loading:
add_filter( 'acf/load_value', function( $value, $fieldPostId, $field ) use ( $postId ) {
static $processing = false;
if ( $processing ) {
return $value; // prevent re-entrant calls from convertRelationshipField
}
// ... existing checks ...
$processing = true;
$currentLanguage = $this->sitepress->get_current_language();
// ... existing logic ...
$processing = false;
return $result;
}, 10, 3 );
2. Cache get_current_language() result in a variable before the closure, rather than calling it per-field inside acf/load_value. This
eliminates ~24 unnecessary FP stack frames per field.
Our current workaround (in theme's functions.php):
We unhook preRenderOnTranslatedOptionsPage from acf/pre_render_fields and replace it with a patched version that (a) caches
get_current_language() and (b) includes a static $processing re-entrancy guard. This fully resolves the crash.
|