diff --git a/guava-fuzzers/pom.xml b/guava-fuzzers/pom.xml new file mode 100644 index 000000000000..e60ec32585d9 --- /dev/null +++ b/guava-fuzzers/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + + + com.google.guava + guava-parent + 999.0.0-HEAD-jre-SNAPSHOT + + + guava-fuzzers + Guava Fuzz Harnesses + + Jazzer fuzz harnesses for Guava, built by OSS-Fuzz. This module is intentionally NOT + listed in the repo-root pom's modules, so ordinary Guava builds and releases do not + compile it, do not depend on Jazzer, and do not ship any harness. OSS-Fuzz builds the + harness directly (google/oss-fuzz#16117). + + + + true + true + + + + + com.google.guava + guava + ${project.version} + + + com.code-intelligence + jazzer-api + 0.24.0 + provided + + + diff --git a/guava-fuzzers/src/main/java/AndroidImmutableCollectionsHashFloodingFuzzer.java b/guava-fuzzers/src/main/java/AndroidImmutableCollectionsHashFloodingFuzzer.java new file mode 100644 index 000000000000..6ef02fc6d0b8 --- /dev/null +++ b/guava-fuzzers/src/main/java/AndroidImmutableCollectionsHashFloodingFuzzer.java @@ -0,0 +1,138 @@ +// Copyright 2026 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. + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +/** Detects algorithmic-complexity hash flooding in Android-flavor immutable collections. */ +public final class AndroidImmutableCollectionsHashFloodingFuzzer { + // A magic prefix prevents ordinary random inputs from repeatedly exercising the maximum-cost + // case. Jazzer's comparison instrumentation can still discover this structured input. + private static final byte[] MAGIC = {'H', 'F', 'v', '1'}; + private static final long MAX_REASONABLE_EQUALS = 1_000_000L; + + private AndroidImmutableCollectionsHashFloodingFuzzer() {} + + public static void fuzzerTestOneInput(byte[] input) { + if (input.length < MAGIC.length + 2 || !hasMagicPrefix(input)) { + return; + } + + int collection = Byte.toUnsignedInt(input[MAGIC.length]) % 3; + int collisionBits = 6 + (Byte.toUnsignedInt(input[MAGIC.length + 1]) % 7); + int count = 1 << collisionBits; + + CollisionKey.resetEqualsCalls(); + switch (collection) { + case 0: + ImmutableMap.Builder map = + ImmutableMap.builderWithExpectedSize(count); + for (int i = 0; i < count; i++) { + map.put(new CollisionKey(i, collisionBits), i); + } + map.buildOrThrow(); + break; + case 1: + ImmutableSet.Builder set = ImmutableSet.builderWithExpectedSize(count); + for (int i = 0; i < count; i++) { + set.add(new CollisionKey(i, collisionBits)); + } + set.build(); + break; + default: + ImmutableBiMap.Builder bimap = ImmutableBiMap.builder(); + for (int i = 0; i < count; i++) { + bimap.put(new CollisionKey(i, collisionBits), i); + } + bimap.buildOrThrow(); + break; + } + + long equalsCalls = CollisionKey.equalsCalls(); + if (equalsCalls > MAX_REASONABLE_EQUALS) { + throw new FuzzerSecurityIssueMedium( + "Probable hash-flooding algorithmic-complexity denial of service in " + + collectionName(collection) + + ": " + + count + + " distinct keys with one String hash required " + + equalsCalls + + " equals calls during construction (limit " + + MAX_REASONABLE_EQUALS + + ")."); + } + } + + private static boolean hasMagicPrefix(byte[] input) { + for (int i = 0; i < MAGIC.length; i++) { + if (input[i] != MAGIC[i]) { + return false; + } + } + return true; + } + + private static String collectionName(int collection) { + switch (collection) { + case 0: + return "ImmutableMap"; + case 1: + return "ImmutableSet"; + default: + return "ImmutableBiMap"; + } + } + + /** Comparable wrapper around distinct strings from Java's Aa/BB collision family. */ + private static final class CollisionKey implements Comparable { + private static long equalsCalls; + private final int id; + private final String value; + + CollisionKey(int id, int bits) { + this.id = id; + StringBuilder result = new StringBuilder(bits * 2); + for (int bit = 0; bit < bits; bit++) { + result.append(((id >>> bit) & 1) == 0 ? "Aa" : "BB"); + } + this.value = result.toString(); + } + + static void resetEqualsCalls() { + equalsCalls = 0; + } + + static long equalsCalls() { + return equalsCalls; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object other) { + equalsCalls++; + return other instanceof CollisionKey && value.equals(((CollisionKey) other).value); + } + + @Override + public int compareTo(CollisionKey other) { + return Integer.compare(id, other.id); + } + } +}