Back to index...
/*
 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.internal.module;
import java.io.PrintStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.WeakHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static java.util.Collections.*;
import jdk.internal.misc.JavaLangAccess;
import jdk.internal.misc.SharedSecrets;
/**
 * Supports logging of access to members of exported and concealed packages
 * that are opened to code in unnamed modules for illegal access.
 */
public final class IllegalAccessLogger {
    /**
     * Logger modes
     */
    public static enum Mode {
        /**
         * Prints a warning when an illegal access succeeds and then
         * discards the logger so that there is no further output.
         */
        ONESHOT,
        /**
         * Print warnings when illegal access succeeds
         */
        WARN,
        /**
         * Prints warnings and a stack trace when illegal access succeeds
         */
        DEBUG,
    }
    /**
     * A builder for IllegalAccessLogger objects.
     */
    public static class Builder {
        private final Mode mode;
        private final PrintStream warningStream;
        private final Map<Module, Set<String>> moduleToConcealedPackages;
        private final Map<Module, Set<String>> moduleToExportedPackages;
        private boolean complete;
        private void ensureNotComplete() {
            if (complete) throw new IllegalStateException();
        }
        /**
         * Creates a builder.
         */
        public Builder(Mode mode, PrintStream warningStream) {
            this.mode = mode;
            this.warningStream = warningStream;
            this.moduleToConcealedPackages = new HashMap<>();
            this.moduleToExportedPackages = new HashMap<>();
        }
        /**
         * Adding logging of reflective-access to any member of a type in
         * otherwise concealed packages.
         */
        public Builder logAccessToConcealedPackages(Module m, Set<String> packages) {
            ensureNotComplete();
            moduleToConcealedPackages.put(m, unmodifiableSet(packages));
            return this;
        }
        /**
         * Adding logging of reflective-access to non-public members/types in
         * otherwise exported (not open) packages.
         */
        public Builder logAccessToExportedPackages(Module m, Set<String> packages) {
            ensureNotComplete();
            moduleToExportedPackages.put(m, unmodifiableSet(packages));
            return this;
        }
        /**
         * Builds the IllegalAccessLogger and sets it as the system-wise logger.
         */
        public void complete() {
            Map<Module, Set<String>> map1 = unmodifiableMap(moduleToConcealedPackages);
            Map<Module, Set<String>> map2 = unmodifiableMap(moduleToExportedPackages);
            logger = new IllegalAccessLogger(mode, warningStream, map1, map2);
            complete = true;
        }
    }
    // need access to java.lang.Module
    private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
    // system-wide IllegalAccessLogger
    private static volatile IllegalAccessLogger logger;
    // logger mode
    private final Mode mode;
    // the print stream to send the warnings
    private final PrintStream warningStream;
    // module -> packages open for illegal access
    private final Map<Module, Set<String>> moduleToConcealedPackages;
    private final Map<Module, Set<String>> moduleToExportedPackages;
    // caller -> usages
    private final Map<Class<?>, Usages> callerToUsages = new WeakHashMap<>();
    private IllegalAccessLogger(Mode mode,
                                PrintStream warningStream,
                                Map<Module, Set<String>> moduleToConcealedPackages,
                                Map<Module, Set<String>> moduleToExportedPackages)
    {
        this.mode = mode;
        this.warningStream = warningStream;
        this.moduleToConcealedPackages = moduleToConcealedPackages;
        this.moduleToExportedPackages = moduleToExportedPackages;
    }
    /**
     * Returns the system-wide IllegalAccessLogger or {@code null} if there is
     * no logger.
     */
    public static IllegalAccessLogger illegalAccessLogger() {
        return logger;
    }
    /**
     * Returns true if the module exports a concealed package for illegal
     * access.
     */
    public boolean isExportedForIllegalAccess(Module module, String pn) {
        Set<String> packages = moduleToConcealedPackages.get(module);
        if (packages != null && packages.contains(pn))
            return true;
        return false;
    }
    /**
     * Returns true if the module opens a concealed or exported package for
     * illegal access.
     */
    public boolean isOpenForIllegalAccess(Module module, String pn) {
        if (isExportedForIllegalAccess(module, pn))
            return true;
        Set<String> packages = moduleToExportedPackages.get(module);
        if (packages != null && packages.contains(pn))
            return true;
        return false;
    }
    /**
     * Logs access to the member of a target class by a caller class if the class
     * is in a package that is exported for illegal access.
     *
     * The {@code whatSupplier} supplies the message that describes the member.
     */
    public void logIfExportedForIllegalAccess(Class<?> caller,
                                              Class<?> target,
                                              Supplier<String> whatSupplier) {
        Module targetModule = target.getModule();
        String targetPackage = target.getPackageName();
        if (isExportedForIllegalAccess(targetModule, targetPackage)) {
            Module callerModule = caller.getModule();
            if (!JLA.isReflectivelyExported(targetModule, targetPackage, callerModule)) {
                log(caller, whatSupplier.get());
            }
        }
    }
    /**
     * Logs access to the member of a target class by a caller class if the class
     * is in a package that is opened for illegal access.
     *
     * The {@code what} parameter supplies the message that describes the member.
     */
    public void logIfOpenedForIllegalAccess(Class<?> caller,
                                            Class<?> target,
                                            Supplier<String> whatSupplier) {
        Module targetModule = target.getModule();
        String targetPackage = target.getPackageName();
        if (isOpenForIllegalAccess(targetModule, targetPackage)) {
            Module callerModule = caller.getModule();
            if (!JLA.isReflectivelyOpened(targetModule, targetPackage, callerModule)) {
                log(caller, whatSupplier.get());
            }
        }
    }
    /**
     * Logs access by caller lookup if the target class is in a package that is
     * opened for illegal access.
     */
    public void logIfOpenedForIllegalAccess(MethodHandles.Lookup caller, Class<?> target) {
        Module targetModule = target.getModule();
        String targetPackage = target.getPackageName();
        if (isOpenForIllegalAccess(targetModule, targetPackage)) {
            Class<?> callerClass = caller.lookupClass();
            Module callerModule = callerClass.getModule();
            if (!JLA.isReflectivelyOpened(targetModule, targetPackage, callerModule)) {
                URL url = codeSource(callerClass);
                final String source;
                if (url == null) {
                    source = callerClass.getName();
                } else {
                    source = callerClass.getName() + " (" + url + ")";
                }
                log(callerClass, target.getName(), () ->
                    "WARNING: Illegal reflective access using Lookup on " + source
                    + " to " + target);
            }
        }
    }
    /**
     * Logs access by a caller class. The {@code what} parameter describes
     * the member being accessed.
     */
    private void log(Class<?> caller, String what) {
        log(caller, what, () -> {
            URL url = codeSource(caller);
            String source = caller.getName();
            if (url != null)
                source += " (" + url + ")";
            return "WARNING: Illegal reflective access by " + source + " to " + what;
        });
    }
    /**
     * Log access by a caller. The {@code what} parameter describes the class or
     * member that is being accessed. The {@code msgSupplier} supplies the log
     * message.
     *
     * To reduce output, this method only logs the access if it hasn't been seen
     * previously. "Seen previously" is implemented as a map of caller class -> Usage,
     * where a Usage is the "what" and a hash of the stack trace. The map has weak
     * keys so it can be expunged when the caller is GC'ed/unloaded.
     */
    private void log(Class<?> caller, String what, Supplier<String> msgSupplier) {
        if (mode == Mode.ONESHOT) {
            synchronized (IllegalAccessLogger.class) {
                // discard the system wide logger
                if (logger == null)
                    return;
                logger = null;
            }
            warningStream.println(loudWarning(caller, msgSupplier));
            return;
        }
        // stack trace without the top-most frames in java.base
        List<StackWalker.StackFrame> stack = StackWalkerHolder.INSTANCE.walk(s ->
            s.dropWhile(this::isJavaBase)
             .limit(32)
             .collect(Collectors.toList())
        );
        // record usage if this is the first (or not recently recorded)
        Usage u = new Usage(what, hash(stack));
        boolean added;
        synchronized (this) {
            added = callerToUsages.computeIfAbsent(caller, k -> new Usages()).add(u);
        }
        // print warning if this is the first (or not a recent) usage
        if (added) {
            String msg = msgSupplier.get();
            if (mode == Mode.DEBUG) {
                StringBuilder sb = new StringBuilder(msg);
                stack.forEach(f ->
                    sb.append(System.lineSeparator()).append("\tat " + f)
                );
                msg = sb.toString();
            }
            warningStream.println(msg);
        }
    }
    /**
     * Returns the code source for the given class or null if there is no code source
     */
    private URL codeSource(Class<?> clazz) {
        PrivilegedAction<ProtectionDomain> pa = clazz::getProtectionDomain;
        CodeSource cs = AccessController.doPrivileged(pa).getCodeSource();
        return (cs != null) ? cs.getLocation() : null;
    }
    private String loudWarning(Class<?> caller,  Supplier<String> msgSupplier) {
        StringJoiner sj = new StringJoiner(System.lineSeparator());
        sj.add("WARNING: An illegal reflective access operation has occurred");
        sj.add(msgSupplier.get());
        sj.add("WARNING: Please consider reporting this to the maintainers of "
                + caller.getName());
        sj.add("WARNING: Use --illegal-access=warn to enable warnings of further"
                + " illegal reflective access operations");
        sj.add("WARNING: All illegal access operations will be denied in a"
                + " future release");
        return sj.toString();
    }
    private static class StackWalkerHolder {
        static final StackWalker INSTANCE;
        static {
            PrivilegedAction<StackWalker> pa = () ->
                StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
            INSTANCE = AccessController.doPrivileged(pa);
        }
    }
    /**
     * Returns true if the stack frame is for a class in java.base.
     */
    private boolean isJavaBase(StackWalker.StackFrame frame) {
        Module caller = frame.getDeclaringClass().getModule();
        return "java.base".equals(caller.getName());
    }
    /**
     * Computes a hash code for the give stack frames. The hash code is based
     * on the class, method name, and BCI.
     */
    private int hash(List<StackWalker.StackFrame> stack) {
        int hash = 0;
        for (StackWalker.StackFrame frame : stack) {
            hash = (31 * hash) + Objects.hash(frame.getDeclaringClass(),
                                              frame.getMethodName(),
                                              frame.getByteCodeIndex());
        }
        return hash;
    }
    private static class Usage {
        private final String what;
        private final int stack;
        Usage(String what, int stack) {
            this.what = what;
            this.stack = stack;
        }
        @Override
        public int hashCode() {
            return what.hashCode() ^ stack;
        }
        @Override
        public boolean equals(Object ob) {
            if (ob instanceof Usage) {
                Usage that = (Usage)ob;
                return what.equals(that.what) && stack == (that.stack);
            } else {
                return false;
            }
        }
    }
    @SuppressWarnings("serial")
    private static class Usages extends LinkedHashMap<Usage, Boolean> {
        Usages() { }
        boolean add(Usage u) {
            return (putIfAbsent(u, Boolean.TRUE) == null);
        }
        @Override
        protected boolean removeEldestEntry(Map.Entry<Usage, Boolean> oldest) {
            // prevent map growing too big, say where a utility class
            // is used by generated code to do illegal access
            return size() > 16;
        }
    }
}
Back to index...