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 4c3e0d0aeac52becb6c451a82d89f93788683173
Author: Ismaël Mejía <[email protected]>
AuthorDate: Sat Apr 4 16:19:36 2026 +0000

    Harden Jazzer fuzz targets and container metadata handling
    
    Surface unexpected runtime failures during fuzzing, expand coverage with 
direct decoding and valid seeds, and fail malformed Avro container schema 
metadata with IOException instead of NullPointerException.
---
 .../org/apache/avro/file/DataFileReader12.java     |  15 +-
 .../java/org/apache/avro/file/DataFileStream.java  |  15 +-
 .../java/org/apache/avro/TestDataFileReader.java   |  38 ++++
 lang/java/fuzz/README.md                           |  52 ++++++
 lang/java/fuzz/REPORT.md                           | 163 +++++------------
 lang/java/fuzz/pom.xml                             |   2 +-
 .../org/apache/avro/fuzz/BinaryDecodingFuzzer.java |  44 ++---
 .../org/apache/avro/fuzz/DataFileReaderFuzzer.java |  13 +-
 .../java/org/apache/avro/fuzz/FuzzSupport.java     | 199 +++++++++++++++++++++
 .../org/apache/avro/fuzz/JsonDecodingFuzzer.java   |  41 ++---
 .../java/org/apache/avro/fuzz/SchemaFuzzer.java    |  24 +--
 .../org/apache/avro/fuzz/SeedCorpusGenerator.java  |  90 ++++++++++
 .../src/test/resources/junit-platform.properties   |   5 +-
 .../fuzzBinaryDecoding/seed-valid-record.bin       | Bin 0 -> 31 bytes
 .../fuzzDirectBinaryDecoding/seed-valid-record.bin | Bin 0 -> 31 bytes
 .../fuzzDataFileReader/seed-valid-container.avro   | Bin 0 -> 516 bytes
 .../fuzzDataFileStream/seed-valid-container.avro   | Bin 0 -> 516 bytes
 .../fuzzJsonDecoding/seed-valid-record.json        |   1 +
 .../fuzzSchemaParse/seed-valid-recursive.avsc      |   1 +
 lang/java/pom.xml                                  |   1 +
 20 files changed, 517 insertions(+), 187 deletions(-)

diff --git 
a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileReader12.java 
b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileReader12.java
index c057a86db7..762b252803 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileReader12.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileReader12.java
@@ -17,8 +17,8 @@
  */
 package org.apache.avro.file;
 
-import java.io.IOException;
 import java.io.Closeable;
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -26,12 +26,13 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 
+import org.apache.avro.AvroRuntimeException;
 import org.apache.avro.InvalidAvroMagicException;
 import org.apache.avro.Schema;
 import org.apache.avro.UnknownAvroCodecException;
+import org.apache.avro.io.BinaryDecoder;
 import org.apache.avro.io.DatumReader;
 import org.apache.avro.io.DecoderFactory;
-import org.apache.avro.io.BinaryDecoder;
 
 /** Read files written by Avro version 1.2. */
 public class DataFileReader12<D> implements FileReader<D>, Closeable {
@@ -88,7 +89,15 @@ public class DataFileReader12<D> implements FileReader<D>, 
Closeable {
     if (codec != null && !codec.equals(NULL_CODEC)) {
       throw new UnknownAvroCodecException("Unknown codec: " + codec);
     }
-    this.schema = new Schema.Parser().parse(getMetaString(SCHEMA));
+    String schemaJson = getMetaString(SCHEMA);
+    if (schemaJson == null) {
+      throw new IOException("Missing required metadata: " + SCHEMA);
+    }
+    try {
+      this.schema = new Schema.Parser().parse(schemaJson);
+    } catch (AvroRuntimeException e) {
+      throw new IOException("Invalid schema in metadata: " + SCHEMA, e);
+    }
     this.reader = reader;
 
     reader.setSchema(schema);
diff --git 
a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java 
b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
index e2e79d8eae..3e5049c7e6 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
@@ -140,8 +140,7 @@ public class DataFileStream<D> implements Iterator<D>, 
Iterable<D>, Closeable {
 
     // finalize the header
     header.metaKeyList = Collections.unmodifiableList(header.metaKeyList);
-    header.schema = new 
Schema.Parser(NameValidator.NO_VALIDATION).setValidateDefaults(false)
-        .parse(getMetaString(DataFileConstants.SCHEMA));
+    header.schema = parseHeaderSchema();
     this.codec = resolveCodec();
     reader.setSchema(header.schema);
   }
@@ -198,6 +197,18 @@ public class DataFileStream<D> implements Iterator<D>, 
Iterable<D>, Closeable {
     return Long.parseLong(getMetaString(key));
   }
 
+  private Schema parseHeaderSchema() throws IOException {
+    String schemaJson = getMetaString(DataFileConstants.SCHEMA);
+    if (schemaJson == null) {
+      throw new IOException("Missing required metadata: " + 
DataFileConstants.SCHEMA);
+    }
+    try {
+      return new 
Schema.Parser(NameValidator.NO_VALIDATION).setValidateDefaults(false).parse(schemaJson);
+    } catch (AvroRuntimeException e) {
+      throw new IOException("Invalid schema in metadata: " + 
DataFileConstants.SCHEMA, e);
+    }
+  }
+
   /**
    * Returns an iterator over entries in this file. Note that this iterator is
    * shared with other users of the file: it does not contain a separate 
pointer
diff --git 
a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java 
b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
index 6ed6b35cc5..16d4ccdcb8 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
@@ -24,19 +24,25 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import com.sun.management.UnixOperatingSystemMXBean;
 import org.apache.avro.file.DataFileReader;
+import org.apache.avro.file.DataFileConstants;
 import org.apache.avro.file.DataFileStream;
 import org.apache.avro.file.DataFileWriter;
 import org.apache.avro.file.FileReader;
+import org.apache.avro.file.SeekableByteArrayInput;
 import org.apache.avro.file.SeekableFileInput;
 import org.apache.avro.file.SeekableInput;
 import org.apache.avro.generic.GenericDatumReader;
 import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.EncoderFactory;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -238,4 +244,36 @@ public class TestDataFileReader {
           () -> DataFileReader.openReader(fileInput, new 
GenericDatumReader<>()));
     }
   }
+
+  @Test
+  void missingSchemaMetadataDoesNotThrowNullPointerException() throws 
IOException {
+    byte[] malformedFile = buildContainerHeaderWithoutSchema();
+
+    IOException streamException = assertThrows(IOException.class,
+        () -> new DataFileStream<>(new ByteArrayInputStream(malformedFile), 
new GenericDatumReader<>()));
+    assertNotNull(streamException.getMessage());
+    
assertTrue(streamException.getMessage().contains(DataFileConstants.SCHEMA));
+
+    IOException readerException = assertThrows(IOException.class,
+        () -> new DataFileReader<>(new SeekableByteArrayInput(malformedFile), 
new GenericDatumReader<>()));
+    assertNotNull(readerException.getMessage());
+    
assertTrue(readerException.getMessage().contains(DataFileConstants.SCHEMA));
+  }
+
+  private static byte[] buildContainerHeaderWithoutSchema() throws IOException 
{
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    output.write(DataFileConstants.MAGIC);
+
+    BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+    encoder.writeMapStart();
+    encoder.setItemCount(1);
+    encoder.startItem();
+    encoder.writeString(DataFileConstants.CODEC);
+    encoder.writeBytes("null".getBytes());
+    encoder.writeMapEnd();
+    encoder.writeFixed(new byte[DataFileConstants.SYNC_SIZE]);
+    encoder.flush();
+
+    return output.toByteArray();
+  }
 }
diff --git a/lang/java/fuzz/README.md b/lang/java/fuzz/README.md
new file mode 100644
index 0000000000..1b63bf8455
--- /dev/null
+++ b/lang/java/fuzz/README.md
@@ -0,0 +1,52 @@
+# Apache Avro Java Fuzzing
+
+This module hosts Jazzer-based fuzz tests for the Java Avro implementation.
+
+## Targets
+
+| Class | Target | Purpose |
+|---|---|---|
+| `SchemaFuzzer` | `fuzzSchemaParse` | Exercises `SchemaParser` with 
structured and arbitrary schema text |
+| `BinaryDecodingFuzzer` | `fuzzBinaryDecoding` | Exercises buffered binary 
decoding and writer/reader schema resolution |
+| `BinaryDecodingFuzzer` | `fuzzDirectBinaryDecoding` | Exercises direct 
binary decoding on `InputStream`-backed inputs |
+| `JsonDecodingFuzzer` | `fuzzJsonDecoding` | Exercises JSON decoding with 
unions, maps, arrays, and logical types |
+| `DataFileReaderFuzzer` | `fuzzDataFileReader` | Exercises seekable object 
container file parsing |
+| `DataFileReaderFuzzer` | `fuzzDataFileStream` | Exercises streaming object 
container file parsing |
+
+## Design Notes
+
+- Targets only suppress exceptions that are expected from malformed Avro input.
+- Unexpected runtime failures are allowed to escape so Jazzer reports them.
+- The checked-in corpus includes both historical crash reproducers and valid 
seeds.
+- Valid seeds help regression mode reach successful decode paths instead of 
replaying only prior failures.
+
+## Running
+
+Run from `lang/java/`:
+
+```bash
+# Replay the checked-in regression corpus only.
+mvn test -pl fuzz
+
+# Run continuous fuzzing for 60 seconds per target.
+mvn test -pl fuzz -Pfuzz
+
+# Run one target continuously.
+mvn test -pl fuzz -Pfuzz -Dtest=BinaryDecodingFuzzer#fuzzDirectBinaryDecoding
+
+# Equivalent environment-variable toggle.
+JAZZER_FUZZ=1 mvn test -pl fuzz
+```
+
+## Corpus Layout
+
+Regression inputs live under 
`src/test/resources/org/apache/avro/fuzz/<Class>Inputs/<target>/`.
+
+- `crash-*` files are preserved reproducers for previously discovered failures.
+- `seed-*` files are valid inputs generated from Avro's own encoders.
+
+## Maintenance
+
+- If a fuzz target is renamed, rename its corpus directory to match.
+- Add new valid seeds whenever a target starts covering a new feature area.
+- Keep dependency versions aligned with the parent POM rather than hard-coding 
them here.
diff --git a/lang/java/fuzz/REPORT.md b/lang/java/fuzz/REPORT.md
index 5fe90b3e48..115a3df849 100644
--- a/lang/java/fuzz/REPORT.md
+++ b/lang/java/fuzz/REPORT.md
@@ -2,148 +2,77 @@
 
 ## Overview
 
-A new `lang/java/fuzz/` Maven module was created with 4 fuzz test classes (5 
total fuzz targets) using 
[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer) 0.22.1, a 
coverage-guided Java fuzzer built on libFuzzer. Two bugs were found and fixed: 
an NPE in schema parsing and a denial-of-service via memory exhaustion in 
binary decoding.
+`lang/java/fuzz/` is a Jazzer-based fuzzing module for the Java Avro 
implementation.
 
-## Fuzz Test Targets
+Current coverage includes 4 fuzz classes and 6 fuzz targets:
 
-| Class | Target | Description |
-|---|---|---|
-| `SchemaFuzzer` | `fuzzSchemaParse` | Feeds arbitrary UTF-8 strings to 
`Schema.parse(String)` |
-| `BinaryDecodingFuzzer` | `fuzzBinaryDecoding` | Feeds arbitrary bytes 
through `BinaryDecoder` + `GenericDatumReader` with a multi-field record schema 
|
-| `JsonDecodingFuzzer` | `fuzzJsonDecoding` | Feeds arbitrary JSON through 
`JsonDecoder` + `GenericDatumReader` |
-| `DataFileReaderFuzzer` | `fuzzDataFileReader` | Feeds arbitrary bytes to 
`DataFileReader` (seekable path) |
-| `DataFileReaderFuzzer` | `fuzzDataFileStream` | Feeds arbitrary bytes to 
`DataFileStream` (streaming path) |
+- schema parsing via `SchemaParser`
+- buffered binary decoding
+- direct binary decoding
+- JSON decoding
+- seekable container-file decoding
+- streaming container-file decoding
+
+The module is wired into `lang/java/pom.xml` and can be run in regression or 
continuous-fuzzing mode through Maven.
 
 ## Bugs Found
 
-### Bug 1: `NullPointerException` in `Schema.parse()` (ParseContext.resolve)
+### Bug 1: `NullPointerException` in schema parsing
 
 - **Severity**: Medium
-- **Location**: `ParseContext.java:330`
-- **Trigger**: Input like `""""""""""""""""""` (18 quote characters)
-- **Root Cause**: When `Schema.parse()` processes certain malformed JSON 
inputs (e.g., nested JSON arrays with string elements that look like type 
references), the parser creates unresolved schema placeholders 
(`UnresolvedSchema_0`). In `ParseContext.resolve()`, it calls 
`requireNonNull(oldSchemas.get(schema.getFullName()))` which throws 
`NullPointerException` instead of a proper `AvroTypeException`.
-- **Impact**: Code that calls `Schema.parse()` and catches 
`SchemaParseException` or `AvroRuntimeException` will miss this error. The NPE 
leaks internal implementation details.
-- **Status**: **Fixed**
-
-**Stack trace:**
-
-```
-java.lang.NullPointerException: Unknown schema: 
org.apache.avro.compiler.UnresolvedSchema_0
-    at java.base/java.util.Objects.requireNonNull(Objects.java:360)
-    at org.apache.avro.ParseContext.resolve(ParseContext.java:330)
-    at org.apache.avro.Schema$Parser.parse(Schema.java:1542)
-    at org.apache.avro.Schema$Parser.parse(Schema.java:1516)
-    at org.apache.avro.Schema.parse(Schema.java:1607)
-```
-
-### Bug 2: Denial-of-Service via Memory Exhaustion in Binary/Container Decoding
-
-- **Severity**: High (security, DoS)
-- **Location**: `Utf8.java:115` via `BinaryDecoder.readString()` at 
`BinaryDecoder.java:299`; also `readBytes()`, `readArrayStart()`, 
`arrayNext()`, `readMapStart()`, `mapNext()`
-- **Trigger**: Crafted binary input containing varint-encoded lengths far 
exceeding available data
-- **Root Cause**: `BinaryDecoder` reads a varint-encoded length, then 
immediately allocates `new byte[length]` before checking whether that many 
bytes are actually available. A few-byte payload encoding a large varint (e.g., 
200 MB+) causes `OutOfMemoryError` before any read can fail with 
`EOFException`. This affects strings, bytes, arrays, and maps.
-- **Impact**: An attacker can craft a small Avro binary payload (just a few 
bytes encoding a large varint) that causes the JVM to run out of memory. This 
is a denial-of-service vector for any service that reads untrusted Avro data.
-- **Status**: **Fixed**
+- **Area**: `ParseContext.resolve()`
+- **Impact**: malformed schema input could escape as `NullPointerException` 
instead of an Avro parse/type exception
+- **Status**: Fixed
 
-**Stack trace:**
+### Bug 2: Denial of service via impossible binary/container lengths
 
-```
-java.lang.OutOfMemoryError: Java heap space
-    at org.apache.avro.util.Utf8.setByteLength(Utf8.java:115)
-    at org.apache.avro.io.BinaryDecoder.readString(BinaryDecoder.java:299)
-    at 
org.apache.avro.io.FastReaderBuilder.lambda$createSimpleStringReader$27(FastReaderBuilder.java:380)
-    at 
org.apache.avro.io.FastReaderBuilder$RecordReader.read(FastReaderBuilder.java:575)
-    at 
org.apache.avro.generic.GenericDatumReader.read(GenericDatumReader.java:150)
-```
+- **Severity**: High
+- **Area**: `BinaryDecoder` string/bytes/array/map length handling
+- **Impact**: tiny hostile inputs could trigger huge allocations before EOF 
was detected
+- **Status**: Fixed
 
-## Fixes Applied
+## Current Module Improvements
 
-### Fix for Bug 1: Proper error type in ParseContext.resolve()
+The fuzz module has been strengthened beyond the initial implementation:
 
-**File**: `ParseContext.java`
+- fuzzers now catch only expected malformed-input failures
+- unexpected runtime exceptions are no longer suppressed
+- schema fuzzing uses `SchemaParser` instead of the deprecated 
`Schema.parse(...)` entry point
+- binary coverage now includes both buffered and direct decoder paths
+- binary decoding uses writer/reader schema pairs to exercise aliases, 
defaults, recursion, fixed, and logical types
+- regression corpora now include valid seeds in addition to historical crash 
reproducers
+- container-file corpora now cover both `fuzzDataFileReader` and 
`fuzzDataFileStream`
+- module dependency alignment now follows `${junit5.version}` from the parent 
POM
 
-Replaced `Objects.requireNonNull(oldSchemas.get(schema.getFullName()), ...)` 
with an explicit null check that throws `AvroTypeException`. This ensures 
malformed schema inputs produce the same exception type as other schema parse 
errors, making catch blocks reliable.
+## BinaryDecoder Fix Notes
 
-**Regression test**: `TestSchema.parseMalformedInputDoesNotThrowNPE` — 
verifies that the trigger input throws `AvroTypeException`, not 
`NullPointerException`.
+The BinaryDecoder hardening works by validating requested lengths against 
known remaining bytes before allocating buffers.
 
-### Fix for Bug 2: Validate-before-allocate in BinaryDecoder
+Key points:
 
-**File**: `BinaryDecoder.java`
+- `ByteArrayByteSource.remainingBytes()` returns an exact remaining count
+- stream-backed buffered decoders use buffered bytes plus 
`InputStream.available()`
+- `ensureAvailableBytes(int length)` throws `EOFException` when the source is 
known to be too short
+- direct decoders are exercised separately because they do not use the same 
buffered `ByteSource` path
 
-Added a protobuf-style validate-before-allocate check: before allocating 
memory for a decoded length, verify that the byte source actually has enough 
bytes remaining.
+## Regression Corpus
 
-Implementation details:
+Checked-in regression inputs now include:
 
-- Added abstract `remainingBytes()` to the inner `ByteSource` class
-- `ByteArrayByteSource.remainingBytes()` returns exact remaining bytes (`lim - 
pos`)
-- `InputStreamByteSource.remainingBytes()` returns `-1` (unknown — 
`InputStream.available()` is unreliable, so stream-backed decoders are not 
guarded by this check)
-- Added `ensureAvailableBytes(int length)` method that throws `EOFException` 
when the requested length exceeds known remaining bytes
-- The check is called in: `readString()`, `readBytes()`, `readArrayStart()`, 
`arrayNext()`, `readMapStart()`, `mapNext()`
-- For `DirectBinaryDecoder`, `source` is always null, so the check is a no-op 
(correct — it has no buffered byte source)
-- For arrays/maps, each item must be at least 1 byte, so item count serves as 
a valid lower bound for bytes needed
-- `InputStreamByteSource.remainingBytes()` returns `buffered + 
in.available()`, which is exact for the finite, in-memory streams used by 
`DataFileReader` and `DataFileStream` (`SeekableInputStream.available()` and 
`ByteArrayInputStream.available()` both report exact remaining). When 
`available()` throws, returns -1 (check skipped). This is safe because the 
buffering `BinaryDecoder` is only used for file/container reading — network/RPC 
streams use `DirectBinaryDecoder` which bypasses this [...]
+- historical `crash-*` reproducers for known findings
+- valid schema text seeds
+- valid JSON record seeds
+- valid binary datum seeds
+- valid object container file seeds for both reader and stream targets
 
-This approach was chosen over lowering the default `SystemLimitException` 
limits because it does not change behavior for production users — only clearly 
impossible allocations (where the claimed length exceeds the actual data) are 
rejected.
-
-**Regression tests**:
-- `TestBinaryDecoder.testStringLengthExceedsAvailableBytes` — verifies 
`EOFException` for a string length exceeding available bytes
-- `TestBinaryDecoder.testBytesLengthExceedsAvailableBytes` — verifies 
`EOFException` for a bytes length exceeding available bytes
-- Updated `testArrayVmMaxSize`, `testArrayMaxCustom`, `testMapVmMaxSize`, 
`testMapMaxCustom` — byte-array-backed decoders now correctly reject impossible 
counts with `EOFException`
+This makes `mvn test -pl fuzz` useful as both a regression suite and a shallow 
successful-path smoke test.
 
 ## Verification
 
-### Test suite
-All 3,415 tests in the `avro` module pass after all fixes.
-
-### Fuzzer re-runs (post-fix)
-- **BinaryDecodingFuzzer**: ~974K iterations, 0 crashes (previously crashed 
within ~4K iterations with OOM)
-- **SchemaFuzzer**: ~1M iterations, 0 crashes (previously found NPE)
-- **DataFileReaderFuzzer**: ~684K iterations, 0 crashes (previously crashed 
within ~4K iterations with OOM via `DataFileStream.initialize()`)
-- **JsonDecodingFuzzer**: ~1M iterations, 0 crashes
-
-All bugs are confirmed fixed.
-
-## What Each Fuzz Test Does
-
-### SchemaFuzzer
-
-Tests the Avro JSON schema parser (`Schema.parse()`). Converts random bytes to 
a UTF-8 string and passes it as a schema definition. This exercises the Jackson 
JSON parser, Avro type resolution, named schema registration, union 
construction, and schema validation. Expected exceptions 
(`AvroRuntimeException`, `SchemaParseException`, `IllegalArgumentException`) 
are caught — any other exception indicates a bug.
-
-### BinaryDecodingFuzzer
-
-Tests the Avro binary wire format decoder. Feeds raw bytes directly into 
`BinaryDecoder` paired with a `GenericDatumReader` using a record schema with 
long, string, double, array, map, and union fields — exercises varint parsing, 
buffer management, collection size decoding, and union branch selection.
-
-### JsonDecodingFuzzer
-
-Tests the Avro JSON data decoder. Converts random bytes to UTF-8 and feeds 
them through `JsonDecoder` with a `GenericDatumReader`. This exercises 
JSON-to-Avro type coercion, union resolution via JSON object wrapping, and 
error handling for malformed or type-mismatched data.
-
-### DataFileReaderFuzzer
-
-Tests the Avro container file (Object Container File) format parser. This is 
the highest-value target as it exercises the full deserialization pipeline: 
magic byte validation, file header parsing, embedded schema deserialization, 
sync marker verification, codec decompression, and datum reading. Two code 
paths are tested:
-
-- **`DataFileReader`** (seekable, random-access via `SeekableByteArrayInput`)
-- **`DataFileStream`** (sequential, non-seekable via `ByteArrayInputStream`)
-
-## How to Run
+Verified locally with:
 
 ```bash
-# Regression mode (default — replays seed corpus only, fast):
 mvn test -pl fuzz
-
-# Continuous fuzzing mode (60s per target):
-mvn test -pl fuzz -Pfuzz
-
-# Fuzz a specific target:
-mvn test -pl fuzz -Pfuzz -Dtest=SchemaFuzzer#fuzzSchemaParse
-
-# Alternative: set JAZZER_FUZZ environment variable:
-JAZZER_FUZZ=1 mvn test -pl fuzz
 ```
 
-## Technical Notes
-
-- **Jazzer version**: 0.22.1 with JUnit 5 integration (`jazzer-junit`)
-- **Fuzzing engine**: libFuzzer (coverage-guided, mutation-based)
-- **Enabling fuzzing mode**: Jazzer 0.22.1 checks the `JAZZER_FUZZ` 
environment variable or the `jazzer.internal.command_line` JUnit Platform 
configuration parameter. The `-Pfuzz` Maven profile sets both.
-- **JUnit version alignment**: Jazzer 0.22.1 ships with JUnit Platform 1.9.0 
dependencies which conflict with the project's JUnit 5.14.3. The POM excludes 
Jazzer's JUnit deps and provides `junit-platform-launcher:1.14.3` explicitly.
-- **Surefire configuration**: The parent POM sets `parallel=all` with 
unlimited threads. The fuzz module overrides this to single-threaded execution, 
which Jazzer requires.
+The module builds, replays the regression corpus, and runs all fuzz targets 
successfully.
diff --git a/lang/java/fuzz/pom.xml b/lang/java/fuzz/pom.xml
index ec06ca2add..a3dfb9c2de 100644
--- a/lang/java/fuzz/pom.xml
+++ b/lang/java/fuzz/pom.xml
@@ -124,7 +124,7 @@
     <dependency>
       <groupId>org.junit.platform</groupId>
       <artifactId>junit-platform-launcher</artifactId>
-      <version>1.14.3</version>
+      <version>${junit-platform.version}</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
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
index e253c3422e..665a6f3aa0 100644
--- 
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
@@ -18,12 +18,12 @@
 package org.apache.avro.fuzz;
 
 import com.code_intelligence.jazzer.junit.FuzzTest;
-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.ByteArrayInputStream;
 import java.io.IOException;
 
 /**
@@ -31,36 +31,36 @@ import java.io.IOException;
  *
  * <p>
  * Feeds arbitrary bytes through {@link BinaryDecoder} and
- * {@link GenericDatumReader} using a compound schema that exercises varint
- * parsing, buffer management, nested records, enums, arrays, maps, unions, and
- * collection size limit enforcement in a single target.
+ * {@link GenericDatumReader} using writer/reader schema pairs that exercise
+ * varint parsing, schema resolution, aliases, defaults, fixed values, logical
+ * types, and recursive records.
  * </p>
  */
 class BinaryDecodingFuzzer {
 
-  /**
-   * A compound schema that nests records, enums, arrays, maps, and unions to
-   * maximise code coverage from a single fuzz target.
-   */
-  private static final Schema SCHEMA = new Schema.Parser()
-      .parse("{\"type\":\"record\",\"name\":\"Root\",\"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\"]},"
-          + 
"{\"name\":\"inner\",\"type\":{\"type\":\"record\",\"name\":\"Inner\",\"fields\":["
-          + 
"{\"name\":\"x\",\"type\":\"int\"},{\"name\":\"y\",\"type\":\"int\"}]}},"
-          + 
"{\"name\":\"status\",\"type\":{\"type\":\"enum\",\"name\":\"Status\","
-          + "\"symbols\":[\"ACTIVE\",\"INACTIVE\",\"PENDING\"]}}," + 
"{\"name\":\"count\",\"type\":\"int\"}" + "]}");
-
   @FuzzTest
   void fuzzBinaryDecoding(byte[] data) {
+    fuzz(data, false);
+  }
+
+  @FuzzTest
+  void fuzzDirectBinaryDecoding(byte[] data) {
+    fuzz(data, true);
+  }
+
+  private void fuzz(byte[] data, boolean direct) {
     try {
-      BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(data, null);
-      GenericDatumReader<GenericRecord> reader = new 
GenericDatumReader<>(SCHEMA);
+      BinaryDecoder decoder = direct ? 
DecoderFactory.get().directBinaryDecoder(new ByteArrayInputStream(data), null)
+          : DecoderFactory.get().binaryDecoder(data, null);
+      GenericDatumReader<GenericRecord> reader = new 
GenericDatumReader<>(FuzzSupport.BINARY_WRITER_SCHEMA,
+          FuzzSupport.BINARY_READER_SCHEMA);
       reader.read(null, decoder);
-    } catch (Exception e) {
+    } catch (IOException e) {
       // Expected for malformed binary data
+    } catch (RuntimeException e) {
+      if (!FuzzSupport.isExpectedDecodingFailure(e)) {
+        throw e;
+      }
     }
   }
 }
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
index 5e274f36a6..8c84acc942 100644
--- 
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
@@ -25,6 +25,7 @@ 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.
@@ -50,8 +51,12 @@ class DataFileReaderFuzzer {
       while (reader.hasNext()) {
         reader.next();
       }
-    } catch (Exception e) {
+    } catch (IOException e) {
       // Expected for malformed container files
+    } catch (RuntimeException e) {
+      if (!FuzzSupport.isExpectedDecodingFailure(e)) {
+        throw e;
+      }
     }
   }
 
@@ -67,8 +72,12 @@ class DataFileReaderFuzzer {
       while (stream.hasNext()) {
         stream.next();
       }
-    } catch (Exception e) {
+    } catch (IOException e) {
       // Expected for malformed container files
+    } catch (RuntimeException e) {
+      if (!FuzzSupport.isExpectedDecodingFailure(e)) {
+        throw e;
+      }
     }
   }
 }
diff --git a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/FuzzSupport.java 
b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/FuzzSupport.java
new file mode 100644
index 0000000000..98d93c1e4b
--- /dev/null
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/FuzzSupport.java
@@ -0,0 +1,199 @@
+/*
+ * 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.api.FuzzedDataProvider;
+import org.apache.avro.AvroRuntimeException;
+import org.apache.avro.AvroTypeException;
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaParseException;
+import org.apache.avro.SystemLimitException;
+import org.apache.avro.UnresolvedUnionException;
+
+final class FuzzSupport {
+  static final Schema BINARY_WRITER_SCHEMA = new Schema.Parser()
+      .parse("{\"type\":\"record\",\"name\":\"WriterRoot\",\"fields\":[" + 
"{\"name\":\"id\",\"type\":\"long\"},"
+          + "{\"name\":\"legacyName\",\"type\":\"string\"},"
+          + 
"{\"name\":\"createdDate\",\"type\":{\"type\":\"int\",\"logicalType\":\"date\"}},"
+          + "{\"name\":\"payload\",\"type\":\"bytes\"},"
+          + 
"{\"name\":\"hash\",\"type\":{\"type\":\"fixed\",\"name\":\"Hash4\",\"size\":4}},"
+          + 
"{\"name\":\"node\",\"type\":{\"type\":\"record\",\"name\":\"Node\",\"fields\":["
+          + "{\"name\":\"value\",\"type\":\"string\"},"
+          + 
"{\"name\":\"next\",\"type\":[\"null\",\"Node\"],\"default\":null}]}}]}");
+
+  static final Schema BINARY_READER_SCHEMA = new Schema.Parser()
+      .parse("{\"type\":\"record\",\"name\":\"ReaderRoot\",\"fields\":[" + 
"{\"name\":\"id\",\"type\":\"long\"},"
+          + 
"{\"name\":\"name\",\"aliases\":[\"legacyName\"],\"type\":\"string\"},"
+          + 
"{\"name\":\"createdDate\",\"type\":{\"type\":\"int\",\"logicalType\":\"date\"}},"
+          + "{\"name\":\"payload\",\"type\":\"bytes\"},"
+          + 
"{\"name\":\"hash\",\"type\":{\"type\":\"fixed\",\"name\":\"Hash4\",\"size\":4}},"
+          + 
"{\"name\":\"node\",\"type\":{\"type\":\"record\",\"name\":\"Node\",\"fields\":["
+          + "{\"name\":\"value\",\"type\":\"string\"},"
+          + 
"{\"name\":\"next\",\"type\":[\"null\",\"Node\"],\"default\":null}]}},"
+          + "{\"name\":\"status\",\"type\":\"string\",\"default\":\"new\"}]}");
+
+  static final Schema JSON_SCHEMA = new 
Schema.Parser().parse("{\"type\":\"record\",\"name\":\"JsonRoot\",\"fields\":["
+      + "{\"name\":\"id\",\"type\":\"long\"}," + 
"{\"name\":\"name\",\"type\":\"string\"},"
+      + 
"{\"name\":\"createdDate\",\"type\":{\"type\":\"int\",\"logicalType\":\"date\"}},"
+      + "{\"name\":\"active\",\"type\":\"boolean\"},"
+      + 
"{\"name\":\"scores\",\"type\":{\"type\":\"array\",\"items\":\"double\"}},"
+      + 
"{\"name\":\"props\",\"type\":{\"type\":\"map\",\"values\":\"string\"}},"
+      + 
"{\"name\":\"extra\",\"type\":[\"null\",\"string\",\"long\"],\"default\":null},"
+      + 
"{\"name\":\"inner\",\"type\":{\"type\":\"record\",\"name\":\"Inner\",\"fields\":["
+      + "{\"name\":\"x\",\"type\":\"int\"}," + 
"{\"name\":\"y\",\"type\":\"int\"}]}}]}");
+
+  private FuzzSupport() {
+  }
+
+  static boolean isExpectedSchemaFailure(RuntimeException exception) {
+    return exception instanceof SchemaParseException || exception instanceof 
AvroTypeException
+        || exception instanceof IllegalArgumentException;
+  }
+
+  static boolean isExpectedDecodingFailure(RuntimeException exception) {
+    return exception instanceof AvroTypeException || exception instanceof 
SystemLimitException
+        || exception instanceof UnresolvedUnionException || exception 
instanceof IllegalArgumentException
+        || exception instanceof UnsupportedOperationException || 
isExpectedAvroRuntimeFailure(exception);
+  }
+
+  static String buildSchemaInput(FuzzedDataProvider data) {
+    int mode = data.consumeInt(0, 3);
+    String name = toIdentifier(data.consumeString(16), "SeedRecord");
+    String fieldName = toIdentifier(data.consumeString(16), "field");
+    String remaining = data.consumeRemainingAsString();
+
+    switch (mode) {
+    case 0:
+      return remaining;
+    case 1:
+      return "{\"type\":\"record\",\"name\":\"" + name + "\",\"fields\":[" + 
remaining + "]}";
+    case 2:
+      return "[\"null\"," + remaining + "]";
+    default:
+      return "{\"type\":\"record\",\"name\":\"" + name + 
"\",\"fields\":[{\"name\":\"" + fieldName + "\",\"type\":"
+          + remaining + "}]}";
+    }
+  }
+
+  static String buildJsonInput(FuzzedDataProvider data) {
+    int mode = data.consumeInt(0, 2);
+    if (mode == 0) {
+      return data.consumeRemainingAsString();
+    }
+
+    String name = escapeJson(data.consumeString(24));
+    String mapKey = toIdentifier(data.consumeString(12), "k");
+    String mapValue = escapeJson(data.consumeString(24));
+    String extraString = escapeJson(data.consumeString(16));
+    double scoreOne = boundedScore(data.consumeInt());
+    double scoreTwo = boundedScore(data.consumeInt());
+    long id = sanitizeLong(data.consumeLong());
+    int createdDate = Math.abs(data.consumeInt(0, 36500));
+    boolean active = data.consumeBoolean();
+    int x = data.consumeInt();
+    int y = data.consumeInt();
+
+    if (mode == 1) {
+      return "{\"id\":" + id + ",\"name\":\"" + name + "\",\"createdDate\":" + 
createdDate + ",\"active\":" + active
+          + ",\"scores\":[" + scoreOne + "," + scoreTwo + "]" + 
",\"props\":{\"" + mapKey + "\":\"" + mapValue
+          + "\"},\"extra\":{\"string\":\"" + extraString + 
"\"},\"inner\":{\"x\":" + x + ",\"y\":" + y + "}}";
+    }
+
+    String trailing = escapeJson(data.consumeRemainingAsString());
+    return "{\"id\":" + id + ",\"name\":\"" + name + trailing + 
"\",\"createdDate\":" + createdDate + ",\"active\":"
+        + active + ",\"scores\":[" + scoreOne + "]" + ",\"props\":{\"" + 
mapKey + "\":\"" + mapValue
+        + "\"},\"extra\":null,\"inner\":{\"x\":" + x + ",\"y\":" + y + "}}";
+  }
+
+  private static double boundedScore(int value) {
+    return (value % 1000) / 10.0d;
+  }
+
+  private static boolean isExpectedAvroRuntimeFailure(RuntimeException 
exception) {
+    if (!(exception instanceof AvroRuntimeException)) {
+      return false;
+    }
+
+    String message = exception.getMessage();
+    if (message == null) {
+      return false;
+    }
+
+    return message.startsWith("Malformed data.") || 
message.startsWith("Unknown datum type")
+        || message.startsWith("Not an array") || message.startsWith("Not a 
map") || message.startsWith("No match for ");
+  }
+
+  private static String escapeJson(String value) {
+    StringBuilder escaped = new StringBuilder(value.length());
+    for (int i = 0; i < value.length(); i++) {
+      char current = value.charAt(i);
+      switch (current) {
+      case '\\':
+        escaped.append("\\\\");
+        break;
+      case '"':
+        escaped.append("\\\"");
+        break;
+      case '\b':
+        escaped.append("\\b");
+        break;
+      case '\f':
+        escaped.append("\\f");
+        break;
+      case '\n':
+        escaped.append("\\n");
+        break;
+      case '\r':
+        escaped.append("\\r");
+        break;
+      case '\t':
+        escaped.append("\\t");
+        break;
+      default:
+        if (current < 0x20) {
+          escaped.append(String.format("\\u%04x", (int) current));
+        } else {
+          escaped.append(current);
+        }
+      }
+    }
+    return escaped.toString();
+  }
+
+  private static long sanitizeLong(long value) {
+    return value == Long.MIN_VALUE ? 0L : Math.abs(value);
+  }
+
+  private static String toIdentifier(String value, String fallback) {
+    StringBuilder identifier = new StringBuilder();
+    for (int i = 0; i < value.length(); i++) {
+      char current = value.charAt(i);
+      if (Character.isLetterOrDigit(current) || current == '_') {
+        identifier.append(current);
+      }
+    }
+
+    if (identifier.length() == 0) {
+      return fallback;
+    }
+    if (!Character.isLetter(identifier.charAt(0)) && identifier.charAt(0) != 
'_') {
+      identifier.insert(0, 'N');
+    }
+    return identifier.toString();
+  }
+}
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
index 362773c838..3c08705191 100644
--- 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
@@ -17,49 +17,38 @@
  */
 package org.apache.avro.fuzz;
 
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
 import com.code_intelligence.jazzer.junit.FuzzTest;
-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.nio.charset.StandardCharsets;
+import java.io.IOException;
 
 /**
  * Fuzz tests for Avro JSON decoding.
  *
  * <p>
- * Feeds arbitrary JSON strings through {@link JsonDecoder} with a compound
- * schema to test the JSON-to-Avro deserialization path, including Jackson JSON
- * parsing, type coercion, union resolution, and error handling for malformed
- * data.
+ * Feeds mostly structured JSON strings through {@link JsonDecoder} to test the
+ * JSON-to-Avro deserialization path, including Jackson JSON parsing, type
+ * coercion, union resolution, logical types, and malformed-input handling.
  * </p>
  */
 class JsonDecodingFuzzer {
-
-  /**
-   * A compound schema with diverse field types: primitives, arrays, maps, and
-   * unions. This is a superset of the previous separate record and simple
-   * schemas.
-   */
-  private static final Schema SCHEMA = new Schema.Parser()
-      .parse("{\"type\":\"record\",\"name\":\"TestRecord\",\"fields\":[" + 
"{\"name\":\"id\",\"type\":\"long\"},"
-          + "{\"name\":\"name\",\"type\":\"string\"}," + 
"{\"name\":\"x\",\"type\":\"int\"},"
-          + "{\"name\":\"active\",\"type\":\"boolean\"},"
-          + 
"{\"name\":\"scores\",\"type\":{\"type\":\"array\",\"items\":\"double\"}},"
-          + 
"{\"name\":\"props\",\"type\":{\"type\":\"map\",\"values\":\"string\"}},"
-          + "{\"name\":\"extra\",\"type\":[\"null\",\"string\",\"long\"]}" + 
"]}");
-
   @FuzzTest
-  void fuzzJsonDecoding(byte[] data) {
-    String jsonData = new String(data, StandardCharsets.UTF_8);
+  void fuzzJsonDecoding(FuzzedDataProvider data) {
+    String jsonData = FuzzSupport.buildJsonInput(data);
     try {
-      JsonDecoder decoder = DecoderFactory.get().jsonDecoder(SCHEMA, jsonData);
-      GenericDatumReader<GenericRecord> reader = new 
GenericDatumReader<>(SCHEMA);
+      JsonDecoder decoder = 
DecoderFactory.get().jsonDecoder(FuzzSupport.JSON_SCHEMA, jsonData);
+      GenericDatumReader<GenericRecord> reader = new 
GenericDatumReader<>(FuzzSupport.JSON_SCHEMA);
       reader.read(null, decoder);
-    } catch (Exception e) {
-      // Expected for malformed JSON or type mismatches
+    } catch (IOException e) {
+      // Expected for malformed JSON input
+    } catch (RuntimeException e) {
+      if (!FuzzSupport.isExpectedDecodingFailure(e)) {
+        throw e;
+      }
     }
   }
 }
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
index f7ef7669cf..f25dc3befe 100644
--- 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
@@ -17,30 +17,30 @@
  */
 package org.apache.avro.fuzz;
 
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
 import com.code_intelligence.jazzer.junit.FuzzTest;
-import org.apache.avro.Schema;
-
-import java.nio.charset.StandardCharsets;
+import org.apache.avro.SchemaParser;
 
 /**
  * Fuzz tests for Avro schema parsing.
  *
  * <p>
- * Targets {@link Schema#parse(String, boolean)} with arbitrary input and name
- * validation enabled. Any exception is treated as expected behavior for 
invalid
- * input; only JVM-level errors (e.g. {@link StackOverflowError}) escape and 
are
- * reported as real findings.
+ * Targets {@link SchemaParser#parse(CharSequence)} with mostly structured
+ * input. Expected parse failures are swallowed, but unexpected runtime 
failures
+ * are allowed to escape so Jazzer can report them as real findings.
  * </p>
  */
 class SchemaFuzzer {
 
   @FuzzTest
-  void fuzzSchemaParse(byte[] data) {
-    String schemaJson = new String(data, StandardCharsets.UTF_8);
+  void fuzzSchemaParse(FuzzedDataProvider data) {
+    String schemaJson = FuzzSupport.buildSchemaInput(data);
     try {
-      Schema.parse(schemaJson, true);
-    } catch (Exception e) {
-      // Expected for invalid schema input
+      new SchemaParser().parse(schemaJson).mainSchema();
+    } catch (RuntimeException e) {
+      if (!FuzzSupport.isExpectedSchemaFailure(e)) {
+        throw e;
+      }
     }
   }
 }
diff --git 
a/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SeedCorpusGenerator.java 
b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SeedCorpusGenerator.java
new file mode 100644
index 0000000000..f5d4ee4237
--- /dev/null
+++ b/lang/java/fuzz/src/test/java/org/apache/avro/fuzz/SeedCorpusGenerator.java
@@ -0,0 +1,90 @@
+/*
+ * 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 org.apache.avro.file.DataFileWriter;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.EncoderFactory;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+class SeedCorpusGenerator {
+  private static final Path RESOURCE_ROOT = 
Path.of("src/test/resources/org/apache/avro/fuzz");
+  private static final org.apache.avro.Schema NODE_SCHEMA = 
FuzzSupport.BINARY_WRITER_SCHEMA.getField("node").schema();
+
+  @Test
+  void generateSeeds() throws IOException {
+    
writeBytes("BinaryDecodingFuzzerInputs/fuzzBinaryDecoding/seed-valid-record.bin",
 writeBinaryRecord());
+    
writeBytes("BinaryDecodingFuzzerInputs/fuzzDirectBinaryDecoding/seed-valid-record.bin",
 writeBinaryRecord());
+    
writeBytes("DataFileReaderFuzzerInputs/fuzzDataFileReader/seed-valid-container.avro",
 writeContainerFile());
+    
writeBytes("DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro",
 writeContainerFile());
+  }
+
+  private static byte[] writeBinaryRecord() throws IOException {
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(output, null);
+    GenericDatumWriter<GenericRecord> writer = new 
GenericDatumWriter<>(FuzzSupport.BINARY_WRITER_SCHEMA);
+    writer.write(sampleBinaryRecord(), encoder);
+    encoder.flush();
+    return output.toByteArray();
+  }
+
+  private static byte[] writeContainerFile() throws IOException {
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    GenericDatumWriter<GenericRecord> datumWriter = new 
GenericDatumWriter<>(FuzzSupport.BINARY_WRITER_SCHEMA);
+    try (DataFileWriter<GenericRecord> fileWriter = new 
DataFileWriter<>(datumWriter)) {
+      fileWriter.create(FuzzSupport.BINARY_WRITER_SCHEMA, output);
+      fileWriter.append(sampleBinaryRecord());
+    }
+    return output.toByteArray();
+  }
+
+  private static GenericRecord sampleBinaryRecord() {
+    GenericRecord nodeTail = new GenericData.Record(NODE_SCHEMA);
+    nodeTail.put("value", "tail");
+    nodeTail.put("next", null);
+
+    GenericRecord nodeHead = new GenericData.Record(NODE_SCHEMA);
+    nodeHead.put("value", "head");
+    nodeHead.put("next", nodeTail);
+
+    GenericRecord record = new 
GenericData.Record(FuzzSupport.BINARY_WRITER_SCHEMA);
+    record.put("id", 7L);
+    record.put("legacyName", "seed");
+    record.put("createdDate", 12);
+    record.put("payload", 
java.nio.ByteBuffer.wrap("payload".getBytes(StandardCharsets.UTF_8)));
+    record.put("hash",
+        new 
GenericData.Fixed(FuzzSupport.BINARY_WRITER_SCHEMA.getField("hash").schema(), 
new byte[] { 1, 2, 3, 4 }));
+    record.put("node", nodeHead);
+    return record;
+  }
+
+  private static void writeBytes(String relativePath, byte[] data) throws 
IOException {
+    Path output = RESOURCE_ROOT.resolve(relativePath);
+    Files.createDirectories(output.getParent());
+    Files.write(output, data);
+  }
+}
diff --git a/lang/java/fuzz/src/test/resources/junit-platform.properties 
b/lang/java/fuzz/src/test/resources/junit-platform.properties
index d0c998e4c5..a9ad9677fd 100644
--- a/lang/java/fuzz/src/test/resources/junit-platform.properties
+++ b/lang/java/fuzz/src/test/resources/junit-platform.properties
@@ -1,6 +1,7 @@
 # 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
+# Run from lang/java/ with:
+#   mvn test -pl fuzz          # regression corpus replay
+#   mvn test -pl fuzz -Pfuzz   # continuous fuzzing
 # or set environment variable JAZZER_FUZZ=1
diff --git 
a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/BinaryDecodingFuzzerInputs/fuzzBinaryDecoding/seed-valid-record.bin
 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/BinaryDecodingFuzzerInputs/fuzzBinaryDecoding/seed-valid-record.bin
new file mode 100644
index 0000000000..69923c7b2d
Binary files /dev/null and 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/BinaryDecodingFuzzerInputs/fuzzBinaryDecoding/seed-valid-record.bin
 differ
diff --git 
a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/BinaryDecodingFuzzerInputs/fuzzDirectBinaryDecoding/seed-valid-record.bin
 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/BinaryDecodingFuzzerInputs/fuzzDirectBinaryDecoding/seed-valid-record.bin
new file mode 100644
index 0000000000..69923c7b2d
Binary files /dev/null and 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/BinaryDecodingFuzzerInputs/fuzzDirectBinaryDecoding/seed-valid-record.bin
 differ
diff --git 
a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReader/seed-valid-container.avro
 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReader/seed-valid-container.avro
new file mode 100644
index 0000000000..1c86ff58f0
Binary files /dev/null and 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileReader/seed-valid-container.avro
 differ
diff --git 
a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro
 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro
new file mode 100644
index 0000000000..ee84b3c292
Binary files /dev/null and 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/DataFileReaderFuzzerInputs/fuzzDataFileStream/seed-valid-container.avro
 differ
diff --git 
a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/JsonDecodingFuzzerInputs/fuzzJsonDecoding/seed-valid-record.json
 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/JsonDecodingFuzzerInputs/fuzzJsonDecoding/seed-valid-record.json
new file mode 100644
index 0000000000..c09eea7648
--- /dev/null
+++ 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/JsonDecodingFuzzerInputs/fuzzJsonDecoding/seed-valid-record.json
@@ -0,0 +1 @@
+{"id":7,"name":"seed","createdDate":12,"active":true,"scores":[1.5,2.5],"props":{"region":"test"},"extra":{"string":"hello"},"inner":{"x":1,"y":2}}
diff --git 
a/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/SchemaFuzzerInputs/fuzzSchemaParse/seed-valid-recursive.avsc
 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/SchemaFuzzerInputs/fuzzSchemaParse/seed-valid-recursive.avsc
new file mode 100644
index 0000000000..10a6d944c8
--- /dev/null
+++ 
b/lang/java/fuzz/src/test/resources/org/apache/avro/fuzz/SchemaFuzzerInputs/fuzzSchemaParse/seed-valid-recursive.avsc
@@ -0,0 +1 @@
+{"type":"record","name":"SeedRecord","fields":[{"name":"id","type":"long"},{"name":"name","type":"string"},{"name":"createdDate","type":{"type":"int","logicalType":"date"}},{"name":"next","type":["null","SeedRecord"],"default":null}]}
diff --git a/lang/java/pom.xml b/lang/java/pom.xml
index ac56a1446f..540b247028 100644
--- a/lang/java/pom.xml
+++ b/lang/java/pom.xml
@@ -49,6 +49,7 @@
     <jetty.version>9.4.58.v20250814</jetty.version>
     <jopt-simple.version>5.0.4</jopt-simple.version>
     <junit5.version>5.14.3</junit5.version>
+    <junit-platform.version>1.14.3</junit-platform.version>
     <maven-core.version>3.9.6</maven-core.version>
     <mockito.version>5.23.0</mockito.version>
     <netty.version>4.2.12.Final</netty.version>

Reply via email to