Commit df1250dc64f for php.net

commit df1250dc64f53db849503618d2f911645cd43e3a
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sat Jun 20 20:50:06 2026 -0400

    ext/gd: fix out-of-bounds write reading font header on short reads

    imageloadfont() read the font header with `(char*)&font[b]`, which scales
    the byte counter b by sizeof(gdFont) rather than advancing one byte, so a
    short php_stream_read() (deliverable by a user stream wrapper) makes the
    loop write hdr_size-b bytes past the emalloc(sizeof(gdFont)) buffer. Index
    the destination by bytes, matching the body read a few lines below.

    Closes GH-22380

diff --git a/ext/gd/gd.c b/ext/gd/gd.c
index 16e871f6bc3..311900ba7bb 100644
--- a/ext/gd/gd.c
+++ b/ext/gd/gd.c
@@ -556,7 +556,7 @@ PHP_FUNCTION(imageloadfont)
 	 */
 	font = (gdFontPtr) emalloc(sizeof(gdFont));
 	b = 0;
-	while (b < hdr_size && (n = php_stream_read(stream, (char*)&font[b], hdr_size - b)) > 0) {
+	while (b < hdr_size && (n = php_stream_read(stream, (char *) font + b, hdr_size - b)) > 0) {
 		b += n;
 	}

diff --git a/ext/gd/tests/imageloadfont_short_read.phpt b/ext/gd/tests/imageloadfont_short_read.phpt
new file mode 100644
index 00000000000..5a7f6a14c9b
--- /dev/null
+++ b/ext/gd/tests/imageloadfont_short_read.phpt
@@ -0,0 +1,65 @@
+--TEST--
+imageloadfont(): header read must stay in bounds on short reads
+--EXTENSIONS--
+gd
+--FILE--
+<?php
+/* A user-space wrapper returns one byte per read, so php_stream_read() hands
+ * imageloadfont()'s header loop a short read on every iteration. The header
+ * (4 ints) plus a single 1x1 glyph byte form a valid font, which only loads
+ * when each short read lands at the correct byte offset. */
+class drip
+{
+    public $context;
+    private string $data;
+    private int $pos = 0;
+
+    public function stream_open($path, $mode, $options, &$opened): bool
+    {
+        $this->data = pack('V4', 1, 32, 1, 1) . "\x00";
+        return true;
+    }
+
+    public function stream_read($count): string
+    {
+        return $this->pos < strlen($this->data) ? $this->data[$this->pos++] : '';
+    }
+
+    public function stream_eof(): bool
+    {
+        return $this->pos >= strlen($this->data);
+    }
+
+    public function stream_stat()
+    {
+        return [];
+    }
+
+    public function stream_tell(): int
+    {
+        return $this->pos;
+    }
+
+    public function stream_seek($offset, $whence): bool
+    {
+        if ($whence === SEEK_CUR) {
+            $this->pos += $offset;
+        } elseif ($whence === SEEK_END) {
+            $this->pos = strlen($this->data) + $offset;
+        } else {
+            $this->pos = $offset;
+        }
+        return true;
+    }
+
+    public function stream_set_option($option, $arg1, $arg2): bool
+    {
+        return false;
+    }
+}
+
+stream_wrapper_register('drip', drip::class);
+var_dump(imageloadfont('drip://font') instanceof GdFont);
+?>
+--EXPECT--
+bool(true)