Commit 8796d753650 for php.net

commit 8796d75365087d4281ae13eb0dcb61459d0aeb5a
Author: ndossche <7771979+ndossche@users.noreply.github.com>
Date:   Sun Mar 15 00:05:23 2026 +0100

    Fix GH-21454: Missing write lock validation in SplHeap

    Closes GH-21448.

diff --git a/NEWS b/NEWS
index d582ab9318a..9cff078b34c 100644
--- a/NEWS
+++ b/NEWS
@@ -33,6 +33,10 @@ PHP                                                                        NEWS
   . Fixed Set-Cookie parsing bug wrong offset while scanning attributes.
     (David Carlier)

+- SPL:
+  . Fixed bug GH-21454 (missing write lock validation in SplHeap).
+    (ndossche)
+
 - Standard:
   . Fixed bug GH-20906 (Assertion failure when messing up output buffers).
     (ndossche)
diff --git a/ext/spl/spl_heap.c b/ext/spl/spl_heap.c
index d4450da4200..e2c090945b6 100644
--- a/ext/spl/spl_heap.c
+++ b/ext/spl/spl_heap.c
@@ -961,7 +961,7 @@ static void spl_heap_it_move_forward(zend_object_iterator *iter) /* {{{ */
 {
 	spl_heap_object *object = Z_SPLHEAP_P(&iter->data);

-	if (UNEXPECTED(spl_heap_consistency_validations(object, false) != SUCCESS)) {
+	if (UNEXPECTED(spl_heap_consistency_validations(object, true) != SUCCESS)) {
 		return;
 	}

@@ -992,6 +992,10 @@ PHP_METHOD(SplHeap, next)
 		RETURN_THROWS();
 	}

+	if (UNEXPECTED(spl_heap_consistency_validations(intern, true) != SUCCESS)) {
+		RETURN_THROWS();
+	}
+
 	spl_ptr_heap_delete_top(intern->heap, NULL, ZEND_THIS);
 }
 /* }}} */
diff --git a/ext/spl/tests/heap_next_write_lock.phpt b/ext/spl/tests/heap_next_write_lock.phpt
new file mode 100644
index 00000000000..fcad94f3ccd
--- /dev/null
+++ b/ext/spl/tests/heap_next_write_lock.phpt
@@ -0,0 +1,34 @@
+--TEST--
+SplHeap::next() write lock
+--CREDITS--
+cnitlrt
+--FILE--
+<?php
+
+class EvilPQ extends SplPriorityQueue {
+    private bool $did = false;
+
+    public function compare(mixed $p1, mixed $p2): int {
+        if (!$this->did) {
+            $this->did = true;
+            // Re-entrant write during internal heap insertion comparison.
+            if (!$this->isEmpty()) {
+                $this->next(); // no write-lock validation
+            }
+        }
+        return parent::compare($p1, $p2);
+    }
+}
+
+$q = new EvilPQ();
+try {
+    for ($i = 0; $i < 200; $i++) {
+        $q->insert("d$i", 100 - $i);
+    }
+} catch (RuntimeException $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+RuntimeException: Heap cannot be changed when it is already being modified.