This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 47d2f35b03 TIKA-5060 -- add a jazzer fuzzer (#3569)
47d2f35b03 is described below
commit 47d2f35b03f63c5a315ab28dfbe10dba145a532d
Author: Tim Allison <[email protected]>
AuthorDate: Sat Nov 7 20:38:37 2026 -0500
TIKA-5060 -- add a jazzer fuzzer (#3569)
* TIKA-5060: add tika-fuzzing module with Jazzer AutoDetectParser fuzz
tests and corpus replay
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.skills/devs/oss-fuzz/SKILL.md | 4 +
pom.xml | 1 +
tika-fuzzing/.gitignore | 7 ++
tika-fuzzing/README.md | 85 +++++++++++++
tika-fuzzing/pom.xml | 92 +++++++++++++++
.../tika/fuzzing/AutoDetectParserFuzzTest.java | 131 +++++++++++++++++++++
.../org/apache/tika/fuzzing/CorpusReplayTest.java | 65 ++++++++++
.../src/test/resources/junit-platform.properties | 18 +++
8 files changed, 403 insertions(+)
diff --git a/.skills/devs/oss-fuzz/SKILL.md b/.skills/devs/oss-fuzz/SKILL.md
index 7b88478dc6..528daf7945 100644
--- a/.skills/devs/oss-fuzz/SKILL.md
+++ b/.skills/devs/oss-fuzz/SKILL.md
@@ -30,6 +30,10 @@ read after this file, wins on conflict.
# Tika OSS-Fuzz — local fuzzing
+For fuzzing or replaying a directory of files without Docker, use the in-repo
+`tika-fuzzing` module (Jazzer `@FuzzTest`, see `tika-fuzzing/README.md`). This
skill
+covers the OSS-Fuzz project, whose targets still live in the oss-fuzz repo.
+
Tika is already in OSS-Fuzz as the **`apache-tika`** project (not `tika`).
It is Jazzer-based (coverage-guided, in-process JVM fuzzing), not the old
`tika-fuzzing` seed-mutation module (removed in TIKA-4506). The fuzz targets
diff --git a/pom.xml b/pom.xml
index 1992c6671b..b7490cfe22 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,7 @@
<module>tika-example</module>
<module>tika-java7</module>
<module>tika-handlers</module>
+ <module>tika-fuzzing</module>
</modules>
<profiles>
diff --git a/tika-fuzzing/.gitignore b/tika-fuzzing/.gitignore
new file mode 100644
index 0000000000..f01f699255
--- /dev/null
+++ b/tika-fuzzing/.gitignore
@@ -0,0 +1,7 @@
+# Jazzer-generated corpus
+.cifuzz-corpus/
+# Jazzer findings: replayed locally, committed only with `git add -f` once
fixed and disclosed
+src/test/resources/**/crash-*
+src/test/resources/**/timeout-*
+src/test/resources/**/oom-*
+src/test/resources/**/slow-unit-*
diff --git a/tika-fuzzing/README.md b/tika-fuzzing/README.md
new file mode 100644
index 0000000000..99f187b1a6
--- /dev/null
+++ b/tika-fuzzing/README.md
@@ -0,0 +1,85 @@
+<!--
+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
+
+ 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.
+-->
+
+# tika-fuzzing
+
+[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer) `@FuzzTest`
targets.
+`AutoDetectParserFuzzTest` runs `AutoDetectParser` (via
`RecursiveParserWrapper`) on
+`TikaInputStream.get(byte[])`, `get(InputStream)` and `get(Path)`. The
classpath holds
+`tika-parsers-standard-package` plus `tika-parser-sqlite3-module`; add a
parser module to
+the pom to fuzz it. Jazzer gets no coverage from native code (sqlite-jdbc),
and a native
+crash kills the JVM.
+
+The targets look for OOM, stack overflow, hangs, and exceptions other than
+`TikaException`/`IOException`/`SAXException`. `CompositeParser` wraps parser
+`RuntimeException`s in `TikaException`; add `-Dtika.fuzz.strict=true` to
report those too.
+
+| Property | Default | |
+|---|---|---|
+| `-Dtika.fuzz.timeout=<s>` | `60` | per-input parse timeout, all modes; a
hang is reported with the stuck stack |
+| `-Dtika.fuzz.xmx=<size>` | `3g` | test JVM heap (OSS-Fuzz uses `-Xmx3000m`) |
+| `JAZZER_MAX_DURATION=<d>` | Jazzer's | length of a fuzzing session |
+
+Do not use JUnit's `junit.jupiter.execution.timeout.*`: in fuzzing mode it
ends the
+whole session, not one input.
+
+## Regression mode (every build)
+
+`./mvnw clean test -pl tika-fuzzing` replays each method's inputs from
+`src/test/resources/org/apache/tika/fuzzing/AutoDetectParserFuzzTestInputs/<method>/`
+and `.cifuzz-corpus/`. An input that exceeds `tika.fuzz.timeout` fails the
build.
+
+## Replaying a directory
+
+For a new parser or a refactor, run every file under a directory through all
three
+input modes, with no fuzzing:
+
+```bash
+./mvnw clean install -pl tika-fuzzing -am -Pfast
+./mvnw clean test -pl tika-fuzzing -Dtest=CorpusReplayTest
-Dtika.fuzz.corpus=<dir> \
+ -Dtika.fuzz.timeout=10
+grep -o 'testcase name="[^"]*"'
tika-fuzzing/target/surefire-reports/TEST-*CorpusReplayTest.xml
+```
+
+The report names the mode and file of each failure; the console shows only an
index.
+
+One JVM runs the whole directory. An OOM ends the run, and a timed-out parse
cannot be
+stopped: it keeps a core and its memory, so files after it can time out
falsely. Trust
+the first timeout; re-run later ones alone.
+
+## Fuzzing
+
+```bash
+./mvnw clean install -pl tika-fuzzing -am -Pfast
+JAZZER_FUZZ=1 JAZZER_MAX_DURATION=10m \
+ ./mvnw clean test -pl tika-fuzzing -Dtest='AutoDetectParserFuzzTest#path'
+```
+
+Fuzzing mode runs one method at a time. To seed it, **copy** real files into
+`.cifuzz-corpus/org.apache.tika.fuzzing.AutoDetectParserFuzzTest/<method>/`
+(git-ignored). libFuzzer adds new inputs to that directory, so never point it
at the
+original corpus. A plain test run also replays everything in it.
+
+Jazzer stops at the first finding and writes it to
+`src/test/resources/org/apache/tika/fuzzing/AutoDetectParserFuzzTestInputs/<method>/`
as
+`crash-*` (exceptions, OOM and hangs alike). Later plain test runs replay it.
Finding
+files are git-ignored. Commit one with `git add -f` only after the bug is
fixed and
+disclosed (see `SECURITY.md`). Commit only small, synthesized files, never
corpus
+documents.
+
+After a session, empty `.cifuzz-corpus/` and remove the finding files you are
not
+keeping, or every later build replays them.
diff --git a/tika-fuzzing/pom.xml b/tika-fuzzing/pom.xml
new file mode 100644
index 0000000000..b84f7c136e
--- /dev/null
+++ b/tika-fuzzing/pom.xml
@@ -0,0 +1,92 @@
+<?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
+
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.tika</groupId>
+ <artifactId>tika-parent</artifactId>
+ <version>${revision}</version>
+ <relativePath>../tika-parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>tika-fuzzing</artifactId>
+ <name>Apache Tika fuzzing</name>
+ <description>
+ Jazzer fuzz targets. A plain test run replays the committed inputs; set
JAZZER_FUZZ=1 to fuzz.
+ </description>
+
+ <properties>
+ <jazzer.version>0.30.0</jazzer.version>
+ <maven.deploy.skip>true</maven.deploy.skip>
+ <!-- OSS-Fuzz runs Tika at -Xmx3000m -->
+ <tika.fuzz.xmx>3g</tika.fuzz.xmx>
+ <tika.fuzz.timeout>60</tika.fuzz.timeout>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-core</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-parsers-standard-package</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-parser-sqlite3-module</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.code-intelligence</groupId>
+ <artifactId>jazzer-junit</artifactId>
+ <version>${jazzer.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <argLine>-Xmx${tika.fuzz.xmx} -Djava.awt.headless=true
"-Djava.io.tmpdir=${tika.test.tmpdir}" @{surefireArgLine}</argLine>
+ <systemPropertyVariables>
+ <tika.fuzz.timeout>${tika.fuzz.timeout}</tika.fuzz.timeout>
+ </systemPropertyVariables>
+ <!-- report the file and mode of a failing corpus replay, not its
index -->
+ <statelessTestsetReporter
implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
+ <usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
+ </statelessTestsetReporter>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/tika-fuzzing/src/test/java/org/apache/tika/fuzzing/AutoDetectParserFuzzTest.java
b/tika-fuzzing/src/test/java/org/apache/tika/fuzzing/AutoDetectParserFuzzTest.java
new file mode 100644
index 0000000000..c1cc4beea2
--- /dev/null
+++
b/tika-fuzzing/src/test/java/org/apache/tika/fuzzing/AutoDetectParserFuzzTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+package org.apache.tika.fuzzing;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import com.code_intelligence.jazzer.junit.FuzzTest;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.AutoDetectParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.RecursiveParserWrapper;
+import org.apache.tika.sax.BasicContentHandlerFactory;
+import org.apache.tika.sax.RecursiveParserWrapperHandler;
+
+/**
+ * AutoDetectParser over the three ways a caller hands Tika bytes. Findings
are Errors
+ * (OOM, stack overflow), hangs, and anything not a
TikaException/IOException/SAXException.
+ * With {@code -Dtika.fuzz.strict=true}, a parser RuntimeException that
CompositeParser
+ * wrapped in a TikaException is a finding too. A parse longer than {@code
tika.fuzz.timeout}
+ * seconds (default 60) is reported with the stuck thread's stack.
+ */
+class AutoDetectParserFuzzTest {
+
+ private static final Parser PARSER = new RecursiveParserWrapper(new
AutoDetectParser());
+ private static final boolean STRICT =
Boolean.getBoolean("tika.fuzz.strict");
+ private static final long TIMEOUT_SECONDS =
Long.getLong("tika.fuzz.timeout", 60);
+
+ private static ExecutorService worker;
+ private static volatile Thread workerThread;
+
+ @FuzzTest
+ void bytes(byte[] data) throws Exception {
+ try (TikaInputStream tis = TikaInputStream.get(data)) {
+ parse(tis);
+ }
+ }
+
+ @FuzzTest
+ void stream(byte[] data) throws Exception {
+ try (TikaInputStream tis = TikaInputStream.get(new
ByteArrayInputStream(data))) {
+ parse(tis);
+ }
+ }
+
+ @FuzzTest
+ void path(byte[] data) throws Exception {
+ Path tmp = Files.createTempFile("tika-fuzz-", ".bin");
+ try {
+ Files.write(tmp, data);
+ try (TikaInputStream tis = TikaInputStream.get(tmp)) {
+ parse(tis);
+ }
+ } finally {
+ Files.deleteIfExists(tmp);
+ }
+ }
+
+ // A JUnit timeout would also cap a whole fuzzing session, so each parse
is timed here.
+ static synchronized void parse(TikaInputStream tis) throws Exception {
+ if (worker == null) {
+ worker = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r, "tika-fuzz-parse");
+ t.setDaemon(true);
+ workerThread = t;
+ return t;
+ });
+ }
+ Future<Void> f = worker.submit(() -> {
+ parseInline(tis);
+ return null;
+ });
+ try {
+ f.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ // a spinning parse ignores interrupts; abandon its thread
+ Thread stuck = workerThread;
+ worker = null;
+ AssertionError hang = new AssertionError(
+ "parse did not finish in " + TIMEOUT_SECONDS + " s");
+ hang.setStackTrace(stuck.getStackTrace());
+ throw hang;
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof Error err) {
+ throw err;
+ }
+ throw (Exception) e.getCause();
+ }
+ }
+
+ private static void parseInline(TikaInputStream tis) throws Exception {
+ RecursiveParserWrapperHandler handler = new
RecursiveParserWrapperHandler(
+ new
BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.TEXT, -1));
+ try {
+ PARSER.parse(tis, handler, new Metadata(), new ParseContext());
+ } catch (TikaException e) {
+ if (STRICT && e.getCause() instanceof RuntimeException) {
+ throw e;
+ }
+ } catch (IOException | SAXException e) {
+ // expected on malformed input
+ }
+ }
+}
diff --git
a/tika-fuzzing/src/test/java/org/apache/tika/fuzzing/CorpusReplayTest.java
b/tika-fuzzing/src/test/java/org/apache/tika/fuzzing/CorpusReplayTest.java
new file mode 100644
index 0000000000..d664376475
--- /dev/null
+++ b/tika-fuzzing/src/test/java/org/apache/tika/fuzzing/CorpusReplayTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+package org.apache.tika.fuzzing;
+
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import org.apache.tika.io.TikaInputStream;
+
+/**
+ * Replays every file under {@code -Dtika.fuzz.corpus=<dir>} through the same
parse as
+ * {@link AutoDetectParserFuzzTest}, once per input mode. Skipped when the
property is unset.
+ */
+class CorpusReplayTest {
+
+ private static final String CORPUS =
System.getProperty("tika.fuzz.corpus", "");
+
+ enum Mode { BYTES, STREAM, PATH }
+
+ static Stream<Arguments> files() throws IOException {
+ if (CORPUS.isEmpty()) {
+ return Stream.of(Arguments.of(Mode.BYTES, null));
+ }
+ Path root = Paths.get(CORPUS);
+ // collect eagerly: Files.walk holds a directory handle until closed
+ return
Files.walk(root).filter(Files::isRegularFile).sorted().toList().stream()
+ .flatMap(p -> Stream.of(Mode.values()).map(m ->
Arguments.of(m, p)));
+ }
+
+ @ParameterizedTest(name = "{0} {1}")
+ @MethodSource("files")
+ void replay(Mode mode, Path file) throws Exception {
+ assumeTrue(file != null, "set -Dtika.fuzz.corpus=<dir> to replay a
corpus");
+ try (TikaInputStream tis = switch (mode) {
+ case BYTES -> TikaInputStream.get(Files.readAllBytes(file));
+ case STREAM -> TikaInputStream.get(Files.newInputStream(file));
+ case PATH -> TikaInputStream.get(file);
+ }) {
+ AutoDetectParserFuzzTest.parse(tis);
+ }
+ }
+}
diff --git a/tika-fuzzing/src/test/resources/junit-platform.properties
b/tika-fuzzing/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000000..6ce0abcdf8
--- /dev/null
+++ b/tika-fuzzing/src/test/resources/junit-platform.properties
@@ -0,0 +1,18 @@
+#
+# 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
+#
+# 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.
+
+# Jazzer instruments only directory classes by default; parsers come from jars.
+jazzer.instrument=org.apache.tika.**