From f88bbeddc830923c21fa374604893f68755a98c1 Mon Sep 17 00:00:00 2001 From: arjupan Date: Mon, 30 Mar 2026 17:25:20 +0200 Subject: [PATCH] web: Fix "too much recursion" problem with arcgis flex sdk (close #22860) --- core/src/display_object.rs | 27 +++++++++++++++++++++++++++ core/src/display_object/container.rs | 20 ++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/core/src/display_object.rs b/core/src/display_object.rs index d7f2143e2ff7..417eb2f422d5 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -691,6 +691,14 @@ impl<'gc> DisplayObjectBase<'gc> { self.set_flag(DisplayObjectFlags::AVM1_PENDING_REMOVAL, value); } + pub fn being_removed(&self) -> bool { + self.contains_flag(DisplayObjectFlags::BEING_REMOVED) + } + + pub fn set_being_removed(&self, value: bool) { + self.set_flag(DisplayObjectFlags::BEING_REMOVED, value); + } + fn scale_rotation_cached(&self) -> bool { self.contains_flag(DisplayObjectFlags::SCALE_ROTATION_CACHED) } @@ -2033,6 +2041,19 @@ pub trait TDisplayObject<'gc>: self.base().set_avm1_pending_removal(value) } + #[no_dynamic] + /// Whether this object is currently dispatching its `removed`/`removedFromStage` events. + /// Used as a reentrancy guard in `dispatch_removed_event` to prevent infinite recursion + /// when an AS3 event listener calls `remove_child` again during event dispatch. + fn being_removed(self) -> bool { + self.base().being_removed() + } + + #[no_dynamic] + fn set_being_removed(self, value: bool) { + self.base().set_being_removed(value) + } + /// Whether this display object is visible. /// Invisible objects are not rendered, but otherwise continue to exist normally. /// Returned by the `_visible`/`visible` ActionScript properties. @@ -2995,6 +3016,12 @@ bitflags! { /// (they need to be instantiated "manually" by /// `Sprite.constructChildren`). const MANUAL_FRAME_CONSTRUCT = 1 << 16; + + /// Whether this object is currently having its `removed`/`removedFromStage` + /// events dispatched. Used to prevent re-entrant calls to `remove_child` + /// during event dispatch (which would cause infinite recursion), matching + /// Flash Player behaviour. + const BEING_REMOVED = 1 << 17; } } diff --git a/core/src/display_object/container.rs b/core/src/display_object/container.rs index f58888c5a1c9..125aa3680952 100644 --- a/core/src/display_object/container.rs +++ b/core/src/display_object/container.rs @@ -44,14 +44,34 @@ pub fn dispatch_removed_from_stage_event<'gc>( /// Dispatch the `removed` event on a child and log any errors encountered /// whilst doing so. +/// +/// This function is protected against re-entrant calls: if an AS3 event listener +/// reacts to `removed`/`removedFromStage` by calling `remove_child` again on the +/// same object (or one of its ancestors), the recursive invocation will find the +/// `BEING_REMOVED` flag set and return immediately, matching Flash Player behaviour +/// and preventing the infinite recursion / "too much recursion" crash. pub fn dispatch_removed_event<'gc>(child: DisplayObject<'gc>, context: &mut UpdateContext<'gc>) { + // Reentrancy guard: if we are already dispatching the removed event for + // this child (e.g. because an event listener called remove_child again), + // bail out immediately to avoid infinite recursion. + if child.being_removed() { + return; + } + if let Some(object) = child.object2() { + child.set_being_removed(true); + let removed_evt = Avm2EventObject::bare_event(context, "removed", true, false); Avm2::dispatch_event(context, removed_evt, object.into()); if child.is_on_stage(context) { dispatch_removed_from_stage_event(child, context) } + + // Reset the flag so that the object can be correctly removed/re-added + // in future operations (e.g. if it is added back to the display list + // and then removed again). + child.set_being_removed(false); } }