Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ PHP NEWS
invalid quality parameter. (David Carlier)
. Check overflow/underflow for imagescale/imagefilter. (David Carlier)
. Added gdImageClone to bundled libgd. (David Carlier)
. Fixed bug GH-21163 (Integer overflow in gdImageCopy* functions). (Pierre Joye, Jordi Kroon)

- Gettext:
. bind_textdomain_codeset, textdomain and d(*)gettext functions
Expand Down
20 changes: 18 additions & 2 deletions ext/gd/libgd/gd.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <string.h>
#include <stdlib.h>
#include "gd.h"
#include "gd_intern.h"
#include "gdhelpers.h"
#include "gd_errors.h"

Expand Down Expand Up @@ -2314,7 +2315,9 @@ void gdImageCopy (gdImagePtr dst, gdImagePtr src, int dstX, int dstY, int srcX,
int tox, toy;
int i;
int colorMap[gdMaxColors];

if (!gdImageClipCopy(dst, &dstX, &dstY, &srcX, &srcY, &w, &h)) {
return;
}
if (dst->trueColor) {
/* 2.0: much easier when the destination is truecolor. */
/* 2.0.10: needs a transparent-index check that is still valid if
Expand Down Expand Up @@ -2394,6 +2397,9 @@ void gdImageCopyMerge (gdImagePtr dst, gdImagePtr src, int dstX, int dstY, int s
int x, y;
int tox, toy;
int ncR, ncG, ncB;
if (!gdImageClipCopy(dst, &dstX, &dstY, &srcX, &srcY, &w, &h)) {
return;
}
toy = dstY;

for (y = srcY; y < (srcY + h); y++) {
Expand Down Expand Up @@ -2435,6 +2441,11 @@ void gdImageCopyMergeGray (gdImagePtr dst, gdImagePtr src, int dstX, int dstY, i
int tox, toy;
int ncR, ncG, ncB;
float g;

if (!gdImageClipCopy(dst, &dstX, &dstY, &srcX, &srcY, &w, &h)) {
return;
}

toy = dstY;

for (y = srcY; (y < (srcY + h)); y++) {
Expand Down Expand Up @@ -2499,7 +2510,9 @@ void gdImageCopyResized (gdImagePtr dst, gdImagePtr src, int dstX, int dstY, int
if (overflow2(sizeof(int), srcH)) {
return;
}

if (!gdImageClipCopyResized(dst, &dstX, &dstY, &dstW, &dstH, &srcX, &srcY, &srcW, &srcH)) {
return;
}
stx = (int *) gdMalloc (sizeof (int) * srcW);
sty = (int *) gdMalloc (sizeof (int) * srcH);

Expand Down Expand Up @@ -2601,6 +2614,9 @@ void gdImageCopyResampled (gdImagePtr dst, gdImagePtr src, int dstX, int dstY, i
gdImageCopyResized (dst, src, dstX, dstY, srcX, srcY, dstW, dstH, srcW, srcH);
return;
}
if (!gdImageClipCopyResized(dst, &dstX, &dstY, &dstW, &dstH, &srcX, &srcY, &srcW, &srcH)) {
return;
}
for (y = dstY; (y < dstY + dstH); y++) {
sy1 = ((double) y - (double) dstY) * (double) srcH / (double) dstH;
sy2 = ((double) (y + 1) - (double) dstY) * (double) srcH / (double) dstH;
Expand Down
124 changes: 123 additions & 1 deletion ext/gd/libgd/gd_intern.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#ifndef GD_INTERN_H
#define GD_INTERN_H

#include <limits.h>

#ifndef MIN
#define MIN(a,b) ((a)<(b)?(a):(b))
#endif
Expand All @@ -10,5 +13,124 @@
#define MAX3(a,b,c) ((a)<(b)?(MAX(b,c)):(MAX(a,c)))
#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))

#endif
/**
* Clip a copy operation to the destination image bounds.
* Adjusts srcX/srcY to match any clipping applied to dstX/dstY.
* Returns 0 if the operation is entirely outside dst (nothing to do),
* 1 if there is work to do.
*/
static inline int gdImageClipCopy(
gdImagePtr dst,
int *dstX, int *dstY,
int *srcX, int *srcY,
int *w, int *h)
{
int x1, y1, x2, y2;

/* overflow-safe dst rect: [dstX, dstY] to [dstX+w, dstY+h] */
x1 = *dstX;
y1 = *dstY;

/* check w/h are positive */
if (*w <= 0 || *h <= 0) {
return 0;
}

/* overflow check for dstX+w and dstY+h */
if (*dstX > 0 && *w > INT_MAX - *dstX) {
x2 = INT_MAX;
} else {
x2 = *dstX + *w;
}
if (*dstY > 0 && *h > INT_MAX - *dstY) {
y2 = INT_MAX;
} else {
y2 = *dstY + *h;
}

/* entirely outside dst? */
if (x1 >= gdImageSX(dst) || y1 >= gdImageSY(dst) ||
x2 <= 0 || y2 <= 0) {
return 0;
}

/* clip left */
if (x1 < 0) {
*srcX -= x1; /* advance srcX by the same amount */
*w += x1; /* reduce width */
*dstX = 0;
}

/* clip top */
if (y1 < 0) {
*srcY -= y1;
*h += y1;
*dstY = 0;
}

/* clip right */
if (*dstX + *w > gdImageSX(dst)) {
*w = gdImageSX(dst) - *dstX;
}

/* clip bottom */
if (*dstY + *h > gdImageSY(dst)) {
*h = gdImageSY(dst) - *dstY;
}

/* sanity: clipping may have reduced w/h to zero */
if (*w <= 0 || *h <= 0) {
return 0;
}

return 1;
}
static inline int gdImageClipCopyResized(
gdImagePtr dst,
int *dstX, int *dstY,
int *dstW, int *dstH,
int *srcX, int *srcY,
int *srcW, int *srcH)
{
int orig_dstW = *dstW;
int orig_dstH = *dstH;

if (*dstW <= 0 || *dstH <= 0 || *srcW <= 0 || *srcH <= 0) {
return 0;
}
if (*dstX >= gdImageSX(dst) || *dstY >= gdImageSY(dst) ||
*dstX + *dstW <= 0 || *dstY + *dstH <= 0) {
return 0;
}

/* clip left — adjust srcX proportionally */
if (*dstX < 0) {
*srcX += (-*dstX) * *srcW / orig_dstW;
*srcW -= (-*dstX) * *srcW / orig_dstW;
*dstW += *dstX;
*dstX = 0;
}
/* clip top */
if (*dstY < 0) {
*srcY += (-*dstY) * *srcH / orig_dstH;
*srcH -= (-*dstY) * *srcH / orig_dstH;
*dstH += *dstY;
*dstY = 0;
}
/* clip right */
if (*dstX + *dstW > gdImageSX(dst)) {
int clip = *dstX + *dstW - gdImageSX(dst);
*srcW -= clip * *srcW / orig_dstW;
*dstW = gdImageSX(dst) - *dstX;
}
/* clip bottom */
if (*dstY + *dstH > gdImageSY(dst)) {
int clip = *dstY + *dstH - gdImageSY(dst);
*srcH -= clip * *srcH / orig_dstH;
*dstH = gdImageSY(dst) - *dstY;
}

if (*dstW <= 0 || *dstH <= 0) return 0;
return 1;
}
#endif /* GD_INTERN_H */
79 changes: 79 additions & 0 deletions ext/gd/tests/gh21163.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
--TEST--
GH-17772 (prevents signed int overflow in gdImageCopy functions)
--EXTENSIONS--
gd
--SKIPIF--
<?php
if (!GD_BUNDLED) die("skip requires bundled GD library");
?>
--FILE--
<?php
function makeImages(): array {
$dst = imagecreatetruecolor(2, 2);
$src = imagecreatetruecolor(2, 2);

$blackDst = imagecolorallocate($dst, 0, 0, 0);
$whiteSrc = imagecolorallocate($src, 255, 255, 255);

imagefilledrectangle($dst, 0, 0, 1, 1, $blackDst);
imagefilledrectangle($src, 0, 0, 1, 1, $whiteSrc);

return [$dst, $src];
}

function assertDstUnchanged(string $label, $dst, int $expectedColor): void {
$actual = imagecolorat($dst, 0, 0);
if ($actual !== $expectedColor) {
echo "FAIL $label: dst changed, got $actual expected $expectedColor\n";
return;
}
echo "OK $label\n";
}

/*
Use values that will overflow a 32 bit signed int when adding w or h.
INT_MAX is 0x7fffffff.
*/
$nearIntMax = 0x7fffffff - 5;
$w = 10;
$h = 10;

/* imagecopy */
[$dst, $src] = makeImages();
$expected = imagecolorat($dst, 0, 0);
imagecopy($dst, $src, $nearIntMax, 0, 0, 0, $w, $h);
assertDstUnchanged('imagecopy', $dst, $expected);

/* imagecopymerge */
[$dst, $src] = makeImages();
$expected = imagecolorat($dst, 0, 0);
imagecopymerge($dst, $src, $nearIntMax, 0, 0, 0, $w, $h, 50);
assertDstUnchanged('imagecopymerge', $dst, $expected);

/* imagecopymergegray */
[$dst, $src] = makeImages();
$expected = imagecolorat($dst, 0, 0);
imagecopymergegray($dst, $src, $nearIntMax, 0, 0, 0, $w, $h, 50);
assertDstUnchanged('imagecopymergegray', $dst, $expected);

/* imagecopyresized */
[$dst, $src] = makeImages();
$expected = imagecolorat($dst, 0, 0);
imagecopyresized($dst, $src, $nearIntMax, 0, 0, 0, $w, $h, 1, 1);
assertDstUnchanged('imagecopyresized', $dst, $expected);

/* imagecopyresampled */
[$dst, $src] = makeImages();
$expected = imagecolorat($dst, 0, 0);
imagecopyresampled($dst, $src, $nearIntMax, 0, 0, 0, $w, $h, 1, 1);
assertDstUnchanged('imagecopyresampled', $dst, $expected);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would like to see more edge cases as follows:

  • A negative dstX (say -5) with a small w/h that partially overlaps the destination. That way we confirm pixels from the right part of src end up in the left of dst,
    not just that nothing crashed.
    • A case where the source rectangle ends up empty after clipping, to make sure the resized variants don't end up dividing by zero internally.
    • One run against a palette image (imagecreate(...) instead of imagecreatetruecolor(...)) for imagecopy, since that goes through a different code path.
    • A very negative dstX (close to INT_MIN) as a mirror of the existing very-large-positive case.

echo "done\n";
?>
--EXPECT--
OK imagecopy
OK imagecopymerge
OK imagecopymergegray
OK imagecopyresized
OK imagecopyresampled
done
Loading