Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -234,6 +248,176 @@ public static void equals(
}
}

private static final LocalDate FALLBACK_LOCAL_DATE = LocalDate.of(2000, 1, 1);
private static final ClassValue<Optional<String>> TEMPORAL_PARSE_FALLBACKS =
new ClassValue<Optional<String>>() {
@Override
protected Optional<String> 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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: @Marcono1234 was faster!
Concern: Some of the hooked classes have two arguments. E.g. java.time.LocalDate.parse(CharSequence text, DateTimeFormatter formatter), and since here we don't specify targetMethodDescriptor, the hooks apply to all versions of the parse function.
For the two argument version, we are guiding towards the default formatter, and not the one that the user specified.
In case of LocalDate it's ISO-8601.

It might make sense to split up the hooks to address the two overloads of parse individually.

In that case it would make sense to double-check if the user formatter is valid (it might be the reason we end up in the catch block). In that case we could safely use our default formatter.
But if the user provides a valid formatter, we should guide towards a date that respects it.

I usually abuse our fuzz test in selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation/ArgumentsMutatorFuzzTest.java for quickly fuzzing anything. A fuzz test like this really struggles finding the correct string:

private static final LocalDateTime TARGET_DATE_TIME =
        LocalDateTime.parse("2000-01-01T00:00:00");

  @SelfFuzzTest
  @Solo
  void fuzzPrimitiveArrays(@NotNull String dateStr) {
    try {
      LocalDateTime ldt = LocalDateTime.parse(dateStr,  DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm:ss", Locale.ENGLISH));
      System.out.println(ldt); // this never happens
      if (ldt.equals(TARGET_DATE_TIME)) {
        throw new FuzzerSecurityIssueLow("Found the target date time!");
      }
    } catch (DateTimeParseException ignore) {
    }
  }

To quickly run use:

bazel run //selffuzz/src/test/java/com/code_intelligence/selffuzz/mutation:ArgumentsMutatorFuzzTest -- -runs=10000000`

Comment on lines +393 to +400
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intended that the annotations for MonthDay and Duration are not sorted alphabetically here?
(might make it a bit more difficult at a glance to see if all classes are covered)

@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);
Copy link
Contributor

@Marcono1234 Marcono1234 Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Edit: See more extensive review comment regarding this in #1051 (comment))

Out of curiosity, the above hook annotations also match the parse overloads with custom DateTimeFormatter (if I see it correctly), could that be a problem when that format completely differs from the standard ISO format?

Or is that considered acceptable, assuming that most likely the format is still somewhat close to the default ISO format?

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) {
Expand Down
14 changes: 14 additions & 0 deletions tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
40 changes: 40 additions & 0 deletions tests/src/test/java/com/example/TimeParseFuzzer.java
Original file line number Diff line number Diff line change
@@ -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) {
}
}
}