Skip to content

Commit 12ec1f7

Browse files
authored
[8.19] Backport EntitlementInitialization refactorings (#127758)
* Move FilesEntitlements validation to a separate class (#127703) Moves FilesEntitlements validation to a separate class. This is the final PR to make EntitlementsInitialization a simpler "orchestrator" of the various steps in the initialization phase. * [Entitlements] Extract instrumentation initialization to a separate class (#127702) * spotless * Fix after merge
1 parent 9d0bbc6 commit 12ec1f7

File tree

5 files changed

+621
-503
lines changed

5 files changed

+621
-503
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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.initialization;
11+
12+
import org.elasticsearch.core.internal.provider.ProviderLocator;
13+
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
14+
import org.elasticsearch.entitlement.instrumentation.CheckMethod;
15+
import org.elasticsearch.entitlement.instrumentation.InstrumentationService;
16+
import org.elasticsearch.entitlement.instrumentation.Instrumenter;
17+
import org.elasticsearch.entitlement.instrumentation.MethodKey;
18+
import org.elasticsearch.entitlement.instrumentation.Transformer;
19+
20+
import java.lang.instrument.Instrumentation;
21+
import java.lang.instrument.UnmodifiableClassException;
22+
import java.net.URI;
23+
import java.nio.channels.spi.SelectorProvider;
24+
import java.nio.file.AccessMode;
25+
import java.nio.file.CopyOption;
26+
import java.nio.file.DirectoryStream;
27+
import java.nio.file.FileStore;
28+
import java.nio.file.FileSystems;
29+
import java.nio.file.LinkOption;
30+
import java.nio.file.OpenOption;
31+
import java.nio.file.Path;
32+
import java.nio.file.WatchEvent;
33+
import java.nio.file.WatchService;
34+
import java.nio.file.attribute.FileAttribute;
35+
import java.nio.file.spi.FileSystemProvider;
36+
import java.util.ArrayList;
37+
import java.util.HashMap;
38+
import java.util.List;
39+
import java.util.Map;
40+
import java.util.Set;
41+
import java.util.concurrent.ExecutorService;
42+
import java.util.function.Function;
43+
import java.util.stream.Collectors;
44+
import java.util.stream.Stream;
45+
import java.util.stream.StreamSupport;
46+
47+
import static org.elasticsearch.entitlement.initialization.EntitlementInitialization.getVersionSpecificCheckerClass;
48+
49+
class DynamicInstrumentation {
50+
51+
interface InstrumentationInfoFactory {
52+
InstrumentationService.InstrumentationInfo of(String methodName, Class<?>... parameterTypes) throws ClassNotFoundException,
53+
NoSuchMethodException;
54+
}
55+
56+
private static final InstrumentationService INSTRUMENTATION_SERVICE = new ProviderLocator<>(
57+
"entitlement",
58+
InstrumentationService.class,
59+
"org.elasticsearch.entitlement.instrumentation",
60+
Set.of()
61+
).get();
62+
63+
/**
64+
* Initializes the dynamic (agent-based) instrumentation:
65+
* <ol>
66+
* <li>
67+
* Finds the version-specific subclass of {@link EntitlementChecker} to use
68+
* </li>
69+
* <li>
70+
* Builds the set of methods to instrument using {@link InstrumentationService#lookupMethods}
71+
* </li>
72+
* <li>
73+
* Augment this set “dynamically” using {@link InstrumentationService#lookupImplementationMethod}
74+
* </li>
75+
* <li>
76+
* Creates an {@link Instrumenter} via {@link InstrumentationService#newInstrumenter}, and adds a new {@link Transformer} (derived from
77+
* {@link java.lang.instrument.ClassFileTransformer}) that uses it. Transformers are invoked when a class is about to load, after its
78+
* bytes have been deserialized to memory but before the class is initialized.
79+
* </li>
80+
* <li>
81+
* Re-transforms all already loaded classes: we force the {@link Instrumenter} to run on classes that might have been already loaded
82+
* before entitlement initialization by calling the {@link java.lang.instrument.Instrumentation#retransformClasses} method on all
83+
* classes that were already loaded.
84+
* </li>
85+
* </ol>
86+
* <p>
87+
* The third step is needed as the JDK exposes some API through interfaces that have different (internal) implementations
88+
* depending on the JVM host platform. As we cannot instrument an interfaces, we find its concrete implementation.
89+
* A prime example is {@link FileSystemProvider}, which has different implementations (e.g. {@code UnixFileSystemProvider} or
90+
* {@code WindowsFileSystemProvider}). At runtime, we find the implementation class which is currently used by the JVM, and add
91+
* its methods to the set of methods to instrument. See e.g. {@link DynamicInstrumentation#fileSystemProviderChecks}.
92+
* </p>
93+
*
94+
* @param inst the JVM instrumentation class instance
95+
* @param checkerInterface the interface to use to find methods to instrument and to use in the injected instrumentation code
96+
* @param verifyBytecode whether we should perform bytecode verification before and after instrumenting each method
97+
*/
98+
static void initialize(Instrumentation inst, Class<?> checkerInterface, boolean verifyBytecode) throws ClassNotFoundException,
99+
NoSuchMethodException, UnmodifiableClassException {
100+
101+
var checkMethods = getMethodsToInstrument(checkerInterface);
102+
var classesToTransform = checkMethods.keySet().stream().map(MethodKey::className).collect(Collectors.toSet());
103+
104+
Instrumenter instrumenter = INSTRUMENTATION_SERVICE.newInstrumenter(checkerInterface, checkMethods);
105+
var transformer = new Transformer(instrumenter, classesToTransform, verifyBytecode);
106+
inst.addTransformer(transformer, true);
107+
108+
var classesToRetransform = findClassesToRetransform(inst.getAllLoadedClasses(), classesToTransform);
109+
try {
110+
inst.retransformClasses(classesToRetransform);
111+
} catch (VerifyError e) {
112+
// Turn on verification and try to retransform one class at the time to get detailed diagnostic
113+
transformer.enableClassVerification();
114+
115+
for (var classToRetransform : classesToRetransform) {
116+
inst.retransformClasses(classToRetransform);
117+
}
118+
119+
// We should have failed already in the loop above, but just in case we did not, rethrow.
120+
throw e;
121+
}
122+
}
123+
124+
private static Map<MethodKey, CheckMethod> getMethodsToInstrument(Class<?> checkerInterface) throws ClassNotFoundException,
125+
NoSuchMethodException {
126+
Map<MethodKey, CheckMethod> checkMethods = new HashMap<>(INSTRUMENTATION_SERVICE.lookupMethods(checkerInterface));
127+
Stream.of(
128+
fileSystemProviderChecks(),
129+
fileStoreChecks(),
130+
pathChecks(),
131+
Stream.of(
132+
INSTRUMENTATION_SERVICE.lookupImplementationMethod(
133+
SelectorProvider.class,
134+
"inheritedChannel",
135+
SelectorProvider.provider().getClass(),
136+
EntitlementChecker.class,
137+
"checkSelectorProviderInheritedChannel"
138+
)
139+
)
140+
)
141+
.flatMap(Function.identity())
142+
.forEach(instrumentation -> checkMethods.put(instrumentation.targetMethod(), instrumentation.checkMethod()));
143+
144+
return checkMethods;
145+
}
146+
147+
private static Stream<InstrumentationService.InstrumentationInfo> fileSystemProviderChecks() throws ClassNotFoundException,
148+
NoSuchMethodException {
149+
var fileSystemProviderClass = FileSystems.getDefault().provider().getClass();
150+
151+
var instrumentation = new InstrumentationInfoFactory() {
152+
@Override
153+
public InstrumentationService.InstrumentationInfo of(String methodName, Class<?>... parameterTypes)
154+
throws ClassNotFoundException, NoSuchMethodException {
155+
return INSTRUMENTATION_SERVICE.lookupImplementationMethod(
156+
FileSystemProvider.class,
157+
methodName,
158+
fileSystemProviderClass,
159+
EntitlementChecker.class,
160+
"check" + Character.toUpperCase(methodName.charAt(0)) + methodName.substring(1),
161+
parameterTypes
162+
);
163+
}
164+
};
165+
166+
var allVersionsMethods = Stream.of(
167+
instrumentation.of("newFileSystem", URI.class, Map.class),
168+
instrumentation.of("newFileSystem", Path.class, Map.class),
169+
instrumentation.of("newInputStream", Path.class, OpenOption[].class),
170+
instrumentation.of("newOutputStream", Path.class, OpenOption[].class),
171+
instrumentation.of("newFileChannel", Path.class, Set.class, FileAttribute[].class),
172+
instrumentation.of("newAsynchronousFileChannel", Path.class, Set.class, ExecutorService.class, FileAttribute[].class),
173+
instrumentation.of("newByteChannel", Path.class, Set.class, FileAttribute[].class),
174+
instrumentation.of("newDirectoryStream", Path.class, DirectoryStream.Filter.class),
175+
instrumentation.of("createDirectory", Path.class, FileAttribute[].class),
176+
instrumentation.of("createSymbolicLink", Path.class, Path.class, FileAttribute[].class),
177+
instrumentation.of("createLink", Path.class, Path.class),
178+
instrumentation.of("delete", Path.class),
179+
instrumentation.of("deleteIfExists", Path.class),
180+
instrumentation.of("readSymbolicLink", Path.class),
181+
instrumentation.of("copy", Path.class, Path.class, CopyOption[].class),
182+
instrumentation.of("move", Path.class, Path.class, CopyOption[].class),
183+
instrumentation.of("isSameFile", Path.class, Path.class),
184+
instrumentation.of("isHidden", Path.class),
185+
instrumentation.of("getFileStore", Path.class),
186+
instrumentation.of("checkAccess", Path.class, AccessMode[].class),
187+
instrumentation.of("getFileAttributeView", Path.class, Class.class, LinkOption[].class),
188+
instrumentation.of("readAttributes", Path.class, Class.class, LinkOption[].class),
189+
instrumentation.of("readAttributes", Path.class, String.class, LinkOption[].class),
190+
instrumentation.of("setAttribute", Path.class, String.class, Object.class, LinkOption[].class)
191+
);
192+
193+
if (Runtime.version().feature() >= 20) {
194+
var java20EntitlementCheckerClass = getVersionSpecificCheckerClass(EntitlementChecker.class, 20);
195+
var java20Methods = Stream.of(
196+
INSTRUMENTATION_SERVICE.lookupImplementationMethod(
197+
FileSystemProvider.class,
198+
"readAttributesIfExists",
199+
fileSystemProviderClass,
200+
java20EntitlementCheckerClass,
201+
"checkReadAttributesIfExists",
202+
Path.class,
203+
Class.class,
204+
LinkOption[].class
205+
),
206+
INSTRUMENTATION_SERVICE.lookupImplementationMethod(
207+
FileSystemProvider.class,
208+
"exists",
209+
fileSystemProviderClass,
210+
java20EntitlementCheckerClass,
211+
"checkExists",
212+
Path.class,
213+
LinkOption[].class
214+
)
215+
);
216+
return Stream.concat(allVersionsMethods, java20Methods);
217+
}
218+
return allVersionsMethods;
219+
}
220+
221+
private static Stream<InstrumentationService.InstrumentationInfo> fileStoreChecks() {
222+
var fileStoreClasses = StreamSupport.stream(FileSystems.getDefault().getFileStores().spliterator(), false)
223+
.map(FileStore::getClass)
224+
.distinct();
225+
return fileStoreClasses.flatMap(fileStoreClass -> {
226+
var instrumentation = new InstrumentationInfoFactory() {
227+
@Override
228+
public InstrumentationService.InstrumentationInfo of(String methodName, Class<?>... parameterTypes)
229+
throws ClassNotFoundException, NoSuchMethodException {
230+
return INSTRUMENTATION_SERVICE.lookupImplementationMethod(
231+
FileStore.class,
232+
methodName,
233+
fileStoreClass,
234+
EntitlementChecker.class,
235+
"check" + Character.toUpperCase(methodName.charAt(0)) + methodName.substring(1),
236+
parameterTypes
237+
);
238+
}
239+
};
240+
241+
try {
242+
return Stream.of(
243+
instrumentation.of("getFileStoreAttributeView", Class.class),
244+
instrumentation.of("getAttribute", String.class),
245+
instrumentation.of("getBlockSize"),
246+
instrumentation.of("getTotalSpace"),
247+
instrumentation.of("getUnallocatedSpace"),
248+
instrumentation.of("getUsableSpace"),
249+
instrumentation.of("isReadOnly"),
250+
instrumentation.of("name"),
251+
instrumentation.of("type")
252+
253+
);
254+
} catch (NoSuchMethodException | ClassNotFoundException e) {
255+
throw new RuntimeException(e);
256+
}
257+
});
258+
}
259+
260+
private static Stream<InstrumentationService.InstrumentationInfo> pathChecks() {
261+
var pathClasses = StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false)
262+
.map(Path::getClass)
263+
.distinct();
264+
return pathClasses.flatMap(pathClass -> {
265+
InstrumentationInfoFactory instrumentation = (String methodName, Class<?>... parameterTypes) -> INSTRUMENTATION_SERVICE
266+
.lookupImplementationMethod(
267+
Path.class,
268+
methodName,
269+
pathClass,
270+
EntitlementChecker.class,
271+
"checkPath" + Character.toUpperCase(methodName.charAt(0)) + methodName.substring(1),
272+
parameterTypes
273+
);
274+
275+
try {
276+
return Stream.of(
277+
instrumentation.of("toRealPath", LinkOption[].class),
278+
instrumentation.of("register", WatchService.class, WatchEvent.Kind[].class),
279+
instrumentation.of("register", WatchService.class, WatchEvent.Kind[].class, WatchEvent.Modifier[].class)
280+
);
281+
} catch (NoSuchMethodException | ClassNotFoundException e) {
282+
throw new RuntimeException(e);
283+
}
284+
});
285+
}
286+
287+
private static Class<?>[] findClassesToRetransform(Class<?>[] loadedClasses, Set<String> classesToTransform) {
288+
List<Class<?>> retransform = new ArrayList<>();
289+
for (Class<?> loadedClass : loadedClasses) {
290+
if (classesToTransform.contains(loadedClass.getName().replace(".", "/"))) {
291+
retransform.add(loadedClass);
292+
}
293+
}
294+
return retransform.toArray(new Class<?>[0]);
295+
}
296+
}

0 commit comments

Comments
 (0)