Skip to content
Merged
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
Binary file added Tests/images/psd-oob-write-overflow.psd
Binary file not shown.
22 changes: 21 additions & 1 deletion Tests/test_file_psd.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from __future__ import annotations

import sys
import warnings

import pytest

from PIL import Image, PsdImagePlugin

from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
from .helper import (
assert_image_equal_tofile,
assert_image_similar,
hopper,
is_pypy,
)

test_file = "Tests/images/hopper.psd"

Expand Down Expand Up @@ -204,3 +210,17 @@ def test_bounds_crash(test_file: str) -> None:

with pytest.raises(ValueError):
im.load()


def test_bounds_crash_overflow() -> None:
with Image.open("Tests/images/psd-oob-write-overflow.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
im.load()
if sys.maxsize <= 2**32:
with pytest.raises(OverflowError):
im.seek(im.n_frames)
else:
im.seek(im.n_frames)

with pytest.raises(ValueError):
im.load()
123 changes: 42 additions & 81 deletions Tests/test_imagefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,52 +316,38 @@ def test_setimage(self) -> None:
with pytest.raises(ValueError):
MockPyDecoder.last.set_as_raw(b"\x00")

def test_extents_none(self) -> None:
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)
im.tile = [ImageFile._Tile("MOCK", None, 32, None)]

im.load()

assert MockPyDecoder.last.state.xoff == 0
assert MockPyDecoder.last.state.yoff == 0
assert MockPyDecoder.last.state.xsize == 200
assert MockPyDecoder.last.state.ysize == 200

def test_negsize(self) -> None:
@pytest.mark.parametrize(
"extents",
(
(-10, yoff, xoff + xsize, yoff + ysize),
(xoff, -10, xoff + xsize, yoff + ysize),
(xoff, yoff, -10, yoff + ysize),
(xoff, yoff, xoff + xsize, -10),
(xoff, yoff, xoff + xsize + 100, yoff + ysize),
(xoff, yoff, xoff + xsize, yoff + ysize + 100),
),
)
def test_extents(self, extents: tuple[int, int, int, int]) -> None:
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)
im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
im.tile = [ImageFile._Tile("MOCK", extents, 32, None)]

with pytest.raises(ValueError):
im.load()

im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
with pytest.raises(ValueError):
im.load()

def test_oversize(self) -> None:
def test_extents_none(self) -> None:
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)
im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None
)
]
im.tile = [ImageFile._Tile("MOCK", None, 32, None)]

with pytest.raises(ValueError):
im.load()
im.load()

im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None
)
]
with pytest.raises(ValueError):
im.load()
assert MockPyDecoder.last.state.xoff == 0
assert MockPyDecoder.last.state.yoff == 0
assert MockPyDecoder.last.state.xsize == 200
assert MockPyDecoder.last.state.ysize == 200

def test_decode(self) -> None:
decoder = ImageFile.PyDecoder("")
Expand Down Expand Up @@ -392,72 +378,47 @@ def test_setimage(self) -> None:
assert MockPyEncoder.last.state.xsize == xsize
assert MockPyEncoder.last.state.ysize == ysize

def test_extents_none(self) -> None:
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)
im.tile = [ImageFile._Tile("MOCK", None, 32, None)]

fp = BytesIO()
ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])

assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == 0
assert MockPyEncoder.last.state.yoff == 0
assert MockPyEncoder.last.state.xsize == 200
assert MockPyEncoder.last.state.ysize == 200

def test_negsize(self) -> None:
@pytest.mark.parametrize(
"extents",
(
(-10, yoff, xoff + xsize, yoff + ysize),
(xoff, -10, xoff + xsize, yoff + ysize),
(xoff, yoff, -10, yoff + ysize),
(xoff, yoff, xoff + xsize, -10),
(xoff, yoff, xoff + xsize + 100, yoff + ysize),
(xoff, yoff, xoff + xsize, yoff + ysize + 100),
),
)
def test_extents(self, extents: tuple[int, int, int, int]) -> None:
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)

fp = BytesIO()
MockPyEncoder.last = None
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")],
)
ImageFile._save(im, fp, [ImageFile._Tile("MOCK", extents, 0, "RGB")])
last: MockPyEncoder | None = MockPyEncoder.last
assert last
assert last.cleanup_called

with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")],
)
ImageFile._save(im, fp, [ImageFile._Tile("MOCK", extents, 0, "RGB")])

def test_oversize(self) -> None:
def test_extents_none(self) -> None:
buf = BytesIO(b"\x00" * 255)

im = MockImageFile(buf)
im.tile = [ImageFile._Tile("MOCK", None, 32, None)]

fp = BytesIO()
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB"
)
],
)
ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])

with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB"
)
],
)
assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == 0
assert MockPyEncoder.last.state.yoff == 0
assert MockPyEncoder.last.state.xsize == 200
assert MockPyEncoder.last.state.ysize == 200

def test_encode(self) -> None:
encoder = ImageFile.PyEncoder("")
Expand Down
11 changes: 4 additions & 7 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,10 @@ def setimage(
if extents:
x0, y0, x1, y1 = extents

if x0 < 0 or y0 < 0 or x1 > self.im.size[0] or y1 > self.im.size[1]:
msg = "Tile cannot extend outside image"
raise ValueError(msg)

self.state.xoff = x0
self.state.yoff = y0
self.state.xsize = x1 - x0
Expand All @@ -817,13 +821,6 @@ def setimage(
msg = "Size must be positive"
raise ValueError(msg)

if (
self.state.xsize + self.state.xoff > self.im.size[0]
or self.state.ysize + self.state.yoff > self.im.size[1]
):
msg = "Tile cannot extend outside image"
raise ValueError(msg)


class PyDecoder(PyCodec):
"""
Expand Down
13 changes: 6 additions & 7 deletions src/decode.c
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) {
}
}

if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0 || x1 > (int)im->xsize ||
y1 > (int)im->ysize) {
PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image");
return NULL;
}

decoder->im = im;

state = &decoder->state;
Expand All @@ -208,13 +214,6 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) {
state->xsize = x1 - x0;
state->ysize = y1 - y0;

if (state->xoff < 0 || state->xsize <= 0 ||
state->xsize + state->xoff > (int)im->xsize || state->yoff < 0 ||
state->ysize <= 0 || state->ysize + state->yoff > (int)im->ysize) {
PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image");
return NULL;
}

/* Allocate memory buffer (if bits field is set) */
if (state->bits > 0) {
if (!state->bytes) {
Expand Down
12 changes: 5 additions & 7 deletions src/encode.c
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) {
return NULL;
}

if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0 || x1 > im->xsize || y1 > im->ysize) {
PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image");
return NULL;
}

encoder->im = im;

state = &encoder->state;
Expand All @@ -278,13 +283,6 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) {
state->xsize = x1 - x0;
state->ysize = y1 - y0;

if (state->xoff < 0 || state->xsize <= 0 ||
state->xsize + state->xoff > im->xsize || state->yoff < 0 ||
state->ysize <= 0 || state->ysize + state->yoff > im->ysize) {
PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image");
return NULL;
}

/* Allocate memory buffer (if bits field is set) */
if (state->bits > 0) {
if (state->xsize > ((INT_MAX / state->bits) - 7)) {
Expand Down
Loading