Skip to content

Commit 343ec0b

Browse files
authored
Restrict apm agent entitlements to the apm package in an unnamed module (#120546)
This change closes a hole where we assumed any check against an unnamed-module from any classloader was for one of our apm agent. This was not the case and made it so scripts could in theory have the same entitlements as apm agent. Instead we now check to see if a class is part of the apm package in an unnamed module to ensure it's actually for the apm agent. Relates to ES-10192
1 parent 0666462 commit 343ec0b

File tree

6 files changed

+132
-10
lines changed

6 files changed

+132
-10
lines changed

docs/changelog/120546.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 120546
2+
summary: Restrict agent entitlements to the system classloader unnamed module
3+
area: Infra/Plugins
4+
type: bug
5+
issues: []

libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
*/
4747
public class EntitlementInitialization {
4848

49+
private static final String AGENTS_PACKAGE_NAME = "co.elastic.apm.agent";
4950
private static final Module ENTITLEMENTS_MODULE = PolicyManager.class.getModule();
5051

5152
private static ElasticsearchEntitlementChecker manager;
@@ -107,7 +108,7 @@ private static PolicyManager createPolicyManager() {
107108
// this should be removed once https://github.com/elastic/elasticsearch/issues/109335 is completed
108109
List<Entitlement> agentEntitlements = List.of(new CreateClassLoaderEntitlement());
109110
var resolver = EntitlementBootstrap.bootstrapArgs().pluginResolver();
110-
return new PolicyManager(serverPolicy, agentEntitlements, pluginPolicies, resolver, ENTITLEMENTS_MODULE);
111+
return new PolicyManager(serverPolicy, agentEntitlements, pluginPolicies, resolver, AGENTS_PACKAGE_NAME, ENTITLEMENTS_MODULE);
111112
}
112113

113114
private static ElasticsearchEntitlementChecker initChecker() {

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ private static Set<Module> findSystemModules() {
8484
.collect(Collectors.toUnmodifiableSet());
8585
}
8686

87+
/**
88+
* The package name containing agent classes.
89+
*/
90+
private final String agentsPackageName;
91+
8792
/**
8893
* Frames originating from this module are ignored in the permission logic.
8994
*/
@@ -94,6 +99,7 @@ public PolicyManager(
9499
List<Entitlement> agentEntitlements,
95100
Map<String, Policy> pluginPolicies,
96101
Function<Class<?>, String> pluginResolver,
102+
String agentsPackageName,
97103
Module entitlementsModule
98104
) {
99105
this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy));
@@ -102,6 +108,7 @@ public PolicyManager(
102108
.stream()
103109
.collect(toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue())));
104110
this.pluginResolver = pluginResolver;
111+
this.agentsPackageName = agentsPackageName;
105112
this.entitlementsModule = entitlementsModule;
106113
}
107114

@@ -318,8 +325,8 @@ private ModuleEntitlements computeEntitlements(Class<?> requestingClass) {
318325
}
319326
}
320327

321-
if (requestingModule.isNamed() == false) {
322-
// agents are the only thing running non-modular
328+
if (requestingModule.isNamed() == false && requestingClass.getPackageName().startsWith(agentsPackageName)) {
329+
// agents are the only thing running non-modular in the system classloader
323330
return ModuleEntitlements.from(agentEntitlements);
324331
}
325332

libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
package org.elasticsearch.entitlement.runtime.policy;
1111

1212
import org.elasticsearch.entitlement.runtime.policy.PolicyManager.ModuleEntitlements;
13+
import org.elasticsearch.entitlement.runtime.policy.agent.TestAgent;
14+
import org.elasticsearch.entitlement.runtime.policy.agent.inner.TestInnerAgent;
1315
import org.elasticsearch.test.ESTestCase;
1416
import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
1517
import org.elasticsearch.test.jar.JarUtils;
@@ -18,6 +20,8 @@
1820
import java.io.IOException;
1921
import java.lang.module.Configuration;
2022
import java.lang.module.ModuleFinder;
23+
import java.net.URL;
24+
import java.net.URLClassLoader;
2125
import java.nio.file.Path;
2226
import java.util.Arrays;
2327
import java.util.List;
@@ -36,6 +40,12 @@
3640

3741
@ESTestCase.WithoutSecurityManager
3842
public class PolicyManagerTests extends ESTestCase {
43+
44+
/**
45+
* A test agent package name for use in tests.
46+
*/
47+
private static final String TEST_AGENTS_PACKAGE_NAME = "org.elasticsearch.entitlement.runtime.policy.agent";
48+
3949
/**
4050
* A module you can use for test cases that don't actually care about the
4151
* entitlement module.
@@ -59,6 +69,7 @@ public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() {
5969
List.of(),
6070
Map.of("plugin1", createPluginPolicy("plugin.module")),
6171
c -> "plugin1",
72+
TEST_AGENTS_PACKAGE_NAME,
6273
NO_ENTITLEMENTS_MODULE
6374
);
6475

@@ -72,7 +83,14 @@ public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() {
7283
}
7384

7485
public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() {
75-
var policyManager = new PolicyManager(createEmptyTestServerPolicy(), List.of(), Map.of(), c -> "plugin1", NO_ENTITLEMENTS_MODULE);
86+
var policyManager = new PolicyManager(
87+
createEmptyTestServerPolicy(),
88+
List.of(),
89+
Map.of(),
90+
c -> "plugin1",
91+
TEST_AGENTS_PACKAGE_NAME,
92+
NO_ENTITLEMENTS_MODULE
93+
);
7694

7795
// Any class from the current module (unnamed) will do
7896
var callerClass = this.getClass();
@@ -84,7 +102,14 @@ public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() {
84102
}
85103

86104
public void testGetEntitlementsFailureIsCached() {
87-
var policyManager = new PolicyManager(createEmptyTestServerPolicy(), List.of(), Map.of(), c -> "plugin1", NO_ENTITLEMENTS_MODULE);
105+
var policyManager = new PolicyManager(
106+
createEmptyTestServerPolicy(),
107+
List.of(),
108+
Map.of(),
109+
c -> "plugin1",
110+
TEST_AGENTS_PACKAGE_NAME,
111+
NO_ENTITLEMENTS_MODULE
112+
);
88113

89114
// Any class from the current module (unnamed) will do
90115
var callerClass = this.getClass();
@@ -106,6 +131,7 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() {
106131
List.of(),
107132
Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))),
108133
c -> "plugin2",
134+
TEST_AGENTS_PACKAGE_NAME,
109135
NO_ENTITLEMENTS_MODULE
110136
);
111137

@@ -117,7 +143,14 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() {
117143
}
118144

119145
public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotFoundException {
120-
var policyManager = new PolicyManager(createTestServerPolicy("example"), List.of(), Map.of(), c -> null, NO_ENTITLEMENTS_MODULE);
146+
var policyManager = new PolicyManager(
147+
createTestServerPolicy("example"),
148+
List.of(),
149+
Map.of(),
150+
c -> null,
151+
TEST_AGENTS_PACKAGE_NAME,
152+
NO_ENTITLEMENTS_MODULE
153+
);
121154

122155
// Tests do not run modular, so we cannot use a server class.
123156
// But we know that in production code the server module and its classes are in the boot layer.
@@ -137,6 +170,7 @@ public void testGetEntitlementsReturnsEntitlementsForServerModule() throws Class
137170
List.of(),
138171
Map.of(),
139172
c -> null,
173+
TEST_AGENTS_PACKAGE_NAME,
140174
NO_ENTITLEMENTS_MODULE
141175
);
142176

@@ -161,6 +195,7 @@ public void testGetEntitlementsReturnsEntitlementsForPluginModule() throws IOExc
161195
List.of(),
162196
Map.of("mock-plugin", createPluginPolicy("org.example.plugin")),
163197
c -> "mock-plugin",
198+
TEST_AGENTS_PACKAGE_NAME,
164199
NO_ENTITLEMENTS_MODULE
165200
);
166201

@@ -181,6 +216,7 @@ public void testGetEntitlementsResultIsCached() {
181216
List.of(),
182217
Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))),
183218
c -> "plugin2",
219+
TEST_AGENTS_PACKAGE_NAME,
184220
NO_ENTITLEMENTS_MODULE
185221
);
186222

@@ -200,16 +236,17 @@ public void testGetEntitlementsResultIsCached() {
200236

201237
public void testRequestingClassFastPath() throws IOException, ClassNotFoundException {
202238
var callerClass = makeClassInItsOwnModule();
203-
assertEquals(callerClass, policyManagerWithEntitlementsModule(NO_ENTITLEMENTS_MODULE).requestingClass(callerClass));
239+
assertEquals(callerClass, policyManager(TEST_AGENTS_PACKAGE_NAME, NO_ENTITLEMENTS_MODULE).requestingClass(callerClass));
204240
}
205241

206242
public void testRequestingModuleWithStackWalk() throws IOException, ClassNotFoundException {
243+
var agentsClass = new TestAgent();
207244
var entitlementsClass = makeClassInItsOwnModule(); // A class in the entitlements library itself
208245
var requestingClass = makeClassInItsOwnModule(); // This guy is always the right answer
209246
var instrumentedClass = makeClassInItsOwnModule(); // The class that called the check method
210247
var ignorableClass = makeClassInItsOwnModule();
211248

212-
var policyManager = policyManagerWithEntitlementsModule(entitlementsClass.getModule());
249+
var policyManager = policyManager(TEST_AGENTS_PACKAGE_NAME, entitlementsClass.getModule());
213250

214251
assertEquals(
215252
"Skip entitlement library and the instrumented method",
@@ -229,15 +266,47 @@ public void testRequestingModuleWithStackWalk() throws IOException, ClassNotFoun
229266
);
230267
}
231268

269+
public void testAgentsEntitlements() throws IOException, ClassNotFoundException {
270+
Path home = createTempDir();
271+
Path unnamedJar = createMockPluginJarForUnnamedModule(home);
272+
var notAgentClass = makeClassInItsOwnModule();
273+
var policyManager = new PolicyManager(
274+
createEmptyTestServerPolicy(),
275+
List.of(new CreateClassLoaderEntitlement()),
276+
Map.of(),
277+
c -> "test",
278+
TEST_AGENTS_PACKAGE_NAME,
279+
NO_ENTITLEMENTS_MODULE
280+
);
281+
ModuleEntitlements agentsEntitlements = policyManager.getEntitlements(TestAgent.class);
282+
assertThat(agentsEntitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(true));
283+
agentsEntitlements = policyManager.getEntitlements(TestInnerAgent.class);
284+
assertThat(agentsEntitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(true));
285+
ModuleEntitlements notAgentsEntitlements = policyManager.getEntitlements(notAgentClass);
286+
assertThat(notAgentsEntitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(false));
287+
try (URLClassLoader classLoader = new URLClassLoader(new URL[] { unnamedJar.toUri().toURL() }, getClass().getClassLoader())) {
288+
var unnamedNotAgentClass = classLoader.loadClass("q.B");
289+
notAgentsEntitlements = policyManager.getEntitlements(unnamedNotAgentClass);
290+
assertThat(notAgentsEntitlements.hasEntitlement(CreateClassLoaderEntitlement.class), is(false));
291+
}
292+
}
293+
232294
private static Class<?> makeClassInItsOwnModule() throws IOException, ClassNotFoundException {
233295
final Path home = createTempDir();
234296
Path jar = createMockPluginJar(home);
235297
var layer = createLayerForJar(jar, "org.example.plugin");
236298
return layer.findLoader("org.example.plugin").loadClass("q.B");
237299
}
238300

239-
private static PolicyManager policyManagerWithEntitlementsModule(Module entitlementsModule) {
240-
return new PolicyManager(createEmptyTestServerPolicy(), List.of(), Map.of(), c -> "test", entitlementsModule);
301+
private static Class<?> makeClassInItsOwnUnnamedModule() throws IOException, ClassNotFoundException {
302+
final Path home = createTempDir();
303+
Path jar = createMockPluginJar(home);
304+
var layer = createLayerForJar(jar, "org.example.plugin");
305+
return layer.findLoader("org.example.plugin").loadClass("q.B");
306+
}
307+
308+
private static PolicyManager policyManager(String agentsPackageName, Module entitlementsModule) {
309+
return new PolicyManager(createEmptyTestServerPolicy(), List.of(), Map.of(), c -> "test", agentsPackageName, entitlementsModule);
241310
}
242311

243312
private static Policy createEmptyTestServerPolicy() {
@@ -262,6 +331,16 @@ private static Policy createPluginPolicy(String... pluginModules) {
262331
);
263332
}
264333

334+
private static Path createMockPluginJarForUnnamedModule(Path home) throws IOException {
335+
Path jar = home.resolve("unnamed-mock-plugin.jar");
336+
337+
Map<String, CharSequence> sources = Map.ofEntries(entry("q.B", "package q; public class B { }"));
338+
339+
var classToBytes = InMemoryJavaCompiler.compile(sources);
340+
JarUtils.createJarWithEntries(jar, Map.ofEntries(entry("q/B.class", classToBytes.get("q.B"))));
341+
return jar;
342+
}
343+
265344
private static Path createMockPluginJar(Path home) throws IOException {
266345
Path jar = home.resolve("mock-plugin.jar");
267346

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.runtime.policy.agent;
11+
12+
/**
13+
* Dummy class for testing agent entitlements.
14+
*/
15+
public class TestAgent {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.runtime.policy.agent.inner;
11+
12+
/**
13+
* Dummy class for testing agent entitlements.
14+
*/
15+
public class TestInnerAgent {}

0 commit comments

Comments
 (0)