Commit 027e4e40 authored by Dale Curtis's avatar Dale Curtis Committed by Chromium LUCI CQ

Add a ImageDecoder fuzzer.

This fuzzer works similarly to the other WebCodecs fuzzers except
for corpus generation. The gist is that a protobuf structure for
a sequence of ImageDecoder operations is defined and the fuzzer
mutates both the number and contents of each operation.

Corpus generation differs from the static corpus used by the other
WebCodecs fuzzers. The image decoder fuzzer takes a base textproto
with unfilled "type" and "data" fields, then fills in those fields
using test data files from web_tests/images/resources. Since these
are somewhat large image files, the corpus is written out in binary
proto form. The generated corpus is ~12mb.

Generating the corpus in this way ensures that the interaction of
the API with various image types and lengths is well covered by
the capabilities accessible to the fuzzer.

Bug: 1166925
Test: Locally fuzzer runs fine with the corpus.

Change-Id: Ie3d18911d235b184256dec6eac48e64fdd69c9c4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2630590
Commit-Queue: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: default avatarJeremy Roman <jbroman@chromium.org>
Reviewed-by: default avatarJonathan Metzman <metzman@chromium.org>
Reviewed-by: default avatarChrome Cunningham <chcunningham@chromium.org>
Cr-Commit-Position: refs/heads/master@{#844241}
parent c3dae933
......@@ -611,6 +611,59 @@ if (use_libfuzzer) {
]
}
image_decoder_fuzzer_seed_corpus_dir =
"$target_gen_dir/webcodecs/image_fuzzer_seed_corpus"
action("generate_image_decoder_fuzzer_corpus") {
script = "webcodecs/fuzzer_seed_corpus/generate_image_corpus.py"
sources = [ "webcodecs/image_decoder_base.textproto" ]
data = [ "//third_party/blink/web_tests/images/resources/" ]
deps = [
"//third_party/blink/renderer/modules/webcodecs:fuzzer_protos",
"//third_party/protobuf:py_proto",
]
pyproto_path = "$root_out_dir/pyproto"
args = [ "-i" ]
args += rebase_path(sources, root_build_dir)
args += [
"-o",
"ignored",
"-d",
rebase_path(image_decoder_fuzzer_seed_corpus_dir, root_build_dir),
"-p",
rebase_path(pyproto_path, root_build_dir),
"-p",
rebase_path("$pyproto_path/third_party/blink/renderer/modules/webcodecs/",
root_build_dir),
]
outputs = [ image_decoder_fuzzer_seed_corpus_dir ]
}
fuzzer_test("webcodecs_image_decoder_fuzzer") {
sources = [
"webcodecs/fuzzer_utils.cc",
"webcodecs/fuzzer_utils.h",
"webcodecs/image_decoder_fuzzer.cc",
]
seed_corpus = image_decoder_fuzzer_seed_corpus_dir
seed_corpus_deps = [ ":generate_image_decoder_fuzzer_corpus" ]
deps = [
":modules",
"//third_party/blink/renderer/core:testing",
"//third_party/blink/renderer/modules/webcodecs:fuzzer_protos",
"//third_party/blink/renderer/platform:blink_fuzzer_test_support",
"//third_party/libprotobuf-mutator",
"//third_party/protobuf:protobuf_lite",
]
}
fuzzer_test("webcodecs_audio_decoder_fuzzer") {
sources = [
"webcodecs/audio_decoder_fuzzer.cc",
......
......@@ -113,5 +113,6 @@ source_set("unit_tests") {
}
fuzzable_proto_library("fuzzer_protos") {
generate_python = true
sources = [ "fuzzer_inputs.proto" ]
}
......@@ -142,4 +142,64 @@ message VideoEncoderApiInvocation {
message VideoEncoderApiInvocationSequence {
repeated VideoEncoderApiInvocation invocations = 1;
}
\ No newline at end of file
}
message DecodeImage {
optional uint32 frame_index = 1;
optional bool complete_frames_only = 2;
}
message DecodeMetadata {}
message SelectTrack {
required uint32 track_id = 1;
}
message ImageDecoderApiInvocation {
oneof Api {
DecodeImage decode_image = 1;
DecodeMetadata decode_metadata = 2;
SelectTrack select_track = 3;
}
}
message ImageBitmapOptions {
enum ImageOrientation {
ORIENTATION_NONE = 0;
FLIPY = 1;
}
enum PremultiplyAlpha {
PREMULTIPLY_NONE = 0;
PREMULTIPLY = 1;
PREMULTIPLY_DEFAULT = 2;
}
enum ColorSpaceConversion {
CS_NONE = 0;
CS_DEFAULT = 1;
}
enum ResizeQuality {
PIXELATED = 0;
LOW = 1;
MEDIUM = 2;
HIGH = 3;
}
optional ImageOrientation image_orientation = 1;
optional PremultiplyAlpha premultiply_alpha = 2;
optional ColorSpaceConversion color_space_conversion = 3;
optional uint32 resize_width = 4;
optional uint32 resize_height = 5;
optional ResizeQuality resize_quality = 6;
}
message ConfigureImageDecoder {
required bytes data = 1;
required string type = 2;
optional ImageBitmapOptions options = 3;
optional bool prefer_animation = 4;
}
message ImageDecoderApiInvocationSequence {
required ConfigureImageDecoder config = 1;
repeated ImageDecoderApiInvocation invocations = 2;
}
# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Assembles a binary protobuf from a base ImageDecoder fuzzer protobuf and the
contents of the web_tests/images/resources/ directory.
"""
import copy
import os
import sys
# go up 6 parent directories to //src/third_party/blink
path_to_blink = os.path.abspath(
os.path.join(os.path.dirname(os.path.abspath(__file__)),
*[os.path.pardir] * 4))
# go up 2 parent directories to //src
path_to_src_root = os.path.join(path_to_blink, *[os.path.pardir] * 2)
# allow importing modules from //src/components/resources/protobufs
sys.path.insert(
0,
os.path.normpath(
os.path.join(path_to_src_root, 'components/resources/protobufs')))
from binary_proto_generator import BinaryProtoGenerator
EXTENSIONS_MAP = {
"avif": "image/avif",
"png": "image/png",
"ico": "image/x-icon",
"bmp": "image/bmp",
"jpg": "image/jpeg",
"gif": "image/gif",
"cur": "image/x-icon",
"webp": "image/webp",
}
class ImageDecoderProtoGenerator(BinaryProtoGenerator):
def ImportProtoModule(self):
import fuzzer_inputs_pb2
globals()['fuzzer_inputs_pb2'] = fuzzer_inputs_pb2
def EmptyProtoInstance(self):
return fuzzer_inputs_pb2.ImageDecoderApiInvocationSequence()
def ProcessPb(self, opts, pb):
self._outdir = opts.outdir
self._processed_pb = pb
if not os.path.exists(self._outdir):
os.makedirs(self._outdir)
def WritePb(self, image_fn, image_type):
pb = copy.deepcopy(self._processed_pb)
pb.config.type = image_type
with open(image_fn, 'rb') as input_image:
pb.config.data = input_image.read()
out_fn = os.path.basename(image_fn) + '.pb'
with open(os.path.join(self._outdir, out_fn), 'wb') as out_file:
out_file.write(pb.SerializeToString())
def main():
generator = ImageDecoderProtoGenerator()
generator.Run()
image_data_dir = os.path.join(path_to_blink, 'web_tests/images/resources/')
for root, _, files in os.walk(os.path.join(image_data_dir)):
for fn in files:
_, ext = os.path.splitext(fn)
ext = ext.lower().split('.')[1]
if ext.lower() in EXTENSIONS_MAP:
generator.WritePb(os.path.join(root, fn),
EXTENSIONS_MAP[ext.lower()])
if __name__ == '__main__':
sys.exit(main())
config: {
data: '$generated',
type: '$generated',
prefer_animation: false,
},
invocations: [
{
decode_metadata {}
},
{
decode_image {
frame_index: 0,
complete_frames_only: true
}
},
{
select_track {
track_id: 0
}
},
{
decode_image {
frame_index: 0,
complete_frames_only: true
}
}
]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/run_loop.h"
#include "testing/libfuzzer/proto/lpm_interface.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_image_bitmap_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_image_decoder_init.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/modules/webcodecs/fuzzer_inputs.pb.h"
#include "third_party/blink/renderer/modules/webcodecs/fuzzer_utils.h"
#include "third_party/blink/renderer/modules/webcodecs/image_decoder_external.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/v8_per_isolate_data.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/testing/blink_fuzzer_test_support.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
#include <string>
namespace blink {
namespace {
String ToImageOrientation(wc_fuzzer::ImageBitmapOptions_ImageOrientation type) {
switch (type) {
case wc_fuzzer::ImageBitmapOptions_ImageOrientation_ORIENTATION_NONE:
return "none";
case wc_fuzzer::ImageBitmapOptions_ImageOrientation_FLIPY:
return "flipY";
}
}
String ToPremultiplyAlpha(wc_fuzzer::ImageBitmapOptions_PremultiplyAlpha type) {
switch (type) {
case wc_fuzzer::ImageBitmapOptions_PremultiplyAlpha_PREMULTIPLY_NONE:
return "none";
case wc_fuzzer::ImageBitmapOptions_PremultiplyAlpha_PREMULTIPLY:
return "premultiply";
case wc_fuzzer::ImageBitmapOptions_PremultiplyAlpha_PREMULTIPLY_DEFAULT:
return "default";
}
}
String ToColorSpaceConversion(
wc_fuzzer::ImageBitmapOptions_ColorSpaceConversion type) {
switch (type) {
case wc_fuzzer::ImageBitmapOptions_ColorSpaceConversion_CS_NONE:
return "none";
case wc_fuzzer::ImageBitmapOptions_ColorSpaceConversion_CS_DEFAULT:
return "default";
}
}
String ToResizeQuality(wc_fuzzer::ImageBitmapOptions_ResizeQuality type) {
switch (type) {
case wc_fuzzer::ImageBitmapOptions_ResizeQuality_PIXELATED:
return "pixelated";
case wc_fuzzer::ImageBitmapOptions_ResizeQuality_LOW:
return "low";
case wc_fuzzer::ImageBitmapOptions_ResizeQuality_MEDIUM:
return "medium";
case wc_fuzzer::ImageBitmapOptions_ResizeQuality_HIGH:
return "high";
}
}
} // namespace
DEFINE_BINARY_PROTO_FUZZER(
const wc_fuzzer::ImageDecoderApiInvocationSequence& proto) {
static BlinkFuzzerTestSupport test_support = BlinkFuzzerTestSupport();
static DummyPageHolder* page_holder = []() {
auto page_holder = std::make_unique<DummyPageHolder>();
page_holder->GetFrame().GetSettings()->SetScriptEnabled(true);
return page_holder.release();
}();
//
// NOTE: GC objects that need to survive iterations of the loop below
// must be Persistent<>!
//
// GC may be triggered by the RunLoop().RunUntilIdle() below, which will GC
// raw pointers on the stack. This is not required in production code because
// GC typically runs at the top of the stack, or is conservative enough to
// keep stack pointers alive.
//
// Scoping Persistent<> refs so GC can collect these at the end.
{
Persistent<ScriptState> script_state =
ToScriptStateForMainWorld(&page_holder->GetFrame());
ScriptState::Scope scope(script_state);
Persistent<ImageDecoderInit> image_decoder_init =
MakeGarbageCollected<ImageDecoderInit>();
image_decoder_init->setType(proto.config().type().c_str());
DOMArrayBuffer* data_copy = DOMArrayBuffer::Create(
proto.config().data().data(), proto.config().data().size());
image_decoder_init->setData(
ArrayBufferOrArrayBufferViewOrReadableStream::FromArrayBuffer(
data_copy));
Persistent<ImageBitmapOptions> options = ImageBitmapOptions::Create();
options->setImageOrientation(
ToImageOrientation(proto.config().options().image_orientation()));
options->setPremultiplyAlpha(
ToPremultiplyAlpha(proto.config().options().premultiply_alpha()));
options->setColorSpaceConversion(ToColorSpaceConversion(
proto.config().options().color_space_conversion()));
options->setResizeWidth(proto.config().options().resize_width());
options->setResizeHeight(proto.config().options().resize_height());
options->setResizeQuality(
ToResizeQuality(proto.config().options().resize_quality()));
image_decoder_init->setOptions(options);
image_decoder_init->setPreferAnimation(proto.config().prefer_animation());
Persistent<ImageDecoderExternal> image_decoder =
ImageDecoderExternal::Create(script_state, image_decoder_init,
IGNORE_EXCEPTION_FOR_TESTING);
// Promises will be fulfilled synchronously since we're using an array
// buffer based source.
for (auto& invocation : proto.invocations()) {
switch (invocation.Api_case()) {
case wc_fuzzer::ImageDecoderApiInvocation::kDecodeImage:
image_decoder->decode(
invocation.decode_image().frame_index(),
invocation.decode_image().complete_frames_only());
break;
case wc_fuzzer::ImageDecoderApiInvocation::kDecodeMetadata:
image_decoder->decodeMetadata();
break;
case wc_fuzzer::ImageDecoderApiInvocation::kSelectTrack:
image_decoder->selectTrack(invocation.select_track().track_id(),
IGNORE_EXCEPTION_FOR_TESTING);
break;
case wc_fuzzer::ImageDecoderApiInvocation::API_NOT_SET:
break;
}
// Give other tasks a chance to run (e.g. calling our output callback).
base::RunLoop().RunUntilIdle();
// TODO(crbug.com/1166925): Push the same image data incrementally into
// the fuzzer via a ReadableSource.
}
}
// Request a V8 GC. Oilpan will be invoked by the GC epilogue.
//
// Multiple GCs may be required to ensure everything is collected (due to
// a chain of persistent handles), so some objects may not be collected until
// a subsequent iteration. This is slow enough as is, so we compromise on one
// major GC, as opposed to the 5 used in V8GCController for unit tests.
V8PerIsolateData::MainThreadIsolate()->RequestGarbageCollectionForTesting(
v8::Isolate::kFullGarbageCollection);
}
} // namespace blink
......@@ -32,7 +32,9 @@ template("fuzzable_proto_library") {
# The plugin will generate cc, so don't ask for it to be done by protoc.
generate_cc = false
generate_python = false
if (!defined(invoker.generate_python)) {
generate_python = false
}
extra_configs = [ "//third_party/protobuf:protobuf_config" ]
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment