1 // Copyright 2021 Code Intelligence GmbH 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.code_intelligence.jazzer.driver; 16 17 import com.code_intelligence.jazzer.agent.AgentInstaller; 18 import com.code_intelligence.jazzer.utils.Log; 19 import com.code_intelligence.jazzer.utils.ZipUtils; 20 import java.io.File; 21 import java.io.FileNotFoundException; 22 import java.io.FileOutputStream; 23 import java.io.IOException; 24 import java.lang.UnsupportedClassVersionError; 25 import java.net.MalformedURLException; 26 import java.net.URL; 27 import java.net.URLClassLoader; 28 import java.nio.file.Files; 29 import java.nio.file.Paths; 30 import java.util.ArrayList; 31 import java.util.Enumeration; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Set; 35 import java.util.jar.JarEntry; 36 import java.util.jar.JarFile; 37 import java.util.zip.ZipOutputStream; 38 39 public class OfflineInstrumentor { 40 /** 41 * Create a new jar file at <jazzer_path>/<jarBaseName>.instrumented.jar 42 * for each jar in passed in, with classes that have Jazzer instrumentation. 43 * 44 * @param jarLists list of jars to instrument 45 * @return a boolean representing the success status 46 */ instrumentJars(List<String> jarLists)47 public static boolean instrumentJars(List<String> jarLists) { 48 AgentInstaller.install(Opt.hooks); 49 50 // Clear Opt.dumpClassesDir before adding new instrumented classes 51 File dumpClassesDir = new File(Opt.dumpClassesDir); 52 if (dumpClassesDir.exists()) { 53 for (String fn : dumpClassesDir.list()) { 54 new File(Opt.dumpClassesDir, fn).delete(); 55 } 56 } 57 58 List<String> errorMessages = new ArrayList<>(); 59 for (String jarPath : jarLists) { 60 String outputBaseName = jarPath; 61 if (outputBaseName.contains(File.separator)) { 62 outputBaseName = outputBaseName.substring( 63 outputBaseName.lastIndexOf(File.separator) + 1, outputBaseName.length()); 64 } 65 66 if (outputBaseName.contains(".jar")) { 67 outputBaseName = outputBaseName.substring(0, outputBaseName.lastIndexOf(".jar")); 68 } 69 70 Log.info("Instrumenting jar file: " + jarPath); 71 72 try { 73 errorMessages = createInstrumentedClasses(jarPath); 74 } catch (IOException e) { 75 errorMessages.add("Failed to instrument jar: " + jarPath 76 + ". Please ensure the file at this location is a jar file. Error Message: " + e); 77 continue; 78 } 79 80 try { 81 createInstrumentedJar(jarPath, Opt.dumpClassesDir + File.separator + outputBaseName, 82 outputBaseName + ".instrumented.jar"); 83 } catch (Exception e) { 84 errorMessages.add("Failed to instrument jar: " + jarPath + ". Error: " + e); 85 } 86 } 87 88 // Log all errors at the end 89 for (String error : errorMessages) { 90 Log.error(error); 91 } 92 93 return errorMessages.isEmpty(); 94 } 95 96 /** 97 * Loops over all classes in jar file and adds instrumentation. The output 98 * of the instrumented classes will be at --dump-classes-dir 99 * 100 * @param jarPath a path to a jar file to instrument. 101 * @return a list of errors that were hit when trying to instrument all classes in jar 102 */ createInstrumentedClasses(String jarPath)103 private static List<String> createInstrumentedClasses(String jarPath) throws IOException { 104 List<String> errorMessages = new ArrayList<>(); 105 List<String> allClasses = new ArrayList<>(); 106 107 // Collect all classes for jar file 108 try (JarFile jarFile = new JarFile(jarPath)) { 109 Enumeration<JarEntry> allEntries = jarFile.entries(); 110 while (allEntries.hasMoreElements()) { 111 JarEntry entry = allEntries.nextElement(); 112 if (entry.isDirectory()) { 113 continue; 114 } 115 116 String name = entry.getName(); 117 if (!name.endsWith(".class")) { 118 Log.info("Skipping instrumenting file: " + name); 119 continue; 120 } 121 122 String className = name.substring(0, name.lastIndexOf(".class")); 123 className = className.replace('/', '.'); 124 allClasses.add(className); 125 Log.info("Found class: " + className); 126 } 127 } 128 129 // No classes found, so none to load. Return errors 130 if (allClasses.size() == 0) { 131 errorMessages.add("Classes is empty for jar: " + jarPath); 132 return errorMessages; 133 } 134 135 // Create class loader to load in all classes 136 File file = new File(jarPath); 137 URL url = file.toURI().toURL(); 138 URL[] urls = new URL[] {url}; 139 ClassLoader cl = new URLClassLoader(urls); 140 141 // Loop through all files and load in all classes, agent will instrument them as they load 142 for (String className : allClasses) { 143 try { 144 cl.loadClass(className); 145 } catch (UnsupportedClassVersionError ucve) { 146 // The classes will still get instrumented here, but warn so the user knows something 147 // happened 148 Log.warn(ucve.toString()); 149 } catch (Throwable e) { 150 // Catch all exceptions/errors and keep instrumenting to give user the option to manually 151 // fix one offs if possible 152 errorMessages.add("Failed to instrument class: " + className + ". Error: " + e); 153 } 154 } 155 156 return errorMessages; 157 } 158 159 /** 160 * This will create a new jar out of specified original jar and the merge in the instrumented 161 * classes from the specified instrumented classes dir 162 * 163 * @param originalJarPath a path to the original jar. 164 * @param instrumentedClassesDir a path to the instrumented classes dir. 165 * @param outputZip output file. 166 */ createInstrumentedJar( String originalJarPath, String instrumentedClassesDir, String outputZip)167 private static void createInstrumentedJar( 168 String originalJarPath, String instrumentedClassesDir, String outputZip) throws IOException { 169 try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outputZip))) { 170 Set<String> dirFilesToSkip = new HashSet<>(); 171 dirFilesToSkip.add(".original.class"); 172 dirFilesToSkip.add(".failed.class"); 173 Set<String> filesMerged = 174 ZipUtils.mergeDirectoryToZip(instrumentedClassesDir, zos, dirFilesToSkip); 175 176 ZipUtils.mergeZipToZip(originalJarPath, zos, filesMerged); 177 } 178 } 179 } 180