Skip to content

Commit 9cc362b

Browse files
prdoyleelasticsearchmachine
andauthored
Entitlements: More robust frame skipping (#118983)
* More robust frame skipping * Cosmetic improvements for clarity * Explicit set of runtime classes * Pass entitlements runtime module to PolicyManager ctor * Use the term "entitlements module" and filter instead of dropWhile * [CI] Auto commit changes from spotless --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent 65faabd commit 9cc362b

File tree

3 files changed

+140
-28
lines changed

3 files changed

+140
-28
lines changed

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
@@ -53,6 +53,7 @@
5353
public class EntitlementInitialization {
5454

5555
private static final String POLICY_FILE_NAME = "entitlement-policy.yaml";
56+
private static final Module ENTITLEMENTS_MODULE = PolicyManager.class.getModule();
5657

5758
private static ElasticsearchEntitlementChecker manager;
5859

@@ -92,7 +93,7 @@ private static PolicyManager createPolicyManager() throws IOException {
9293
"server",
9394
List.of(new Scope("org.elasticsearch.server", List.of(new ExitVMEntitlement(), new CreateClassLoaderEntitlement())))
9495
);
95-
return new PolicyManager(serverPolicy, pluginPolicies, EntitlementBootstrap.bootstrapArgs().pluginResolver());
96+
return new PolicyManager(serverPolicy, pluginPolicies, EntitlementBootstrap.bootstrapArgs().pluginResolver(), ENTITLEMENTS_MODULE);
9697
}
9798

9899
private static Map<String, Policy> createPluginPolicies(Collection<EntitlementBootstrap.PluginData> pluginData) throws IOException {

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

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.elasticsearch.logging.LogManager;
1616
import org.elasticsearch.logging.Logger;
1717

18+
import java.lang.StackWalker.StackFrame;
1819
import java.lang.module.ModuleFinder;
1920
import java.lang.module.ModuleReference;
2021
import java.util.ArrayList;
@@ -29,6 +30,10 @@
2930
import java.util.stream.Collectors;
3031
import java.util.stream.Stream;
3132

33+
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
34+
import static java.util.Objects.requireNonNull;
35+
import static java.util.function.Predicate.not;
36+
3237
public class PolicyManager {
3338
private static final Logger logger = LogManager.getLogger(ElasticsearchEntitlementChecker.class);
3439

@@ -63,6 +68,11 @@ public <E extends Entitlement> Stream<E> getEntitlements(Class<E> entitlementCla
6368

6469
private static final Set<Module> systemModules = findSystemModules();
6570

71+
/**
72+
* Frames originating from this module are ignored in the permission logic.
73+
*/
74+
private final Module entitlementsModule;
75+
6676
private static Set<Module> findSystemModules() {
6777
var systemModulesDescriptors = ModuleFinder.ofSystem()
6878
.findAll()
@@ -77,13 +87,18 @@ private static Set<Module> findSystemModules() {
7787
.collect(Collectors.toUnmodifiableSet());
7888
}
7989

80-
public PolicyManager(Policy defaultPolicy, Map<String, Policy> pluginPolicies, Function<Class<?>, String> pluginResolver) {
81-
this.serverEntitlements = buildScopeEntitlementsMap(Objects.requireNonNull(defaultPolicy));
82-
this.pluginsEntitlements = Objects.requireNonNull(pluginPolicies)
83-
.entrySet()
90+
public PolicyManager(
91+
Policy defaultPolicy,
92+
Map<String, Policy> pluginPolicies,
93+
Function<Class<?>, String> pluginResolver,
94+
Module entitlementsModule
95+
) {
96+
this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(defaultPolicy));
97+
this.pluginsEntitlements = requireNonNull(pluginPolicies).entrySet()
8498
.stream()
8599
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue())));
86100
this.pluginResolver = pluginResolver;
101+
this.entitlementsModule = entitlementsModule;
87102
}
88103

89104
private static Map<String, List<Entitlement>> buildScopeEntitlementsMap(Policy policy) {
@@ -185,29 +200,51 @@ private static boolean isServerModule(Module requestingModule) {
185200
return requestingModule.isNamed() && requestingModule.getLayer() == ModuleLayer.boot();
186201
}
187202

188-
private static Module requestingModule(Class<?> callerClass) {
203+
/**
204+
* Walks the stack to determine which module's entitlements should be checked.
205+
*
206+
* @param callerClass when non-null will be used if its module is suitable;
207+
* this is a fast-path check that can avoid the stack walk
208+
* in cases where the caller class is available.
209+
* @return the requesting module, or {@code null} if the entire call stack
210+
* comes from modules that are trusted.
211+
*/
212+
Module requestingModule(Class<?> callerClass) {
189213
if (callerClass != null) {
190214
Module callerModule = callerClass.getModule();
191215
if (systemModules.contains(callerModule) == false) {
192216
// fast path
193217
return callerModule;
194218
}
195219
}
196-
int framesToSkip = 1 // getCallingClass (this method)
197-
+ 1 // the checkXxx method
198-
+ 1 // the runtime config method
199-
+ 1 // the instrumented method
200-
;
201-
Optional<Module> module = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
202-
.walk(
203-
s -> s.skip(framesToSkip)
204-
.map(f -> f.getDeclaringClass().getModule())
205-
.filter(m -> systemModules.contains(m) == false)
206-
.findFirst()
207-
);
220+
Optional<Module> module = StackWalker.getInstance(RETAIN_CLASS_REFERENCE)
221+
.walk(frames -> findRequestingModule(frames.map(StackFrame::getDeclaringClass)));
208222
return module.orElse(null);
209223
}
210224

225+
/**
226+
* Given a stream of classes corresponding to the frames from a {@link StackWalker},
227+
* returns the module whose entitlements should be checked.
228+
*
229+
* @throws NullPointerException if the requesting module is {@code null}
230+
*/
231+
Optional<Module> findRequestingModule(Stream<Class<?>> classes) {
232+
return classes.map(Objects::requireNonNull)
233+
.map(PolicyManager::moduleOf)
234+
.filter(m -> m != entitlementsModule) // Ignore the entitlements library itself
235+
.filter(not(systemModules::contains)) // Skip trusted JDK modules
236+
.findFirst();
237+
}
238+
239+
private static Module moduleOf(Class<?> c) {
240+
var result = c.getModule();
241+
if (result == null) {
242+
throw new NullPointerException("Entitlements system does not support non-modular class [" + c.getName() + "]");
243+
} else {
244+
return result;
245+
}
246+
}
247+
211248
private static boolean isTriviallyAllowed(Module requestingModule) {
212249
if (requestingModule == null) {
213250
logger.debug("Entitlement trivially allowed: entire call stack is in composed of classes in system modules");

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

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Set;
25+
import java.util.stream.Stream;
2526

2627
import static java.util.Map.entry;
2728
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED;
@@ -37,11 +38,14 @@
3738
@ESTestCase.WithoutSecurityManager
3839
public class PolicyManagerTests extends ESTestCase {
3940

41+
private static final Module NO_ENTITLEMENTS_MODULE = null;
42+
4043
public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() {
4144
var policyManager = new PolicyManager(
4245
createEmptyTestServerPolicy(),
4346
Map.of("plugin1", createPluginPolicy("plugin.module")),
44-
c -> "plugin1"
47+
c -> "plugin1",
48+
NO_ENTITLEMENTS_MODULE
4549
);
4650

4751
// Any class from the current module (unnamed) will do
@@ -62,7 +66,7 @@ public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() {
6266
}
6367

6468
public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() {
65-
var policyManager = new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "plugin1");
69+
var policyManager = new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "plugin1", NO_ENTITLEMENTS_MODULE);
6670

6771
// Any class from the current module (unnamed) will do
6872
var callerClass = this.getClass();
@@ -82,7 +86,7 @@ public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() {
8286
}
8387

8488
public void testGetEntitlementsFailureIsCached() {
85-
var policyManager = new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "plugin1");
89+
var policyManager = new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "plugin1", NO_ENTITLEMENTS_MODULE);
8690

8791
// Any class from the current module (unnamed) will do
8892
var callerClass = this.getClass();
@@ -103,7 +107,8 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() {
103107
var policyManager = new PolicyManager(
104108
createEmptyTestServerPolicy(),
105109
Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))),
106-
c -> "plugin2"
110+
c -> "plugin2",
111+
NO_ENTITLEMENTS_MODULE
107112
);
108113

109114
// Any class from the current module (unnamed) will do
@@ -115,7 +120,7 @@ public void testGetEntitlementsReturnsEntitlementsForPluginUnnamedModule() {
115120
}
116121

117122
public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotFoundException {
118-
var policyManager = new PolicyManager(createTestServerPolicy("example"), Map.of(), c -> null);
123+
var policyManager = new PolicyManager(createTestServerPolicy("example"), Map.of(), c -> null, NO_ENTITLEMENTS_MODULE);
119124

120125
// Tests do not run modular, so we cannot use a server class.
121126
// But we know that in production code the server module and its classes are in the boot layer.
@@ -138,7 +143,7 @@ public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotF
138143
}
139144

140145
public void testGetEntitlementsReturnsEntitlementsForServerModule() throws ClassNotFoundException {
141-
var policyManager = new PolicyManager(createTestServerPolicy("jdk.httpserver"), Map.of(), c -> null);
146+
var policyManager = new PolicyManager(createTestServerPolicy("jdk.httpserver"), Map.of(), c -> null, NO_ENTITLEMENTS_MODULE);
142147

143148
// Tests do not run modular, so we cannot use a server class.
144149
// But we know that in production code the server module and its classes are in the boot layer.
@@ -155,12 +160,13 @@ public void testGetEntitlementsReturnsEntitlementsForServerModule() throws Class
155160
public void testGetEntitlementsReturnsEntitlementsForPluginModule() throws IOException, ClassNotFoundException {
156161
final Path home = createTempDir();
157162

158-
Path jar = creteMockPluginJar(home);
163+
Path jar = createMockPluginJar(home);
159164

160165
var policyManager = new PolicyManager(
161166
createEmptyTestServerPolicy(),
162167
Map.of("mock-plugin", createPluginPolicy("org.example.plugin")),
163-
c -> "mock-plugin"
168+
c -> "mock-plugin",
169+
NO_ENTITLEMENTS_MODULE
164170
);
165171

166172
var layer = createLayerForJar(jar, "org.example.plugin");
@@ -179,7 +185,8 @@ public void testGetEntitlementsResultIsCached() {
179185
var policyManager = new PolicyManager(
180186
createEmptyTestServerPolicy(),
181187
Map.ofEntries(entry("plugin2", createPluginPolicy(ALL_UNNAMED))),
182-
c -> "plugin2"
188+
c -> "plugin2",
189+
NO_ENTITLEMENTS_MODULE
183190
);
184191

185192
// Any class from the current module (unnamed) will do
@@ -197,6 +204,73 @@ public void testGetEntitlementsResultIsCached() {
197204
assertThat(entitlementsAgain, sameInstance(cachedResult));
198205
}
199206

207+
public void testRequestingModuleFastPath() throws IOException, ClassNotFoundException {
208+
var callerClass = makeClassInItsOwnModule();
209+
assertEquals(callerClass.getModule(), policyManagerWithEntitlementsModule(NO_ENTITLEMENTS_MODULE).requestingModule(callerClass));
210+
}
211+
212+
public void testRequestingModuleWithStackWalk() throws IOException, ClassNotFoundException {
213+
var requestingClass = makeClassInItsOwnModule();
214+
var runtimeClass = makeClassInItsOwnModule(); // A class in the entitlements library itself
215+
var ignorableClass = makeClassInItsOwnModule();
216+
var systemClass = Object.class;
217+
218+
var policyManager = policyManagerWithEntitlementsModule(runtimeClass.getModule());
219+
220+
var requestingModule = requestingClass.getModule();
221+
222+
assertEquals(
223+
"Skip one system frame",
224+
requestingModule,
225+
policyManager.findRequestingModule(Stream.of(systemClass, requestingClass, ignorableClass)).orElse(null)
226+
);
227+
assertEquals(
228+
"Skip multiple system frames",
229+
requestingModule,
230+
policyManager.findRequestingModule(Stream.of(systemClass, systemClass, systemClass, requestingClass, ignorableClass))
231+
.orElse(null)
232+
);
233+
assertEquals(
234+
"Skip system frame between runtime frames",
235+
requestingModule,
236+
policyManager.findRequestingModule(Stream.of(runtimeClass, systemClass, runtimeClass, requestingClass, ignorableClass))
237+
.orElse(null)
238+
);
239+
assertEquals(
240+
"Skip runtime frame between system frames",
241+
requestingModule,
242+
policyManager.findRequestingModule(Stream.of(systemClass, runtimeClass, systemClass, requestingClass, ignorableClass))
243+
.orElse(null)
244+
);
245+
assertEquals(
246+
"No system frames",
247+
requestingModule,
248+
policyManager.findRequestingModule(Stream.of(requestingClass, ignorableClass)).orElse(null)
249+
);
250+
assertEquals(
251+
"Skip runtime frames up to the first system frame",
252+
requestingModule,
253+
policyManager.findRequestingModule(Stream.of(runtimeClass, runtimeClass, systemClass, requestingClass, ignorableClass))
254+
.orElse(null)
255+
);
256+
assertThrows(
257+
"Non-modular caller frames are not supported",
258+
NullPointerException.class,
259+
() -> policyManager.findRequestingModule(Stream.of(systemClass, null))
260+
);
261+
}
262+
263+
private static Class<?> makeClassInItsOwnModule() throws IOException, ClassNotFoundException {
264+
final Path home = createTempDir();
265+
Path jar = createMockPluginJar(home);
266+
var layer = createLayerForJar(jar, "org.example.plugin");
267+
return layer.findLoader("org.example.plugin").loadClass("q.B");
268+
}
269+
270+
private static PolicyManager policyManagerWithEntitlementsModule(Module entitlementsModule) {
271+
return new PolicyManager(createEmptyTestServerPolicy(), Map.of(), c -> "test", entitlementsModule);
272+
}
273+
200274
private static Policy createEmptyTestServerPolicy() {
201275
return new Policy("server", List.of());
202276
}
@@ -219,7 +293,7 @@ private static Policy createPluginPolicy(String... pluginModules) {
219293
);
220294
}
221295

222-
private static Path creteMockPluginJar(Path home) throws IOException {
296+
private static Path createMockPluginJar(Path home) throws IOException {
223297
Path jar = home.resolve("mock-plugin.jar");
224298

225299
Map<String, CharSequence> sources = Map.ofEntries(

0 commit comments

Comments
 (0)