[php-src] Issue #22121: Double free in `gdImageSetStyle()`

From: Date: Fri, 22 May 2026 10:33:34 +0000
Subject: [php-src] Issue #22121: Double free in `gdImageSetStyle()`
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-252114@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/22121 Author: therealcoiffeur ### Description ### Summary im->style is freed at line 2883 but never set to NULL. When overflow2(sizeof(int), noOfPixels) returns true (i.e., noOfPixels > INT_MAX / sizeof(int)), the function returns at line 2886 with im->style still holding the now-freed pointer. Two separate code paths will subsequently free the dangling pointer: 1. Next call to gdImageSetStyle(): line 2882 checks if (im->style) (still non-NULL), then line 2883 calls gdFree(im->style) again. 2. GD object destructor: When the PHP GdImage object is GC'd, php_gd_image_object_free (gd.c:180) calls gdImageDestroy, which hits if (im->style) { gdFree(im->style); } at lines 265-266. This fires automatically on unset($im) or script shutdown. > Note: > The explicit imagedestroy() call is deprecated since PHP 8.5 > and has no > direct effect. The double free comes from the destructor during PHP's GC > sweep, not from imagedestroy()` itself. ### Vulnerable Source Code ```c void gdImageSetStyle(gdImagePtr im, int *style, int noOfPixels) { if (im->style) { gdFree(im->style); // frees old style -- no NULL assignment follows } if (overflow2(sizeof(int), noOfPixels)) { return; // early return leaves im->style dangling } im->style = (int *) gdMalloc(sizeof(int) * noOfPixels); memcpy(im->style, style, sizeof(int) * noOfPixels); im->styleLength = noOfPixels; im->stylePos = 0; } ``` ### How to Trigger ```php <?php $im = imagecreatetruecolor(1, 1); imagesetstyle($im, [0]); imagesetstyle($im, array_fill(0, 536870912, 0)); ``` <u>Command:</u> ```bash USE_ZEND_ALLOC=0 sapi/cli/php ../../Results/Findings/f3/poc.php ``` In the POC, line 2 allocates im->style. Line 3 frees it and returns early (overflow), leaving im->style dangling. Line 4 triggers the overflow. The double free fires when PHP GC's $im at script end via php_gd_image_object_free -> gdImageDestroy -> gdFree(im->style). <u>Output:</u> ``` Warning: imagesetstyle(): Product of memory allocation multiplication would exceed INT_MAX, failing operation gracefully in /Users/terrence/Documents/Research/Targets/PHP/Results/Findings/f3/poc.php on line 4 ================================================================= ==35745==ERROR: AddressSanitizer: attempting double-free on 0x602000007670 in thread T0: #0 0x000107e14f10 in free+0x74 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x54f10) #1 0x0001044d123c in __zend_free zend_alloc.c:3571 #2 0x0001044d4f50 in _efree zend_alloc.c:2788 #3 0x00010346142c in php_gd_gdImageDestroy gd.c:266 #4 0x000103402794 in php_gd_image_object_free gd.c:180 #5 0x000104b88d30 in zend_objects_store_del zend_objects_API.c:193 #6 0x000104beef2c in rc_dtor_func zend_variables.c:56 #7 0x000104bef040 in i_zval_ptr_dtor zend_variables.h:44 #8 0x000104beef68 in zval_ptr_dtor zend_variables.c:83 #9 0x000104a83d74 in _zend_hash_del_el_ex zend_hash.c:1502 #10 0x000104a82cfc in _zend_hash_del_el zend_hash.c:1529 #11 0x000104a8bf94 in zend_hash_reverse_apply zend_hash.c:2245 #12 0x00010460c140 in shutdown_destructors zend_execute_API.c:259 #13 0x000104c07f78 in zend_call_destructors zend.c:1339 #14 0x00010425ee4c in php_request_shutdown main.c:1983 #15 0x000104c11808 in do_cli php_cli.c:1166 #16 0x000104c0e0cc in main php_cli.c:1370 #17 0x00018c9cfdfc in start+0x1b4c (dyld:arm64e+0x1fdfc) 0x602000007670 is located 0 bytes inside of 4-byte region [0x602000007670,0x602000007674) freed by thread T0 here: #0 0x000107e14f10 in free+0x74 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x54f10) #1 0x0001044d123c in __zend_free zend_alloc.c:3571 #2 0x0001044d4f50 in _efree zend_alloc.c:2788 #3 0x0001034696cc in php_gd_gdImageSetStyle gd.c:2883 #4 0x0001033bffc8 in zif_imagesetstyle gd.c:657 #5 0x000104969f40 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_TAILCALL_HANDLER zend_vm_execute.h:54091 #6 0x000104636194 in execute_ex zend_vm_execute.h:110168 #7 0x000104636b28 in zend_execute zend_vm_execute.h:115586 #8 0x000104c09350 in zend_execute_script zend.c:1971 #9 0x0001042624f4 in php_execute_script_ex main.c:2646 #10 0x000104262a64 in php_execute_script main.c:2686 #11 0x000104c0fb0c in do_cli php_cli.c:947 #12 0x000104c0e0cc in main php_cli.c:1370 #13 0x00018c9cfdfc in start+0x1b4c (dyld:arm64e+0x1fdfc) previously allocated by thread T0 here: #0 0x000107e14e24 in malloc+0x70 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x54e24) #1 0x0001044d5560 in __zend_malloc zend_alloc.c:3543 #2 0x0001044d4e24 in _emalloc zend_alloc.c:2778 #3 0x00010346970c in php_gd_gdImageSetStyle gd.c:2888 #4 0x0001033bffc8 in zif_imagesetstyle gd.c:657 #5 0x000104969f40 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_TAILCALL_HANDLER zend_vm_execute.h:54091 #6 0x000104636194 in execute_ex zend_vm_execute.h:110168 #7 0x000104636b28 in zend_execute zend_vm_execute.h:115586 #8 0x000104c09350 in zend_execute_script zend.c:1971 #9 0x0001042624f4 in php_execute_script_ex main.c:2646 #10 0x000104262a64 in php_execute_script main.c:2686 #11 0x000104c0fb0c in do_cli php_cli.c:947 #12 0x000104c0e0cc in main php_cli.c:1370 #13 0x00018c9cfdfc in start+0x1b4c (dyld:arm64e+0x1fdfc) SUMMARY: AddressSanitizer: double-free zend_alloc.c:3571 in __zend_free ==35745==ABORTING [1] 35745 abort USE_ZEND_ALLOC=0 sapi/cli/php ../../Results/Findings/f3/poc.php ``` > Note: Even though this could be used to execute arbitrary code or bypass disabled functions, > GHSA-2233-r24f-4m45 is not part of PHP's threat model (which is wrong, but that's not my > call). > "This is a bug, but not a security issue. Please report as a regular issue." ### PHP Version ```plain PHP 8.6.0-dev (cli) (built: May 16 2026 16:38:50) (NTS DEBUG) Copyright © The PHP Group and Contributors Zend Engine v4.6.0-dev, Copyright © Zend by Perforce with Zend OPcache v8.6.0-dev, Copyright ©, by Zend by Perforce ``` ### Operating System _No response_

« previous php.bugs (#252114) next »