• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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