/* * Copyright (c) 2017, 2020, 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.access.JavaLangAccess; import jdk.internal.access.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 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> moduleToConcealedPackages; private final Map> 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 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 packages) { ensureNotComplete(); moduleToExportedPackages.put(m, unmodifiableSet(packages)); return this; } /** * Builds the IllegalAccessLogger and sets it as the system-wide logger. */ public void complete() { Map> map1 = unmodifiableMap(moduleToConcealedPackages); Map> 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> moduleToConcealedPackages; private final Map> moduleToExportedPackages; // caller -> usages private final Map, Usages> callerToUsages = new WeakHashMap<>(); private IllegalAccessLogger(Mode mode, PrintStream warningStream, Map> moduleToConcealedPackages, Map> 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 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 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 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 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 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 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 pa = clazz::getProtectionDomain; CodeSource cs = AccessController.doPrivileged(pa).getCodeSource(); return (cs != null) ? cs.getLocation() : null; } private String loudWarning(Class caller, Supplier 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 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 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 { Usages() { } boolean add(Usage u) { return (putIfAbsent(u, Boolean.TRUE) == null); } @Override protected boolean removeEldestEntry(Map.Entry oldest) { // prevent map growing too big, say where a utility class // is used by generated code to do illegal access return size() > 16; } } }