1 /*
  2  * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package jdk.internal.misc;
 27 
 28 import java.io.BufferedReader;
 29 import java.io.File;
 30 import java.io.InputStreamReader;
 31 import java.io.InputStream;
 32 import java.io.IOException;
 33 import java.io.PrintStream;
 34 import java.nio.file.Files;
 35 import java.nio.file.Path;
 36 import java.util.Arrays;
 37 import java.util.ArrayList;
 38 import java.util.HashMap;
 39 import java.util.List;
 40 import java.util.Map;
 41 import java.util.Objects;
 42 import java.util.jar.JarFile;
 43 import java.util.stream.Stream;
 44 
 45 import jdk.internal.access.SharedSecrets;
 46 import jdk.internal.util.StaticProperty;
 47 
 48 public class CDS {
 49     // Must be in sync with cdsConfig.hpp
 50     private static final int IS_DUMPING_AOT_LINKED_CLASSES   = 1 << 0;
 51     private static final int IS_DUMPING_ARCHIVE              = 1 << 1;
 52     private static final int IS_DUMPING_METHOD_HANDLES       = 1 << 2;
 53     private static final int IS_DUMPING_STATIC_ARCHIVE       = 1 << 3;
 54     private static final int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 4;
 55     private static final int IS_USING_ARCHIVE                = 1 << 5;
 56     private static final int IS_DUMPING_HEAP                 = 1 << 6;
 57     private static final int IS_LOGGING_DYNAMIC_PROXIES      = 1 << 7;
 58 
 59     private static final int configStatus = getCDSConfigStatus();
 60 
 61     /**
 62      * Should we log the use of lambda form invokers?
 63      */
 64     public static boolean isLoggingLambdaFormInvokers() {
 65         return (configStatus & IS_LOGGING_LAMBDA_FORM_INVOKERS) != 0;
 66     }
 67 
 68     /**
 69       * Is the VM writing to a (static or dynamic) CDS archive.
 70       */
 71     public static boolean isDumpingArchive() {
 72         return (configStatus & IS_DUMPING_ARCHIVE) != 0;
 73     }
 74 
 75     /**
 76       * Is the VM using at least one CDS archive?
 77       */
 78     public static boolean isUsingArchive() {
 79         return (configStatus & IS_USING_ARCHIVE) != 0;
 80     }
 81 
 82     /**
 83       * Is dumping static archive.
 84       */
 85     public static boolean isDumpingStaticArchive() {
 86         return (configStatus & IS_DUMPING_STATIC_ARCHIVE) != 0;
 87     }
 88 
 89     public static boolean isDumpingHeap() {
 90         return (configStatus & IS_DUMPING_HEAP) != 0;
 91     }
 92 
 93     public static boolean isLoggingDynamicProxies() {
 94         return (configStatus & IS_LOGGING_DYNAMIC_PROXIES) != 0;
 95     }
 96 
 97     public static boolean isDumpingAOTLinkedClasses() {
 98         return (configStatus & IS_DUMPING_AOT_LINKED_CLASSES) != 0;
 99     }
100 
101     public static boolean isSingleThreadVM() {
102         return isDumpingStaticArchive();
103     }
104 
105     private static native int getCDSConfigStatus();
106     private static native void logLambdaFormInvoker(String line);
107 
108 
109     // Used only when dumping static archive to keep weak references alive to
110     // ensure that Soft/Weak Reference objects can be reliably archived.
111     private static ArrayList<Object> keepAliveList;
112 
113     public static void keepAlive(Object s) {
114         assert isSingleThreadVM(); // no need for synchronization
115         assert isDumpingStaticArchive();
116         if (keepAliveList == null) {
117             keepAliveList = new ArrayList<>();
118         }
119         keepAliveList.add(s);
120     }
121 
122     // This is called by native JVM code at the very end of Java execution before
123     // dumping the static archive.
124     // It collects the objects from keepAliveList so that they can be easily processed
125     // by the native JVM code to check that any Reference objects that need special
126     // clean up must have been registed with keepAlive()
127     private static Object[] getKeepAliveObjects() {
128         return keepAliveList.toArray();
129     }
130 
131     /**
132      * Initialize archived static fields in the given Class using archived
133      * values from CDS dump time. Also initialize the classes of objects in
134      * the archived graph referenced by those fields.
135      *
136      * Those static fields remain as uninitialized if there is no mapped CDS
137      * java heap data or there is any error during initialization of the
138      * object class in the archived graph.
139      */
140     public static native void initializeFromArchive(Class<?> c);
141 
142     /**
143      * Ensure that the native representation of all archived java.lang.Module objects
144      * are properly restored.
145      */
146     public static native void defineArchivedModules(ClassLoader platformLoader, ClassLoader systemLoader);
147 
148     /**
149      * Returns a predictable "random" seed derived from the VM's build ID and version,
150      * to be used by java.util.ImmutableCollections to ensure that archived
151      * ImmutableCollections are always sorted the same order for the same VM build.
152      */
153     public static native long getRandomSeedForDumping();
154 
155     /**
156      * log lambda form invoker holder, name and method type
157      */
158     public static void logLambdaFormInvoker(String prefix, String holder, String name, String type) {
159         if (isLoggingLambdaFormInvokers()) {
160             logLambdaFormInvoker(prefix + " " + holder + " " + name + " " + type);
161         }
162     }
163 
164     /**
165       * log species
166       */
167     public static void logSpeciesType(String prefix, String cn) {
168         if (isLoggingLambdaFormInvokers()) {
169             logLambdaFormInvoker(prefix + " " + cn);
170         }
171     }
172 
173     public static void logDynamicProxy(ClassLoader loader, String proxyName,
174                                        Class<?>[] interfaces, int accessFlags) {
175         Objects.requireNonNull(proxyName);
176         Objects.requireNonNull(interfaces);
177         logDynamicProxy0(loader, proxyName, interfaces, accessFlags);
178     }
179     private static native void logDynamicProxy0(ClassLoader loader, String proxyName,
180                                                 Class<?>[] interfaces, int accessFlags);
181 
182     static final String DIRECT_HOLDER_CLASS_NAME  = "java.lang.invoke.DirectMethodHandle$Holder";
183     static final String DELEGATING_HOLDER_CLASS_NAME = "java.lang.invoke.DelegatingMethodHandle$Holder";
184     static final String BASIC_FORMS_HOLDER_CLASS_NAME = "java.lang.invoke.LambdaForm$Holder";
185     static final String INVOKERS_HOLDER_CLASS_NAME = "java.lang.invoke.Invokers$Holder";
186 
187     private static boolean isValidHolderName(String name) {
188         return name.equals(DIRECT_HOLDER_CLASS_NAME)      ||
189                name.equals(DELEGATING_HOLDER_CLASS_NAME)  ||
190                name.equals(BASIC_FORMS_HOLDER_CLASS_NAME) ||
191                name.equals(INVOKERS_HOLDER_CLASS_NAME);
192     }
193 
194     private static boolean isBasicTypeChar(char c) {
195          return "LIJFDV".indexOf(c) >= 0;
196     }
197 
198     private static boolean isValidMethodType(String type) {
199         String[] typeParts = type.split("_");
200         // check return type (second part)
201         if (typeParts.length != 2 || typeParts[1].length() != 1
202                 || !isBasicTypeChar(typeParts[1].charAt(0))) {
203             return false;
204         }
205         // first part
206         if (!isBasicTypeChar(typeParts[0].charAt(0))) {
207             return false;
208         }
209         for (int i = 1; i < typeParts[0].length(); i++) {
210             char c = typeParts[0].charAt(i);
211             if (!isBasicTypeChar(c)) {
212                 if (!(c >= '0' && c <= '9')) {
213                     return false;
214                 }
215             }
216         }
217         return true;
218     }
219 
220     // Throw exception on invalid input
221     private static void validateInputLines(String[] lines) {
222         for (String s: lines) {
223             if (!s.startsWith("[LF_RESOLVE]") && !s.startsWith("[SPECIES_RESOLVE]")) {
224                 throw new IllegalArgumentException("Wrong prefix: " + s);
225             }
226 
227             String[] parts = s.split(" ");
228             boolean isLF = s.startsWith("[LF_RESOLVE]");
229 
230             if (isLF) {
231                 if (parts.length != 4) {
232                     throw new IllegalArgumentException("Incorrect number of items in the line: " + parts.length);
233                 }
234                 if (!isValidHolderName(parts[1])) {
235                     throw new IllegalArgumentException("Invalid holder class name: " + parts[1]);
236                 }
237                 if (!isValidMethodType(parts[3])) {
238                     throw new IllegalArgumentException("Invalid method type: " + parts[3]);
239                 }
240             } else {
241                 if (parts.length != 2) {
242                    throw new IllegalArgumentException("Incorrect number of items in the line: " + parts.length);
243                 }
244            }
245       }
246     }
247 
248     /**
249      * called from vm to generate MethodHandle holder classes
250      * @return {@code Object[]} if holder classes can be generated.
251      * @param lines in format of LF_RESOLVE or SPECIES_RESOLVE output
252      */
253     private static Object[] generateLambdaFormHolderClasses(String[] lines) {
254         Objects.requireNonNull(lines);
255         validateInputLines(lines);
256         Stream<String> lineStream = Arrays.stream(lines);
257         Map<String, byte[]> result = SharedSecrets.getJavaLangInvokeAccess().generateHolderClasses(lineStream);
258         int size = result.size();
259         Object[] retArray = new Object[size * 2];
260         int index = 0;
261         for (Map.Entry<String, byte[]> entry : result.entrySet()) {
262             retArray[index++] = entry.getKey();
263             retArray[index++] = entry.getValue();
264         };
265         return retArray;
266     }
267 
268     private static native void dumpClassList(String listFileName);
269     private static native void dumpDynamicArchive(String archiveFileName);
270 
271     private static String drainOutput(InputStream stream, long pid, String tail, List<String> cmds) {
272         String fileName  = "java_pid" + pid + "_" + tail;
273         new Thread( ()-> {
274             try (InputStreamReader isr = new InputStreamReader(stream);
275                  BufferedReader rdr = new BufferedReader(isr);
276                  PrintStream prt = new PrintStream(fileName)) {
277                 prt.println("Command:");
278                 for (String s : cmds) {
279                     prt.print(s + " ");
280                 }
281                 prt.println("");
282                 String line;
283                 while((line = rdr.readLine()) != null) {
284                     prt.println(line);
285                 }
286             } catch (IOException e) {
287                 throw new RuntimeException("IOException happens during drain stream to file " +
288                                            fileName + ": " + e.getMessage());
289             }}).start();
290         return fileName;
291     }
292 
293     private static String[] excludeFlags = {
294          "-XX:DumpLoadedClassList=",
295          "-XX:+RecordDynamicDumpInfo",
296          "-Xshare:",
297          "-XX:SharedClassListFile=",
298          "-XX:SharedArchiveFile=",
299          "-XX:ArchiveClassesAtExit="};
300     private static boolean containsExcludedFlags(String testStr) {
301        for (String e : excludeFlags) {
302            if (testStr.contains(e)) {
303                return true;
304            }
305        }
306        return false;
307     }
308 
309     /**
310     * called from jcmd VM.cds to dump static or dynamic shared archive
311     * @param isStatic true for dump static archive or false for dynnamic archive.
312     * @param fileName user input archive name, can be null.
313     * @return The archive name if successfully dumped.
314     */
315     private static String dumpSharedArchive(boolean isStatic, String fileName) throws Exception {
316         String cwd = new File("").getAbsolutePath(); // current dir used for printing message.
317         String currentPid = String.valueOf(ProcessHandle.current().pid());
318         String archiveFileName =  fileName != null ? fileName :
319             "java_pid" + currentPid + (isStatic ? "_static.jsa" : "_dynamic.jsa");
320 
321         String tempArchiveFileName = archiveFileName + ".temp";
322         File tempArchiveFile = new File(tempArchiveFileName);
323         // The operation below may cause exception if the file or its dir is protected.
324         if (!tempArchiveFile.exists()) {
325             tempArchiveFile.createNewFile();
326         }
327         tempArchiveFile.delete();
328 
329         if (isStatic) {
330             String listFileName = archiveFileName + ".classlist";
331             File listFile = new File(listFileName);
332             if (listFile.exists()) {
333                 listFile.delete();
334             }
335             dumpClassList(listFileName);
336             String jdkHome = StaticProperty.javaHome();
337             String classPath = System.getProperty("java.class.path");
338             List<String> cmds = new ArrayList<String>();
339             cmds.add(jdkHome + File.separator + "bin" + File.separator + "java"); // java
340             cmds.add("-cp");
341             cmds.add(classPath);
342             cmds.add("-Xlog:cds");
343             cmds.add("-Xshare:dump");
344             cmds.add("-XX:SharedClassListFile=" + listFileName);
345             cmds.add("-XX:SharedArchiveFile=" + tempArchiveFileName);
346 
347             // All runtime args.
348             String[] vmArgs = VM.getRuntimeArguments();
349             if (vmArgs != null) {
350                 for (String arg : vmArgs) {
351                     if (arg != null && !containsExcludedFlags(arg)) {
352                         cmds.add(arg);
353                     }
354                 }
355             }
356 
357             Process proc = Runtime.getRuntime().exec(cmds.toArray(new String[0]));
358 
359             // Drain stdout/stderr to files in new threads.
360             String stdOutFileName = drainOutput(proc.getInputStream(), proc.pid(), "stdout", cmds);
361             String stdErrFileName = drainOutput(proc.getErrorStream(), proc.pid(), "stderr", cmds);
362 
363             proc.waitFor();
364             // done, delete classlist file.
365             listFile.delete();
366 
367             // Check if archive has been successfully dumped. We won't reach here if exception happens.
368             // Throw exception if file is not created.
369             if (!tempArchiveFile.exists()) {
370                 throw new RuntimeException("Archive file " + tempArchiveFileName +
371                                            " is not created, please check stdout file " +
372                                             cwd + File.separator + stdOutFileName + " or stderr file " +
373                                             cwd + File.separator + stdErrFileName + " for more detail");
374             }
375         } else {
376             dumpDynamicArchive(tempArchiveFileName);
377             if (!tempArchiveFile.exists()) {
378                 throw new RuntimeException("Archive file " + tempArchiveFileName +
379                                            " is not created, please check current working directory " +
380                                            cwd  + " for process " +
381                                            currentPid + " output for more detail");
382             }
383         }
384         // Override the existing archive file
385         File archiveFile = new File(archiveFileName);
386         if (archiveFile.exists()) {
387             archiveFile.delete();
388         }
389         if (!tempArchiveFile.renameTo(archiveFile)) {
390             throw new RuntimeException("Cannot rename temp file " + tempArchiveFileName + " to archive file" + archiveFileName);
391         }
392         // Everything goes well, print out the file name.
393         String archiveFilePath = new File(archiveFileName).getAbsolutePath();
394         System.out.println("The process was attached by jcmd and dumped a " + (isStatic ? "static" : "dynamic") + " archive " + archiveFilePath);
395         return archiveFilePath;
396     }
397 
398     /**
399      * Detects if we need to emit explicit class initialization checks in
400      * AOT-cached MethodHandles and VarHandles before accessing static fields
401      * and methods.
402      * @see jdk.internal.misc.Unsafe::shouldBeInitialized
403      *
404      * @return false only if a call to {@code ensureClassInitialized} would have
405      * no effect during the application's production run.
406      */
407     public static boolean needsClassInitBarrier(Class<?> c) {
408         if (c == null) {
409             throw new NullPointerException();
410         }
411 
412         if ((configStatus & IS_DUMPING_METHOD_HANDLES) == 0) {
413             return false;
414         } else {
415             return needsClassInitBarrier0(c);
416         }
417     }
418 
419     private static native boolean needsClassInitBarrier0(Class<?> c);
420 
421     /**
422      * This class is used only by native JVM code at CDS dump time for loading
423      * "unregistered classes", which are archived classes that are intended to
424      * be loaded by custom class loaders during runtime.
425      * See src/hotspot/share/cds/unregisteredClasses.cpp.
426      */
427     private static class UnregisteredClassLoader extends ClassLoader {
428         static {
429             registerAsParallelCapable();
430         }
431 
432         static interface Source {
433             public byte[] readClassFile(String className) throws IOException;
434         }
435 
436         static class JarSource implements Source {
437             private final JarFile jar;
438 
439             JarSource(File file) throws IOException {
440                 jar = new JarFile(file);
441             }
442 
443             @Override
444             public byte[] readClassFile(String className) throws IOException {
445                 final var entryName = className.replace('.', '/').concat(".class");
446                 final var entry = jar.getEntry(entryName);
447                 if (entry == null) {
448                     throw new IOException("No such entry: " + entryName + " in " + jar.getName());
449                 }
450                 try (final var in = jar.getInputStream(entry)) {
451                     return in.readAllBytes();
452                 }
453             }
454         }
455 
456         static class DirSource implements Source {
457             private final String basePath;
458 
459             DirSource(File dir) {
460                 assert dir.isDirectory();
461                 basePath = dir.toString();
462             }
463 
464             @Override
465             public byte[] readClassFile(String className) throws IOException {
466                 final var subPath = className.replace('.', File.separatorChar).concat(".class");
467                 final var fullPath = Path.of(basePath, subPath);
468                 return Files.readAllBytes(fullPath);
469             }
470         }
471 
472         private final HashMap<String, Source> sources = new HashMap<>();
473 
474         private Source resolveSource(String path) throws IOException {
475             Source source = sources.get(path);
476             if (source != null) {
477                 return source;
478             }
479 
480             final var file = new File(path);
481             if (!file.exists()) {
482                 throw new IOException("No such file: " + path);
483             }
484             if (file.isFile()) {
485                 source = new JarSource(file);
486             } else if (file.isDirectory()) {
487                 source = new DirSource(file);
488             } else {
489                 throw new IOException("Not a normal file: " + path);
490             }
491             sources.put(path, source);
492 
493             return source;
494         }
495 
496         /**
497          * Load the class of the given <code>name</code> from the given <code>source</code>.
498          * <p>
499          * All super classes and interfaces of the named class must have already been loaded:
500          * either defined by this class loader (unregistered ones) or loaded, possibly indirectly,
501          * by the system class loader (registered ones).
502          * <p>
503          * If the named class has a registered super class or interface named N there should be no
504          * unregistered class or interface named N loaded yet.
505          *
506          * @param name the name of the class to be loaded.
507          * @param source path to a directory or a JAR file from which the named class should be
508          *               loaded.
509          */
510         private Class<?> load(String name, String source) throws IOException {
511             final Source resolvedSource = resolveSource(source);
512             final byte[] bytes = resolvedSource.readClassFile(name);
513             // 'defineClass()' may cause loading of supertypes of this unregistered class by VM
514             // calling 'this.loadClass()'.
515             //
516             // For any supertype S named SN specified in the classlist the following is ensured by
517             // the CDS implementation:
518             // - if S is an unregistered class it must have already been defined by this class
519             //   loader and thus will be found by 'this.findLoadedClass(SN)',
520             // - if S is not an unregistered class there should be no unregistered class named SN
521             //   loaded yet so either S has previously been (indirectly) loaded by this class loader
522             //   and thus it will be found when calling 'this.findLoadedClass(SN)' or it will be
523             //   found when delegating to the system class loader, which must have already loaded S,
524             //   by calling 'this.getParent().loadClass(SN, false)'.
525             // See the implementation of 'ClassLoader.loadClass()' for details.
526             //
527             // Therefore, we should resolve all supertypes to the expected ones as specified by the
528             // "super:" and "interfaces:" attributes in the classlist. This invariant is validated
529             // by the C++ function 'ClassListParser::load_class_from_source()'.
530             assert getParent() == getSystemClassLoader();
531             return defineClass(name, bytes, 0, bytes.length);
532         }
533     }
534 
535     /**
536      * This class is used only by native JVM code to spawn a child JVM process to assemble
537      * the AOT cache. <code>args[]</code> are passed in the <code>JAVA_TOOL_OPTIONS</code>
538      * environment variable.
539      */
540     private static class ProcessLauncher {
541         static int execWithJavaToolOptions(String javaLauncher, String args[]) throws IOException, InterruptedException {
542             ProcessBuilder pb = new ProcessBuilder().inheritIO().command(javaLauncher);
543             StringBuilder sb = new StringBuilder();
544 
545             // Encode the args as described in
546             // https://docs.oracle.com/en/java/javase/24/docs/specs/jvmti.html#tooloptions
547             String prefix = "";
548             for (String arg : args) {
549                 sb.append(prefix);
550 
551                 for (int i = 0; i < arg.length(); i++) {
552                     char c = arg.charAt(i);
553                     if (c == '"' || Character.isWhitespace(c)) {
554                         sb.append('\'');
555                         sb.append(c);
556                         sb.append('\'');
557                     } else if (c == '\'') {
558                         sb.append('"');
559                         sb.append(c);
560                         sb.append('"');
561                     } else {
562                         sb.append(c);
563                     }
564                 }
565 
566                 prefix = " ";
567             }
568 
569             Map<String, String> env = pb.environment();
570             env.put("JAVA_TOOL_OPTIONS", sb.toString());
571             env.remove("_JAVA_OPTIONS");
572             env.remove("CLASSPATH");
573             Process process = pb.start();
574             return process.waitFor();
575         }
576     }
577 }