Skip to content

Commit 3953331

Browse files
prdoylemark-vieira
andauthored
Entitlements for System.exit (#114015)
* Entitlements for System.exit * Respond to Simon's comments * Rename trampoline -> bridge * Require exactly one bridge jar * Use Type helpers to generate descriptor strings * Various cleanup from PR comments * Remove null "receiver" for static methods * Use List<Type> instead of voidDescriptor * Clarifying comment * Whoops, getMethod * SuppressForbidden System.exit * Spotless * Use embedded provider plugin to keep ASM off classpath * Oops... forgot the punchline * Move ASM license to impl * Use ProviderLocator and simplify bridgeJar logic * Avoid eager resolution of configurations during task configuration * Remove compile-time dependency agent->bridge --------- Co-authored-by: Mark Vieira <[email protected]>
1 parent cd0f9a4 commit 3953331

File tree

30 files changed

+764
-33
lines changed

30 files changed

+764
-33
lines changed

distribution/tools/entitlement-agent/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### Entitlement Agent
22

3-
This is a java agent that instruments sensitive class library methods with calls into the `entitlement-runtime` module to check for permissions granted under the _entitlements_ system.
3+
This is a java agent that instruments sensitive class library methods with calls into the `entitlement-bridge` module to check for permissions granted under the _entitlements_ system.
44

55
The entitlements system provides an alternative to the legacy `SecurityManager` system, which is deprecated for removal.
66
With this agent, the Elasticsearch server can retain some control over which class library methods can be invoked by which callers.

distribution/tools/entitlement-agent/build.gradle

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,44 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10+
import static java.util.stream.Collectors.joining
11+
1012
apply plugin: 'elasticsearch.build'
13+
apply plugin: 'elasticsearch.embedded-providers'
14+
15+
embeddedProviders {
16+
impl 'entitlement-agent', project(':distribution:tools:entitlement-agent:impl')
17+
}
1118

1219
configurations {
13-
entitlementRuntime
20+
entitlementBridge
1421
}
1522

1623
dependencies {
17-
entitlementRuntime project(":libs:elasticsearch-entitlement-runtime")
18-
implementation project(":libs:elasticsearch-entitlement-runtime")
24+
entitlementBridge project(":distribution:tools:entitlement-bridge")
25+
compileOnly project(":libs:elasticsearch-core")
26+
compileOnly project(":distribution:tools:entitlement-runtime")
1927
testImplementation project(":test:framework")
28+
testImplementation project(":distribution:tools:entitlement-bridge")
29+
testImplementation project(":distribution:tools:entitlement-agent:impl")
2030
}
2131

2232
tasks.named('test').configure {
33+
systemProperty "tests.security.manager", "false"
2334
dependsOn('jar')
24-
jvmArgs "-javaagent:${ tasks.named('jar').flatMap{ it.archiveFile }.get()}"
35+
36+
// Register an argument provider to avoid eager resolution of configurations
37+
jvmArgumentProviders.add(new CommandLineArgumentProvider() {
38+
@Override
39+
Iterable<String> asArguments() {
40+
return ["-javaagent:${tasks.jar.archiveFile.get()}", "-Des.entitlements.bridgeJar=${configurations.entitlementBridge.singleFile}"]
41+
}
42+
})
43+
44+
45+
// The Elasticsearch build plugin automatically adds all compileOnly deps as testImplementation.
46+
// We must not add the bridge this way because it is also on the boot classpath, and that would lead to jar hell.
47+
classpath -= files(configurations.entitlementBridge)
2548
}
2649

2750
tasks.named('jar').configure {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
apply plugin: 'elasticsearch.build'
11+
12+
dependencies {
13+
compileOnly project(':distribution:tools:entitlement-agent')
14+
implementation 'org.ow2.asm:asm:9.7'
15+
}
16+
17+
tasks.named('forbiddenApisMain').configure {
18+
replaceSignatureFiles 'jdk-signatures'
19+
}
20+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Copyright (c) 2012 France Télécom
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions
6+
are met:
7+
1. Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
2. Redistributions in binary form must reproduce the above copyright
10+
notice, this list of conditions and the following disclaimer in the
11+
documentation and/or other materials provided with the distribution.
12+
3. Neither the name of the copyright holders nor the names of its
13+
contributors may be used to endorse or promote products derived from
14+
this software without specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
26+
THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
11+
import org.elasticsearch.entitlement.instrumentation.impl.InstrumentationServiceImpl;
12+
13+
module org.elasticsearch.entitlement.agent.impl {
14+
requires org.objectweb.asm;
15+
requires org.elasticsearch.entitlement.agent;
16+
17+
provides InstrumentationService with InstrumentationServiceImpl;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.instrumentation.impl;
11+
12+
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
13+
import org.elasticsearch.entitlement.instrumentation.Instrumenter;
14+
import org.elasticsearch.entitlement.instrumentation.MethodKey;
15+
import org.objectweb.asm.Type;
16+
17+
import java.lang.reflect.Method;
18+
import java.lang.reflect.Modifier;
19+
import java.util.Map;
20+
import java.util.stream.Stream;
21+
22+
public class InstrumentationServiceImpl implements InstrumentationService {
23+
@Override
24+
public Instrumenter newInstrumenter(String classNameSuffix, Map<MethodKey, Method> instrumentationMethods) {
25+
return new InstrumenterImpl(classNameSuffix, instrumentationMethods);
26+
}
27+
28+
/**
29+
* @return a {@link MethodKey} suitable for looking up the given {@code targetMethod} in the entitlements trampoline
30+
*/
31+
public MethodKey methodKeyForTarget(Method targetMethod) {
32+
Type actualType = Type.getMethodType(Type.getMethodDescriptor(targetMethod));
33+
return new MethodKey(
34+
Type.getInternalName(targetMethod.getDeclaringClass()),
35+
targetMethod.getName(),
36+
Stream.of(actualType.getArgumentTypes()).map(Type::getInternalName).toList(),
37+
Modifier.isStatic(targetMethod.getModifiers())
38+
);
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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.instrumentation.impl;
11+
12+
import org.elasticsearch.entitlement.instrumentation.Instrumenter;
13+
import org.elasticsearch.entitlement.instrumentation.MethodKey;
14+
import org.objectweb.asm.AnnotationVisitor;
15+
import org.objectweb.asm.ClassReader;
16+
import org.objectweb.asm.ClassVisitor;
17+
import org.objectweb.asm.ClassWriter;
18+
import org.objectweb.asm.MethodVisitor;
19+
import org.objectweb.asm.Opcodes;
20+
import org.objectweb.asm.Type;
21+
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.lang.reflect.Method;
25+
import java.util.Map;
26+
import java.util.stream.Stream;
27+
28+
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
29+
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
30+
import static org.objectweb.asm.Opcodes.ACC_STATIC;
31+
import static org.objectweb.asm.Opcodes.GETSTATIC;
32+
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
33+
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
34+
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
35+
36+
public class InstrumenterImpl implements Instrumenter {
37+
/**
38+
* To avoid class name collisions during testing without an agent to replace classes in-place.
39+
*/
40+
private final String classNameSuffix;
41+
private final Map<MethodKey, Method> instrumentationMethods;
42+
43+
public InstrumenterImpl(String classNameSuffix, Map<MethodKey, Method> instrumentationMethods) {
44+
this.classNameSuffix = classNameSuffix;
45+
this.instrumentationMethods = instrumentationMethods;
46+
}
47+
48+
public ClassFileInfo instrumentClassFile(Class<?> clazz) throws IOException {
49+
ClassFileInfo initial = getClassFileInfo(clazz);
50+
return new ClassFileInfo(initial.fileName(), instrumentClass(Type.getInternalName(clazz), initial.bytecodes()));
51+
}
52+
53+
public static ClassFileInfo getClassFileInfo(Class<?> clazz) throws IOException {
54+
String internalName = Type.getInternalName(clazz);
55+
String fileName = "/" + internalName + ".class";
56+
byte[] originalBytecodes;
57+
try (InputStream classStream = clazz.getResourceAsStream(fileName)) {
58+
if (classStream == null) {
59+
throw new IllegalStateException("Classfile not found in jar: " + fileName);
60+
}
61+
originalBytecodes = classStream.readAllBytes();
62+
}
63+
return new ClassFileInfo(fileName, originalBytecodes);
64+
}
65+
66+
@Override
67+
public byte[] instrumentClass(String className, byte[] classfileBuffer) {
68+
ClassReader reader = new ClassReader(classfileBuffer);
69+
ClassWriter writer = new ClassWriter(reader, COMPUTE_FRAMES | COMPUTE_MAXS);
70+
ClassVisitor visitor = new EntitlementClassVisitor(Opcodes.ASM9, writer, className);
71+
reader.accept(visitor, 0);
72+
return writer.toByteArray();
73+
}
74+
75+
class EntitlementClassVisitor extends ClassVisitor {
76+
final String className;
77+
78+
EntitlementClassVisitor(int api, ClassVisitor classVisitor, String className) {
79+
super(api, classVisitor);
80+
this.className = className;
81+
}
82+
83+
@Override
84+
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
85+
super.visit(version, access, name + classNameSuffix, signature, superName, interfaces);
86+
}
87+
88+
@Override
89+
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
90+
var mv = super.visitMethod(access, name, descriptor, signature, exceptions);
91+
boolean isStatic = (access & ACC_STATIC) != 0;
92+
var key = new MethodKey(
93+
className,
94+
name,
95+
Stream.of(Type.getArgumentTypes(descriptor)).map(Type::getInternalName).toList(),
96+
isStatic
97+
);
98+
var instrumentationMethod = instrumentationMethods.get(key);
99+
if (instrumentationMethod != null) {
100+
// LOGGER.debug("Will instrument method {}", key);
101+
return new EntitlementMethodVisitor(Opcodes.ASM9, mv, isStatic, descriptor, instrumentationMethod);
102+
} else {
103+
// LOGGER.trace("Will not instrument method {}", key);
104+
}
105+
return mv;
106+
}
107+
}
108+
109+
static class EntitlementMethodVisitor extends MethodVisitor {
110+
private final boolean instrumentedMethodIsStatic;
111+
private final String instrumentedMethodDescriptor;
112+
private final Method instrumentationMethod;
113+
private boolean hasCallerSensitiveAnnotation = false;
114+
115+
EntitlementMethodVisitor(
116+
int api,
117+
MethodVisitor methodVisitor,
118+
boolean instrumentedMethodIsStatic,
119+
String instrumentedMethodDescriptor,
120+
Method instrumentationMethod
121+
) {
122+
super(api, methodVisitor);
123+
this.instrumentedMethodIsStatic = instrumentedMethodIsStatic;
124+
this.instrumentedMethodDescriptor = instrumentedMethodDescriptor;
125+
this.instrumentationMethod = instrumentationMethod;
126+
}
127+
128+
@Override
129+
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
130+
if (visible && descriptor.endsWith("CallerSensitive;")) {
131+
hasCallerSensitiveAnnotation = true;
132+
}
133+
return super.visitAnnotation(descriptor, visible);
134+
}
135+
136+
@Override
137+
public void visitCode() {
138+
pushEntitlementChecksObject();
139+
pushCallerClass();
140+
forwardIncomingArguments();
141+
invokeInstrumentationMethod();
142+
super.visitCode();
143+
}
144+
145+
private void pushEntitlementChecksObject() {
146+
mv.visitMethodInsn(
147+
INVOKESTATIC,
148+
"org/elasticsearch/entitlement/api/EntitlementProvider",
149+
"checks",
150+
"()Lorg/elasticsearch/entitlement/api/EntitlementChecks;",
151+
false
152+
);
153+
}
154+
155+
private void pushCallerClass() {
156+
if (hasCallerSensitiveAnnotation) {
157+
mv.visitMethodInsn(
158+
INVOKESTATIC,
159+
"jdk/internal/reflect/Reflection",
160+
"getCallerClass",
161+
Type.getMethodDescriptor(Type.getType(Class.class)),
162+
false
163+
);
164+
} else {
165+
mv.visitFieldInsn(
166+
GETSTATIC,
167+
Type.getInternalName(StackWalker.Option.class),
168+
"RETAIN_CLASS_REFERENCE",
169+
Type.getDescriptor(StackWalker.Option.class)
170+
);
171+
mv.visitMethodInsn(
172+
INVOKESTATIC,
173+
Type.getInternalName(StackWalker.class),
174+
"getInstance",
175+
Type.getMethodDescriptor(Type.getType(StackWalker.class), Type.getType(StackWalker.Option.class)),
176+
false
177+
);
178+
mv.visitMethodInsn(
179+
INVOKEVIRTUAL,
180+
Type.getInternalName(StackWalker.class),
181+
"getCallerClass",
182+
Type.getMethodDescriptor(Type.getType(Class.class)),
183+
false
184+
);
185+
}
186+
}
187+
188+
private void forwardIncomingArguments() {
189+
int localVarIndex = 0;
190+
if (instrumentedMethodIsStatic == false) {
191+
mv.visitVarInsn(Opcodes.ALOAD, localVarIndex++);
192+
}
193+
for (Type type : Type.getArgumentTypes(instrumentedMethodDescriptor)) {
194+
mv.visitVarInsn(type.getOpcode(Opcodes.ILOAD), localVarIndex);
195+
localVarIndex += type.getSize();
196+
}
197+
198+
}
199+
200+
private void invokeInstrumentationMethod() {
201+
mv.visitMethodInsn(
202+
INVOKEINTERFACE,
203+
Type.getInternalName(instrumentationMethod.getDeclaringClass()),
204+
instrumentationMethod.getName(),
205+
Type.getMethodDescriptor(instrumentationMethod),
206+
true
207+
);
208+
}
209+
}
210+
211+
// private static final Logger LOGGER = LogManager.getLogger(Instrumenter.class);
212+
213+
public record ClassFileInfo(String fileName, byte[] bytecodes) {}
214+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
org.elasticsearch.entitlement.instrumentation.impl.InstrumentationServiceImpl

0 commit comments

Comments
 (0)