diff --git a/projects/openexr/Dockerfile b/projects/openexr/Dockerfile index 2cc49455f768..b123f0cba396 100644 --- a/projects/openexr/Dockerfile +++ b/projects/openexr/Dockerfile @@ -16,6 +16,17 @@ FROM gcr.io/oss-fuzz-base/base-builder RUN apt-get update && apt-get install -y make autoconf automake libtool zlib1g-dev +RUN apt-get update -y && \ + apt-get install -y make autoconf automake libtool zlib1g-dev \ + binutils ninja-build liblzma-dev libz-dev pkg-config + RUN git clone --depth 1 https://github.com/AcademySoftwareFoundation/openexr openexr + +RUN git clone --depth 1 https://github.com/google/libprotobuf-mutator.git +RUN (mkdir LPM && cd LPM && cmake ../libprotobuf-mutator -GNinja -DLIB_PROTO_MUTATOR_DOWNLOAD_PROTOBUF=ON -DLIB_PROTO_MUTATOR_TESTING=OFF -DCMAKE_BUILD_TYPE=Release && ninja) + WORKDIR openexr + +COPY *.dict *.proto *.h *.cc $SRC/ + COPY build.sh $SRC/ diff --git a/projects/openexr/build.sh b/projects/openexr/build.sh index 2b4f07f92ee9..b501dda98eb1 100755 --- a/projects/openexr/build.sh +++ b/projects/openexr/build.sh @@ -32,3 +32,26 @@ cmake -S $SRC/openexr -B $BUILD_DIR --preset oss_fuzz cmake --build $BUILD_DIR --target oss_fuzz -j"$(nproc)" cmake --install $BUILD_DIR --component oss_fuzz +# ProtobufMutator does not currently support MSan +if [[ $CFLAGS = *sanitize=memory* ]]; then + exit 0 +fi + +# For fuzz-introspector, exclude files we don't want +export FUZZ_INTROSPECTOR_CONFIG=$SRC/fuzz_introspector_exclusion.config +cat > $FUZZ_INTROSPECTOR_CONFIG <<EOF +FILES_TO_AVOID +LPM +genfiles +EOF + +# Compile json proto. +rm -rf genfiles && mkdir genfiles && $SRC/LPM/external.protobuf/bin/protoc exr.proto --cpp_out=genfiles --proto_path=$SRC + +# Compile LPM fuzzer. +$CXX $CXXFLAGS -std=c++14 -pthread ${INCLUDES[@]} -I genfiles -I $SRC/libprotobuf-mutator/ -I $SRC/LPM/external.protobuf/include $LIB_FUZZING_ENGINE \ + $SRC/exr_proto_fuzzer.cc genfiles/exr.pb.cc $SRC/exr_proto_converter.cc \ + $SRC/LPM/src/libfuzzer/libprotobuf-mutator-libfuzzer.a \ + $SRC/LPM/src/libprotobuf-mutator.a \ + -Wl,--start-group $SRC/LPM/external.protobuf/lib/lib*.a -Wl,--end-group \ + ${LIBS[@]} -lz -o $OUT/exr_proto_fuzzer diff --git a/projects/openexr/exr.proto b/projects/openexr/exr.proto new file mode 100644 index 000000000000..a71f1c5ce5e1 --- /dev/null +++ b/projects/openexr/exr.proto @@ -0,0 +1,73 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +syntax = "proto2"; +// Very simple proto description of the EXR format, +// described at https://openexr.com/en/latest/OpenEXRFileLayout.html + +// we will calculate xMax and yMax from with width & height +message Exr_box2i { + required int32 xMin = 1; + required int32 yMin = 2; + required int32 w = 3; + required int32 h = 4; +} + +message Exr_channel { + required string chname = 1; + required int32 pixel_type = 2; + required uint32 pLinear = 3; + // here there should be three \x00 chars + required int32 xSampling = 4; + required int32 ySampling = 5; +} + +message Exr_chlist { + // this should be null terminated + repeated Exr_channel channels = 1; +} + +message Exr_compression { + required uint32 compression = 1; +} + +message Exr_v2f { + required float f1 = 1; + required float f2 = 2; +} + +message Exr_lineOrder { + required uint32 lineOrder = 1; +} + +message ExrHeader { + required Exr_chlist channel_list = 1; + required Exr_compression compression = 2; + required Exr_box2i dataWindow = 3; + required Exr_box2i displayWindow = 4; + required Exr_lineOrder lineOrder = 5; + required float pixelAspectRatio = 6; + required Exr_v2f screenWindowCenter = 7; + required float screenWindowWidth = 8; + required bytes other_attr = 9; +} + +message ExrProto { + required ExrHeader header = 1; + required bytes scanlines = 2; +} + +// package fuzzer_examples; \ No newline at end of file diff --git a/projects/openexr/exr_proto_converter.cc b/projects/openexr/exr_proto_converter.cc new file mode 100644 index 000000000000..3ecbbaf21b42 --- /dev/null +++ b/projects/openexr/exr_proto_converter.cc @@ -0,0 +1,192 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +#include "exr_proto_converter.h" + +static void WriteInt(std::stringstream &out, uint32_t x) { + //x = __builtin_bswap32(x); + out.write((char *)&x, sizeof(x)); +} + +static void WriteLong(std::stringstream &out, uint64_t x) { + //x = __builtin_bswap64(x); + out.write((char *)&x, sizeof(x)); +} + +static void WriteFloat(std::stringstream &out, float x) { + out.write((char *)&x, sizeof(x)); +} + +static void WriteByte(std::stringstream &out, uint8_t x) { + out.write((char *)&x, sizeof(x)); +} + +static void WriteString(std::stringstream &out, std::string x) { + out.write(x.data(), x.size()); + out.write("\x00", 1); +} + +static void WriteNString(std::stringstream &out, std::string x) { + out.write(x.data(), x.size()); +} + +static void WriteTypeInfo(std::stringstream &out, std::string name, std::string type, int size) { + WriteString(out, name); + WriteString(out, type); + WriteInt(out, size); +} + +static int CalculateChannelSize (int chan_type) { + int size = 0; + switch (chan_type % 3) { + case 1: + size = 2; + break; + default: + size = 4; + } + return size; +} + +const float MIN_PIXEL_ASPECT_RATIO = 1e-6f; +const float MAX_PIXEL_ASPECT_RATIO = 1e+6f; + +std::string ProtoToExr(const ExrProto &exr_proto) { + std::stringstream all; + const unsigned char magic[] = {0x76, 0x2f, 0x31, 0x01}; + all.write((const char*)magic, sizeof(magic)); + + // we only support version2 in single flat scanlines for now + const unsigned char version[] = {0x02, 0x00, 0x00, 0x00}; + all.write((const char*)version, sizeof(version)); + + auto &header = exr_proto.header(); + + std::stringstream channels; + std::unordered_set<std::string> used_channels; + int pixel_data_size = 0; + for (const auto& chan : header.channel_list().channels()) { + std::string chname = "G"; + if (!chan.chname().empty() && std::isprint(static_cast<unsigned char>(chan.chname()[0]))) { + chname = chan.chname(); + } + if (used_channels.find(chname) != used_channels.end()) { + continue; // we don't care about this one. bad but at least works + } else { + used_channels.insert(chname); + } + WriteString(channels, chname); + + WriteInt(channels, chan.pixel_type() % 3); // UINT, HALF, FLOAT + WriteByte(channels, chan.plinear() % 2); // 0 or 1 + WriteByte(channels, 0); + WriteByte(channels, 0); + WriteByte(channels, 0); + WriteInt(channels, std::max(chan.xsampling(), 1)); + WriteInt(channels, std::max(chan.ysampling(), 1)); + + pixel_data_size += CalculateChannelSize(chan.pixel_type()); + } + WriteTypeInfo(all, "channels", "chlist", channels.str().size() + 1); + WriteString(all, channels.str()); // value + + WriteTypeInfo(all, "compression", "compression", 1); + WriteByte(all, header.compression().compression() % 10); //value + + // this is the base point + auto xmin = header.datawindow().xmin(); + auto ymin = header.datawindow().ymin(); + + // this are the lenght of the sides (h and w) so we compute the other coordinates + auto xmax = xmin + std::max(std::min(header.datawindow().w(), 0), 20); + auto ymax = ymin + std::max(std::min(header.datawindow().h(), 0), 20); + + WriteTypeInfo(all, "dataWindow", "box2i", 16); + WriteInt(all, xmin); + WriteInt(all, ymin); + WriteInt(all, xmax); + WriteInt(all, ymax); + + // we are copying datawindow values to decrease the number of operations + // but this also reduce the fuzzed testcases + WriteTypeInfo(all, "displayWindow", "box2i", 16); + WriteInt(all, xmin); + WriteInt(all, ymin); + WriteInt(all, xmax); + WriteInt(all, ymax); + + WriteTypeInfo(all, "lineOrder", "lineOrder", 1); + WriteByte(all, header.lineorder().lineorder() % 3); + + WriteTypeInfo(all, "pixelAspectRatio", "float", 4); + auto par = std::max(MIN_PIXEL_ASPECT_RATIO, std::min(header.pixelaspectratio(), MAX_PIXEL_ASPECT_RATIO)); + if (par == 0.0) { par += 0.1337; } + WriteFloat(all, par); + + WriteTypeInfo(all, "screenWindowCenter", "v2f", 8); + WriteFloat(all, header.screenwindowcenter().f1()); + WriteFloat(all, header.screenwindowcenter().f2()); + + WriteTypeInfo(all, "screenWindowWidth", "float", 4); + WriteFloat(all, header.screenwindowwidth()); + + // end of header + WriteByte(all, 0); + + auto n_channels = header.channel_list().channels_size(); + auto lines = (xmax - xmin); + auto n_pixels = (ymax - ymin); + + pixel_data_size = pixel_data_size * n_pixels; + + // base_offset contains the header size and the offset table size + // the offset table contains 64bit integers for every scanline + auto base_offset = static_cast<int>(all.tellp()) + (lines * 8); + + std::stringstream offset_table; + std::stringstream scanlines; + + for (int i = 0; i<= lines; i++) { + // write the offset for the current line + // (4 + 4 + n_pixels * n_channels) for every scanline + WriteInt(offset_table, base_offset + (i * (8 + pixel_data_size))); + + // write scanline number + WriteInt(scanlines, i); + // write pixel data size times pixel number + WriteInt(scanlines, pixel_data_size); + + // write the pixel data + for (const auto& chan : header.channel_list().channels()) { + std::string pixels(n_pixels * CalculateChannelSize(chan.pixel_type()), '\xea'); + pixels.replace(0, exr_proto.scanlines().size(), exr_proto.scanlines()); + WriteNString(scanlines, pixels); + } + } + + WriteNString(all, offset_table.str()); + WriteByte(all, 255); + WriteNString(all, scanlines.str()); + + std::string res = all.str(); + if (const char *dump_path = getenv("PROTO_FUZZER_DUMP_PATH")) { + // With libFuzzer binary run this to generate a EXR file x.exr: + // PROTO_FUZZER_DUMP_PATH=x.exr ./a.out proto-input + std::ofstream of(dump_path, std::ios::binary); + of.write(res.data(), res.size()); + } + return res; +} diff --git a/projects/openexr/exr_proto_converter.h b/projects/openexr/exr_proto_converter.h new file mode 100644 index 000000000000..ead93c9169ab --- /dev/null +++ b/projects/openexr/exr_proto_converter.h @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef EXR_PROTO_CONVERTER_H +#define EXR_PROTO_CONVERTER_H + +#include "libprotobuf-mutator/src/libfuzzer/libfuzzer_macro.h" +#include "exr.pb.h" + +#include <string> +#include <sstream> +#include <fstream> +#include <cstdlib> +#include <algorithm> + +std::string ProtoToExr(const ExrProto &exr_proto); + +#endif // EXR_PROTO_CONVERTER_H \ No newline at end of file diff --git a/projects/openexr/exr_proto_fuzzer.cc b/projects/openexr/exr_proto_fuzzer.cc new file mode 100644 index 000000000000..5205a264febd --- /dev/null +++ b/projects/openexr/exr_proto_fuzzer.cc @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +#include "libprotobuf-mutator/src/libfuzzer/libfuzzer_macro.h" +#include "exr.pb.h" + +#include <ImfNamespace.h> +#include <ImfCheckFile.h> +#include <string> + +#include "exr_proto_converter.h" + +DEFINE_PROTO_FUZZER(const ExrProto &exr) { + std::string data = ProtoToExr(exr); + + Imf::checkOpenEXRFile (data.c_str() , data.size() , true , true , true); + return; +} \ No newline at end of file diff --git a/projects/openexr/project.yaml b/projects/openexr/project.yaml index e5093509c5a6..13e686f378a5 100644 --- a/projects/openexr/project.yaml +++ b/projects/openexr/project.yaml @@ -5,6 +5,7 @@ auto_ccs: - "cbpilm@gmail.com" - "security@openexr.org" - "kdt3rd@gmail.com" + - "research@shielder.it" main_repo: 'https://github.com/AcademySoftwareFoundation/openexr' fuzzing_engines: