blob: cecadc841c21dc5c5af70acf171ecf3d36f4630b [file]
/*
* Copyright 2026 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/codec/SkAndroidCodec.h"
#include "include/codec/SkCodec.h"
#include "include/codec/SkPngChunkReader.h"
#include "include/codec/SkPngRustDecoder.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSize.h"
#include "include/core/SkStream.h"
#include "include/core/SkSurface.h"
#include "include/private/SkGainmapInfo.h"
#include "include/private/SkTemplates.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <utility>
namespace {
// Reads every byte of each unknown chunk (e.g. `npTc` when
// `skia_use_rust_png_for_android=true`) so that ASAN catches out-of-bounds
// spans handed across the Rust/C++ boundary.
class FuzzChunkReader final : public SkPngChunkReader {
public:
bool readChunk(const char tag[], const void* data, size_t length) override {
volatile uint8_t sink = 0;
for (size_t i = 0; i < 4; ++i) {
sink ^= static_cast<uint8_t>(tag[i]);
}
const auto* bytes = static_cast<const uint8_t*>(data);
for (size_t i = 0; i < length; ++i) {
sink ^= bytes[i];
}
return true;
}
};
} // namespace
static void draw_if_decoded(const SkBitmap& bm, SkCodec::Result result) {
switch (result) {
case SkCodec::kSuccess:
case SkCodec::kIncompleteInput:
case SkCodec::kErrorInInput:
break;
default:
return;
}
if (auto surface = SkSurfaces::Raster(SkImageInfo::MakeN32Premul(bm.dimensions()))) {
surface->getCanvas()->drawImage(bm.asImage(), 0, 0);
}
}
// Exercises one-shot full decode of up to the first 10 frames.
static void fuzz_frames(SkCodec* codec) {
const SkImageInfo info = codec->getInfo();
SkBitmap bm;
if (!bm.tryAllocPixels(info)) {
return;
}
const int frameCount = std::min(codec->getFrameCount(), 10);
for (int i = 0; i < frameCount; ++i) {
SkCodec::Options options;
options.fFrameIndex = i;
const SkCodec::Result result =
codec->getPixels(info, bm.getPixels(), bm.rowBytes(), &options);
draw_if_decoded(bm, result);
}
}
// Exercises incremental decode. A second call on `kIncompleteInput` without new
// data checks that retrying a recoverable EOF remains safe.
static void fuzz_incremental_decode(SkCodec* codec) {
const SkImageInfo info = codec->getInfo();
SkBitmap bm;
if (!bm.tryAllocPixels(info)) {
return;
}
if (codec->startIncrementalDecode(info, bm.getPixels(), bm.rowBytes()) != SkCodec::kSuccess) {
return;
}
// Deliberately uninitialized to verify (under MSAN) that incrementalDecode
// initializes it when it returns kIncompleteInput or kErrorInInput.
int rowsDecoded;
SkCodec::Result result = codec->incrementalDecode(&rowsDecoded);
if (result == SkCodec::kIncompleteInput) {
result = codec->incrementalDecode(&rowsDecoded);
}
if ((result == SkCodec::kIncompleteInput || result == SkCodec::kErrorInInput) &&
rowsDecoded < bm.height()) {
// Mirrors what clients do with `rowsDecoded`; ASAN/MSAN catch bogus values.
void* dst = SkTAddOffset<void>(bm.getPixels(), rowsDecoded * bm.rowBytes());
sk_bzero(dst, (bm.height() - rowsDecoded) * bm.rowBytes());
}
draw_if_decoded(bm, result);
}
// Decodes `codec` at `sampleSize`, restricted to `subset` if non-null.
static void decode_android(SkAndroidCodec* codec, int sampleSize, SkIRect* subset) {
const SkISize size = subset ? codec->getSampledSubsetDimensions(sampleSize, *subset)
: codec->getSampledDimensions(sampleSize);
SkBitmap bm;
if (!bm.tryAllocPixels(SkImageInfo::MakeN32Premul(size))) {
return;
}
SkAndroidCodec::AndroidOptions options;
options.fSampleSize = sampleSize;
options.fSubset = subset;
const SkCodec::Result result =
codec->getAndroidPixels(bm.info(), bm.getPixels(), bm.rowBytes(), &options);
draw_if_decoded(bm, result);
}
// Exercises SkAndroidCodec (SkSampledCodec) sampled, subset, and gainmap decodes,
// as used by BitmapFactory, ImageDecoder, and BitmapRegionDecoder.
static void fuzz_android_codec(std::unique_ptr<SkAndroidCodec> codec, const uint8_t params[5]) {
if (!codec) {
return;
}
const int sampleSize = (params[0] % 8) + 1;
decode_android(codec.get(), sampleSize, /*subset=*/nullptr);
const SkISize dims = codec->getInfo().dimensions();
if (!dims.isEmpty()) {
const int x = params[1] % dims.width();
const int y = params[2] % dims.height();
const int w = (params[3] % (dims.width() - x)) + 1;
const int h = (params[4] % (dims.height() - y)) + 1;
SkIRect subset = SkIRect::MakeXYWH(x, y, w, h);
if (codec->getSupportedSubset(&subset)) {
decode_android(codec.get(), sampleSize, &subset);
}
}
// PNG gainmaps are only exposed through getGainmapAndroidCodec() (PNG does not
// implement getAndroidGainmap()), which exercises SkPngRustCodec::onDecodeGainmap().
SkGainmapInfo gainmapInfo;
std::unique_ptr<SkAndroidCodec> gainmapCodec;
if (codec->getGainmapAndroidCodec(&gainmapInfo, &gainmapCodec) && gainmapCodec) {
decode_android(gainmapCodec.get(), sampleSize, /*subset=*/nullptr);
}
}
bool FuzzPNGRustDecoder(const uint8_t* data, size_t size) {
// Need enough trailing bytes to derive the sampling/subset parameters above.
if (size < 5) {
return false;
}
auto chunkReader = sk_make_sp<FuzzChunkReader>();
SkCodec::Result result;
std::unique_ptr<SkCodec> codec = SkPngRustDecoder::Decode(
SkMemoryStream::MakeDirect(data, size), &result, chunkReader.get());
if (!codec || result != SkCodec::kSuccess) {
return false;
}
fuzz_frames(codec.get());
fuzz_incremental_decode(codec.get());
// Derive sampling/subset parameters from the trailing bytes rather than a prefix,
// so that plain PNG files remain valid seeds.
std::unique_ptr<SkAndroidCodec> androidCodec =
SkAndroidCodec::MakeFromCodec(SkPngRustDecoder::Decode(
SkMemoryStream::MakeDirect(data, size), &result, chunkReader.get()));
fuzz_android_codec(std::move(androidCodec), data + size - 5);
return true;
}
#if defined(SK_BUILD_FOR_LIBFUZZER)
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
// Limit input size to prevent timeouts on heavily-compressed streams.
if (size > 65536) {
return 0;
}
FuzzPNGRustDecoder(data, size);
return 0;
}
#endif