From 281e5a5e5ea1c34681c4fa6ba3daf485a7facc07 Mon Sep 17 00:00:00 2001 From: Timofei Pushkin Date: Fri, 16 May 2025 13:51:58 +0000 Subject: [PATCH] 8315130: java.lang.IllegalAccessError when processing classlist to create CDS archive Reviewed-by: iklam, ccheung --- src/hotspot/share/cds/classListParser.cpp | 59 ++++-- src/hotspot/share/cds/classListParser.hpp | 3 +- src/hotspot/share/cds/unregisteredClasses.cpp | 98 ++++------ src/hotspot/share/cds/unregisteredClasses.hpp | 18 +- .../classfile/systemDictionaryShared.cpp | 21 +- .../classfile/systemDictionaryShared.hpp | 1 + .../share/classes/jdk/internal/misc/CDS.java | 182 +++++++++--------- .../customLoader/DifferentSourcesTest.java | 79 ++++++++ .../customLoader/RegUnregSuperTest.java | 88 +++++++++ .../test-classes/CustomLoadee5.java | 28 +++ .../test-classes/CustomLoadee5Child.java | 28 +++ .../test-classes/DifferentSourcesApp.java | 41 ++++ .../test-classes/RegUnregSuperApp.java | 108 +++++++++++ 13 files changed, 573 insertions(+), 181 deletions(-) create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/customLoader/DifferentSourcesTest.java create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/customLoader/RegUnregSuperTest.java create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5.java create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5Child.java create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/DifferentSourcesApp.java create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/RegUnregSuperApp.java diff --git a/src/hotspot/share/cds/classListParser.cpp b/src/hotspot/share/cds/classListParser.cpp index f8ba385bd62..719da14bdd8 100644 --- a/src/hotspot/share/cds/classListParser.cpp +++ b/src/hotspot/share/cds/classListParser.cpp @@ -397,17 +397,13 @@ bool ClassListParser::parse_uint_option(const char* option_name, int* value) { return false; } -objArrayOop ClassListParser::get_specified_interfaces(TRAPS) { +GrowableArray ClassListParser::get_specified_interfaces() { const int n = _interfaces->length(); - if (n == 0) { - return nullptr; - } else { - objArrayOop array = oopFactory::new_objArray(vmClasses::Class_klass(), n, CHECK_NULL); - for (int i = 0; i < n; i++) { - array->obj_at_put(i, lookup_class_by_id(_interfaces->at(i))->java_mirror()); - } - return array; + GrowableArray specified_interfaces(n); + for (int i = 0; i < n; i++) { + specified_interfaces.append(lookup_class_by_id(_interfaces->at(i))); } + return specified_interfaces; } void ClassListParser::print_specified_interfaces() { @@ -469,6 +465,25 @@ void ClassListParser::error(const char* msg, ...) { va_end(ap); } +// If an unregistered class U is specified to have a registered supertype S1 +// named SN but an unregistered class S2 also named SN has already been loaded +// S2 will be incorrectly used as the supertype of U instead of S1 due to +// limitations in the loading mechanism of unregistered classes. +void ClassListParser::check_supertype_obstruction(int specified_supertype_id, const InstanceKlass* specified_supertype, TRAPS) { + if (specified_supertype->is_shared_unregistered_class()) { + return; // Only registered supertypes can be obstructed + } + const InstanceKlass* obstructor = SystemDictionaryShared::get_unregistered_class(specified_supertype->name()); + if (obstructor == nullptr) { + return; // No unregistered types with the same name have been loaded, i.e. no obstruction + } + // 'specified_supertype' is S1, 'obstructor' is S2 from the explanation above + ResourceMark rm; + THROW_MSG(vmSymbols::java_lang_UnsupportedOperationException(), + err_msg("%s (id %d) has super-type %s (id %d) obstructed by another class with the same name", + _class_name, _id, specified_supertype->external_name(), specified_supertype_id)); +} + // This function is used for loading classes for customized class loaders // during archive dumping. InstanceKlass* ClassListParser::load_class_from_source(Symbol* class_name, TRAPS) { @@ -493,13 +508,18 @@ InstanceKlass* ClassListParser::load_class_from_source(Symbol* class_name, TRAPS } ResourceMark rm; - char * source_path = os::strdup_check_oom(ClassLoader::uri_to_path(_source)); InstanceKlass* specified_super = lookup_class_by_id(_super); - Handle super_class(THREAD, specified_super->java_mirror()); - objArrayOop r = get_specified_interfaces(CHECK_NULL); - objArrayHandle interfaces(THREAD, r); - InstanceKlass* k = UnregisteredClasses::load_class(class_name, source_path, - super_class, interfaces, CHECK_NULL); + GrowableArray specified_interfaces = get_specified_interfaces(); + // Obstruction must be checked before the class loading attempt because it may + // cause class loading errors (JVMS 5.3.5.3-5.3.5.4) + check_supertype_obstruction(_super, specified_super, CHECK_NULL); + for (int i = 0; i < _interfaces->length(); i++) { + check_supertype_obstruction(_interfaces->at(i), specified_interfaces.at(i), CHECK_NULL); + } + + const char* source_path = ClassLoader::uri_to_path(_source); + InstanceKlass* k = UnregisteredClasses::load_class(class_name, source_path, CHECK_NULL); + if (k->java_super() != specified_super) { error("The specified super class %s (id %d) does not match actual super class %s", specified_super->external_name(), _super, @@ -511,6 +531,15 @@ InstanceKlass* ClassListParser::load_class_from_source(Symbol* class_name, TRAPS error("The number of interfaces (%d) specified in class list does not match the class file (%d)", _interfaces->length(), k->local_interfaces()->length()); } + for (int i = 0; i < _interfaces->length(); i++) { + InstanceKlass* specified_interface = specified_interfaces.at(i); + if (!k->local_interfaces()->contains(specified_interface)) { + print_specified_interfaces(); + print_actual_interfaces(k); + error("Specified interface %s (id %d) is not directly implemented", + specified_interface->external_name(), _interfaces->at(i)); + } + } assert(k->is_shared_unregistered_class(), "must be"); diff --git a/src/hotspot/share/cds/classListParser.hpp b/src/hotspot/share/cds/classListParser.hpp index 4234cd436ee..51dbb7fd39d 100644 --- a/src/hotspot/share/cds/classListParser.hpp +++ b/src/hotspot/share/cds/classListParser.hpp @@ -134,7 +134,8 @@ class ClassListParser : public StackObj { ClassListParser(const char* file, ParseMode _parse_mode); ~ClassListParser(); - objArrayOop get_specified_interfaces(TRAPS); + GrowableArray get_specified_interfaces(); + void check_supertype_obstruction(int specified_supertype_id, const InstanceKlass* specified_supertype, TRAPS); public: static int parse_classlist(const char* classlist_path, ParseMode parse_mode, TRAPS); diff --git a/src/hotspot/share/cds/unregisteredClasses.cpp b/src/hotspot/share/cds/unregisteredClasses.cpp index 641f84c3dcd..7af9d5b45b3 100644 --- a/src/hotspot/share/cds/unregisteredClasses.cpp +++ b/src/hotspot/share/cds/unregisteredClasses.cpp @@ -24,37 +24,49 @@ #include "precompiled.hpp" #include "cds/unregisteredClasses.hpp" -#include "classfile/classFileStream.hpp" -#include "classfile/classLoader.inline.hpp" -#include "classfile/classLoaderExt.hpp" -#include "classfile/javaClasses.inline.hpp" #include "classfile/symbolTable.hpp" #include "classfile/systemDictionary.hpp" #include "classfile/vmSymbols.hpp" -#include "memory/oopFactory.hpp" -#include "memory/resourceArea.hpp" #include "oops/instanceKlass.hpp" +#include "oops/oopHandle.hpp" #include "oops/oopHandle.inline.hpp" +#include "runtime/handles.hpp" #include "runtime/handles.inline.hpp" #include "runtime/javaCalls.hpp" #include "services/threadService.hpp" -InstanceKlass* UnregisteredClasses::_UnregisteredClassLoader_klass = nullptr; +static InstanceKlass* _UnregisteredClassLoader_klass; +static InstanceKlass* _UnregisteredClassLoader_Source_klass; +static OopHandle _unregistered_class_loader; void UnregisteredClasses::initialize(TRAPS) { - if (_UnregisteredClassLoader_klass == nullptr) { - // no need for synchronization as this function is called single-threaded. - Symbol* klass_name = SymbolTable::new_symbol("jdk/internal/misc/CDS$UnregisteredClassLoader"); - Klass* k = SystemDictionary::resolve_or_fail(klass_name, true, CHECK); - _UnregisteredClassLoader_klass = InstanceKlass::cast(k); + if (_UnregisteredClassLoader_klass != nullptr) { + return; } + + Symbol* klass_name; + Klass* k; + + // no need for synchronization as this function is called single-threaded. + klass_name = SymbolTable::new_symbol("jdk/internal/misc/CDS$UnregisteredClassLoader"); + k = SystemDictionary::resolve_or_fail(klass_name, true, CHECK); + _UnregisteredClassLoader_klass = InstanceKlass::cast(k); + + klass_name = SymbolTable::new_symbol("jdk/internal/misc/CDS$UnregisteredClassLoader$Source"); + k = SystemDictionary::resolve_or_fail(klass_name, true, CHECK); + _UnregisteredClassLoader_Source_klass = InstanceKlass::cast(k); + + precond(_unregistered_class_loader.is_empty()); + HandleMark hm(THREAD); + const Handle cl = JavaCalls::construct_new_instance(_UnregisteredClassLoader_klass, + vmSymbols::void_method_signature(), CHECK); + _unregistered_class_loader = OopHandle(Universe::vm_global(), cl()); } // Load the class of the given name from the location given by path. The path is specified by // the "source:" in the class list file (see classListParser.cpp), and can be a directory or // a JAR file. -InstanceKlass* UnregisteredClasses::load_class(Symbol* name, const char* path, - Handle super_class, objArrayHandle interfaces, TRAPS) { +InstanceKlass* UnregisteredClasses::load_class(Symbol* name, const char* path, TRAPS) { assert(name != nullptr, "invariant"); assert(DumpSharedSpaces, "this function is only used with -Xshare:dump"); @@ -62,59 +74,33 @@ InstanceKlass* UnregisteredClasses::load_class(Symbol* name, const char* path, THREAD->get_thread_stat()->perf_timers_addr(), PerfClassTraceTime::CLASS_LOAD); - // Call CDS$UnregisteredClassLoader::load(String name, Class superClass, Class[] interfaces) + assert(!_unregistered_class_loader.is_empty(), "not initialized"); + Handle classloader(THREAD, _unregistered_class_loader.resolve()); + + // Call CDS$UnregisteredClassLoader::load(String name, String source) Symbol* methodName = SymbolTable::new_symbol("load"); - Symbol* methodSignature = SymbolTable::new_symbol("(Ljava/lang/String;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/Class;"); - Symbol* path_symbol = SymbolTable::new_symbol(path); - Handle classloader = get_classloader(path_symbol, CHECK_NULL); + Symbol* methodSignature = SymbolTable::new_symbol("(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Class;"); Handle ext_class_name = java_lang_String::externalize_classname(name, CHECK_NULL); + Handle path_string = java_lang_String::create_from_str(path, CHECK_NULL); JavaValue result(T_OBJECT); - JavaCallArguments args(3); - args.set_receiver(classloader); - args.push_oop(ext_class_name); - args.push_oop(super_class); - args.push_oop(interfaces); JavaCalls::call_virtual(&result, - UnregisteredClassLoader_klass(), + classloader, + _UnregisteredClassLoader_klass, methodName, methodSignature, - &args, + ext_class_name, + path_string, CHECK_NULL); assert(result.get_type() == T_OBJECT, "just checking"); - oop obj = result.get_oop(); - return InstanceKlass::cast(java_lang_Class::as_Klass(obj)); -} - -class UnregisteredClasses::ClassLoaderTable : public ResourceHashtable< - Symbol*, OopHandle, - 137, // prime number - AnyObj::C_HEAP> {}; - -static UnregisteredClasses::ClassLoaderTable* _classloader_table = nullptr; -Handle UnregisteredClasses::create_classloader(Symbol* path, TRAPS) { - ResourceMark rm(THREAD); - JavaValue result(T_OBJECT); - Handle path_string = java_lang_String::create_from_str(path->as_C_string(), CHECK_NH); - Handle classloader = JavaCalls::construct_new_instance( - UnregisteredClassLoader_klass(), - vmSymbols::string_void_signature(), - path_string, CHECK_NH); - return classloader; + return InstanceKlass::cast(java_lang_Class::as_Klass(result.get_oop())); } -Handle UnregisteredClasses::get_classloader(Symbol* path, TRAPS) { - if (_classloader_table == nullptr) { - _classloader_table = new (mtClass)ClassLoaderTable(); - } - OopHandle* classloader_ptr = _classloader_table->get(path); - if (classloader_ptr != nullptr) { - return Handle(THREAD, (*classloader_ptr).resolve()); - } else { - Handle classloader = create_classloader(path, CHECK_NH); - _classloader_table->put(path, OopHandle(Universe::vm_global(), classloader())); - path->increment_refcount(); - return classloader; +bool UnregisteredClasses::check_for_exclusion(const InstanceKlass* k) { + if (_UnregisteredClassLoader_klass == nullptr) { + return false; // Uninitialized } + return k == _UnregisteredClassLoader_klass || + k->implements_interface(_UnregisteredClassLoader_Source_klass); } diff --git a/src/hotspot/share/cds/unregisteredClasses.hpp b/src/hotspot/share/cds/unregisteredClasses.hpp index ea3a308c2de..d61c3462504 100644 --- a/src/hotspot/share/cds/unregisteredClasses.hpp +++ b/src/hotspot/share/cds/unregisteredClasses.hpp @@ -33,22 +33,10 @@ class Symbol; class UnregisteredClasses: AllStatic { public: - static InstanceKlass* load_class(Symbol* h_name, const char* path, - Handle super_class, objArrayHandle interfaces, - TRAPS); + static InstanceKlass* load_class(Symbol* name, const char* path, TRAPS); static void initialize(TRAPS); - static InstanceKlass* UnregisteredClassLoader_klass() { - return _UnregisteredClassLoader_klass; - } - - class ClassLoaderTable; - -private: - // Don't put this in vmClasses as it's used only with CDS dumping. - static InstanceKlass* _UnregisteredClassLoader_klass; - - static Handle create_classloader(Symbol* path, TRAPS); - static Handle get_classloader(Symbol* path, TRAPS); + // Returns true if the class is loaded internally for dumping unregistered classes. + static bool check_for_exclusion(const InstanceKlass* k); }; #endif // SHARE_CDS_UNREGISTEREDCLASSES_HPP diff --git a/src/hotspot/share/classfile/systemDictionaryShared.cpp b/src/hotspot/share/classfile/systemDictionaryShared.cpp index 22a94133a9a..b7b09dc69d8 100644 --- a/src/hotspot/share/classfile/systemDictionaryShared.cpp +++ b/src/hotspot/share/classfile/systemDictionaryShared.cpp @@ -314,6 +314,12 @@ bool SystemDictionaryShared::check_for_exclusion_impl(InstanceKlass* k) { return true; } + if (UnregisteredClasses::check_for_exclusion(k)) { + ResourceMark rm; + log_info(cds)("Skipping %s: used only when dumping CDS archive", k->name()->as_C_string()); + return true; + } + InstanceKlass* super = k->java_super(); if (super != nullptr && check_for_exclusion(super, nullptr)) { ResourceMark rm; @@ -332,12 +338,6 @@ bool SystemDictionaryShared::check_for_exclusion_impl(InstanceKlass* k) { } } - if (k == UnregisteredClasses::UnregisteredClassLoader_klass()) { - ResourceMark rm; - log_info(cds)("Skipping %s: used only when dumping CDS archive", k->name()->as_C_string()); - return true; - } - return false; // false == k should NOT be excluded } @@ -456,6 +456,15 @@ bool SystemDictionaryShared::add_unregistered_class(Thread* current, InstanceKla return (klass == *v); } +InstanceKlass* SystemDictionaryShared::get_unregistered_class(Symbol* name) { + assert(Arguments::is_dumping_archive() || ClassListWriter::is_enabled(), "sanity"); + if (_unregistered_classes_table == nullptr) { + return nullptr; + } + InstanceKlass** k = _unregistered_classes_table->get(name); + return k != nullptr ? *k : nullptr; +} + void SystemDictionaryShared::set_shared_class_misc_info(InstanceKlass* k, ClassFileStream* cfs) { Arguments::assert_is_dumping_archive(); assert(!is_builtin(k), "must be unregistered class"); diff --git a/src/hotspot/share/classfile/systemDictionaryShared.hpp b/src/hotspot/share/classfile/systemDictionaryShared.hpp index f43ab838ae6..27a0c74ed44 100644 --- a/src/hotspot/share/classfile/systemDictionaryShared.hpp +++ b/src/hotspot/share/classfile/systemDictionaryShared.hpp @@ -287,6 +287,7 @@ class SystemDictionaryShared: public SystemDictionary { return (k->shared_classpath_index() != UNREGISTERED_INDEX); } static bool add_unregistered_class(Thread* current, InstanceKlass* k); + static InstanceKlass* get_unregistered_class(Symbol* name); static void check_excluded_classes(); static bool check_for_exclusion(InstanceKlass* k, DumpTimeClassInfo* info); diff --git a/src/java.base/share/classes/jdk/internal/misc/CDS.java b/src/java.base/share/classes/jdk/internal/misc/CDS.java index a963a45a813..4b0eb06c4a3 100644 --- a/src/java.base/share/classes/jdk/internal/misc/CDS.java +++ b/src/java.base/share/classes/jdk/internal/misc/CDS.java @@ -31,15 +31,15 @@ import java.io.InputStream; import java.io.IOException; import java.io.PrintStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.InvalidPathException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.jar.JarFile; import java.util.stream.Stream; import jdk.internal.access.SharedSecrets; @@ -344,105 +344,111 @@ private static String dumpSharedArchive(boolean isStatic, String fileName) throw * be loaded by custom class loaders during runtime. * See src/hotspot/share/cds/unregisteredClasses.cpp. */ - private static class UnregisteredClassLoader extends URLClassLoader { - private String currentClassName; - private Class currentSuperClass; - private Class[] currentInterfaces; + private static class UnregisteredClassLoader extends ClassLoader { + static { + registerAsParallelCapable(); + } - /** - * Used only by native code. Construct an UnregisteredClassLoader for loading - * unregistered classes from the specified file. If the file doesn't exist, - * the exception will be caughted by native code which will print a warning message and continue. - * - * @param fileName path of the the JAR file to load unregistered classes from. - */ - private UnregisteredClassLoader(String fileName) throws InvalidPathException, IOException { - super(toURLArray(fileName), /*parent*/null); - currentClassName = null; - currentSuperClass = null; - currentInterfaces = null; + static interface Source { + public byte[] readClassFile(String className) throws IOException; } - private static URL[] toURLArray(String fileName) throws InvalidPathException, IOException { - if (!((new File(fileName)).exists())) { - throw new IOException("No such file: " + fileName); + static class JarSource implements Source { + private final JarFile jar; + + JarSource(File file) throws IOException { + jar = new JarFile(file); + } + + @Override + public byte[] readClassFile(String className) throws IOException { + final var entryName = className.replace('.', '/').concat(".class"); + final var entry = jar.getEntry(entryName); + if (entry == null) { + throw new IOException("No such entry: " + entryName + " in " + jar.getName()); + } + try (final var in = jar.getInputStream(entry)) { + return in.readAllBytes(); + } } - return new URL[] { - // Use an intermediate File object to construct a URI/URL without - // authority component as URLClassPath can't handle URLs with a UNC - // server name in the authority component. - Path.of(fileName).toRealPath().toFile().toURI().toURL() - }; } + static class DirSource implements Source { + private final String basePath; - /** - * Load the class of the given /name from the JAR file that was given to - * the constructor of the current UnregisteredClassLoader instance. This class must be - * a direct subclass of superClass. This class must be declared to implement - * the specified interfaces. - *

- * This method must be called in a single threaded context. It will never be recursed (thus - * the asserts) - * - * @param name the name of the class to be loaded. - * @param superClass must not be null. The named class must have a super class. - * @param interfaces could be null if the named class does not implement any interfaces. - */ - private Class load(String name, Class superClass, Class[] interfaces) - throws ClassNotFoundException - { - assert currentClassName == null; - assert currentSuperClass == null; - assert currentInterfaces == null; - - try { - currentClassName = name; - currentSuperClass = superClass; - currentInterfaces = interfaces; - - return findClass(name); - } finally { - currentClassName = null; - currentSuperClass = null; - currentInterfaces = null; + DirSource(File dir) { + assert dir.isDirectory(); + basePath = dir.toString(); + } + + @Override + public byte[] readClassFile(String className) throws IOException { + final var subPath = className.replace('.', File.separatorChar).concat(".class"); + final var fullPath = Path.of(basePath, subPath); + return Files.readAllBytes(fullPath); } } - /** - * This method must be called from inside the load() method. The /name - * can be only: - *

    - *
  • the name parameter for load() - *
  • the name of the superClass parameter for load() - *
  • the name of one of the interfaces in interfaces parameter for load() - *
      - * - * For all other cases, a ClassNotFoundException will be thrown. - */ - protected Class findClass(final String name) - throws ClassNotFoundException - { - Objects.requireNonNull(currentClassName); - Objects.requireNonNull(currentSuperClass); - - if (name.equals(currentClassName)) { - // Note: the following call will call back to this.findClass(name) to - // resolve the super types of the named class. - return super.findClass(name); + private final HashMap sources = new HashMap<>(); + + private Source resolveSource(String path) throws IOException { + Source source = sources.get(path); + if (source != null) { + return source; } - if (name.equals(currentSuperClass.getName())) { - return currentSuperClass; + + final var file = new File(path); + if (!file.exists()) { + throw new IOException("No such file: " + path); } - if (currentInterfaces != null) { - for (Class c : currentInterfaces) { - if (name.equals(c.getName())) { - return c; - } - } + if (file.isFile()) { + source = new JarSource(file); + } else if (file.isDirectory()) { + source = new DirSource(file); + } else { + throw new IOException("Not a normal file: " + path); } + sources.put(path, source); - throw new ClassNotFoundException(name); + return source; + } + + /** + * Load the class of the given name from the given source. + *

      + * All super classes and interfaces of the named class must have already been loaded: + * either defined by this class loader (unregistered ones) or loaded, possibly indirectly, + * by the system class loader (registered ones). + *

      + * If the named class has a registered super class or interface named N there should be no + * unregistered class or interface named N loaded yet. + * + * @param name the name of the class to be loaded. + * @param source path to a directory or a JAR file from which the named class should be + * loaded. + */ + private Class load(String name, String source) throws IOException { + final Source resolvedSource = resolveSource(source); + final byte[] bytes = resolvedSource.readClassFile(name); + // 'defineClass()' may cause loading of supertypes of this unregistered class by VM + // calling 'this.loadClass()'. + // + // For any supertype S named SN specified in the classlist the following is ensured by + // the CDS implementation: + // - if S is an unregistered class it must have already been defined by this class + // loader and thus will be found by 'this.findLoadedClass(SN)', + // - if S is not an unregistered class there should be no unregistered class named SN + // loaded yet so either S has previously been (indirectly) loaded by this class loader + // and thus it will be found when calling 'this.findLoadedClass(SN)' or it will be + // found when delegating to the system class loader, which must have already loaded S, + // by calling 'this.getParent().loadClass(SN, false)'. + // See the implementation of 'ClassLoader.loadClass()' for details. + // + // Therefore, we should resolve all supertypes to the expected ones as specified by the + // "super:" and "interfaces:" attributes in the classlist. This invariant is validated + // by the C++ function 'ClassListParser::load_class_from_source()'. + assert getParent() == getSystemClassLoader(); + return defineClass(name, bytes, 0, bytes.length); } } } diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/DifferentSourcesTest.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/DifferentSourcesTest.java new file mode 100644 index 00000000000..b372c3b7289 --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/DifferentSourcesTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Files; +import java.nio.file.Path; + +import jdk.test.lib.process.OutputAnalyzer; + +/** + * @test + * @bug 8315130 + * @summary Tests archiving a hierarchy of package-private classes loaded from + * different sources. + * + * @requires vm.cds + * @requires vm.cds.custom.loaders + * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds + * @compile test-classes/DifferentSourcesApp.java test-classes/CustomLoadee5.java test-classes/CustomLoadee5Child.java + * @run main DifferentSourcesTest + */ +public class DifferentSourcesTest { + public static void main(String[] args) throws Exception { + // Setup: + // - CustomLoadee5 is package-private + // - CustomLoadee5Child extends CustomLoadee5 + // + // This setup requires CustomLoadee5 and CustomLoadee5Child to be in the + // same run-time package. Since their package name is the same (empty) + // this boils down to "be loaded by the same class loader". + // + // DifferentSourcesApp adheres to this requirement. + // + // This test checks that CDS adheres to this requirement too when + // creating a static app archive, even if CustomLoadee5 and + // CustomLoadee5Child are in different sources. + + OutputAnalyzer output; + + // The main check: the archive is created without IllegalAccessError + JarBuilder.build("base", "CustomLoadee5"); + JarBuilder.build("sub", "CustomLoadee5Child"); + final String classlist[] = new String[] { + "java/lang/Object id: 0", + "CustomLoadee5 id: 1 super: 0 source: base.jar", + "CustomLoadee5Child id: 2 super: 1 source: sub.jar", + }; + output = TestCommon.testDump(null, classlist); + output.shouldNotContain("java.lang.IllegalAccessError: class CustomLoadee5Child cannot access its superclass CustomLoadee5"); + output.shouldNotContain("Cannot find CustomLoadee5Child"); + + // Sanity check: the archive is used as expected + output = TestCommon.execCommon("-Xlog:class+load", "DifferentSourcesApp"); + TestCommon.checkExec( + output, + "CustomLoadee5 source: shared objects file", + "CustomLoadee5Child source: shared objects file" + ); + } +} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/RegUnregSuperTest.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/RegUnregSuperTest.java new file mode 100644 index 00000000000..85f1657098e --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/RegUnregSuperTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.test.lib.process.OutputAnalyzer; + +/** + * @test id=unreg + * @summary Tests that if a super class is listed as unregistered it is archived + * as such even if a class with the same name has also been loaded from the + * classpath. + * + * @requires vm.cds + * @requires vm.cds.custom.loaders + * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds + * @compile test-classes/RegUnregSuperApp.java test-classes/CustomLoadee3.java test-classes/CustomLoadee3Child.java + * @run main RegUnregSuperTest unreg + */ + +/** + * @test id=reg + * @summary If an unregistered class U is specified to have a registered + * supertype S1 named SN but an unregistered class S2 also named SN has already + * been loaded S2 will be incorrectly used as the supertype of U instead of S1 + * due to limitations in the loading mechanism of unregistered classes. For this + * reason U should not be loaded at all and an appropriate warning should be + * printed. + * + * @requires vm.cds + * @requires vm.cds.custom.loaders + * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds + * @compile test-classes/RegUnregSuperApp.java test-classes/CustomLoadee3.java test-classes/CustomLoadee3Child.java + * @run main RegUnregSuperTest reg + */ + +public class RegUnregSuperTest { + public static void main(String[] args) throws Exception { + final String variant = args[0]; + + final String appJar = JarBuilder.build( + "app", "RegUnregSuperApp", "DirectClassLoader", "CustomLoadee3", "CustomLoadee3Child" + ); + OutputAnalyzer out; + + final String classlist[] = new String[] { + "java/lang/Object id: 0", + "CustomLoadee3 id: 1", + "CustomLoadee3 id: 2 super: 0 source: " + appJar, + "CustomLoadee3Child id: 3 super: " + ("reg".equals(variant) ? "1" : "2") + " source: " + appJar + }; + out = TestCommon.testDump(appJar, classlist, "-Xlog:cds+class=debug"); + out.shouldContain("app CustomLoadee3"); // Not using \n as below because it'll be "app CustomLoadee3 aot-linked" with AOTClassLinking + out.shouldNotContain("app CustomLoadee3Child"); + out.shouldContain("unreg CustomLoadee3\n"); // Accepts "unreg CustomLoadee3" but not "unreg CustomLoadee3Child" + if ("reg".equals(variant)) { + out.shouldNotContain("unreg CustomLoadee3Child"); + out.shouldContain("CustomLoadee3Child (id 3) has super-type CustomLoadee3 (id 1) obstructed by another class with the same name"); + } else { + out.shouldContain("unreg CustomLoadee3Child"); + out.shouldNotContain("[warning]"); + } + + out = TestCommon.exec(appJar, "-Xlog:class+load", "RegUnregSuperApp", variant); + TestCommon.checkExec( + out, + "CustomLoadee3Child source: " + ("reg".equals(variant) ? "file:" : "shared objects file") + ); + } +} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5.java new file mode 100644 index 00000000000..f3cda51c9fa --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +class CustomLoadee5 { + public String toString() { + return "this is CustomLoadee5"; + } +} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5Child.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5Child.java new file mode 100644 index 00000000000..82684ce327d --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5Child.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +class CustomLoadee5Child extends CustomLoadee5 { + public String toString() { + return "this is CustomLoadee5Child"; + } +} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/DifferentSourcesApp.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/DifferentSourcesApp.java new file mode 100644 index 00000000000..21efd565ce4 --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/DifferentSourcesApp.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.Path; +import java.nio.file.FileSystems; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * See ../DifferentSourcesTest.java for details. + */ +public class DifferentSourcesApp { + public static void main(String args[]) throws Exception { + Path base = FileSystems.getDefault().getPath("base.jar"); + Path sub = FileSystems.getDefault().getPath("sub.jar"); + URL[] urls = new URL[] { base.toUri().toURL(), sub.toUri().toURL() }; + URLClassLoader cl = new URLClassLoader(urls, /* parent = */ null); + Class cls = cl.loadClass("CustomLoadee5Child"); + System.out.println(cls.getName()); + } +} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/RegUnregSuperApp.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/RegUnregSuperApp.java new file mode 100644 index 00000000000..01581d52db2 --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/RegUnregSuperApp.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.file.FileSystems; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Set; + +class DirectClassLoader extends URLClassLoader { + private final Set directlyLoadedNames; + + public DirectClassLoader(URL url, String... directlyLoadedNames) { + super(new URL[] { url }); + this.directlyLoadedNames = Set.of(directlyLoadedNames); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + if (directlyLoadedNames.contains(name)) { + c = findClass(name); + } else { + c = super.loadClass(name); + } + } + return c; + } + } +} + +/** + * See ../RegUnregSuperTest.java for details. + */ +public class RegUnregSuperApp { + private static final URL APP_JAR; + static { + final URL appJar; + try { + appJar = FileSystems.getDefault().getPath("app.jar").toUri().toURL(); + } catch (Exception e) { + throw new RuntimeException(e); + } + APP_JAR = appJar; + } + + public static void main(String args[]) throws Exception { + switch (args[0]) { + case "reg" -> loadWithRegisteredSuper(); + case "unreg" -> loadWithUnregisteredSuper(); + default -> throw new IllegalArgumentException("Unknown variant: " + args[0]); + } + } + + private static void loadWithRegisteredSuper() throws Exception { + // Load unregistered super + final var unregisteredBaseCl = new DirectClassLoader(APP_JAR, "CustomLoadee3"); + Class unregisteredBase = unregisteredBaseCl.loadClass("CustomLoadee3"); + checkClassLoader(unregisteredBase, unregisteredBaseCl); + + // Load unregistered child with REGISTERED super + final var registeredBaseCl = new DirectClassLoader(APP_JAR, "CustomLoadee3Child"); + Class unregisteredChild = registeredBaseCl.loadClass("CustomLoadee3Child"); + checkClassLoader(unregisteredChild, registeredBaseCl); + checkClassLoader(unregisteredChild.getSuperclass(), ClassLoader.getSystemClassLoader()); + } + + private static void loadWithUnregisteredSuper() throws Exception { + // Load registered super + final var systemCl = ClassLoader.getSystemClassLoader(); + Class registeredBase = systemCl.loadClass("CustomLoadee3"); + checkClassLoader(registeredBase, systemCl); + + // Load unregistered child with UNREGISTERED super + final var unregisteredBaseCl = new DirectClassLoader(APP_JAR, "CustomLoadee3", "CustomLoadee3Child"); + Class unregisteredChild = unregisteredBaseCl.loadClass("CustomLoadee3Child"); + checkClassLoader(unregisteredChild, unregisteredBaseCl); + checkClassLoader(unregisteredChild.getSuperclass(), unregisteredBaseCl); + } + + private static void checkClassLoader(Class c, ClassLoader cl) { + ClassLoader realCl = c.getClassLoader(); + if (realCl != cl) { + throw new RuntimeException(c + " has wrong loader: expected " + cl + ", got " + realCl); + } + } +}