[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