diff --git a/workloads/eclair/Dockerfile b/workloads/eclair/Dockerfile
index 3f070778..174ffc5c 100644
--- a/workloads/eclair/Dockerfile
+++ b/workloads/eclair/Dockerfile
@@ -46,7 +46,7 @@ RUN find eclair-node/target -name "eclair-node-*-bin.zip" \
-exec unzip {} -d /opt \; && \
mv /opt/eclair-node-* /opt/eclair
-# Build the eclair-sancov Java agent.
+# Build the eclair-sancov coverage instrumenter and runtime.
COPY workloads/eclair/eclair-sancov/ /eclair-sancov/
WORKDIR /eclair-sancov
RUN mvn package
@@ -59,13 +59,13 @@ RUN cc -shared -fPIC \
src/main/c/shmutil.c \
-o /usr/local/lib/libeclair-sancov.so
-# Count instrumentable Eclair methods to determine TARGET_MAP_SIZE.
-# EclairEdgeCounter scans the same fr/acinq/eclair/ methods that
-# EclairTransformer will instrument, so the count matches exactly.
-RUN java -cp target/eclair-sancov-0.0.0.jar EclairEdgeCounter \
- $(find /opt/eclair/lib -name "*.jar" | tr '\n' ' ') \
+# Instrument Eclair's JARs in place; the probe count becomes TARGET_MAP_SIZE.
+# The probes call EclairSanCov, placed in lib/ because eclair-node.sh puts
+# that directory itself on the classpath.
+RUN java -cp target/eclair-sancov-0.0.0.jar EclairInstrumenter /opt/eclair/lib \
> /tmp/eclair-edge-count.txt && \
- echo "Eclair edge count: $(cat /tmp/eclair-edge-count.txt)"
+ cp target/classes/EclairSanCov.class /opt/eclair/lib/ && \
+ echo "Eclair probe count: $(cat /tmp/eclair-edge-count.txt)"
# Copy smite workspace files and build all scenario binaries
WORKDIR /smite
@@ -106,8 +106,7 @@ COPY --from=builder /opt/eclair /opt/eclair
COPY --from=builder /usr/local/bin/bitcoind /usr/local/bin/bitcoind
COPY --from=builder /usr/local/bin/bitcoin-cli /usr/local/bin/bitcoin-cli
-# Copy coverage agent and JNI shared library
-COPY --from=builder /eclair-sancov/target/eclair-sancov-0.0.0.jar /eclair-sancov.jar
+# Copy the JNI library EclairSanCov uses to map the AFL shared memory
COPY --from=builder /usr/local/lib/libeclair-sancov.so /usr/local/lib/libeclair-sancov.so
# Copy crash handlers and eclair scenario binary
diff --git a/workloads/eclair/eclair-sancov/pom.xml b/workloads/eclair/eclair-sancov/pom.xml
index c7f9f388..9c0d26ca 100644
--- a/workloads/eclair/eclair-sancov/pom.xml
+++ b/workloads/eclair/eclair-sancov/pom.xml
@@ -16,32 +16,22 @@
-
+
org.ow2.asm
asm
9.7
+
+ org.ow2.asm
+ asm-tree
+ 9.7
+
-
-
- org.apache.maven.plugins
- maven-jar-plugin
- 3.3.0
-
-
-
-
- EclairSanCov
- true
-
-
-
-
-
+
org.apache.maven.plugins
maven-shade-plugin
@@ -54,29 +44,6 @@
false
-
-
-
- org.objectweb.asm
- eclair.sancov.asm
-
-
-
-
-
-
- EclairSanCov
- true
-
-
-
diff --git a/workloads/eclair/eclair-sancov/src/main/c/shmutil.c b/workloads/eclair/eclair-sancov/src/main/c/shmutil.c
index 6214d714..3312ad6b 100644
--- a/workloads/eclair/eclair-sancov/src/main/c/shmutil.c
+++ b/workloads/eclair/eclair-sancov/src/main/c/shmutil.c
@@ -9,7 +9,7 @@
// to read/write coverage counters directly at this address, avoiding the bounds
// check and field load overhead of DirectByteBuffer.get/put.
//
-// Called once from EclairSanCov.premain() before any class transformation.
+// Called once from EclairSanCov's static initializer, on the first probe.
JNIEXPORT jlong JNICALL Java_EclairSanCov_mapShmAddr(JNIEnv *env, jclass cls,
jint shmId) {
void *ptr = shmat((int)shmId, NULL, 0);
diff --git a/workloads/eclair/eclair-sancov/src/main/java/EclairEdgeCounter.java b/workloads/eclair/eclair-sancov/src/main/java/EclairEdgeCounter.java
deleted file mode 100644
index 6daf7ac9..00000000
--- a/workloads/eclair/eclair-sancov/src/main/java/EclairEdgeCounter.java
+++ /dev/null
@@ -1,105 +0,0 @@
-import java.io.InputStream;
-import java.util.Enumeration;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.Label;
-import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
-
-// Counts the total number of instrumentation probes across all instrumented
-// packages (fr/acinq/, scala/, scodec/).
-//
-// Used at Docker build time to determine TARGET_MAP_SIZE for the smite scenario
-// binary. Counts the same probes that EclairSanCov.prescan() assigns IDs to:
-// one entry probe per non-abstract, non-native method, plus one probe per
-// conditional branch fall-through and one per label/basic-block entry within
-// each method.
-//
-// Usage: java -cp eclair-sancov.jar EclairEdgeCounter [ ...]
-//
-// Prints the total probe count to stdout.
-public class EclairEdgeCounter {
-
- static final String[] INSTRUMENTED_PREFIXES = {
- "fr/acinq/",
- "scala/",
- "scodec/",
- };
-
- static boolean shouldInstrument(String name) {
- for (String prefix : INSTRUMENTED_PREFIXES) {
- if (name.startsWith(prefix))
- return true;
- }
- return false;
- }
-
- public static void main(String[] args) throws Exception {
- if (args.length == 0) {
- System.err.println("Usage: EclairEdgeCounter [ ...]");
- System.exit(1);
- }
-
- // probeCount[0] is used instead of a plain int because the anonymous
- // ClassVisitor requires any captured variable to be effectively final.
- int[] probeCount = {0};
-
- for (String jarPath : args) {
- if (!jarPath.endsWith(".jar")) {
- continue;
- }
- try (JarFile jar = new JarFile(jarPath)) {
- Enumeration entries = jar.entries();
- while (entries.hasMoreElements()) {
- JarEntry je = entries.nextElement();
- String name = je.getName();
- if (!name.endsWith(".class") || !shouldInstrument(name)) {
- continue;
- }
- try (InputStream is = jar.getInputStream(je)) {
- byte[] bytecode = is.readAllBytes();
- ClassReader reader = new ClassReader(bytecode);
- reader.accept(
- new ClassVisitor(Opcodes.ASM9) {
- @Override
- public MethodVisitor visitMethod(
- int access, String name, String descriptor,
- String signature, String[] exceptions) {
- if ((access & Opcodes.ACC_ABSTRACT) != 0 ||
- (access & Opcodes.ACC_NATIVE) != 0) {
- return null;
- }
- ++probeCount[0]; // entry probe
- // Count body probes (conditional fall-throughs + labels).
- return new MethodVisitor(Opcodes.ASM9) {
- @Override
- public void visitJumpInsn(int opcode, Label label) {
- if (opcode != Opcodes.GOTO && opcode != Opcodes.JSR)
- ++probeCount[0];
- }
-
- @Override
- public void visitLabel(Label label) {
- ++probeCount[0];
- }
- };
- }
- },
- // SKIP_DEBUG suppresses line-number labels so only
- // branch-target and exception-handler labels appear in
- // visitLabel, matching the flags used in EclairTransformer and
- // prescan().
- ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
- }
- }
- } catch (Exception e) {
- throw new RuntimeException(
- "EclairEdgeCounter: failed to scan JAR: " + jarPath, e);
- }
- }
-
- System.out.println(probeCount[0]);
- }
-}
diff --git a/workloads/eclair/eclair-sancov/src/main/java/EclairInstrumenter.java b/workloads/eclair/eclair-sancov/src/main/java/EclairInstrumenter.java
new file mode 100644
index 00000000..84d739c0
--- /dev/null
+++ b/workloads/eclair/eclair-sancov/src/main/java/EclairInstrumenter.java
@@ -0,0 +1,276 @@
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.InsnList;
+import org.objectweb.asm.tree.JumpInsnNode;
+import org.objectweb.asm.tree.LabelNode;
+import org.objectweb.asm.tree.LdcInsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+// Build-time coverage instrumentation for Eclair.
+//
+// Rewrites the JARs in a directory in place, inserting EclairSanCov.edge(id)
+// probes into every non-abstract, non-native method of the instrumented
+// packages, and prints the probe count (TARGET_MAP_SIZE) to stdout. Doing this
+// at build time keeps ASM out of the fuzzed JVM: a load-time agent re-ran the
+// transform on every execution for classes first loaded after the snapshot.
+//
+// Each method gets a probe at entry, one after each conditional jump (the
+// fall-through arc) and one at each label (block entry, covering taken arcs).
+// IDs are sequential over classes sorted by name, then bytecode order, so they
+// are stable across builds of the same JARs.
+//
+// Usage: java -cp eclair-sancov.jar EclairInstrumenter
+public class EclairInstrumenter {
+
+ static final String[] INSTRUMENTED_PREFIXES = {
+ "fr/acinq/", // Eclair, bitcoin-lib, bitcoin-kmp, secp256k1-kmp
+ "scala/", // collections, Option, pattern matching runtime
+ "scodec/", // binary codec for BOLT wire message serialization
+ };
+
+ // Superclass of every class in the JARs, so NonLoadingClassWriter can
+ // compute frames without loading Eclair's classes.
+ static final Map superclassMap = new HashMap<>();
+
+ public static void main(String[] args) throws IOException {
+ if (args.length != 1) {
+ System.err.println("Usage: EclairInstrumenter ");
+ System.exit(1);
+ }
+
+ List jars;
+ try (Stream files = Files.list(Path.of(args[0]))) {
+ jars = files.filter(p -> p.toString().endsWith(".jar")).sorted().toList();
+ }
+
+ // Sorted by class name so IDs don't depend on JAR order.
+ TreeMap classes = new TreeMap<>();
+ Set jarsToRewrite = new HashSet<>();
+ for (Path jar : jars) {
+ if (readJar(jar, classes)) {
+ jarsToRewrite.add(jar);
+ }
+ }
+
+ int[] nextId = {0};
+ Map instrumented = new HashMap<>();
+ for (Map.Entry e : classes.entrySet()) {
+ instrumented.put(e.getKey(), instrument(e.getValue(), nextId));
+ }
+ if (nextId[0] > EclairSanCov.SCRATCH_MAP_SIZE) {
+ throw new IllegalStateException("eclair-sancov: " + nextId[0] +
+ " probes exceed SCRATCH_MAP_SIZE");
+ }
+
+ for (Path jar : jarsToRewrite) {
+ rewriteJar(jar, instrumented);
+ }
+
+ System.err.printf("eclair-sancov: %d probes in %d classes across %d JARs%n",
+ nextId[0], classes.size(), jarsToRewrite.size());
+ System.out.println(nextId[0]);
+ }
+
+ static boolean shouldInstrument(String className) {
+ for (String prefix : INSTRUMENTED_PREFIXES) {
+ if (className.startsWith(prefix))
+ return true;
+ }
+ return false;
+ }
+
+ // Returns the internal class name for a .class entry, or null otherwise.
+ static String classNameOf(String entryName) {
+ return entryName.endsWith(".class")
+ ? entryName.substring(0, entryName.length() - 6)
+ : null;
+ }
+
+ // Records every class's superclass and collects instrumented classes into
+ // `classes`. Returns whether the JAR holds any instrumented class.
+ static boolean readJar(Path jar, Map classes)
+ throws IOException {
+ boolean hasInstrumented = false;
+ boolean signed = false;
+ try (ZipFile zip = new ZipFile(jar.toFile())) {
+ for (ZipEntry entry : Collections.list(zip.entries())) {
+ String name = entry.getName();
+ signed |= name.startsWith("META-INF/") && name.endsWith(".SF");
+ String className = classNameOf(name);
+ if (className == null) {
+ continue;
+ }
+ byte[] bytecode = zip.getInputStream(entry).readAllBytes();
+ String superName = new ClassReader(bytecode).getSuperName();
+ if (superName != null) {
+ superclassMap.put(className, superName);
+ }
+ if (!shouldInstrument(className)) {
+ continue;
+ }
+ // Two copies would share probe IDs, so refuse rather than guess.
+ if (classes.put(className, bytecode) != null) {
+ throw new IllegalStateException(
+ "eclair-sancov: duplicate class " + className + " in " + jar);
+ }
+ hasInstrumented = true;
+ }
+ }
+ if (hasInstrumented && signed) {
+ throw new IllegalStateException(
+ "eclair-sancov: " + jar + " is signed; instrumenting it would fail " +
+ "signature verification");
+ }
+ return hasInstrumented;
+ }
+
+ static byte[] instrument(byte[] bytecode, int[] nextId) {
+ ClassReader reader = new ClassReader(bytecode);
+ // Debug info stays in the output: libraries read class files back from the
+ // JAR for reflection, and Eclair's json4s-based bitcoind RPC fails without
+ // it. Probe positions come from a pass without it, so line-number labels
+ // don't add probes.
+ ClassNode withoutDebug = new ClassNode();
+ reader.accept(withoutDebug, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ ClassNode full = new ClassNode();
+ reader.accept(full, ClassReader.SKIP_FRAMES);
+
+ for (int i = 0; i < full.methods.size(); i++) {
+ MethodNode method = full.methods.get(i);
+ if ((method.access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) == 0) {
+ insertProbes(method, blockStarts(withoutDebug.methods.get(i)), nextId);
+ }
+ }
+
+ // Probes change the stack depth where they are inserted, so frames are
+ // recomputed.
+ ClassWriter writer = new NonLoadingClassWriter(ClassWriter.COMPUTE_FRAMES);
+ full.accept(writer);
+ return writer.toByteArray();
+ }
+
+ // Indices of the instructions a label sits in front of. Without debug info,
+ // every label is a branch target, exception handler or try range bound.
+ static BitSet blockStarts(MethodNode method) {
+ BitSet starts = new BitSet();
+ int index = 0;
+ for (AbstractInsnNode node : method.instructions) {
+ if (node instanceof LabelNode) {
+ starts.set(index);
+ } else if (node.getOpcode() >= 0) {
+ index++;
+ }
+ }
+ return starts;
+ }
+
+ // Inserts edge() probes at method entry, after each conditional jump (the
+ // fall-through arc) and after each block-start label (covering taken arcs),
+ // taking IDs from the shared counter in bytecode order.
+ static void insertProbes(MethodNode method, BitSet blockStarts,
+ int[] nextId) {
+ InsnList insns = method.instructions;
+ AbstractInsnNode[] original = insns.toArray();
+ insns.insert(probe(nextId));
+ int index = 0;
+ for (AbstractInsnNode node : original) {
+ if (node instanceof LabelNode && blockStarts.get(index)) {
+ insns.insert(node, probe(nextId));
+ } else if (node instanceof JumpInsnNode &&
+ node.getOpcode() != Opcodes.GOTO &&
+ node.getOpcode() != Opcodes.JSR) {
+ insns.insert(node, probe(nextId));
+ }
+ if (node.getOpcode() >= 0) {
+ index++;
+ }
+ }
+ }
+
+ // ldc id; invokestatic EclairSanCov.edge(I)V
+ static InsnList probe(int[] nextId) {
+ InsnList probe = new InsnList();
+ probe.add(new LdcInsnNode(nextId[0]++));
+ probe.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "EclairSanCov", "edge",
+ "(I)V", false));
+ return probe;
+ }
+
+ // Writes the JAR again with its instrumented classes replaced, keeping
+ // every other entry and the entry order.
+ static void rewriteJar(Path jar, Map instrumented)
+ throws IOException {
+ Path tmp = jar.resolveSibling(jar.getFileName() + ".tmp");
+ try (ZipFile in = new ZipFile(jar.toFile());
+ ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(tmp))) {
+ for (ZipEntry entry : Collections.list(in.entries())) {
+ String name = entry.getName();
+ out.putNextEntry(new ZipEntry(name));
+ String className = classNameOf(name);
+ byte[] bytecode = className == null ? null : instrumented.get(className);
+ if (bytecode != null) {
+ out.write(bytecode);
+ } else {
+ in.getInputStream(entry).transferTo(out);
+ }
+ out.closeEntry();
+ }
+ }
+ Files.move(tmp, jar, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ // Returns the superclass of an internal class name, falling back to the
+ // JDK for classes outside the JARs.
+ static String superOf(String type) {
+ String s = superclassMap.get(type);
+ if (s != null)
+ return s;
+ try {
+ Class> c = Class.forName(type.replace('/', '.'), false, null);
+ Class> p = c.getSuperclass();
+ return p != null ? p.getName().replace('.', '/') : null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ // ClassWriter that resolves type hierarchies via superclassMap instead of
+ // loading the classes, which aren't on this tool's classpath.
+ static class NonLoadingClassWriter extends ClassWriter {
+
+ NonLoadingClassWriter(int flags) {
+ super(flags);
+ }
+
+ @Override
+ protected String getCommonSuperClass(String type1, String type2) {
+ Set ancestors = new HashSet<>();
+ for (String t = type1; t != null && ancestors.add(t); t = superOf(t))
+ ;
+ for (String t = type2; t != null; t = superOf(t))
+ if (ancestors.contains(t))
+ return t;
+ return "java/lang/Object";
+ }
+ }
+}
diff --git a/workloads/eclair/eclair-sancov/src/main/java/EclairSanCov.java b/workloads/eclair/eclair-sancov/src/main/java/EclairSanCov.java
index 6169d224..32c35d4e 100644
--- a/workloads/eclair/eclair-sancov/src/main/java/EclairSanCov.java
+++ b/workloads/eclair/eclair-sancov/src/main/java/EclairSanCov.java
@@ -1,397 +1,58 @@
-import java.io.File;
-import java.io.InputStream;
-import java.lang.instrument.ClassFileTransformer;
-import java.lang.instrument.Instrumentation;
-import java.lang.reflect.Field; // for Unsafe.theUnsafe access
-import java.security.ProtectionDomain;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.ClassWriter;
-import org.objectweb.asm.Label;
-import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
+import java.lang.reflect.Field;
import sun.misc.Unsafe;
-// Java agent that provides AFL coverage feedback for Eclair.
-//
-// When loaded via -javaagent:eclair-sancov.jar, this agent maps the AFL shared
-// memory region and instruments classes to record basic-block-level coverage.
-// Each instrumented method receives:
-// - One probe at method entry
-// - One probe after each conditional jump (fall-through arc)
-// - One probe at each label / basic-block entry (taken arc)
-//
-// Instrumented packages:
-// - fr/acinq/ (Eclair, bitcoin-lib, secp256k1)
-// - scala/ (collections, Option, pattern matching runtime)
-// - scodec/ (binary codec used for BOLT wire message serialization)
-//
-// Edge IDs are pre-assigned by scanning all JARs on the classpath before any
-// class is loaded, sorted by class name then declaration order within each
-// class. This gives stable IDs across restarts, which is required for afl-cmin
-// and cross-session coverage comparisons to work correctly.
-public class EclairSanCov {
+// Runtime for the coverage probes EclairInstrumenter compiles into Eclair's
+// JARs. Each probe calls edge(id), which bumps byte `id` of the AFL coverage
+// map. The map is attached on the first probe, during Eclair's startup and so
+// before the Nyx snapshot.
+public final class EclairSanCov {
- // Map from "InternalClassName#methodName#descriptor" to pre-assigned edge ID.
- // Populated by prescan() before any class transformation occurs.
- static Map edgeIds = null;
+ // Size of the scratch map probes write to when no AFL map is shared (e.g.
+ // smitebot reproduce). EclairInstrumenter fails the build if IDs exceed it.
+ static final int SCRATCH_MAP_SIZE = 1 << 24;
- // Map from class name to superclass name, built from all classes on the
- // classpath during prescan. Used by NonLoadingClassWriter to resolve type
- // hierarchies without Class.forName().
- static Map superclassMap = null;
+ // Unsafe.getByte/putByte avoid the bounds check and address field load that
+ // DirectByteBuffer.get/put perform on every call.
+ private static final Unsafe UNSAFE = getUnsafe();
- // Unsafe instance for direct memory access to the AFL shared memory region.
- // Using Unsafe.getByte/putByte avoids the bounds check and address field load
- // that DirectByteBuffer.get/put perform on every call.
- private static final Unsafe UNSAFE;
- static {
- try {
- Field f = Unsafe.class.getDeclaredField("theUnsafe");
- f.setAccessible(true);
- UNSAFE = (Unsafe)f.get(null);
- } catch (Exception e) {
- throw new RuntimeException("eclair-sancov: failed to get Unsafe", e);
- }
- }
+ // Base address of the coverage map, final so the JIT can fold it into each
+ // probe.
+ private static final long MAP_ADDR = mapCoverage();
- // Base address of the AFL shared memory region, obtained via JNI shmat().
- static long shmAddr;
+ private EclairSanCov() {}
- // Java agent entry point, called by the JVM before main().
- public static void premain(String args, Instrumentation inst) {
- String shmIdStr = System.getenv("__AFL_SHM_ID");
- if (shmIdStr == null) {
- throw new RuntimeException("eclair-sancov: __AFL_SHM_ID not set");
- }
-
- edgeIds = prescan();
-
- System.loadLibrary("eclair-sancov");
- shmAddr = mapShmAddr(Integer.parseInt(shmIdStr));
- if (shmAddr == 0) {
- throw new RuntimeException("eclair-sancov: mapShmAddr returned null");
- }
-
- inst.addTransformer(new EclairTransformer());
- }
-
- // Maps the AFL shared memory segment via shmat and returns the raw pointer
- // as a long. Implemented in shmutil.c via JNI.
- private static native long mapShmAddr(int shmId);
-
- // Records coverage for an instrumented probe. Called from every instrumented
- // coverage point. edgeId is a pre-assigned sequential integer always in
- // [0, AFL_MAP_SIZE).
+ // Records a hit on probe `edgeId`, which is always in [0, probe count).
public static void edge(int edgeId) {
- long addr = shmAddr + edgeId;
+ long addr = MAP_ADDR + edgeId;
UNSAFE.putByte(addr, (byte)(UNSAFE.getByte(addr) + 1));
}
- // Package prefixes to instrument. A class is instrumented if its internal
- // name starts with any of these prefixes.
- static final String[] INSTRUMENTED_PREFIXES = {
- "fr/acinq/", // Eclair, bitcoin-lib, secp256k1-kmp
- "scala/", // collections, Option, pattern matching runtime
- "scodec/", // binary codec for BOLT wire message serialization
- };
-
- static boolean shouldInstrument(String className) {
- for (String prefix : INSTRUMENTED_PREFIXES) {
- if (className.startsWith(prefix))
- return true;
+ private static long mapCoverage() {
+ String shmId = System.getenv("__AFL_SHM_ID");
+ if (shmId == null) {
+ // The JARs are always instrumented, so probes need memory to write to
+ // even when nothing reads it.
+ return UNSAFE.allocateMemory(SCRATCH_MAP_SIZE);
}
- return false;
- }
-
- // Scans all JARs on the classpath and assigns sequential probe IDs to every
- // non-abstract, non-native method in instrumented classes. Each method
- // receives one ID for its entry probe plus one ID per body probe (conditional
- // fall-throughs and label/basic-block entries). Classes are processed in
- // sorted order by internal name; methods and their instructions are processed
- // in declaration/bytecode order (as visited by ASM). This gives deterministic
- // IDs independent of class loading order at runtime.
- static Map prescan() {
- // Collect the set of JAR files to scan. java.class.path contains the
- // explicit -cp arguments, which may include JAR files and directories.
- List jarPaths = new ArrayList<>();
- String classpath = System.getProperty("java.class.path", "");
- for (String entry : classpath.split(":")) {
- if (entry.endsWith(".jar")) {
- jarPaths.add(entry);
- } else {
- File dir = new File(entry);
- if (dir.isDirectory()) {
- File[] jars = dir.listFiles(f -> f.getName().endsWith(".jar"));
- if (jars != null) {
- for (File jar : jars) {
- jarPaths.add(jar.getAbsolutePath());
- }
- }
- }
- }
- }
-
- // Scan all classes in all JARs. Instrumented classes go into sortedClasses
- // for probe ID assignment; all classes contribute to superclassMap for
- // type hierarchy resolution.
- TreeMap sortedClasses = new TreeMap<>();
- superclassMap = new HashMap<>();
-
- for (String entry : jarPaths) {
- try (JarFile jar = new JarFile(entry)) {
- Enumeration entries = jar.entries();
- while (entries.hasMoreElements()) {
- JarEntry je = entries.nextElement();
- String name = je.getName();
- if (!name.endsWith(".class")) {
- continue;
- }
- String className =
- name.substring(0, name.length() - 6); // strip .class
- try (InputStream is = jar.getInputStream(je)) {
- byte[] bytecode = is.readAllBytes();
- // Record superclass for every class on the classpath.
- ClassReader cr = new ClassReader(bytecode);
- String superName = cr.getSuperName();
- if (superName != null) {
- superclassMap.put(className, superName);
- }
- if (shouldInstrument(name)) {
- sortedClasses.put(className, bytecode);
- }
- }
- }
- } catch (Exception e) {
- throw new RuntimeException(
- "eclair-sancov: failed to scan JAR: " + entry, e);
- }
- }
-
- // Assign sequential IDs by visiting each class in sorted order. counter[0]
- // is used instead of a plain int because the anonymous ClassVisitor
- // requires any captured variable to be effectively final.
- Map ids = new HashMap<>();
- int[] counter = {0};
-
- for (Map.Entry entry : sortedClasses.entrySet()) {
- String className = entry.getKey();
- byte[] bytecode = entry.getValue();
-
- ClassReader reader = new ClassReader(bytecode);
- reader.accept(
- new ClassVisitor(Opcodes.ASM9) {
- @Override
- public MethodVisitor visitMethod(
- int access, String name, String descriptor, String signature,
- String[] exceptions) {
- if ((access & Opcodes.ACC_ABSTRACT) != 0 ||
- (access & Opcodes.ACC_NATIVE) != 0) {
- return null;
- }
- // We include the descriptor in the key because overloaded methods
- // share the same name. The descriptor encodes the parameter and
- // return types (e.g. "(I)V" = takes int, returns void), making it
- // unique per overload.
- String key = className + "#" + name + "#" + descriptor;
- ids.put(key, counter[0]++); // entry probe ID
- // Count body probes to reserve their IDs after the entry probe:
- // one per conditional fall-through, one per label/basic-block
- // entry.
- return new MethodVisitor(Opcodes.ASM9) {
- @Override
- public void visitJumpInsn(int opcode, Label label) {
- if (opcode != Opcodes.GOTO && opcode != Opcodes.JSR)
- ++counter[0]; // fall-through arc probe
- }
-
- @Override
- public void visitLabel(Label label) {
- ++counter[0]; // basic block entry probe
- }
- };
- }
- },
- // SKIP_DEBUG suppresses line-number labels so only branch-target and
- // exception-handler labels appear in visitLabel, matching the flags
- // used in EclairTransformer.
- ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ System.loadLibrary("eclair-sancov");
+ long addr = mapShmAddr(Integer.parseInt(shmId));
+ if (addr == 0) {
+ throw new IllegalStateException("eclair-sancov: mapShmAddr failed");
}
-
- return ids;
+ return addr;
}
- // ASM ClassFileTransformer that instruments every non-abstract, non-native
- // method in instrumented classes with edge() probes at the method entry, each
- // conditional branch fall-through, and each basic-block entry label.
- static class EclairTransformer implements ClassFileTransformer {
-
- @Override
- public byte[] transform(ClassLoader loader, String className,
- Class> classBeingRedefined,
- ProtectionDomain protectionDomain,
- byte[] classfileBuffer) {
-
- if (className == null || !shouldInstrument(className)) {
- return null; // null = no transformation
- }
-
- try {
- ClassReader reader = new ClassReader(classfileBuffer);
- // COMPUTE_FRAMES tells ASM to recompute stack map frames from scratch
- // after transformation (inserting INVOKESTATIC probes changes stack
- // depth). SKIP_DEBUG | SKIP_FRAMES must match the prescan() flags so
- // both passes see the same label set and probe counts agree.
- ClassWriter writer =
- new NonLoadingClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
- reader.accept(new EclairClassVisitor(writer, className),
- ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
- return writer.toByteArray();
- } catch (Throwable e) {
- System.err.println("eclair-sancov: transform failed for " + className +
- ": " + e);
- return null;
- }
- }
- }
+ // Attaches the AFL shared memory segment via shmat and returns its address.
+ // Implemented in shmutil.c via JNI.
+ private static native long mapShmAddr(int shmId);
- // Returns the superclass of an internal class name. Checks superclassMap
- // (classpath JARs) first, then falls back to Class.forName() for JDK classes
- // not present in those JARs. The fallback is safe because JDK classes don't
- // match INSTRUMENTED_PREFIXES, so the transformer returns null immediately
- // for them -- no recursive transformation, no ClassCircularityError.
- static String superOf(String type) {
- String s = superclassMap.get(type);
- if (s != null)
- return s;
+ private static Unsafe getUnsafe() {
try {
- Class> c = Class.forName(type.replace('/', '.'), false, null);
- Class> p = c.getSuperclass();
- return p != null ? p.getName().replace('.', '/') : null;
- } catch (Exception e) {
- return null;
- }
- }
-
- // ClassWriter that resolves type hierarchies via superclassMap instead of
- // the default Class.forName(). The default getCommonSuperClass() triggers
- // class loading, which causes ClassCircularityError in deep hierarchies like
- // scala/collection/ where transforming class A requires loading class B and
- // vice versa.
- static class NonLoadingClassWriter extends ClassWriter {
-
- NonLoadingClassWriter(ClassReader reader, int flags) {
- super(reader, flags);
- }
-
- @Override
- protected String getCommonSuperClass(String type1, String type2) {
- java.util.Set ancestors = new java.util.HashSet<>();
- for (String t = type1; t != null && ancestors.add(t); t = superOf(t))
- ;
- for (String t = type2; t != null; t = superOf(t))
- if (ancestors.contains(t))
- return t;
- return "java/lang/Object";
- }
- }
-
- static class EclairClassVisitor extends ClassVisitor {
-
- private final String className;
-
- EclairClassVisitor(ClassVisitor cv, String className) {
- super(Opcodes.ASM9, cv);
- this.className = className;
- }
-
- @Override
- public MethodVisitor visitMethod(int access, String name, String descriptor,
- String signature, String[] exceptions) {
-
- MethodVisitor mv =
- super.visitMethod(access, name, descriptor, signature, exceptions);
-
- if ((access & Opcodes.ACC_ABSTRACT) != 0 ||
- (access & Opcodes.ACC_NATIVE) != 0) {
- return mv;
- }
-
- String key = className + "#" + name + "#" + descriptor;
- Integer edgeId = EclairSanCov.edgeIds.get(key);
- if (edgeId == null) {
- // Class was not found in the prescan (e.g. dynamically generated at
- // runtime). Scala closures compiled into JARs are picked up by the
- // prescan; true runtime-generated classes (e.g. reflection proxies)
- // typically have non-Eclair names and are excluded by the prefix filter
- // before reaching here.
- return mv;
- }
-
- return new EclairMethodVisitor(mv, edgeId);
- }
- }
-
- // Instruments a single method with edge() probes at:
- // - Method entry
- // - After each conditional jump (fall-through arc)
- // - After each label (basic-block entry: covers all taken arcs, loop
- // headers, switch cases, exception handlers, and merge points)
- //
- // Probe IDs are allocated starting at edgeId (entry) then edgeId+1,
- // edgeId+2, ... for body probes in instruction-visit order. prescan() uses
- // the same visit order and flags to reserve the correct number of IDs per
- // method, guaranteeing stable, non-overlapping IDs across restarts.
- static class EclairMethodVisitor extends MethodVisitor {
-
- private int currentProbeId;
-
- EclairMethodVisitor(MethodVisitor mv, int edgeId) {
- super(Opcodes.ASM9, mv);
- currentProbeId = edgeId;
- }
-
- // Emits: ldc probeId; invokestatic EclairSanCov.edge(I)V
- private void emitProbe() {
- super.visitLdcInsn(currentProbeId++);
- super.visitMethodInsn(Opcodes.INVOKESTATIC, "EclairSanCov", "edge",
- "(I)V", false);
- }
-
- @Override
- public void visitCode() {
- // visitCode() opens the Code attribute and must be called first. The
- // probe inserted after it fires at method entry.
- super.visitCode();
- emitProbe();
- }
-
- // Probe inserted AFTER the jump so it fires only on the fall-through arc.
- // The taken arc is covered by the visitLabel probe at the jump's target.
- // Unconditional GOTO and JSR do not produce fall-through probes.
- @Override
- public void visitJumpInsn(int opcode, Label label) {
- super.visitJumpInsn(opcode, label);
- if (opcode != Opcodes.GOTO && opcode != Opcodes.JSR)
- emitProbe();
- }
-
- // Probe inserted AFTER the label so it fires whenever any jump reaches
- // this basic-block entry. With SKIP_DEBUG, only branch-target and
- // exception-handler labels appear here (not line-number markers), so
- // every probe corresponds to a real control-flow edge.
- @Override
- public void visitLabel(Label label) {
- super.visitLabel(label);
- emitProbe();
+ Field f = Unsafe.class.getDeclaredField("theUnsafe");
+ f.setAccessible(true);
+ return (Unsafe)f.get(null);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("eclair-sancov: failed to get Unsafe", e);
}
}
}
diff --git a/workloads/eclair/init.sh b/workloads/eclair/init.sh
index 71d0caa2..f0d3da7e 100644
--- a/workloads/eclair/init.sh
+++ b/workloads/eclair/init.sh
@@ -20,8 +20,8 @@ export SMITE_CRASH_HANDLER=/nyx-jvm-crash-handler.so
# entirely. C2 runs expensive optimizations in background threads which get
# repeated every time we restore the VM snapshot, reducing fuzzing speed.
#
-# -javaagent: Coverage agent that instruments bytecode and writes edge counters
-# to AFL shared memory via JNI.
-export JAVA_OPTS="-XX:TieredStopAtLevel=1 -javaagent:/eclair-sancov.jar -Djava.library.path=/usr/local/lib"
+# -Djava.library.path: Where EclairSanCov loads the JNI library that maps the
+# AFL shared memory its probes write to.
+export JAVA_OPTS="-XX:TieredStopAtLevel=1 -Djava.library.path=/usr/local/lib"
/eclair-scenario > /init.log 2>&1