diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/HardcodedEntitlements.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/HardcodedEntitlements.java index bdc4c92b404aa..a1cbf7772ae29 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/HardcodedEntitlements.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/HardcodedEntitlements.java @@ -114,7 +114,13 @@ private static List createServerEntitlements(Path pidFile) { new FilesEntitlement(serverModuleFileDatas) ) ), - new Scope("java.desktop", List.of(new LoadNativeLibrariesEntitlement())), + new Scope( + "java.desktop", + List.of( + new LoadNativeLibrariesEntitlement(), + new ManageThreadsEntitlement() // For sun.java2d.Disposer. TODO: https://elasticco.atlassian.net/browse/ES-12888 + ) + ), new Scope( "java.xml", List.of( diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index 3c5948d7824b7..0a02be6bcbe68 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -54,7 +54,7 @@ public class PolicyManager { */ static final Logger generalLogger = LogManager.getLogger(PolicyManager.class); - static final Set MODULES_EXCLUDED_FROM_SYSTEM_MODULES = Set.of("java.desktop", "java.xml"); + public static final Set MODULES_EXCLUDED_FROM_SYSTEM_MODULES = Set.of("java.desktop", "java.xml"); /** * Identifies a particular entitlement {@link Scope} within a {@link Policy}. @@ -94,7 +94,7 @@ public enum ComponentKind { * If this kind corresponds to a single component, this is that component's name; * otherwise null. */ - final String componentName; + public final String componentName; ComponentKind(String componentName) { this.componentName = componentName; diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/bootstrap/HardcodedEntitlementsTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/bootstrap/HardcodedEntitlementsTests.java new file mode 100644 index 0000000000000..e297f64453cfc --- /dev/null +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/bootstrap/HardcodedEntitlementsTests.java @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.entitlement.bootstrap; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.ESTestCase.WithEntitlementsOnTestCode; + +import java.io.ByteArrayInputStream; + +import javax.imageio.stream.MemoryCacheImageInputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@WithEntitlementsOnTestCode +public class HardcodedEntitlementsTests extends ESTestCase { + + /** + * The Tika library can do some things we don't ordinarily want to allow. + *

+ * Note that {@link MemoryCacheImageInputStream} doesn't even use {@code Disposer} in JDK 26, + * so it's an open question how much effort this deserves. + */ + public void testTikaPDF() { + new MemoryCacheImageInputStream(new ByteArrayInputStream("test test".getBytes(UTF_8))); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java b/test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java index 3a42485822f3c..91662f3f35773 100644 --- a/test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java +++ b/test/framework/src/main/java/org/elasticsearch/bootstrap/TestScopeResolver.java @@ -9,11 +9,14 @@ package org.elasticsearch.bootstrap; +import org.elasticsearch.core.Nullable; import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.entitlement.runtime.policy.PolicyManager; +import org.elasticsearch.entitlement.runtime.policy.PolicyManager.PolicyScope; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; import java.net.MalformedURLException; import java.net.URL; import java.util.List; @@ -22,39 +25,78 @@ import java.util.TreeMap; import java.util.function.Function; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toSet; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.PLUGIN; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ComponentKind.SERVER; +import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.MODULES_EXCLUDED_FROM_SYSTEM_MODULES; -public record TestScopeResolver(Map scopeMap) { +public final class TestScopeResolver { private static final Logger logger = LogManager.getLogger(TestScopeResolver.class); + private final Map scopeMap; + private static final Map excludedSystemPackageScopes = computeExcludedSystemPackageScopes(); - PolicyManager.PolicyScope getScope(Class callerClass) { + public TestScopeResolver(Map scopeMap) { + this.scopeMap = scopeMap; + } + + private static Map computeExcludedSystemPackageScopes() { + // Within any one module layer, module names are unique, so we just need the names + Set systemModuleNames = ModuleFinder.ofSystem() + .findAll() + .stream() + .map(ref -> ref.descriptor().name()) + .filter(MODULES_EXCLUDED_FROM_SYSTEM_MODULES::contains) + .collect(toSet()); + + Map result = new TreeMap<>(); + ModuleLayer.boot().modules().stream().filter(m -> systemModuleNames.contains(m.getName())).forEach(m -> { + ModuleDescriptor desc = m.getDescriptor(); + if (desc != null) { + desc.packages().forEach(pkg -> + // Our component identification logic returns SERVER for these + result.put(pkg, new PolicyScope(SERVER, SERVER.componentName, m.getName()))); + } + }); + return result; + } + + public static @Nullable PolicyScope getExcludedSystemPackageScope(Class callerClass) { + return excludedSystemPackageScopes.get(callerClass.getPackageName()); + } + + PolicyScope getScope(Class callerClass) { var callerCodeSource = callerClass.getProtectionDomain().getCodeSource(); - assert callerCodeSource != null; + if (callerCodeSource == null) { + // This only happens for JDK classes. Furthermore, for trivially allowed modules, we shouldn't even get here. + // Hence, this must be an excluded system module, so check for that. + return requireNonNull(getExcludedSystemPackageScope(callerClass)); + } var location = callerCodeSource.getLocation().toString(); var scope = scopeMap.get(location); if (scope == null) { // Special cases for libraries not handled by our automatically-generated scopeMap if (callerClass.getPackageName().startsWith("org.bouncycastle")) { - scope = new PolicyManager.PolicyScope(PLUGIN, "security", ALL_UNNAMED); + scope = new PolicyScope(PLUGIN, "security", ALL_UNNAMED); logger.debug("Assuming bouncycastle is part of the security plugin"); } } if (scope == null) { logger.warn("Cannot identify a scope for class [{}], location [{}]", callerClass.getName(), location); - return PolicyManager.PolicyScope.unknown(location); + return PolicyScope.unknown(location); } return scope; } - public static Function, PolicyManager.PolicyScope> createScopeResolver( + public static Function, PolicyScope> createScopeResolver( TestBuildInfo serverBuildInfo, List pluginsBuildInfo, Set modularPlugins ) { - Map scopeMap = new TreeMap<>(); // Sorted to make it easier to read during debugging + Map scopeMap = new TreeMap<>(); // Sorted to make it easier to read during debugging for (var pluginBuildInfo : pluginsBuildInfo) { boolean isModular = modularPlugins.contains(pluginBuildInfo.component()); for (var location : pluginBuildInfo.locations()) { @@ -66,7 +108,7 @@ public static Function, PolicyManager.PolicyScope> createScopeResolver( String module = isModular ? location.module() : ALL_UNNAMED; scopeMap.put( getCodeSource(codeSource, location.representativeClass()), - PolicyManager.PolicyScope.plugin(pluginBuildInfo.component(), module) + PolicyScope.plugin(pluginBuildInfo.component(), module) ); } catch (MalformedURLException e) { throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]", e); @@ -81,7 +123,7 @@ public static Function, PolicyManager.PolicyScope> createScopeResolver( continue; } try { - scopeMap.put(getCodeSource(classUrl, location.representativeClass()), PolicyManager.PolicyScope.server(location.module())); + scopeMap.put(getCodeSource(classUrl, location.representativeClass()), PolicyScope.server(location.module())); } catch (MalformedURLException e) { throw new IllegalArgumentException("Cannot locate class [" + location.representativeClass() + "]", e); } diff --git a/test/framework/src/main/java/org/elasticsearch/entitlement/runtime/policy/TestPolicyManager.java b/test/framework/src/main/java/org/elasticsearch/entitlement/runtime/policy/TestPolicyManager.java index f7397c8f898ed..83f85458b2f55 100644 --- a/test/framework/src/main/java/org/elasticsearch/entitlement/runtime/policy/TestPolicyManager.java +++ b/test/framework/src/main/java/org/elasticsearch/entitlement/runtime/policy/TestPolicyManager.java @@ -9,6 +9,7 @@ package org.elasticsearch.entitlement.runtime.policy; +import org.elasticsearch.bootstrap.TestScopeResolver; import org.elasticsearch.common.util.ArrayUtils; import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement; import org.elasticsearch.test.ESTestCase; @@ -97,6 +98,10 @@ public final void clearModuleEntitlementsCache() { @Override protected boolean isTrustedSystemClass(Class requestingClass) { + if (TestScopeResolver.getExcludedSystemPackageScope(requestingClass) != null) { + // We don't trust the excluded packages even though they are in system modules + return false; + } ClassLoader loader = requestingClass.getClassLoader(); return loader == null || loader == ClassLoader.getPlatformClassLoader(); }