[wp-trac] [WordPress Trac] #64653: WP_Hook::resort_active_iterations() skips next priority when callback removes itself during execution
WordPress Trac
noreply at wordpress.org
Tue Feb 17 00:14:20 UTC 2026
#64653: WP_Hook::resort_active_iterations() skips next priority when callback
removes itself during execution
--------------------------+-----------------------------
Reporter: mrcasual | Owner: (none)
Type: defect (bug) | Status: new
Priority: normal | Milestone: Awaiting Review
Component: Plugins | Version: trunk
Severity: normal | Keywords: has-patch
Focuses: |
--------------------------+-----------------------------
When a callback removes itself (or is the only callback at its priority)
during
`do_action()`/`apply_filters()` execution, the next registered priority is
silently skipped. This seems to affect all versions since the introduction
of `WP_Hook`.
To reproduce, run this as an `mu-plugin`:
{{{#!php
<?php
function wp_hook_bug_self_removing() {
remove_action( 'wp_hook_bug_test', 'wp_hook_bug_self_removing', 50 );
error_log( 'Priority 50 executed (removed itself).' );
}
add_action( 'init', function () {
add_action( 'wp_hook_bug_test', function () {
error_log( 'Priority 10 executed.' );
}, 10 );
add_action( 'wp_hook_bug_test', 'wp_hook_bug_self_removing', 50 );
add_action( 'wp_hook_bug_test', function () {
error_log( 'Priority 100 executed.' );
}, 100 );
do_action( 'wp_hook_bug_test' );
} );
}}}
Expected: all three priorities execute (10, 50, 100).
Actual: priority 100 is silently skipped. Only 10 and 50 execute.
The bug requires all three:
1. A callback removes itself (or its entire priority group) during hook
execution.
2. It is the only callback at that priority (so the priority is fully
removed from the array).
3. There is at least one priority lower than the removed one. If the
removed priority is the lowest, the `array_unshift` branch in
`resort_active_iterations()` handles it correctly.
In `WP_Hook::resort_active_iterations()` (`wp-includes/class-wp-
hook.php`), when a priority is removed during iteration, the method
rebuilds the iteration array and repositions the internal pointer:
{{{#!php
while ( current( $iteration ) < $current ) {
if ( false === next( $iteration ) ) {
break;
}
}
}}}
This positions the pointer at the first remaining priority >= `$current`
(the removed priority). Since `$current` no longer exists, the pointer
lands on the next priority (e.g., 100).
Back in `apply_filters()`, the do...while loop calls `next()` at the
bottom of each iteration, which advances past 100. The loop ends, and
priority 100 never executes.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/64653>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list