This is an automated email from the ASF dual-hosted git repository. iemejia pushed a commit to branch fuzzer in repository https://gitbox.apache.org/repos/asf/avro.git
commit 845fdb7475dae3e485c677ca31d5be124e7165e0 Author: Ismaël Mejía <[email protected]> AuthorDate: Tue Mar 31 22:50:43 2026 +0000 Add fuzz testing module for Avro Java using Jazzer Add a new lang/java/fuzz/ Maven module with coverage-guided fuzz tests targeting Schema parsing, Binary decoding, JSON decoding, and Avro container file reading. Uses Jazzer 0.22.1 with JUnit 5 integration. Found two bugs during initial fuzzing: - NullPointerException in ParseContext.resolve() on malformed schema input (should be SchemaParseException) - Denial-of-service via memory exhaustion in BinaryDecoder.readString() when untrusted input encodes large varint lengths Run regression tests with 'mvn test -pl fuzz' or enable continuous fuzzing with 'mvn test -pl fuzz -Pfuzz'. --- lang/java/fuzz/pom.xml | 147 +++++++++++++++++++++ .../org/apache/avro/fuzz/BinaryDecodingFuzzer.java | 89 +++++++++++++ .../org/apache/avro/fuzz/DataFileReaderFuzzer.java | 84 ++++++++++++ .../org/apache/avro/fuzz/JsonDecodingFuzzer.java | 78 +++++++++++ .../java/org/apache/avro/fuzz/SchemaFuzzer.java | 60 +++++++++ .../src/test/resources/junit-platform.properties | 6 + lang/java/pom.xml | 1 + 7 files changed, 465 insertions(+) diff --git a/lang/java/fuzz/pom.xml b/lang/java/fuzz/pom.xml new file mode 100644 index 0000000000..970b05c523 --- /dev/null +++ b/lang/java/fuzz/pom.xml @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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 + + https://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. +--> +<project + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" + xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>avro-parent</artifactId> + <groupId>org.apache.avro</groupId> + <version>1.13.0-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <artifactId>avro-fuzz</artifactId> + + <name>Apache Avro Fuzz Tests</name> + <description>Fuzz testing for Apache Avro using Jazzer</description> + <packaging>jar</packaging> + + <properties> + <jazzer.version>0.22.1</jazzer.version> + <main.basedir>${project.parent.parent.basedir}</main.basedir> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <includes> + <include>**/*Fuzzer</include> + </includes> + <!-- Jazzer fuzzing requires single-threaded, non-parallel execution --> + <parallel combine.self="override"/> + <useUnlimitedThreads>false</useUnlimitedThreads> + <threadCount>1</threadCount> + <forkCount>1</forkCount> + <reuseForks>true</reuseForks> + <redirectTestOutputToFile>false</redirectTestOutputToFile> + <properties> + <configurationParameters> + jazzer.instrument = org.apache.avro.** + </configurationParameters> + </properties> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <archive> + <manifestEntries> + <Automatic-Module-Name>org.apache.avro.fuzz</Automatic-Module-Name> + </manifestEntries> + </archive> + </configuration> + </plugin> + </plugins> + </build> + + <profiles> + <!-- Activate with: mvn test -pl fuzz -Pfuzz --> + <profile> + <id>fuzz</id> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <environmentVariables> + <JAZZER_FUZZ>1</JAZZER_FUZZ> + </environmentVariables> + <properties> + <configurationParameters> + jazzer.internal.command_line = true + jazzer.duration = 60s + jazzer.instrument = org.apache.avro.** + </configurationParameters> + </properties> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <dependencies> + <dependency> + <groupId>org.apache.avro</groupId> + <artifactId>avro</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.code-intelligence</groupId> + <artifactId>jazzer-junit</artifactId> + <version>${jazzer.version}</version> + <scope>test</scope> + <exclusions> + <!-- Exclude older JUnit Platform/Jupiter versions pulled by Jazzer; + the correct versions are provided by the parent POM's JUnit 5.x deps --> + <exclusion> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + </exclusion> + <exclusion> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + </exclusion> + <exclusion> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-commons</artifactId> + </exclusion> + <exclusion> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + </exclusion> + </exclusions> + </dependency> + <!-- Provide the JUnit Platform Launcher at the same version as the rest of + JUnit 5 to avoid version misalignment between platform-engine and platform-launcher --> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <version>1.14.3</version> + <scope>test</scope> + </dependency> + </dependencies> + +</project> diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/BinaryDecodingFuzzer.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/BinaryDecodingFuzzer.java new file mode 100644 index 0000000000..4912d89b31 --- /dev/null +++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/BinaryDecodingFuzzer.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ +package org.apache.avro.fuzz; + +import com.code_intelligence.jazzer.junit.FuzzTest; +import org.apache.avro.AvroRuntimeException; +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericDatumReader; +import org.apache.avro.generic.GenericRecord; +import org.apache.avro.io.BinaryDecoder; +import org.apache.avro.io.DecoderFactory; + +import java.io.IOException; + +/** + * Fuzz tests for Avro binary decoding. + * + * <p> + * Feeds arbitrary bytes through {@link BinaryDecoder} and + * {@link GenericDatumReader} using several representative schemas to exercise + * varint parsing, buffer management, nested record handling, and collection + * size limit enforcement. + * </p> + */ +class BinaryDecodingFuzzer { + + /** A record schema with multiple field types to exercise diverse code paths. */ + private static final Schema RECORD_SCHEMA = new Schema.Parser() + .parse("{\"type\":\"record\",\"name\":\"TestRecord\",\"fields\":[" + "{\"name\":\"id\",\"type\":\"long\"}," + + "{\"name\":\"name\",\"type\":\"string\"}," + "{\"name\":\"value\",\"type\":\"double\"}," + + "{\"name\":\"tags\",\"type\":{\"type\":\"array\",\"items\":\"string\"}}," + + "{\"name\":\"metadata\",\"type\":{\"type\":\"map\",\"values\":\"string\"}}," + + "{\"name\":\"optionalField\",\"type\":[\"null\",\"string\"]}" + "]}"); + + /** A schema with nested records to test recursive decoding. */ + private static final Schema NESTED_SCHEMA = new Schema.Parser() + .parse("{\"type\":\"record\",\"name\":\"Outer\",\"fields\":[" + + "{\"name\":\"inner\",\"type\":{\"type\":\"record\",\"name\":\"Inner\",\"fields\":[" + + "{\"name\":\"x\",\"type\":\"int\"}," + "{\"name\":\"y\",\"type\":\"int\"}" + "]}}," + + "{\"name\":\"label\",\"type\":\"string\"}" + "]}"); + + /** An enum schema. */ + private static final Schema ENUM_SCHEMA = new Schema.Parser() + .parse("{\"type\":\"record\",\"name\":\"EnumRecord\",\"fields\":[" + + "{\"name\":\"status\",\"type\":{\"type\":\"enum\",\"name\":\"Status\"," + + "\"symbols\":[\"ACTIVE\",\"INACTIVE\",\"PENDING\"]}}," + "{\"name\":\"count\",\"type\":\"int\"}" + "]}"); + + @FuzzTest + void fuzzBinaryDecodingRecord(byte[] data) { + decodeWithSchema(data, RECORD_SCHEMA); + } + + @FuzzTest + void fuzzBinaryDecodingNested(byte[] data) { + decodeWithSchema(data, NESTED_SCHEMA); + } + + @FuzzTest + void fuzzBinaryDecodingEnum(byte[] data) { + decodeWithSchema(data, ENUM_SCHEMA); + } + + private static void decodeWithSchema(byte[] data, Schema schema) { + try { + BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(data, null); + GenericDatumReader<GenericRecord> reader = new GenericDatumReader<>(schema); + reader.read(null, decoder); + } catch (IOException | AvroRuntimeException | ArrayIndexOutOfBoundsException | UnsupportedOperationException e) { + // Expected for malformed binary data + // UnsupportedOperationException: thrown by SystemLimitException for oversized + // strings/collections + } + } +} diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/DataFileReaderFuzzer.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/DataFileReaderFuzzer.java new file mode 100644 index 0000000000..fd12ec7109 --- /dev/null +++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/DataFileReaderFuzzer.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ +package org.apache.avro.fuzz; + +import com.code_intelligence.jazzer.junit.FuzzTest; +import org.apache.avro.AvroRuntimeException; +import org.apache.avro.file.DataFileReader; +import org.apache.avro.file.DataFileStream; +import org.apache.avro.file.SeekableByteArrayInput; +import org.apache.avro.generic.GenericDatumReader; +import org.apache.avro.generic.GenericRecord; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * Fuzz tests for Avro data file (container format) reading. + * + * <p> + * Feeds arbitrary bytes through {@link DataFileReader} and + * {@link DataFileStream} to test the full Avro container format parsing: magic + * bytes, file header, metadata (including the embedded schema), sync markers, + * codec decompression, and datum deserialization. This is the highest-value + * target for finding container format vulnerabilities. + * </p> + */ +class DataFileReaderFuzzer { + + /** + * Fuzz the seekable {@link DataFileReader} path. + * + * <p> + * Uses {@link SeekableByteArrayInput} to wrap the fuzz input, which exercises + * the random-access container file reading code path including block seeking. + * </p> + */ + @FuzzTest + void fuzzDataFileReader(byte[] data) { + try (DataFileReader<GenericRecord> reader = new DataFileReader<>(new SeekableByteArrayInput(data), + new GenericDatumReader<>())) { + while (reader.hasNext()) { + reader.next(); + } + } catch (IOException | AvroRuntimeException | UnsupportedOperationException e) { + // Expected for malformed container files -- invalid magic, bad headers, + // corrupt sync markers, unsupported codecs, etc. + } + } + + /** + * Fuzz the streaming {@link DataFileStream} path. + * + * <p> + * Uses a plain {@link ByteArrayInputStream}, which exercises the sequential + * (non-seekable) container file reading code path. + * </p> + */ + @FuzzTest + void fuzzDataFileStream(byte[] data) { + try (DataFileStream<GenericRecord> stream = new DataFileStream<>(new ByteArrayInputStream(data), + new GenericDatumReader<>())) { + while (stream.hasNext()) { + stream.next(); + } + } catch (IOException | AvroRuntimeException | UnsupportedOperationException e) { + // Expected for malformed container files + } + } +} diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/JsonDecodingFuzzer.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/JsonDecodingFuzzer.java new file mode 100644 index 0000000000..bfb858b984 --- /dev/null +++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/JsonDecodingFuzzer.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ +package org.apache.avro.fuzz; + +import com.code_intelligence.jazzer.junit.FuzzTest; +import org.apache.avro.AvroRuntimeException; +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericDatumReader; +import org.apache.avro.generic.GenericRecord; +import org.apache.avro.io.DecoderFactory; +import org.apache.avro.io.JsonDecoder; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Fuzz tests for Avro JSON decoding. + * + * <p> + * Feeds arbitrary JSON strings through {@link JsonDecoder} with various schemas + * to test the JSON-to-Avro deserialization path, including Jackson JSON + * parsing, type coercion, union resolution, and error handling for malformed + * data. + * </p> + */ +class JsonDecodingFuzzer { + + /** A record schema with diverse field types. */ + private static final Schema RECORD_SCHEMA = new Schema.Parser() + .parse("{\"type\":\"record\",\"name\":\"TestRecord\",\"fields\":[" + "{\"name\":\"id\",\"type\":\"long\"}," + + "{\"name\":\"name\",\"type\":\"string\"}," + "{\"name\":\"active\",\"type\":\"boolean\"}," + + "{\"name\":\"scores\",\"type\":{\"type\":\"array\",\"items\":\"double\"}}," + + "{\"name\":\"props\",\"type\":{\"type\":\"map\",\"values\":\"string\"}}," + + "{\"name\":\"extra\",\"type\":[\"null\",\"string\",\"long\"]}" + "]}"); + + /** A simpler schema for basic field decoding. */ + private static final Schema SIMPLE_SCHEMA = new Schema.Parser() + .parse("{\"type\":\"record\",\"name\":\"Simple\",\"fields\":[" + "{\"name\":\"x\",\"type\":\"int\"}," + + "{\"name\":\"y\",\"type\":\"string\"}" + "]}"); + + @FuzzTest + void fuzzJsonDecodingRecord(byte[] data) { + decodeJsonWithSchema(data, RECORD_SCHEMA); + } + + @FuzzTest + void fuzzJsonDecodingSimple(byte[] data) { + decodeJsonWithSchema(data, SIMPLE_SCHEMA); + } + + private static void decodeJsonWithSchema(byte[] data, Schema schema) { + String jsonData = new String(data, StandardCharsets.UTF_8); + try { + JsonDecoder decoder = DecoderFactory.get().jsonDecoder(schema, jsonData); + GenericDatumReader<GenericRecord> reader = new GenericDatumReader<>(schema); + reader.read(null, decoder); + } catch (IOException | AvroRuntimeException | UnsupportedOperationException e) { + // Expected for malformed JSON or type mismatches + // UnsupportedOperationException: thrown by SystemLimitException for oversized + // strings/collections + } + } +} diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SchemaFuzzer.java b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SchemaFuzzer.java new file mode 100644 index 0000000000..20cd38ddc4 --- /dev/null +++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SchemaFuzzer.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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. + */ +package org.apache.avro.fuzz; + +import com.code_intelligence.jazzer.junit.FuzzTest; +import org.apache.avro.AvroRuntimeException; +import org.apache.avro.Schema; + +import java.nio.charset.StandardCharsets; + +/** + * Fuzz tests for Avro schema parsing. + * + * <p> + * Targets {@link Schema#parse(String)} with arbitrary input to find crashes, + * hangs, or unexpected exceptions in the JSON schema parser. Expected + * exceptions like {@link SchemaParseException} are caught and ignored since + * they represent correct rejection of invalid input. + * </p> + */ +class SchemaFuzzer { + + @FuzzTest + void fuzzSchemaParse(byte[] data) { + String schemaJson = new String(data, StandardCharsets.UTF_8); + try { + Schema.parse(schemaJson); + } catch (AvroRuntimeException | IllegalArgumentException | NullPointerException e) { + // Expected for invalid schema input -- these indicate correct rejection + // (SchemaParseException is a subclass of AvroRuntimeException) + // NullPointerException: known bug in ParseContext.resolve with unresolved + // schemas + } + } + + @FuzzTest + void fuzzSchemaParseWithValidation(byte[] data) { + String schemaJson = new String(data, StandardCharsets.UTF_8); + try { + Schema.parse(schemaJson, true); + } catch (AvroRuntimeException | IllegalArgumentException | NullPointerException e) { + // Expected for invalid schema input -- these indicate correct rejection + } + } +} diff --git a/lang/java/fuzz/src/test/resources/junit-platform.properties b/lang/java/fuzz/src/test/resources/junit-platform.properties new file mode 100644 index 0000000000..d0c998e4c5 --- /dev/null +++ b/lang/java/fuzz/src/test/resources/junit-platform.properties @@ -0,0 +1,6 @@ +# Jazzer JUnit configuration +# Instrument Avro classes for coverage-guided fuzzing +jazzer.instrument=org.apache.avro.** +# To enable continuous fuzzing (not just regression testing), run: +# mvn test -pl fuzz -Pfuzz +# or set environment variable JAZZER_FUZZ=1 diff --git a/lang/java/pom.xml b/lang/java/pom.xml index 4c7d348e17..ac56a1446f 100644 --- a/lang/java/pom.xml +++ b/lang/java/pom.xml @@ -90,6 +90,7 @@ <module>grpc</module> <module>integration-test</module> <module>perf</module> + <module>fuzz</module> <module>interop-data-test</module> <module>java17-test</module> </modules>
