jdk/src/java.base/share/classes/jdk/internal/module/IllegalAccessLogger.java
author alanb
Wed, 22 Mar 2017 16:26:27 +0000
changeset 44359 c6761862ca0b
child 44364 9cc9dc782213
permissions -rw-r--r--
8174823: Module system implementation refresh (3/2017) Reviewed-by: chegar, mchung, alanb Contributed-by: alan.bateman@oracle.com, mandy.chung@oracle.com, sundararajan.athijegannathan@oracle.com, peter.levart@gmail.com

/*
 * 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.lang.invoke.MethodHandles;
import java.lang.reflect.Module;
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Supports logging of access to members of API packages that are exported or
 * opened via backdoor mechanisms to code in unnamed modules.
 */

public final class IllegalAccessLogger {

    // true to print stack trace
    private static final boolean PRINT_STACK_TRACE;
    static {
        String s = System.getProperty("sun.reflect.debugModuleAccessChecks");
        PRINT_STACK_TRACE = "access".equals(s);
    }

    private static final StackWalker STACK_WALKER
        = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);

    // the maximum number of frames to capture
    private static final int MAX_STACK_FRAMES = 32;

    // lock to avoid interference when printing stack traces
    private static final Object OUTPUT_LOCK = new Object();

    // caller -> usages
    private final Map<Class<?>, Set<Usage>> callerToUsages = new WeakHashMap<>();

    // module -> (package name -> CLI option)
    private final Map<Module, Map<String, String>> exported;
    private final Map<Module, Map<String, String>> opened;

    private IllegalAccessLogger(Map<Module, Map<String, String>> exported,
                                Map<Module, Map<String, String>> opened) {
        this.exported = deepCopy(exported);
        this.opened = deepCopy(opened);
    }

    /**
     * Returns that a Builder that is seeded with the packages known to this logger.
     */
    public Builder toBuilder() {
        return new Builder(exported, opened);
    }

    /**
     * Logs access to the member of a target class by a caller class if the class
     * is in a package that is exported via a backdoor mechanism.
     *
     * The {@code whatSupplier} supplies the message that describes the member.
     */
    public void logIfExportedByBackdoor(Class<?> caller,
                                        Class<?> target,
                                        Supplier<String> whatSupplier) {
        Map<String, String> packages = exported.get(target.getModule());
        if (packages != null) {
            String how = packages.get(target.getPackageName());
            if (how != null) {
                log(caller, whatSupplier.get(), how);
            }
        }
    }

    /**
     * Logs access to the member of a target class by a caller class if the class
     * is in a package that is opened via a backdoor mechanism.
     *
     * The {@code what} parameter supplies the message that describes the member.
     */
    public void logIfOpenedByBackdoor(Class<?> caller,
                                      Class<?> target,
                                      Supplier<String> whatSupplier) {
        Map<String, String> packages = opened.get(target.getModule());
        if (packages != null) {
            String how = packages.get(target.getPackageName());
            if (how != null) {
                log(caller, whatSupplier.get(), how);
            }
        }
    }

    /**
     * Logs access by a caller class. The {@code what} parameter describes
     * the member is accessed, the {@code how} parameter is the means by which
     * access is allocated (CLI option for example).
     */
    private void log(Class<?> caller, String what, String how) {
        log(caller, what, () -> {
            PrivilegedAction<ProtectionDomain> pa = caller::getProtectionDomain;
            CodeSource cs = AccessController.doPrivileged(pa).getCodeSource();
            URL url = (cs != null) ? cs.getLocation() : null;
            String source = caller.getName();
            if (url != null)
                source += " (" + url + ")";
            return "WARNING: Illegal access by " + source + " to " + what
                    + " (permitted by " + how + ")";
        });
    }


    /**
     * Logs access to caller class if the class is in a package that is opened via
     * a backdoor mechanism.
     */
    public void logIfOpenedByBackdoor(MethodHandles.Lookup caller, Class<?> target) {
        Map<String, String> packages = opened.get(target.getModule());
        if (packages != null) {
            String how = packages.get(target.getPackageName());
            if (how != null) {
                log(caller.lookupClass(), target.getName(), () ->
                    "WARNING: Illegal access using Lookup on " + caller.lookupClass()
                    + " to " + target + " (permitted by " + how + ")");
            }
        }
    }

    /**
     * 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) {
        // stack trace without the top-most frames in java.base
        List<StackWalker.StackFrame> stack = STACK_WALKER.walk(s ->
            s.dropWhile(this::isJavaBase)
             .limit(MAX_STACK_FRAMES)
             .collect(Collectors.toList())
        );

        // check if the access has already been recorded
        Usage u = new Usage(what, hash(stack));
        boolean firstUsage;
        synchronized (this) {
            firstUsage = callerToUsages.computeIfAbsent(caller, k -> new HashSet<>()).add(u);
        }

        // log message if first usage
        if (firstUsage) {
            String msg = msgSupplier.get();
            if (PRINT_STACK_TRACE) {
                synchronized (OUTPUT_LOCK) {
                    System.err.println(msg);
                    stack.forEach(f -> System.err.println("\tat " + f));
                }
            } else {
                System.err.println(msg);
            }
        }
    }

    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;
            }
        }
    }

    /**
     * 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;
    }

    // system-wide IllegalAccessLogger
    private static volatile IllegalAccessLogger logger;

    /**
     * Sets the system-wide IllegalAccessLogger
     */
    public static void setIllegalAccessLogger(IllegalAccessLogger l) {
        if (l.exported.isEmpty() && l.opened.isEmpty()) {
            logger = null;
        } else {
            logger = l;
        }
    }

    /**
     * Returns the system-wide IllegalAccessLogger or {@code null} if there is
     * no logger.
     */
    public static IllegalAccessLogger illegalAccessLogger() {
        return logger;
    }

    /**
     * A builder for IllegalAccessLogger objects.
     */
    public static class Builder {
        private Map<Module, Map<String, String>> exported;
        private Map<Module, Map<String, String>> opened;

        public Builder() { }

        public Builder(Map<Module, Map<String, String>> exported,
                       Map<Module, Map<String, String>> opened) {
            this.exported = deepCopy(exported);
            this.opened = deepCopy(opened);
        }

        public void logAccessToExportedPackage(Module m, String pn, String how) {
            if (!m.isExported(pn)) {
                if (exported == null)
                    exported = new HashMap<>();
                exported.computeIfAbsent(m, k -> new HashMap<>()).putIfAbsent(pn, how);
            }
        }

        public void logAccessToOpenPackage(Module m, String pn, String how) {
            // opens implies exported at run-time.
            logAccessToExportedPackage(m, pn, how);

            if (!m.isOpen(pn)) {
                if (opened == null)
                    opened = new HashMap<>();
                opened.computeIfAbsent(m, k -> new HashMap<>()).putIfAbsent(pn, how);
            }
        }

        /**
         * Builds the logger.
         */
        public IllegalAccessLogger build() {
            return new IllegalAccessLogger(exported, opened);
        }
    }


    static Map<Module, Map<String, String>> deepCopy(Map<Module, Map<String, String>> map) {
        if (map == null || map.isEmpty()) {
            return new HashMap<>();
        } else {
            Map<Module, Map<String, String>> newMap = new HashMap<>();
            for (Map.Entry<Module, Map<String, String>> e : map.entrySet()) {
                newMap.put(e.getKey(), new HashMap<>(e.getValue()));
            }
            return newMap;
        }
    }
}