diff --git a/libs/entitlement/asm-provider/src/main/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterImpl.java b/libs/entitlement/asm-provider/src/main/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterImpl.java index ed9a5b2cc9c6b..ed13f6d67014b 100644 --- a/libs/entitlement/asm-provider/src/main/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterImpl.java +++ b/libs/entitlement/asm-provider/src/main/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterImpl.java @@ -37,10 +37,8 @@ import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; import static org.objectweb.asm.Opcodes.ACC_STATIC; -import static org.objectweb.asm.Opcodes.GETSTATIC; import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; import static org.objectweb.asm.Opcodes.INVOKESTATIC; -import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; public class InstrumenterImpl implements Instrumenter { private static final Logger logger = LogManager.getLogger(InstrumenterImpl.class); @@ -286,22 +284,9 @@ private void pushCallerClass() { false ); } else { - mv.visitFieldInsn( - GETSTATIC, - Type.getInternalName(StackWalker.Option.class), - "RETAIN_CLASS_REFERENCE", - Type.getDescriptor(StackWalker.Option.class) - ); mv.visitMethodInsn( INVOKESTATIC, - Type.getInternalName(StackWalker.class), - "getInstance", - Type.getMethodDescriptor(Type.getType(StackWalker.class), Type.getType(StackWalker.Option.class)), - false - ); - mv.visitMethodInsn( - INVOKEVIRTUAL, - Type.getInternalName(StackWalker.class), + "org/elasticsearch/entitlement/bridge/Util", "getCallerClass", Type.getMethodDescriptor(Type.getType(Class.class)), false diff --git a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementCheckerHandle.java b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementCheckerHandle.java index 26c9c83b8eb51..c0344e4a8c10c 100644 --- a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementCheckerHandle.java +++ b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementCheckerHandle.java @@ -18,6 +18,7 @@ public class EntitlementCheckerHandle { * This is how the bytecodes injected by our instrumentation access the {@link EntitlementChecker} * so they can call the appropriate check method. */ + @SuppressWarnings("unused") public static EntitlementChecker instance() { return Holder.instance; } diff --git a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/Util.java b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/Util.java new file mode 100644 index 0000000000000..9fd34d3b72c2c --- /dev/null +++ b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/Util.java @@ -0,0 +1,44 @@ +/* + * 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.bridge; + +import java.util.Optional; + +import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; + +public class Util { + /** + * A special value representing the case where a method has no caller. + * This can occur if it's called directly from the JVM. + * + * @see StackWalker#getCallerClass() + */ + public static final Class NO_CLASS = new Object() { + }.getClass(); + + /** + * Why would we write this instead of using {@link StackWalker#getCallerClass()}? + * Because that method throws {@link IllegalCallerException} if called from the "outermost frame", + * which includes at least some cases of a method called from a native frame. + * + * @return the class that called the method which called this; or {@link #NO_CLASS} from the outermost frame. + */ + @SuppressWarnings("unused") // Called reflectively from InstrumenterImpl + public static Class getCallerClass() { + Optional> callerClassIfAny = StackWalker.getInstance(RETAIN_CLASS_REFERENCE) + .walk( + frames -> frames.skip(2) // Skip this method and its caller + .findFirst() + .map(StackWalker.StackFrame::getDeclaringClass) + ); + return callerClassIfAny.orElse(NO_CLASS); + } + +} 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 2aed7e001d762..ce59bf107b788 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 @@ -57,6 +57,7 @@ import static java.util.stream.Collectors.toUnmodifiableMap; import static java.util.zip.ZipFile.OPEN_DELETE; import static java.util.zip.ZipFile.OPEN_READ; +import static org.elasticsearch.entitlement.bridge.Util.NO_CLASS; public class PolicyManager { /** @@ -712,8 +713,6 @@ Class requestingClass(Class callerClass) { /** * Given a stream of {@link StackFrame}s, identify the one whose entitlements should be checked. - * - * @throws NullPointerException if the requesting module is {@code null} */ Optional findRequestingFrame(Stream frames) { return frames.filter(f -> f.getDeclaringClass().getModule() != entitlementsModule) // ignore entitlements library @@ -732,6 +731,10 @@ private static boolean isTriviallyAllowed(Class requestingClass) { generalLogger.debug("Entitlement trivially allowed: no caller frames outside the entitlement library"); return true; } + if (requestingClass == NO_CLASS) { + generalLogger.debug("Entitlement trivially allowed from outermost frame"); + return true; + } if (SYSTEM_LAYER_MODULES.contains(requestingClass.getModule())) { generalLogger.debug("Entitlement trivially allowed from system module [{}]", requestingClass.getModule().getName()); return true; diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/bridge/UtilTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/bridge/UtilTests.java new file mode 100644 index 0000000000000..e1bf161174bb3 --- /dev/null +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/bridge/UtilTests.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.bridge; + +import org.elasticsearch.test.ESTestCase; + +import static org.elasticsearch.entitlement.bridge.UtilTests.MockSensitiveClass.mockSensitiveMethod; + +@ESTestCase.WithoutSecurityManager +public class UtilTests extends ESTestCase { + + public void testCallerClass() { + assertEquals(UtilTests.class, mockSensitiveMethod()); + } + + /** + * A separate class so the stack walk can discern the sensitive method's own class + * from that of its caller. + */ + static class MockSensitiveClass { + public static Class mockSensitiveMethod() { + return Util.getCallerClass(); + } + } + +}