From bac1c2a2b3eb9998703c7166d6b625817e19a0bf Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Thu, 1 Oct 2026 16:24:10 -0300 Subject: [PATCH] workloads/eclair: instrument JARs at build time instead of with an agent The agent re-ran the ASM transform on every execution for classes first loaded after the snapshot. Rewriting the JARs at image build gives the same probes and IDs; bench-exec funding flow goes from 2.53 to 3.12 execs/s. --- workloads/eclair/Dockerfile | 17 +- workloads/eclair/eclair-sancov/pom.xml | 47 +- .../eclair/eclair-sancov/src/main/c/shmutil.c | 2 +- .../src/main/java/EclairEdgeCounter.java | 105 ----- .../src/main/java/EclairInstrumenter.java | 276 ++++++++++++ .../src/main/java/EclairSanCov.java | 415 ++---------------- workloads/eclair/init.sh | 6 +- 7 files changed, 333 insertions(+), 535 deletions(-) delete mode 100644 workloads/eclair/eclair-sancov/src/main/java/EclairEdgeCounter.java create mode 100644 workloads/eclair/eclair-sancov/src/main/java/EclairInstrumenter.java 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