Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ed6d1d8
Non-modular plugins with synthetic module
ldematte Nov 26, 2024
e5e5ce5
Modularize non-modular plugins: pass multiple layers to UberModuleCla…
ldematte Nov 27, 2024
4c3f232
Fix sql x-pack plugin to include all references as extendedPlugins
ldematte Nov 27, 2024
bdcdb30
Modular spatial plugin
ldematte Nov 27, 2024
d188bec
Handling modules from multiple parent layers (including extensions)
ldematte Nov 27, 2024
374731a
Fix jvm-crash plugin: bypass server ProtectionDomain (UberModuleClass…
ldematte Nov 27, 2024
2eeb099
Fix test + license header
ldematte Nov 27, 2024
a9a768c
Blocklist for apache enum package name
ldematte Nov 27, 2024
241a5b5
Modular gcs plugin (very MAYBE)
ldematte Nov 27, 2024
c3c0127
Revert "Modular gcs plugin (very MAYBE)"
ldematte Nov 27, 2024
2ad48c9
Recursive parent layers
ldematte Nov 27, 2024
daf710c
Merge remote-tracking branch 'upstream/main' into all-plugins-module-…
ldematte Nov 27, 2024
7933ada
Merge remote-tracking branch 'upstream/main' into all-plugins-module-…
ldematte Nov 28, 2024
8707701
Merge remote-tracking branch 'upstream/main' into all-plugins-module-…
ldematte Dec 2, 2024
56c050e
GCS uses via services files
ldematte Dec 2, 2024
298405e
identity-provider uses via services files
ldematte Dec 2, 2024
a5594ea
More org.apache.commons.lang.enum filtering
ldematte Dec 2, 2024
a4d39de
Fix ingest-attachment (TikaImpl ClassLoader match)
ldematte Dec 2, 2024
0f56ab9
spotless
ldematte Dec 2, 2024
990b04d
Merge remote-tracking branch 'upstream/main' into all-plugins-module-…
ldematte Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.jdk.JarHell;
import org.elasticsearch.plugins.UberModuleClassLoader;

import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -140,14 +141,17 @@ static PermissionCollection getRestrictedPermissions() {
// classpath
addReadPermissions(perms, JarHell.parseClassPath());
// plugin jars
if (TikaImpl.class.getClassLoader() instanceof URLClassLoader urlClassLoader) {
URL[] urls = urlClassLoader.getURLs();
Set<URL> set = new LinkedHashSet<>(Arrays.asList(urls));
if (set.size() != urls.length) {
throw new AssertionError("duplicate jars: " + Arrays.toString(urls));
}
addReadPermissions(perms, set);
final URL[] urls = switch (TikaImpl.class.getClassLoader()) {
case URLClassLoader urlClassLoader -> urlClassLoader.getURLs();
case UberModuleClassLoader uberClassLoader -> uberClassLoader.getInternalLoader().getURLs();
default -> throw new IllegalStateException("Unexpected value: " + TikaImpl.class.getClassLoader());
};
Set<URL> set = new LinkedHashSet<>(Arrays.asList(urls));
if (set.size() != urls.length) {
throw new AssertionError("duplicate jars: " + Arrays.toString(urls));
}
addReadPermissions(perms, set);

// jvm's java.io.tmpdir (needs read/write)
FilePermissionUtils.addDirectoryPath(
perms,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
package org.elasticsearch.plugins;

import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;

import java.io.BufferedReader;
import java.io.IOException;
Expand Down Expand Up @@ -42,6 +44,10 @@
*/
public class ModuleSupport {

private static final Logger logger = LogManager.getLogger(ModuleSupport.class);

private static final Set<String> FORBIDDEN_PACKAGE_NAMES = Set.of("org.apache.commons.lang.enum");

private ModuleSupport() {
throw new AssertionError("Utility class, should not be instantiated");
}
Expand Down Expand Up @@ -82,31 +88,44 @@ static ModuleDescriptor createModuleDescriptor(
Map<String, List<String>> allBundledProviders = new HashMap<>();
Set<String> servicesUsedInBundle = new HashSet<>();
for (Path path : jarPaths) {
logger.info("Scanning JAR " + path + " for services");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: potentially long strings are often best placed at the end of a log message to make the message easier to read:

logger.info("Scanning JAR for services: {}", path);

assert path.getFileName().toString().endsWith(".jar") : "expected jars suffix, in path: " + path;
try (JarFile jf = new JarFile(path.toFile(), true, ZipFile.OPEN_READ, Runtime.version())) {
// if we have a module declaration, trust its uses/provides
JarEntry moduleInfo = jf.getJarEntry("module-info.class");
if (moduleInfo != null) {
var descriptor = getDescriptorForModularJar(path);
pkgs.addAll(descriptor.packages());
pkgs.addAll(descriptor.packages().stream().filter(Predicate.not(FORBIDDEN_PACKAGE_NAMES::contains)).toList());
servicesUsedInBundle.addAll(descriptor.uses());
logger.info("Adding modular uses " + String.join(";", descriptor.uses()));

for (ModuleDescriptor.Provides p : descriptor.provides()) {
String serviceName = p.service();
List<String> providersInModule = p.providers();
logger.info("Adding modular providers " + String.join(";", providersInModule));

allBundledProviders.compute(serviceName, (k, v) -> createListOrAppend(v, providersInModule));
servicesUsedInBundle.add(serviceName);
}
} else {
var scan = scan(jf);
scan.classFiles().stream().map(cf -> toPackageName(cf, "/")).flatMap(Optional::stream).forEach(pkgs::add);
scan.classFiles()
.stream()
.map(cf -> toPackageName(cf, "/"))
.flatMap(Optional::stream)
.filter(Predicate.not(FORBIDDEN_PACKAGE_NAMES::contains))
.forEach(pkgs::add);

// read providers from the list of service files
for (String serviceFileName : scan.serviceFiles()) {
String serviceName = getServiceName(serviceFileName);
List<String> providersInJar = getProvidersFromServiceFile(jf, serviceFileName);

allBundledProviders.compute(serviceName, (k, v) -> createListOrAppend(v, providersInJar));
if (providersInJar.isEmpty() == false) {
logger.info("Adding non-modular providers " + String.join(";", providersInJar));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit 2: it's best practice to use {} instead of string formatting; otherwise, you pay the cost of the string formatting operation even when logs are disabled. Since providersInJar is already a list, you can probably just log it as-is:

logger.info("Adding non-modular providers: {}", providersInJar);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep in mind this is WIP, very "debugging" PR - these info logs will become debug or be removed altogether, (if they stay, they'll probably become the lambda accepting kind, to avoid computing the String.join if not needed)

allBundledProviders.compute(serviceName, (k, v) -> createListOrAppend(v, providersInJar));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you didn't write it, but this createListOrAppend thing is unnecessary. There's already an idiom for this.

allBundledProviders.computeIfAbsent(serviceName, n -> new ArrayList<>()).addAll(providersInJar);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, noticed the same, and there are also some addAll mixed with foreach add; if this ever turns into a real PR I'll fix them.

}
logger.info("Adding non-modular uses " + serviceName);
servicesUsedInBundle.add(serviceName);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;
Expand Down Expand Up @@ -100,8 +101,8 @@ public record LayerAndLoader(ModuleLayer layer, ClassLoader loader) {
Objects.requireNonNull(loader);
}

public static LayerAndLoader ofLoader(ClassLoader loader) {
return new LayerAndLoader(ModuleLayer.boot(), loader);
public static LayerAndLoader ofLoader(UberModuleClassLoader loader) {
return new LayerAndLoader(loader.getLayer(), loader);
}
}

Expand Down Expand Up @@ -273,7 +274,7 @@ static LayerAndLoader createSPI(
);
} else {
logger.debug(() -> "Loading bundle: " + plugin.getName() + ", creating spi, non-modular");
return LayerAndLoader.ofLoader(URLClassLoader.newInstance(bundle.spiUrls.toArray(new URL[0]), parentLoader));
return new LayerAndLoader(ModuleLayer.boot(), URLClassLoader.newInstance(bundle.spiUrls.toArray(new URL[0]), parentLoader));
}
}

Expand All @@ -286,26 +287,39 @@ static LayerAndLoader createPlugin(
) {
final PluginDescriptor plugin = bundle.plugin;
if (plugin.getModuleName().isPresent()) {
logger.debug(() -> "Loading bundle: " + plugin.getName() + ", modular");
logger.info(() -> "Loading bundle: " + plugin.getName() + ", modular");
var parentLayers = Stream.concat(
Stream.ofNullable(spiLayerAndLoader != null ? spiLayerAndLoader.layer() : null),
extendedPlugins.stream().map(LoadedPluginLayer::spiModuleLayer)
).toList();
return createPluginModuleLayer(bundle, pluginParentLoader, parentLayers, qualifiedExports);
} else if (plugin.isStable()) {
logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular as synthetic module");
logger.info(() -> "Loading bundle: " + plugin.getName() + ", non-modular as synthetic module");
return LayerAndLoader.ofLoader(
UberModuleClassLoader.getInstance(
pluginParentLoader,
ModuleLayer.boot(),
List.of(ModuleLayer.boot()),
"synthetic." + toModuleName(plugin.getName()),
bundle.allUrls,
Set.of("org.elasticsearch.server") // TODO: instead of denying server, allow only jvm + stable API modules
)
);
} else {
logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular");
return LayerAndLoader.ofLoader(URLClassLoader.newInstance(bundle.urls.toArray(URL[]::new), pluginParentLoader));
var syntheticName = "synthetic." + toModuleName2(plugin.getName());
logger.info(() -> "Loading bundle: " + plugin.getName() + ", non-modular, as synthetic module " + syntheticName);
var parentLayers = Stream.concat(
Stream.ofNullable(spiLayerAndLoader != null ? spiLayerAndLoader.layer() : null),
Stream.concat(extendedPlugins.stream().map(LoadedPluginLayer::spiModuleLayer), Stream.of(ModuleLayer.boot()))
).toList();
var parentModuleNames = parentLayers.stream()
.flatMap(l -> l.modules().stream())
.map(Module::getName)
.distinct()
.collect(Collectors.joining(";"));
logger.info("Parent modules for {}: {}", syntheticName, parentModuleNames);
return LayerAndLoader.ofLoader(
UberModuleClassLoader.getInstance(pluginParentLoader, parentLayers, syntheticName, bundle.allUrls, Set.of())
);
}
}

Expand Down Expand Up @@ -343,7 +357,7 @@ static LayerAndLoader createPluginModuleLayer(
);
}

static LayerAndLoader createModuleLayer(
private static LayerAndLoader createModuleLayer(
String className,
String moduleName,
Path[] paths,
Expand Down Expand Up @@ -389,6 +403,16 @@ static String toModuleName(String name) {
return result;
}

static String toModuleName2(String name) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very hacky, but I did not want to break the existing name schema for stable plugins.
And there is a ml-package-loader loader unfortunately -- ml.package.loader is not a OK module name, as package is a reserved keyword.

String result = name.replace('-', '_')
.replaceAll("\\W+", ".") // replace non-alphanumeric character strings with dots
.replaceAll("(^[^A-Za-z_]*)", "") // trim non-alpha or underscore characters from start
.replaceAll("\\.$", "") // trim trailing dot
.toLowerCase(Locale.getDefault());
assert ModuleSupport.isPackageName(result);
return result;
}

static final String toPackageName(String className) {
assert className.endsWith(".") == false;
int index = className.lastIndexOf('.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.security.PrivilegedAction;
import java.security.SecureClassLoader;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -58,43 +59,74 @@ public class UberModuleClassLoader extends SecureClassLoader implements AutoClos
private final ModuleLayer.Controller moduleController;
private final Set<String> packageNames;

private static Map<String, Set<String>> getModuleToServiceMap(ModuleLayer moduleLayer) {
Set<String> unqualifiedExports = moduleLayer.modules()
private static Set<String> unqualifiedExportsForLayer(ModuleLayer layer) {
return layer.modules()
.stream()
.flatMap(module -> module.getDescriptor().exports().stream())
.filter(Predicate.not(ModuleDescriptor.Exports::isQualified))
.map(ModuleDescriptor.Exports::source)
.collect(Collectors.toSet());
return moduleLayer.modules()
}

private static Set<ModuleDescriptor> readableModulesForLayer(ModuleLayer layer) {
return layer.modules()
.stream()
.map(Module::getDescriptor)
.filter(ModuleSupport::hasAtLeastOneUnqualifiedExport)
.filter(md -> ModuleSupport.hasAtLeastOneUnqualifiedExport(md) || md.isOpen() || md.isAutomatic())
.collect(Collectors.toSet());
}

private static Map<String, Set<String>> getModuleToServiceMap(List<ModuleLayer> moduleLayers) {
Set<String> allUnqualifiedExports = new HashSet<>();
Set<ModuleDescriptor> allReadableModules = new HashSet<>();
collectLayersInfo(moduleLayers, allUnqualifiedExports, allReadableModules, true);

return allReadableModules.stream()
.collect(
Collectors.toMap(
ModuleDescriptor::name,
md -> md.provides()
.stream()
.map(ModuleDescriptor.Provides::service)
.filter(name -> unqualifiedExports.contains(packageName(name)))
.filter(name -> allUnqualifiedExports.contains(packageName(name)))
.collect(Collectors.toSet())
)
);
}

private static void collectLayersInfo(
List<ModuleLayer> moduleLayers,
Set<String> allUnqualifiedExports,
Set<ModuleDescriptor> allReadableModules,
boolean allowBootLayer
) {
if (moduleLayers == null || moduleLayers.isEmpty()) {
return;
}
for (var layer : moduleLayers) {
// ModuleLayer.boot() contains all ES libs too, so we want it to be an explicit parent
if (allowBootLayer || layer != ModuleLayer.boot()) {
allUnqualifiedExports.addAll(unqualifiedExportsForLayer(layer));
allReadableModules.addAll(readableModulesForLayer(layer));
collectLayersInfo(layer.parents(), allUnqualifiedExports, allReadableModules, false);
}
}
}

static UberModuleClassLoader getInstance(ClassLoader parent, String moduleName, Set<URL> jarUrls) {
return getInstance(parent, ModuleLayer.boot(), moduleName, jarUrls, Set.of());
return getInstance(parent, List.of(ModuleLayer.boot()), moduleName, jarUrls, Set.of());
}

@SuppressWarnings("removal")
static UberModuleClassLoader getInstance(
ClassLoader parent,
ModuleLayer parentLayer,
List<ModuleLayer> parentLayers,
String moduleName,
Set<URL> jarUrls,
Set<String> moduleDenyList
) {
Path[] jarPaths = jarUrls.stream().map(UberModuleClassLoader::urlToPathUnchecked).toArray(Path[]::new);
var parentLayerModuleToServiceMap = getModuleToServiceMap(parentLayer);
var parentLayerModuleToServiceMap = getModuleToServiceMap(parentLayers);
Set<String> requires = parentLayerModuleToServiceMap.keySet()
.stream()
.filter(Predicate.not(moduleDenyList::contains))
Expand All @@ -110,10 +142,15 @@ static UberModuleClassLoader getInstance(
jarPaths,
requires,
uses,
s -> isPackageInLayers(s, parentLayer)
s -> isPackageInLayers(s, parentLayers)
);
// TODO: check that denied modules are not brought as transitive dependencies (or switch to allow-list?)
Configuration cf = parentLayer.configuration().resolve(finder, ModuleFinder.of(), Set.of(moduleName));
Configuration cf = Configuration.resolve(
finder,
parentLayers.stream().map(ModuleLayer::configuration).toList(),
ModuleFinder.of(),
Set.of(moduleName)
);

Set<String> packageNames = finder.find(moduleName).map(ModuleReference::descriptor).map(ModuleDescriptor::packages).orElseThrow();

Expand All @@ -122,20 +159,25 @@ static UberModuleClassLoader getInstance(
moduleName,
jarUrls.toArray(new URL[0]),
cf,
parentLayer,
parentLayers,
packageNames
);
return AccessController.doPrivileged(pa);
}

private static boolean isPackageInLayers(String packageName, ModuleLayer moduleLayer) {
if (moduleLayer.modules().stream().map(Module::getPackages).anyMatch(p -> p.contains(packageName))) {
private static boolean isPackageInLayers(String packageName, List<ModuleLayer> moduleLayers) {
if (moduleLayers.stream().flatMap(x -> x.modules().stream()).map(Module::getPackages).anyMatch(p -> p.contains(packageName))) {
return true;
}
if (moduleLayer.parents().equals(List.of(ModuleLayer.empty()))) {
return false;
for (var moduleLayer : moduleLayers) {
if (moduleLayer.parents().equals(List.of(ModuleLayer.empty()))) {
continue;
}
if (moduleLayer.parents().stream().anyMatch(ml -> isPackageInLayers(packageName, List.of(ml)))) {
return true;
}
}
return moduleLayer.parents().stream().anyMatch(ml -> isPackageInLayers(packageName, ml));
return false;
}

/**
Expand All @@ -146,7 +188,7 @@ private UberModuleClassLoader(
String moduleName,
URL[] jarURLs,
Configuration cf,
ModuleLayer mparent,
List<ModuleLayer> mparent,
Set<String> packageNames
) {
super(parent);
Expand All @@ -157,7 +199,7 @@ private UberModuleClassLoader(
// Defining a module layer tells the Java virtual machine about the
// classes that may be loaded from the module, and is what makes the
// Class::getModule call return the name of our ubermodule.
this.moduleController = ModuleLayer.defineModules(cf, List.of(mparent), s -> this);
this.moduleController = ModuleLayer.defineModules(cf, mparent, s -> this);
this.module = this.moduleController.layer().findModule(moduleName).orElseThrow();

this.packageNames = packageNames;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ public String toString() {
try (
UberModuleClassLoader denyListLoader = UberModuleClassLoader.getInstance(
UberModuleClassLoaderTests.class.getClassLoader(),
ModuleLayer.boot(),
List.of(ModuleLayer.boot()),
"synthetic",
Set.of(toUrl(jar)),
Set.of("java.sql", "java.sql.rowset") // if present, java.sql.rowset requires java.sql transitively
Expand Down Expand Up @@ -614,7 +614,7 @@ private static UberModuleClassLoader getServiceTestLoader(boolean includeOptiona
Set<Path> jarPaths = new HashSet<>(Set.of(modularJar, nonModularJar, serviceCallerJar));
return UberModuleClassLoader.getInstance(
parentLayer.findLoader(includeOptionalDeps ? "p.optional" : "p.required"),
parentLayer,
List.of(parentLayer),
"synthetic",
jarPaths.stream().map(UberModuleClassLoaderTests::pathToUrlUnchecked).collect(Collectors.toSet()),
Set.of()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public class RestJvmCrashAction implements RestHandler {
static {
try {
AccessController.doPrivileged((PrivilegedExceptionAction<?>) () -> {
Class<?> unsafe = Class.forName("sun.misc.Unsafe");
// Bypass UberModuleClassLoader, as server does not have our very dangerous permissions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shows an interesting side effect of using UberModuleClassLoader: for class loading, the server module/ProtectionDomain gets in the way. So if it does not have permissions (like accessClassInPackage.sun.misc), no matter if we grant them to the plugin, it won't work. But I don't think this is a problem.

Class<?> unsafe = ClassLoader.getSystemClassLoader().loadClass("sun.misc.Unsafe");

FREE_MEMORY = unsafe.getMethod("freeMemory", long.class);
Field f = unsafe.getDeclaredField("theUnsafe");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ grant {
permission java.lang.RuntimePermission "accessDeclaredMembers";
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.lang.RuntimePermission "accessClassInPackage.sun.misc";
permission java.lang.RuntimePermission "getClassLoader";
};
Loading