Skip to content

Commit 305c6e2

Browse files
committed
feat: add float/double comparison tracking to instrumentor
1 parent 41ae169 commit 305c6e2

File tree

10 files changed

+269
-9
lines changed

10 files changed

+269
-9
lines changed

docs/advanced.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ These hooks correspond to [clang's data flow hooks](https://clang.llvm.org/docs/
9393
The particular instrumentation types to apply can be specified using the `--trace` flag, which accepts the following values:
9494

9595
* `cov`: AFL-style edge coverage
96-
* `cmp`: compares (int, long, String) and switch cases
96+
* `cmp`: compares (int, long, float, double, String) and switch cases
9797
* `div`: divisors in integer divisions
9898
* `gep`: constant array indexes
9999
* `indir`: call through `Method#invoke`

src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ import org.objectweb.asm.tree.MethodNode
3232
import org.objectweb.asm.tree.TableSwitchInsnNode
3333
import org.objectweb.asm.tree.VarInsnNode
3434

35+
private val TRACE_COMPARISON_METHODS =
36+
setOf(
37+
"traceCmpLongWrapper",
38+
"traceCmpDoubleWrapper",
39+
"traceCmpFloatWrapper",
40+
)
41+
3542
internal class TraceDataFlowInstrumentor(
3643
private val types: Set<InstrumentationType>,
3744
private val callbackInternalClassName: String = "com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks",
@@ -65,6 +72,18 @@ internal class TraceDataFlowInstrumentor(
6572
method.instructions.insertBefore(inst, longCmpInstrumentation())
6673
method.instructions.remove(inst)
6774
}
75+
Opcodes.DCMPG, Opcodes.DCMPL -> {
76+
if (InstrumentationType.CMP !in types) continue@loop
77+
val nanResult = if (inst.opcode == Opcodes.DCMPG) 1 else -1
78+
method.instructions.insertBefore(inst, doubleCmpInstrumentation(nanResult))
79+
method.instructions.remove(inst)
80+
}
81+
Opcodes.FCMPG, Opcodes.FCMPL -> {
82+
if (InstrumentationType.CMP !in types) continue@loop
83+
val nanResult = if (inst.opcode == Opcodes.FCMPG) 1 else -1
84+
method.instructions.insertBefore(inst, floatCmpInstrumentation(nanResult))
85+
method.instructions.remove(inst)
86+
}
6887
Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE,
6988
Opcodes.IF_ICMPLT, Opcodes.IF_ICMPLE,
7089
Opcodes.IF_ICMPGT, Opcodes.IF_ICMPGE,
@@ -78,15 +97,14 @@ internal class TraceDataFlowInstrumentor(
7897
-> {
7998
if (InstrumentationType.CMP !in types) continue@loop
8099
// The IF* opcodes are often used to branch based on the result of a compare
81-
// instruction for a type other than int. The operands of this compare will
82-
// already be reported via the instrumentation above (for non-floating point
83-
// numbers) and the follow-up compare does not provide a good signal as all
84-
// operands will be in {-1, 0, 1}. Skip instrumentation for it.
85-
if (inst.previous?.opcode in listOf(Opcodes.DCMPG, Opcodes.DCMPL, Opcodes.FCMPG, Opcodes.DCMPL) ||
86-
(inst.previous as? MethodInsnNode)?.name == "traceCmpLongWrapper"
87-
) {
100+
// instruction for a type other than int (long, float, double). The operands
101+
// of this compare will already be reported via the instrumentation above and
102+
// the follow-up compare does not provide a good signal as all operands will
103+
// be in {-1, 0, 1}. Skip instrumentation for it.
104+
if ((inst.previous as? MethodInsnNode)?.name in TRACE_COMPARISON_METHODS) {
88105
continue@loop
89106
}
107+
90108
method.instructions.insertBefore(inst, ifInstrumentation())
91109
}
92110
Opcodes.LOOKUPSWITCH, Opcodes.TABLESWITCH -> {
@@ -256,6 +274,20 @@ internal class TraceDataFlowInstrumentor(
256274
add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpLongWrapper", "(JJI)I", false))
257275
}
258276

277+
private fun doubleCmpInstrumentation(nanResult: Int) =
278+
InsnList().apply {
279+
add(LdcInsnNode(nanResult))
280+
pushFakePc()
281+
add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpDoubleWrapper", "(DDII)I", false))
282+
}
283+
284+
private fun floatCmpInstrumentation(nanResult: Int) =
285+
InsnList().apply {
286+
add(LdcInsnNode(nanResult))
287+
pushFakePc()
288+
add(MethodInsnNode(Opcodes.INVOKESTATIC, callbackInternalClassName, "traceCmpFloatWrapper", "(FFII)I", false))
289+
}
290+
259291
private fun intCmpInstrumentation() =
260292
InsnList().apply {
261293
add(InsnNode(Opcodes.DUP2))

src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,56 @@ public static void integerCompareTo(
8585
((Number) thisObject).intValue(), ((Number) arguments[0]).intValue(), hookId);
8686
}
8787

88+
@MethodHook(
89+
type = HookType.BEFORE,
90+
targetClassName = "java.lang.Float",
91+
targetMethod = "compare",
92+
targetMethodDescriptor = "(FF)I")
93+
public static void floatCompare(
94+
MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) {
95+
TraceDataFlowNativeCallbacks.traceCmpInt(
96+
Float.floatToRawIntBits((float) arguments[0]),
97+
Float.floatToRawIntBits((float) arguments[1]),
98+
hookId);
99+
}
100+
101+
@MethodHook(
102+
type = HookType.BEFORE,
103+
targetClassName = "java.lang.Double",
104+
targetMethod = "compare",
105+
targetMethodDescriptor = "(DD)I")
106+
public static void doubleCompare(
107+
MethodHandle method, Object alwaysNull, Object[] arguments, int hookId) {
108+
TraceDataFlowNativeCallbacks.traceCmpLong(
109+
Double.doubleToRawLongBits((double) arguments[0]),
110+
Double.doubleToRawLongBits((double) arguments[1]),
111+
hookId);
112+
}
113+
114+
@MethodHook(
115+
type = HookType.BEFORE,
116+
targetClassName = "java.lang.Float",
117+
targetMethod = "compareTo",
118+
targetMethodDescriptor = "(Ljava/lang/Float;)I")
119+
public static void floatCompareTo(
120+
MethodHandle method, Float thisObject, Object[] arguments, int hookId) {
121+
TraceDataFlowNativeCallbacks.traceCmpInt(
122+
Float.floatToRawIntBits(thisObject), Float.floatToRawIntBits((float) arguments[0]), hookId);
123+
}
124+
125+
@MethodHook(
126+
type = HookType.BEFORE,
127+
targetClassName = "java.lang.Double",
128+
targetMethod = "compareTo",
129+
targetMethodDescriptor = "(Ljava/lang/Double;)I")
130+
public static void doubleCompareTo(
131+
MethodHandle method, Double thisObject, Object[] arguments, int hookId) {
132+
TraceDataFlowNativeCallbacks.traceCmpLong(
133+
Double.doubleToRawLongBits(thisObject),
134+
Double.doubleToRawLongBits((double) arguments[0]),
135+
hookId);
136+
}
137+
88138
@MethodHook(
89139
type = HookType.BEFORE,
90140
targetClassName = "java.lang.Long",

src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ public static int traceCmpLongWrapper(long arg1, long arg2, int pc) {
8181
return Long.compare(arg1, arg2);
8282
}
8383

84+
public static int traceCmpDoubleWrapper(double arg1, double arg2, int nanResult, int pc) {
85+
traceCmpLong(Double.doubleToRawLongBits(arg1), Double.doubleToRawLongBits(arg2), pc);
86+
if (Double.isNaN(arg1) || Double.isNaN(arg2)) return nanResult;
87+
// Mirror DCMPG/DCMPL semantics: in particular, -0.0 == +0.0 must yield 0.
88+
if (arg1 > arg2) return 1;
89+
if (arg1 == arg2) return 0;
90+
return -1;
91+
}
92+
93+
public static int traceCmpFloatWrapper(float arg1, float arg2, int nanResult, int pc) {
94+
traceCmpInt(Float.floatToRawIntBits(arg1), Float.floatToRawIntBits(arg2), pc);
95+
if (Float.isNaN(arg1) || Float.isNaN(arg2)) return nanResult;
96+
// Mirror FCMPG/FCMPL semantics: in particular, -0.0 == +0.0 must yield 0.
97+
if (arg1 > arg2) return 1;
98+
if (arg1 == arg2) return 0;
99+
return -1;
100+
}
101+
84102
// The caller has to ensure that arg1 and arg2 have the same class.
85103
public static void traceGenericCmp(Object arg1, Object arg2, int pc) {
86104
if (arg1 instanceof CharSequence) {
@@ -89,6 +107,11 @@ public static void traceGenericCmp(Object arg1, Object arg2, int pc) {
89107
traceCmpInt((int) arg1, (int) arg2, pc);
90108
} else if (arg1 instanceof Long) {
91109
traceCmpLong((long) arg1, (long) arg2, pc);
110+
} else if (arg1 instanceof Float) {
111+
traceCmpInt(Float.floatToRawIntBits((float) arg1), Float.floatToRawIntBits((float) arg2), pc);
112+
} else if (arg1 instanceof Double) {
113+
traceCmpLong(
114+
Double.doubleToRawLongBits((double) arg1), Double.doubleToRawLongBits((double) arg2), pc);
92115
} else if (arg1 instanceof Short) {
93116
traceCmpInt((short) arg1, (short) arg2, pc);
94117
} else if (arg1 instanceof Byte) {

src/test/java/com/code_intelligence/jazzer/instrumentor/MockTraceDataFlowCallbacks.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,20 @@ public static int traceCmpLongWrapper(long value1, long value2, int pc) {
109109
// (behaviour is the same)
110110
return Long.compare(value1, value2);
111111
}
112+
113+
public static int traceCmpDoubleWrapper(double value1, double value2, int nanResult, int pc) {
114+
traceCmpLong(Double.doubleToRawLongBits(value1), Double.doubleToRawLongBits(value2), pc);
115+
if (Double.isNaN(value1) || Double.isNaN(value2)) return nanResult;
116+
if (value1 > value2) return 1;
117+
if (value1 == value2) return 0;
118+
return -1;
119+
}
120+
121+
public static int traceCmpFloatWrapper(float value1, float value2, int nanResult, int pc) {
122+
traceCmpInt(Float.floatToRawIntBits(value1), Float.floatToRawIntBits(value2), pc);
123+
if (Float.isNaN(value1) || Float.isNaN(value2)) return nanResult;
124+
if (value1 > value2) return 1;
125+
if (value1 == value2) return 0;
126+
return -1;
127+
}
112128
}

src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ public class TraceDataFlowInstrumentationTarget implements DynamicTestContract {
3737
volatile int int3 = 6;
3838
volatile int int4 = 5;
3939

40+
volatile double double1 = 1.5;
41+
volatile double double2 = 1.5;
42+
volatile double double3 = 2.5;
43+
volatile double double4 = 3.5;
44+
volatile double doubleNaN = Double.NaN;
45+
volatile double doubleNegZero = -0.0;
46+
volatile double doublePosZero = 0.0;
47+
48+
volatile float float1 = 1.5f;
49+
volatile float float2 = 1.5f;
50+
volatile float float3 = 2.5f;
51+
volatile float float4 = 3.5f;
52+
volatile float floatNaN = Float.NaN;
53+
volatile float floatNegZero = -0.0f;
54+
volatile float floatPosZero = 0.0f;
55+
4056
volatile int switchValue = 1200;
4157

4258
@SuppressWarnings("ReturnValueIgnored")
@@ -47,6 +63,17 @@ public Map<String, Boolean> selfCheck() {
4763
results.put("longCompareEq", long1 == long2);
4864
results.put("longCompareNe", long3 != long4);
4965

66+
results.put("doubleCompareEq", double1 == double2);
67+
results.put("doubleCompareNe", double3 != double4);
68+
results.put("floatCompareEq", float1 == float2);
69+
results.put("floatCompareNe", float3 != float4);
70+
results.put("doubleCompareSignedZeroEq", doubleNegZero == doublePosZero);
71+
results.put("floatCompareSignedZeroEq", floatNegZero == floatPosZero);
72+
results.put("doubleCompareNaNLessFalse", !(doubleNaN < double1));
73+
results.put("doubleCompareNaNGreaterFalse", !(doubleNaN > double1));
74+
results.put("floatCompareNaNLessFalse", !(floatNaN < float1));
75+
results.put("floatCompareNaNGreaterFalse", !(floatNaN > float1));
76+
5077
results.put("intCompareEq", int1 == int2);
5178
results.put("intCompareNe", int3 != int4);
5279
results.put("intCompareLt", int4 < int3);

src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ class TraceDataFlowInstrumentationTest {
5858
// long compares
5959
"LCMP: 1, 1",
6060
"LCMP: 2, 3",
61+
// double compares
62+
"LCMP: 4609434218613702656, 4609434218613702656",
63+
"LCMP: 4612811918334230528, 4615063718147915776",
64+
// float compares
65+
"ICMP: 1069547520, 1069547520",
66+
"ICMP: 1075838976, 1080033280",
67+
// signed zero compares
68+
"LCMP: -9223372036854775808, 0",
69+
"ICMP: -2147483648, 0",
70+
// NaN compares
71+
"LCMP: 4609434218613702656, 9221120237041090560",
72+
"LCMP: 4609434218613702656, 9221120237041090560",
73+
"ICMP: 1069547520, 2143289344",
74+
"ICMP: 1069547520, 2143289344",
6175
// int compares
6276
"ICMP: 4, 4",
6377
"ICMP: 5, 6",
@@ -87,9 +101,10 @@ class TraceDataFlowInstrumentationTest {
87101
"ICMP: 3, 3",
88102
// doubleArray[4] == 4
89103
"GEP: 4",
104+
"LCMP: 4616189618054758400, 4616189618054758400",
90105
// floatArray[5] == 5
91106
"GEP: 5",
92-
"CICMP: 0, 0",
107+
"ICMP: 1084227584, 1084227584",
93108
// intArray[6] == 6
94109
"GEP: 6",
95110
"ICMP: 6, 6",

src/test/java/com/code_intelligence/jazzer/runtime/TraceCmpHooksTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.code_intelligence.jazzer.runtime;
1818

19+
import static org.junit.Assert.assertEquals;
20+
1921
import java.util.HashMap;
2022
import java.util.Map;
2123
import java.util.concurrent.ExecutorService;
@@ -61,4 +63,46 @@ public void handlesNullValuesInArrayCompare() {
6163
TraceCmpHooks.arraysEquals(null, null, new Object[] {b1, b2}, 1, false);
6264
TraceCmpHooks.arraysCompare(null, null, new Object[] {b1, b2}, 1, 1);
6365
}
66+
67+
@Test
68+
public void traceCmpDoubleWrapperShouldMatchDcmpSemantics() {
69+
assertEquals(0, invokeTraceCmpDoubleWrapper(-0.0d, +0.0d, /* nanResult= */ -1));
70+
assertEquals(0, invokeTraceCmpDoubleWrapper(+0.0d, -0.0d, /* nanResult= */ 1));
71+
assertEquals(-1, invokeTraceCmpDoubleWrapper(Double.NaN, 1.0d, /* nanResult= */ -1));
72+
assertEquals(1, invokeTraceCmpDoubleWrapper(Double.NaN, 1.0d, /* nanResult= */ 1));
73+
}
74+
75+
@Test
76+
public void traceCmpFloatWrapperShouldMatchFcmpSemantics() {
77+
assertEquals(0, invokeTraceCmpFloatWrapper(-0.0f, +0.0f, /* nanResult= */ -1));
78+
assertEquals(0, invokeTraceCmpFloatWrapper(+0.0f, -0.0f, /* nanResult= */ 1));
79+
assertEquals(-1, invokeTraceCmpFloatWrapper(Float.NaN, 1.0f, /* nanResult= */ -1));
80+
assertEquals(1, invokeTraceCmpFloatWrapper(Float.NaN, 1.0f, /* nanResult= */ 1));
81+
}
82+
83+
private static int invokeTraceCmpDoubleWrapper(double arg1, double arg2, int nanResult) {
84+
try {
85+
Class<?> callbacksClass =
86+
Class.forName("com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks");
87+
return (int)
88+
callbacksClass
89+
.getMethod("traceCmpDoubleWrapper", double.class, double.class, int.class, int.class)
90+
.invoke(null, arg1, arg2, nanResult, 1);
91+
} catch (ReflectiveOperationException e) {
92+
throw new AssertionError(e);
93+
}
94+
}
95+
96+
private static int invokeTraceCmpFloatWrapper(float arg1, float arg2, int nanResult) {
97+
try {
98+
Class<?> callbacksClass =
99+
Class.forName("com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks");
100+
return (int)
101+
callbacksClass
102+
.getMethod("traceCmpFloatWrapper", float.class, float.class, int.class, int.class)
103+
.invoke(null, arg1, arg2, nanResult, 1);
104+
} catch (ReflectiveOperationException e) {
105+
throw new AssertionError(e);
106+
}
107+
}
64108
}

tests/BUILD.bazel

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,28 @@ java_fuzz_target_test(
846846
],
847847
)
848848

849+
java_fuzz_target_test(
850+
name = "FloatDoubleCmpFuzzer",
851+
timeout = "short",
852+
srcs = ["src/test/java/com/example/FloatDoubleCmpFuzzer.java"],
853+
allowed_findings = [
854+
"com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow",
855+
],
856+
fuzzer_args = [
857+
"-use_value_profile=1",
858+
],
859+
target_class = "com.example.FloatDoubleCmpFuzzer",
860+
verify_crash_reproducer = False,
861+
runtime_deps = [
862+
"@maven//:org_junit_jupiter_junit_jupiter_engine",
863+
],
864+
deps = [
865+
"//deploy:jazzer-junit",
866+
"@maven//:org_junit_jupiter_junit_jupiter_api",
867+
"@maven//:org_junit_jupiter_junit_jupiter_params",
868+
],
869+
)
870+
849871
java_fuzz_target_test(
850872
name = "LocalDateTimeFuzzer",
851873
timeout = "short",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example;
18+
19+
import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow;
20+
import com.code_intelligence.jazzer.junit.FuzzTest;
21+
22+
public class FloatDoubleCmpFuzzer {
23+
@FuzzTest
24+
void floatDoubleCmp(float f, double d) {
25+
float fx = f * f - 8.625f * f;
26+
double dx = d * d - 10.8125 * d;
27+
if (fx == -12.65625f && dx == -24.3046875) {
28+
throw new FuzzerSecurityIssueLow("Float/double comparison tracking works!");
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)