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)