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