From 0ff550a5c766ca95e6a482914ac6d0d410b0651b Mon Sep 17 00:00:00 2001 From: psy Date: Mon, 2 Mar 2026 10:56:09 +0100 Subject: [PATCH] feat: add hooks for temporal parse and string compare in equals methods --- .../jazzer/runtime/TraceCmpHooks.java | 184 ++++++++++++++++++ tests/BUILD.bazel | 14 ++ .../java/com/example/TimeParseFuzzer.java | 40 ++++ 3 files changed, 238 insertions(+) create mode 100644 tests/src/test/java/com/example/TimeParseFuzzer.java diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java index 43f513fa9..f6f3ed040 100644 --- a/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java +++ b/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java @@ -19,6 +19,20 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.MethodHook; import java.lang.invoke.MethodHandle; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.*; @SuppressWarnings("unused") @@ -234,6 +248,176 @@ public static void equals( } } + private static final LocalDate FALLBACK_LOCAL_DATE = LocalDate.of(2000, 1, 1); + private static final ClassValue> TEMPORAL_PARSE_FALLBACKS = + new ClassValue>() { + @Override + protected Optional computeValue(Class type) { + if (type == Duration.class) return Optional.of(Duration.ZERO.toString()); + if (type == Instant.class) return Optional.of(Instant.EPOCH.toString()); + if (type == LocalDate.class) return Optional.of(FALLBACK_LOCAL_DATE.toString()); + if (type == LocalDateTime.class) + return Optional.of(FALLBACK_LOCAL_DATE.atStartOfDay().toString()); + if (type == LocalTime.class) return Optional.of(LocalTime.MIDNIGHT.toString()); + if (type == OffsetDateTime.class) { + return Optional.of( + OffsetDateTime.of(FALLBACK_LOCAL_DATE, LocalTime.MIDNIGHT, ZoneOffset.UTC) + .toString()); + } + if (type == OffsetTime.class) { + return Optional.of(OffsetTime.of(LocalTime.MIDNIGHT, ZoneOffset.UTC).toString()); + } + if (type == Period.class) return Optional.of(Period.ZERO.toString()); + if (type == Year.class) + return Optional.of(Year.of(FALLBACK_LOCAL_DATE.getYear()).toString()); + if (type == YearMonth.class) { + return Optional.of( + YearMonth.of(FALLBACK_LOCAL_DATE.getYear(), FALLBACK_LOCAL_DATE.getMonth()) + .toString()); + } + if (type == MonthDay.class) + return Optional.of(MonthDay.from(FALLBACK_LOCAL_DATE).toString()); + if (type == ZonedDateTime.class) { + return Optional.of( + ZonedDateTime.of(FALLBACK_LOCAL_DATE, LocalTime.MIDNIGHT, ZoneId.of("Europe/Paris")) + .toString()); + } + return Optional.empty(); + } + }; + + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.Duration", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.Instant", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.LocalDate", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.LocalDateTime", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.LocalTime", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.MonthDay", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.OffsetDateTime", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.OffsetTime", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.Period", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.Year", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.YearMonth", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.time.ZonedDateTime", + targetMethod = "equals", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + public static void temporalEquals( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean areEqual) { + if (!areEqual + && arguments.length == 1 + && arguments[0] != null + && thisObject.getClass() == arguments[0].getClass()) { + TraceDataFlowNativeCallbacks.traceStrcmp( + thisObject.toString(), arguments[0].toString(), 1, hookId); + } + } + + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.Instant", + targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.LocalDate", + targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.LocalDateTime", + targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.LocalTime", + targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.OffsetDateTime", + targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.OffsetTime", + targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.ZonedDateTime", + targetMethod = "parse") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.time.Year", targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.YearMonth", + targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.MonthDay", + targetMethod = "parse") + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.time.Duration", + targetMethod = "parse") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.time.Period", targetMethod = "parse") + public static Object temporalParse( + MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) throws Throwable { + try { + return method.invokeWithArguments(arguments); + } catch (Throwable throwable) { + if (throwable instanceof Error) { + throw throwable; + } + + if (arguments[0] != null) { + String fallback = TEMPORAL_PARSE_FALLBACKS.get(method.type().returnType()).orElse(null); + if (fallback != null) { + TraceDataFlowNativeCallbacks.traceStrcmp(arguments[0].toString(), fallback, 1, hookId); + } + } + throw throwable; + } + } + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Objects", targetMethod = "equals") public static void genericObjectsEquals( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean areEqual) { diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index bd857bfd2..d2229231b 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -774,6 +774,20 @@ java_fuzz_target_test( ], ) +java_fuzz_target_test( + name = "TimeParseFuzzer", + srcs = ["src/test/java/com/example/TimeParseFuzzer.java"], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium"], + fuzzer_args = [ + "-runs=1000000", + ], + target_class = "com.example.TimeParseFuzzer", + verify_crash_reproducer = False, + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + ], +) + sh_test( name = "jazzer_from_path_test", srcs = ["src/test/shell/jazzer_from_path_test.sh"], diff --git a/tests/src/test/java/com/example/TimeParseFuzzer.java b/tests/src/test/java/com/example/TimeParseFuzzer.java new file mode 100644 index 000000000..370d219a5 --- /dev/null +++ b/tests/src/test/java/com/example/TimeParseFuzzer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Code Intelligence GmbH + * + * 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. + */ + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium; +import com.code_intelligence.jazzer.mutation.annotation.Ascii; +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; + +public final class TimeParseFuzzer { + private static final OffsetDateTime TARGET_DATE_TIME = + OffsetDateTime.parse("2001-12-04T00:00:00-05:00"); + + private TimeParseFuzzer() {} + + public static void fuzzerTestOneInput(@NotNull @Ascii String offsetDateInput) { + try { + OffsetDateTime offsetDateTime = OffsetDateTime.parse(offsetDateInput); + if (TARGET_DATE_TIME.equals(offsetDateTime)) { + throw new FuzzerSecurityIssueMedium(); + } + } catch (DateTimeParseException ignored) { + } + } +}