| /* | |
| * 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 |