Skip to content

Fix infinite recursion in Outbox::add() causing OOM#3146

Closed
jeherve wants to merge 1 commit intotrunkfrom
fix/outbox-add-infinite-recursion
Closed

Fix infinite recursion in Outbox::add() causing OOM#3146
jeherve wants to merge 1 commit intotrunkfrom
fix/outbox-add-infinite-recursion

Conversation

@jeherve
Copy link
Copy Markdown
Member

@jeherve jeherve commented Apr 3, 2026

Fixes #3145

Proposed changes:

  • Temporarily unhook Post::triage from wp_after_insert_post before Outbox::add() calls wp_insert_post() and wp_update_post(), preventing an infinite recursion loop that exhausts memory.
  • Uses a conditional guard ($has_triage) mirroring the existing $has_kses pattern in the same method, so the hook is only removed/restored if it was registered in the first place.

Other information:

  • Have you written new tests for your changes, if applicable?

Testing instructions:

  • Confirm that publishing a new post still creates an outbox entry (check ap_outbox posts in the database or via the REST API).
  • Confirm that updating a published post creates an Update activity in the outbox.
  • If possible, test on a site where a plugin or filter adds activitypub support to the ap_outbox post type, or overrides activitypub_is_post_disabled to return false for outbox posts — this was the scenario that triggered the infinite loop.

Changelog entry

  • Automatically create a changelog entry from the details below.
Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Added - for new features
  • Changed - for changes in existing functionality
  • Deprecated - for soon-to-be removed features
  • Removed - for now removed features
  • Fixed - for any bug fixes
  • Security - in case of vulnerabilities

Message

Fix an infinite loop when saving activities to the outbox on sites where the outbox post type passes through content filters.

Outbox::add() calls wp_insert_post() which fires wp_after_insert_post,
re-entering Post::triage() and creating an infinite loop on sites where
the outbox post type passes through content filters. Temporarily unhook
Post::triage before inserting, mirroring the existing kses guard pattern.
Copilot AI review requested due to automatic review settings April 3, 2026 16:01
@jeherve jeherve added the Bug Something isn't working label Apr 3, 2026
@jeherve jeherve self-assigned this Apr 3, 2026
@jeherve jeherve requested a review from a team April 3, 2026 16:01
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an infinite recursion/OOM scenario in the ActivityPub outbox insertion path by temporarily disabling the post triage hook while creating/updating ap_outbox posts.

Changes:

  • Temporarily unhooks Activitypub\Scheduler\Post::triage around wp_insert_post() / wp_update_post() inside Outbox::add().
  • Adds PHPUnit coverage to assert the triage hook is unhooked during the outbox insert and restored afterward.
  • Adds a patch-level changelog entry describing the fix.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
includes/collection/class-outbox.php Prevents wp_after_insert_postPost::triage() re-entry during outbox item creation/update.
tests/phpunit/tests/includes/collection/class-test-outbox.php Adds regression tests ensuring triage is unhooked during insert and restored after.
.github/changelog/fix-outbox-add-infinite-recursion Documents the patch fix in the changelog system.

Comment on lines +129 to +134
// Prevent infinite recursion: wp_insert_post fires wp_after_insert_post,
// which would re-enter Post::triage() -> add_to_outbox() -> Outbox::add().
$has_triage = false !== \has_action( 'wp_after_insert_post', array( Scheduler\Post::class, 'triage' ) );
if ( $has_triage ) {
\remove_action( 'wp_after_insert_post', array( Scheduler\Post::class, 'triage' ), 33 );
}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$has_triage is derived from has_action(), but the removal is hard-coded to priority 33. If Post::triage is attached at a different priority (or multiple priorities), this will leave it hooked and the recursion/OOM can still occur; additionally, the re-add always forces priority 33 even if it was different before. Consider capturing the priority returned by has_action() and removing/restoring at that priority (and/or removing all priorities where the callback is registered).

Copilot uses AI. Check for mistakes.
Comment on lines +446 to +470

// Check whether Post::triage is hooked when wp_after_insert_post fires
// during the outbox insert.
\add_action(
'wp_after_insert_post',
function ( $post_id ) use ( &$triage_hooked_during_insert ) {
if ( Outbox::POST_TYPE === \get_post_type( $post_id ) ) {
$triage_hooked_during_insert = \has_action(
'wp_after_insert_post',
array( \Activitypub\Scheduler\Post::class, 'triage' )
);
}
},
0 // Run before priority 33 to inspect hook state.
);

$object = new Base_Object();
$object->set_id( 'https://example.com/recursion-test' );
$object->set_type( 'Note' );
$object->set_content( '<p>Recursion test</p>' );

$id = \Activitypub\add_to_outbox( $object, 'Create', self::$user_id );

$this->assertIsInt( $id );
$this->assertFalse( $triage_hooked_during_insert, 'Post::triage should be unhooked during Outbox::add() to prevent recursion.' );
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test adds an anonymous wp_after_insert_post action and never removes it. Because the closure captures $triage_hooked_during_insert by reference, it will persist beyond this test and can leak state / affect later tests that insert outbox posts. Store the closure in a variable and remove_action() it at the end of the test (or in tear_down()).

Suggested change
// Check whether Post::triage is hooked when wp_after_insert_post fires
// during the outbox insert.
\add_action(
'wp_after_insert_post',
function ( $post_id ) use ( &$triage_hooked_during_insert ) {
if ( Outbox::POST_TYPE === \get_post_type( $post_id ) ) {
$triage_hooked_during_insert = \has_action(
'wp_after_insert_post',
array( \Activitypub\Scheduler\Post::class, 'triage' )
);
}
},
0 // Run before priority 33 to inspect hook state.
);
$object = new Base_Object();
$object->set_id( 'https://example.com/recursion-test' );
$object->set_type( 'Note' );
$object->set_content( '<p>Recursion test</p>' );
$id = \Activitypub\add_to_outbox( $object, 'Create', self::$user_id );
$this->assertIsInt( $id );
$this->assertFalse( $triage_hooked_during_insert, 'Post::triage should be unhooked during Outbox::add() to prevent recursion.' );
$inspect_triage_hook = function ( $post_id ) use ( &$triage_hooked_during_insert ) {
if ( Outbox::POST_TYPE === \get_post_type( $post_id ) ) {
$triage_hooked_during_insert = \has_action(
'wp_after_insert_post',
array( \Activitypub\Scheduler\Post::class, 'triage' )
);
}
};
// Check whether Post::triage is hooked when wp_after_insert_post fires
// during the outbox insert.
\add_action(
'wp_after_insert_post',
$inspect_triage_hook,
0 // Run before priority 33 to inspect hook state.
);
try {
$object = new Base_Object();
$object->set_id( 'https://example.com/recursion-test' );
$object->set_type( 'Note' );
$object->set_content( '<p>Recursion test</p>' );
$id = \Activitypub\add_to_outbox( $object, 'Create', self::$user_id );
$this->assertIsInt( $id );
$this->assertFalse( $triage_hooked_during_insert, 'Post::triage should be unhooked during Outbox::add() to prevent recursion.' );
} finally {
\remove_action( 'wp_after_insert_post', $inspect_triage_hook, 0 );
}

Copilot uses AI. Check for mistakes.
@jeherve jeherve closed this Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Infinite recursion in Outbox::add() causes OOM

2 participants