Commit f87368b55e2 for php
commit f87368b55e2b6518db472612545ed332dbb62439
Author: lacatoire <la.catoire@gmail.com>
Date: Tue Oct 20 09:12:43 2026 +0200
ext/gd: keep the brush and tile images alive while the destination uses them
imagesetbrush() and imagesettile() store a raw pointer to the brush or tile
image in libgd, but the destination GdImage keeps no reference to it. Once the
brush object is released, drawing with IMG_COLOR_BRUSHED or IMG_COLOR_TILED
reads freed memory. Keep a reference to both images in the destination object
and expose them to the GC.
Close GH-24444
diff --git a/NEWS b/NEWS
index 2478151740c..34a5a7506dc 100644
--- a/NEWS
+++ b/NEWS
@@ -33,6 +33,10 @@ PHP NEWS
. Fixed bug GH-25268 (NULL php_libxml_ref_obj dereference in
dom_objects_store_clone_obj when cloning Dom\Notation). (David Carlier)
+- GD:
+ . Fixed use-after-free when drawing with a brush or tile image that was
+ released after imagesetbrush() or imagesettile(). (lacatoire)
+
- MBString:
. Fixed stack overflow in mb_check_encoding() and mb_convert_encoding() with
deeply nested arrays. (Eyüp Can Akman)
diff --git a/ext/gd/gd.c b/ext/gd/gd.c
index 9f6e38a2e78..2b8d7341bce 100644
--- a/ext/gd/gd.c
+++ b/ext/gd/gd.c
@@ -135,6 +135,9 @@ zend_class_entry *gd_image_ce;
typedef struct _gd_ext_image_object {
gdImagePtr image;
+ /* libgd only keeps a pointer to the brush and tile images: hold them alive */
+ zval brush;
+ zval tile;
zend_object std;
} php_gd_image_object;
@@ -183,9 +186,23 @@ static void php_gd_image_object_free(zend_object *intern)
if (img_obj_ptr->image) {
gdImageDestroy(img_obj_ptr->image);
}
+ zval_ptr_dtor(&img_obj_ptr->brush);
+ zval_ptr_dtor(&img_obj_ptr->tile);
zend_object_std_dtor(intern);
}
+static HashTable *php_gd_image_object_get_gc(zend_object *intern, zval **table, int *n)
+{
+ php_gd_image_object *img_obj_ptr = php_gd_exgdimage_from_zobj_p(intern);
+ zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();
+
+ zend_get_gc_buffer_add_zval(gc_buffer, &img_obj_ptr->brush);
+ zend_get_gc_buffer_add_zval(gc_buffer, &img_obj_ptr->tile);
+ zend_get_gc_buffer_use(gc_buffer, table, n);
+
+ return zend_std_get_properties(intern);
+}
+
/**
* Creates a new GdImage object wrapping the gdImagePtr and attaches it
* to the zval (usually return_value).
@@ -208,6 +225,7 @@ static void php_gd_object_minit_helper(void)
memcpy(&php_gd_image_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
php_gd_image_object_handlers.clone_obj = NULL;
php_gd_image_object_handlers.free_obj = php_gd_image_object_free;
+ php_gd_image_object_handlers.get_gc = php_gd_image_object_get_gc;
php_gd_image_object_handlers.get_constructor = php_gd_image_object_get_constructor;
php_gd_image_object_handlers.compare = zend_objects_not_comparable;
php_gd_image_object_handlers.offset = XtOffsetOf(php_gd_image_object, std);
@@ -1274,6 +1292,10 @@ PHP_FUNCTION(imagesettile)
gdImageSetTile(im, tile);
+ php_gd_image_object *dst = php_gd_exgdimage_from_zobj_p(Z_OBJ_P(IM));
+ zval_ptr_dtor(&dst->tile);
+ ZVAL_COPY(&dst->tile, TILE);
+
RETURN_TRUE;
}
/* }}} */
@@ -1294,6 +1316,10 @@ PHP_FUNCTION(imagesetbrush)
gdImageSetBrush(im, tile);
+ php_gd_image_object *dst = php_gd_exgdimage_from_zobj_p(Z_OBJ_P(IM));
+ zval_ptr_dtor(&dst->brush);
+ ZVAL_COPY(&dst->brush, TILE);
+
RETURN_TRUE;
}
/* }}} */
diff --git a/ext/gd/tests/imagesetbrush_tile_keep_alive.phpt b/ext/gd/tests/imagesetbrush_tile_keep_alive.phpt
new file mode 100644
index 00000000000..24a8c5bea64
--- /dev/null
+++ b/ext/gd/tests/imagesetbrush_tile_keep_alive.phpt
@@ -0,0 +1,47 @@
+--TEST--
+imagesetbrush(), imagesettile(): the brush and tile images stay alive while they are in use
+--EXTENSIONS--
+gd
+--FILE--
+<?php
+$im = imagecreatetruecolor(20, 20);
+$brush = imagecreatetruecolor(4, 4);
+imagefill($brush, 0, 0, 0xff0000);
+imagesetbrush($im, $brush);
+unset($brush);
+
+$junk = [];
+for ($i = 0; $i < 50; $i++) {
+ $j = imagecreatetruecolor(4, 4);
+ imagefill($j, 0, 0, 0x00ff00);
+ $junk[] = $j;
+}
+imageline($im, 2, 10, 17, 10, IMG_COLOR_BRUSHED);
+printf("%06x\n", imagecolorat($im, 5, 10));
+
+$tile = imagecreatetruecolor(2, 2);
+imagefill($tile, 0, 0, 0x0000ff);
+imagesettile($im, $tile);
+unset($tile);
+$junk = [];
+for ($i = 0; $i < 50; $i++) {
+ $j = imagecreatetruecolor(2, 2);
+ imagefill($j, 0, 0, 0x00ff00);
+ $junk[] = $j;
+}
+imagefilledrectangle($im, 0, 0, 19, 19, IMG_COLOR_TILED);
+printf("%06x\n", imagecolorat($im, 5, 5));
+
+// A new brush replaces the old one, and a cycle is collected
+$a = imagecreatetruecolor(4, 4);
+imagesetbrush($a, $a);
+imagesettile($a, $a);
+$w = WeakReference::create($a);
+unset($a);
+gc_collect_cycles();
+var_dump($w->get());
+?>
+--EXPECT--
+ff0000
+0000ff
+NULL