[skif] Check for resolve failure/int overflows
Bug: oss-fuzz:70128
Bug: oss-fuzz:69848
Bug: oss-fuzz:70134
Change-Id: Iccf4fb389f16cccff3668d21d02ba439f54d80be
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/900497
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
diff --git a/src/core/SkImageFilterTypes.cpp b/src/core/SkImageFilterTypes.cpp
index 0d8bd81..612337f 100644
--- a/src/core/SkImageFilterTypes.cpp
+++ b/src/core/SkImageFilterTypes.cpp
@@ -129,12 +129,12 @@
double cropHeight = crop.bottom() - cropT;
// Calculate normalized periodic coordinates of 'output' relative to the 'crop' being tiled.
- int periodL = sk_double_floor2int((output.left() - cropL) / cropWidth);
- int periodT = sk_double_floor2int((output.top() - cropT) / cropHeight);
- int periodR = sk_double_ceil2int((output.right() - cropL) / cropWidth);
- int periodB = sk_double_ceil2int((output.bottom() - cropT) / cropHeight);
+ double periodL = std::floor((output.left() - cropL) / cropWidth);
+ double periodT = std::floor((output.top() - cropT) / cropHeight);
+ double periodR = std::ceil((output.right() - cropL) / cropWidth);
+ double periodB = std::ceil((output.bottom() - cropT) / cropHeight);
- if (periodR - periodL <= 1 && periodB - periodT <= 1) {
+ if (periodR - periodL <= 1.0 && periodB - periodT <= 1.0) {
// The tiling pattern won't be visible, so we can draw the image without tiling and an
// adjusted transform. We calculate the final translation in double to be exact and then
// verify that it can round-trip as a float.
@@ -144,12 +144,13 @@
double ty = -cropT;
if (tileMode == SkTileMode::kMirror) {
- // Flip image when in odd periods on each axis.
- if (periodL % 2 != 0) {
+ // Flip image when in odd periods on each axis. The periods are stored as doubles but
+ // hold integer values since they came from floor or ceil.
+ if (std::fmod(periodL, 2.f) > SK_ScalarNearlyZero) {
sx = -1.f;
tx = cropWidth - tx;
}
- if (periodT % 2 != 0) {
+ if (std::fmod(periodT, 2.f) > SK_ScalarNearlyZero) {
sy = -1.f;
ty = cropHeight - ty;
}
@@ -536,7 +537,7 @@
public:
AutoSurface(const Context& ctx,
const LayerSpace<SkIRect>& dstBounds,
- [[maybe_unused]] PixelBoundary boundary,
+ PixelBoundary boundary,
bool renderInParameterSpace,
const SkSurfaceProps* props = nullptr)
: fDstBounds(dstBounds)
@@ -547,7 +548,21 @@
// to align with the actual desired output via FilterResult metadata).
sk_sp<SkDevice> device = nullptr;
if (!dstBounds.isEmpty()) {
- fDstBounds.outset(LayerSpace<SkISize>({this->padding(), this->padding()}));
+ int padding = this->padding();
+ if (padding) {
+ fDstBounds.outset(LayerSpace<SkISize>({padding, padding}));
+ // If we are dealing with pathological inputs, the bounds may be near the maximum
+ // represented by an int, in which case the outset gets saturated and we don't end
+ // up with the expected padding pixels. We could downgrade the boundary value in
+ // this case, but given that these values are going to be causing problems for any
+ // of the floating point math during rendering we just fail.
+ if (fDstBounds.left() >= dstBounds.left() ||
+ fDstBounds.right() <= dstBounds.right() ||
+ fDstBounds.top() >= dstBounds.top() ||
+ fDstBounds.bottom() <= dstBounds.bottom()) {
+ return;
+ }
+ }
device = ctx.backend()->makeDevice(SkISize(fDstBounds.size()),
ctx.refColorSpace(),
props);
@@ -1719,6 +1734,10 @@
// the rescaling steps because, for better or worse, the deferred transform does not
// otherwise participate in progressive scaling so we should be consistent.
image = image.resolve(ctx, srcRect);
+ if (!image) {
+ // Early out if the resolve failed
+ return {};
+ }
if (!cfBorder) {
// This sets the resolved image to match either kDecal or the deferred tile mode.
image.fTileMode = tileMode;
diff --git a/tests/FilterResultTest.cpp b/tests/FilterResultTest.cpp
index e58dbc7..3692e35 100644
--- a/tests/FilterResultTest.cpp
+++ b/tests/FilterResultTest.cpp
@@ -400,8 +400,6 @@
sk_sp<SkSpecialImage> source,
LayerSpace<SkIPoint> origin,
const LayerSpace<SkIRect>& desiredOutput) const {
- SkASSERT(source);
-
Expect effectiveExpectation = fExpectation;
SkISize size(desiredOutput.size());
if (desiredOutput.isEmpty()) {
@@ -410,17 +408,21 @@
}
auto device = ctx.backend()->makeDevice(size, ctx.refColorSpace());
+ if (!device) {
+ return nullptr;
+ }
SkCanvas canvas{device};
canvas.clear(SK_ColorTRANSPARENT);
canvas.translate(-desiredOutput.left(), -desiredOutput.top());
- LayerSpace<SkIRect> sourceBounds{
- SkIRect::MakeXYWH(origin.x(), origin.y(), source->width(), source->height())};
- LayerSpace<SkIRect> expectedBounds = this->expectedBounds(sourceBounds);
-
- canvas.clipIRect(SkIRect(expectedBounds), SkClipOp::kIntersect);
-
if (effectiveExpectation != Expect::kEmptyImage) {
+ SkASSERT(source);
+ LayerSpace<SkIRect> sourceBounds{
+ SkIRect::MakeXYWH(origin.x(), origin.y(), source->width(), source->height())};
+ LayerSpace<SkIRect> expectedBounds = this->expectedBounds(sourceBounds);
+
+ canvas.clipIRect(SkIRect(expectedBounds), SkClipOp::kIntersect);
+
SkPaint paint;
paint.setAntiAlias(true);
paint.setBlendMode(SkBlendMode::kSrc);
@@ -648,7 +650,11 @@
const FilterResult& actual,
float allowedPercentImageDiff,
int transparentCheckBorderTolerance) {
- SkASSERT(expectedImage);
+ if (!expectedImage) {
+ // For pathological desired outputs, we can't actually produce an expected image so
+ // just carry on w/o validating.
+ return true;
+ }
SkBitmap expectedBM = this->readPixels(expectedImage);
@@ -1202,7 +1208,12 @@
// resolved image, which can make its layer bounds larger than the desired output.
if (correctedExpectation == Expect::kDeferredImage ||
!FilterResultTestAccess::IsIntegerTransform(output)) {
- REPORTER_ASSERT(fRunner, actualBounds.intersect(desiredOutputs[i]));
+ // Skip the check if the desiredOutputs's SkIRect reports empty.
+ // LayerSpace<SkIRect> won't be empty but since the W/H don't fit into 32-bit
+ // SkIRect::intersect() will report false.
+ REPORTER_ASSERT(fRunner, SkIRect(desiredOutputs[i]).isEmpty() ||
+ actualBounds.intersect(desiredOutputs[i]));
+
}
REPORTER_ASSERT(fRunner, !expectedBounds.isEmpty());
REPORTER_ASSERT(fRunner, SkIRect(actualBounds) == SkIRect(expectedBounds));
@@ -1612,11 +1623,18 @@
.applyCrop({-5, -5, 15, 15}, tm, Expect::kNewImage)
.run(/*requestedOutput=*/{10, 10, 20, 20});
- TestCase(r, "Periodic applyCropp() with visible edge and complex transform creates image")
+ TestCase(r, "Periodic applyCrop() with visible edge and complex transform creates image")
.source({0, 0, 20, 20})
.applyTransform(SkMatrix::RotateDeg(15.f, {10.f, 10.f}), Expect::kDeferredImage)
.applyCrop({-5, -5, 25, 25}, tm, Expect::kNewImage)
.run(/*requestedOutput=*/{20, 20, 50, 50});
+
+ // oss-fuzz:70128 ensure period calculations don't overflow (but will fail image creation)
+ TestCase(r, "Pathologically large crop rect")
+ .source({0, 0, 10, 10})
+ .applyCrop({0, 0, 1, 2}, tm, Expect::kDeferredImage)
+ .applyTransform(SkMatrix::RotateDeg(1.f, {5.f, 5.f}), Expect::kEmptyImage)
+ .run(/*requestedOutput=*/{-726713344, 7, 1464866662, 15});
}
}
@@ -1950,6 +1968,14 @@
.applyColorFilter(alpha_modulate(0.5f), Expect::kDeferredImage)
.applyColorFilter(affect_transparent(SkColors::kBlue), Expect::kDeferredImage)
.run(/*requestedOutput=*/{-8, -8, 32, 32});
+
+ // oss-fuzz:70134 Requesting the output of an infinite image (e.g. transparency affecting
+ // color filter) at the limit of 32-bit ints doesn't leave room for border padding pixels.
+ // This should cleanly fail to an empty image.
+ TestCase(r, "Pathologic output bounds with transparency-affecting color filter is empty")
+ .source({0, 0, 16, 16})
+ .applyColorFilter(affect_transparent(SkColors::kRed), Expect::kEmptyImage)
+ .run(/*requestedOutput=*/{-INT32_MAX, 0, -INT32_MAX + 10, 16});
}
DEF_TEST_SUITE(TransformedColorFilter, r, CtsEnforcement::kApiLevel_T,