Opened 4 weeks ago
Last modified 4 weeks ago
#64653 new defect (bug)
WP_Hook::resort_active_iterations() skips next priority when callback removes itself during execution
| Reported by: |
|
Owned by: | |
|---|---|---|---|
| Milestone: | Awaiting Review | Priority: | normal |
| Severity: | normal | Version: | trunk |
| Component: | Plugins | Keywords: | has-patch has-unit-tests |
| Focuses: | Cc: |
Description
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 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:
- A callback removes itself (or its entire priority group) during hook execution.
- It is the only callback at that priority (so the priority is fully removed from the array).
- There is at least one priority lower than the removed one. If the removed priority is the lowest, the
array_unshiftbranch inresort_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:
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.
Change History (2)
This ticket was mentioned in PR #10949 on WordPress/wordpress-develop by mrcasual.
4 weeks ago
#1
- Keywords has-unit-tests added
Tac ticket: https://core.trac.wordpress.org/ticket/64653
When a callback removes itself during
do_action()/apply_filters()and is the sole callback at its priority,resort_active_iterations()positions the internal array pointer one step too far. The main loop'snext()call then skips the following priority entirely.## Steps to reproduce
The bug requires: (1) a lower priority exists, (2) the removed callback is the only one at its priority.