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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ load("//sanitizers:sanitizers.bzl", "SANITIZER_CLASSES")
java_library(
name = "clojure_lang_hooks",
srcs = ["ClojureLangHooks.java"],
visibility = [
"//sanitizers:__pkg__",
"//sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers:__pkg__",
],
deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public final class ClojureLangHooks {
targetMethod = "getRawRoot")
public static void clojureMarkContains(
MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object result) {
if (stringContainsFuncNames.contains(result.getClass().getCanonicalName())) {
if (result != null && stringContainsFuncNames.contains(result.getClass().getCanonicalName())) {
stringContainsFuncs.get().add(result);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
load("@contrib_rules_jvm//java:defs.bzl", "JUNIT5_DEPS", "java_junit5_test")

java_junit5_test(
name = "ClojureLangHooksTest",
srcs = ["ClojureLangHooksTest.java"],
deps = JUNIT5_DEPS + [
"//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:clojure_lang_hooks",
"@clojure_jar//jar",
"@maven//:org_junit_jupiter_junit_jupiter_api",
],
)

java_junit5_test(
name = "FilePathTraversalTest",
srcs = ["FilePathTraversalTest.java"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2026 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.code_intelligence.jazzer.sanitizers;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

/**
* Regression tests for the {@code clojure.lang.Var#getRawRoot} AFTER hook. A non-dynamic Clojure
* var can have a {@code null} root, e.g. {@code (def ^:private bb? (System/getProperty ...))} in
* riddley 0.2.2 on the JVM, which previously caused a NullPointerException in the hook.
*/
public class ClojureLangHooksTest {
@Test
void clojureMarkContainsDoesNotThrowOnNullResult() {
assertDoesNotThrow(() -> ClojureLangHooks.clojureMarkContains(null, null, null, 0, null));
}

@Test
void clojureMarkContainsIgnoresNonStringContainsFunctions() {
Object func = new Object();
ClojureLangHooks.clojureMarkContains(null, null, null, 0, func);
assertFalse(ClojureLangHooks.stringContainsFuncs.get().contains(func));
}

@Test
void clojureMarkContainsTracksStringContainsFunctions() throws Exception {
Object func =
Class.forName("clojure.string$includes_QMARK_").getDeclaredConstructor().newInstance();
ClojureLangHooks.clojureMarkContains(null, null, null, 0, func);
assertTrue(ClojureLangHooks.stringContainsFuncs.get().contains(func));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1025,11 +1025,15 @@ private static <K, V> void mapHookInternal(Map<K, V> map, K currentKey, int hook
// map was modified by another thread, skip this invocation
return;
}
// Modify the hook ID so that compares against distinct valid keys are traced separately.
if (lowerBoundKey != null) {
// traceGenericCmp requires both operands to have the same runtime class.
// Bracketing keys are found via compareTo, which may succeed across incompatible Number
// implementations
// (e.g. clojure.lang.Ratio vs. java.lang.Double) and would make the unboxing in traceGenericCmp
// throw a ClassCastException.
if (lowerBoundKey != null && lowerBoundKey.getClass() == currentKey.getClass()) {
TraceDataFlowNativeCallbacks.traceGenericCmp(currentKey, lowerBoundKey, hookId);
}
if (upperBoundKey != null) {
if (upperBoundKey != null && upperBoundKey.getClass() == currentKey.getClass()) {
TraceDataFlowNativeCallbacks.traceGenericCmp(currentKey, upperBoundKey, 31 * hookId + 11);
}
}
Expand Down Expand Up @@ -1063,10 +1067,11 @@ private static <E> void setHookInternal(Set<E> set, E currentElement, int hookId
return;
}

if (lowerBoundElement != null) {
// See the comment in mapHookInternal on why the element classes have to match.
if (lowerBoundElement != null && lowerBoundElement.getClass() == currentElement.getClass()) {
TraceDataFlowNativeCallbacks.traceGenericCmp(currentElement, lowerBoundElement, hookId);
}
if (upperBoundElement != null) {
if (upperBoundElement != null && upperBoundElement.getClass() == currentElement.getClass()) {
TraceDataFlowNativeCallbacks.traceGenericCmp(
currentElement, upperBoundElement, 31 * hookId + 11);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
import static org.junit.Assert.assertEquals;

import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -64,6 +67,81 @@ public void handlesNullValuesInArrayCompare() {
TraceCmpHooks.arraysCompare(null, null, new Object[] {b1, b2}, 1, 1);
}

/**
* Map/set hooks compare the lookup key against bracketing elements via {@code
* TraceDataFlowNativeCallbacks.traceGenericCmp}, which assumes that both operands have the same
* runtime class. Comparisons may succeed across incompatible {@link Number} implementations
* without throwing, e.g. {@code clojure.lang.Ratio.compareTo} accepts any {@code Number}. Such a
* heterogeneous match previously caused a {@link ClassCastException} in the unboxing casts of
* {@code traceGenericCmp}; this happens, for instance, when the Clojure compiler registers {@code
* Double} constants under the Jazzer agent. The hooks must simply skip the guidance in that case.
*/
@Test
public void mapGetShouldTolerateHeterogeneousComparableKeys() {
Map<Object, Object> map = new HashMap<>();
map.put(new LenientNumber(2), "two");
map.put(new LenientNumber(4), "four");
// A Double sorts between the two LenientNumber keys, so lower and upper bracketing keys exist
// and are found via compareTo despite the different classes.
TraceCmpHooks.mapGet(null, map, new Object[] {3.1d}, 1, null);
}

@Test
public void setContainsShouldTolerateHeterogeneousComparableElements() {
Set<Object> set = new HashSet<>();
set.add(new LenientNumber(2));
set.add(new LenientNumber(4));
TraceCmpHooks.setContains(null, set, new Object[] {3.1d}, 1, false);
TraceCmpHooks.setRemove(null, set, new Object[] {3.1d}, 1, false);
}

@Test
public void containsKeyShouldTolerateHeterogeneousComparableKeys() {
// Mirror the actual Clojure compiler call site, which uses an IdentityHashMap.
Map<Object, Object> map = new IdentityHashMap<>();
map.put(new LenientNumber(2), 1);
map.put(new LenientNumber(4), 2);
TraceCmpHooks.containsKey(null, map, new Object[] {3.1d}, 1, false);
}

/**
* A {@link Number} whose {@code compareTo} accepts any other {@code Number}, like {@code
* clojure.lang.Ratio}.
*/
@SuppressWarnings("ComparableType")
private static final class LenientNumber extends Number implements Comparable<Number> {
private final long value;

LenientNumber(long value) {
this.value = value;
}

@Override
public int compareTo(Number other) {
return Long.compare(value, other.longValue());
}

@Override
public int intValue() {
return (int) value;
}

@Override
public long longValue() {
return value;
}

@Override
public float floatValue() {
return value;
}

@Override
public double doubleValue() {
return value;
}
}

@Test
public void traceCmpDoubleWrapperShouldMatchDcmpSemantics() {
assertEquals(0, invokeTraceCmpDoubleWrapper(-0.0d, +0.0d, /* nanResult= */ -1));
Expand Down