1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.internal.module;
  27 
  28 import java.lang.invoke.MethodHandles;
  29 import java.lang.reflect.Module;
  30 import java.net.URL;
  31 import java.security.AccessController;
  32 import java.security.CodeSource;
  33 import java.security.PrivilegedAction;
  34 import java.security.ProtectionDomain;
  35 import java.util.HashMap;
  36 import java.util.HashSet;
  37 import java.util.List;
  38 import java.util.Map;
  39 import java.util.Objects;
  40 import java.util.Set;
  41 import java.util.WeakHashMap;
  42 import java.util.function.Supplier;
  43 import java.util.stream.Collectors;
  44 
  45 /**
  46  * Supports logging of access to members of API packages that are exported or
  47  * opened via backdoor mechanisms to code in unnamed modules.
  48  */
  49 
  50 public final class IllegalAccessLogger {
  51 
  52     // true to print stack trace
  53     private static final boolean PRINT_STACK_TRACE;
  54     static {
  55         String s = System.getProperty("sun.reflect.debugModuleAccessChecks");
  56         PRINT_STACK_TRACE = "access".equals(s);
  57     }
  58 
  59     private static final StackWalker STACK_WALKER
  60         = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
  61 
  62     // the maximum number of frames to capture
  63     private static final int MAX_STACK_FRAMES = 32;
  64 
  65     // lock to avoid interference when printing stack traces
  66     private static final Object OUTPUT_LOCK = new Object();
  67 
  68     // caller -> usages
  69     private final Map<Class<?>, Set<Usage>> callerToUsages = new WeakHashMap<>();
  70 
  71     // module -> (package name -> CLI option)
  72     private final Map<Module, Map<String, String>> exported;
  73     private final Map<Module, Map<String, String>> opened;
  74 
  75     private IllegalAccessLogger(Map<Module, Map<String, String>> exported,
  76                                 Map<Module, Map<String, String>> opened) {
  77         this.exported = deepCopy(exported);
  78         this.opened = deepCopy(opened);
  79     }
  80 
  81     /**
  82      * Returns that a Builder that is seeded with the packages known to this logger.
  83      */
  84     public Builder toBuilder() {
  85         return new Builder(exported, opened);
  86     }
  87 
  88     /**
  89      * Logs access to the member of a target class by a caller class if the class
  90      * is in a package that is exported via a backdoor mechanism.
  91      *
  92      * The {@code whatSupplier} supplies the message that describes the member.
  93      */
  94     public void logIfExportedByBackdoor(Class<?> caller,
  95                                         Class<?> target,
  96                                         Supplier<String> whatSupplier) {
  97         Map<String, String> packages = exported.get(target.getModule());
  98         if (packages != null) {
  99             String how = packages.get(target.getPackageName());
 100             if (how != null) {
 101                 log(caller, whatSupplier.get(), how);
 102             }
 103         }
 104     }
 105 
 106     /**
 107      * Logs access to the member of a target class by a caller class if the class
 108      * is in a package that is opened via a backdoor mechanism.
 109      *
 110      * The {@code what} parameter supplies the message that describes the member.
 111      */
 112     public void logIfOpenedByBackdoor(Class<?> caller,
 113                                       Class<?> target,
 114                                       Supplier<String> whatSupplier) {
 115         Map<String, String> packages = opened.get(target.getModule());
 116         if (packages != null) {
 117             String how = packages.get(target.getPackageName());
 118             if (how != null) {
 119                 log(caller, whatSupplier.get(), how);
 120             }
 121         }
 122     }
 123 
 124     /**
 125      * Logs access by a caller class. The {@code what} parameter describes
 126      * the member is accessed, the {@code how} parameter is the means by which
 127      * access is allocated (CLI option for example).
 128      */
 129     private void log(Class<?> caller, String what, String how) {
 130         log(caller, what, () -> {
 131             PrivilegedAction<ProtectionDomain> pa = caller::getProtectionDomain;
 132             CodeSource cs = AccessController.doPrivileged(pa).getCodeSource();
 133             URL url = (cs != null) ? cs.getLocation() : null;
 134             String source = caller.getName();
 135             if (url != null)
 136                 source += " (" + url + ")";
 137             return "WARNING: Illegal access by " + source + " to " + what
 138                     + " (permitted by " + how + ")";
 139         });
 140     }
 141 
 142 
 143     /**
 144      * Logs access to caller class if the class is in a package that is opened via
 145      * a backdoor mechanism.
 146      */
 147     public void logIfOpenedByBackdoor(MethodHandles.Lookup caller, Class<?> target) {
 148         Map<String, String> packages = opened.get(target.getModule());
 149         if (packages != null) {
 150             String how = packages.get(target.getPackageName());
 151             if (how != null) {
 152                 log(caller.lookupClass(), target.getName(), () ->
 153                     "WARNING: Illegal access using Lookup on " + caller.lookupClass()
 154                     + " to " + target + " (permitted by " + how + ")");
 155             }
 156         }
 157     }
 158 
 159     /**
 160      * Log access by a caller. The {@code what} parameter describes the class or
 161      * member that is being accessed. The {@code msgSupplier} supplies the log
 162      * message.
 163      *
 164      * To reduce output, this method only logs the access if it hasn't been seen
 165      * previously. "Seen previously" is implemented as a map of caller class -> Usage,
 166      * where a Usage is the "what" and a hash of the stack trace. The map has weak
 167      * keys so it can be expunged when the caller is GC'ed/unloaded.
 168      */
 169     private void log(Class<?> caller, String what, Supplier<String> msgSupplier) {
 170         // stack trace without the top-most frames in java.base
 171         List<StackWalker.StackFrame> stack = STACK_WALKER.walk(s ->
 172             s.dropWhile(this::isJavaBase)
 173              .limit(MAX_STACK_FRAMES)
 174              .collect(Collectors.toList())
 175         );
 176 
 177         // check if the access has already been recorded
 178         Usage u = new Usage(what, hash(stack));
 179         boolean firstUsage;
 180         synchronized (this) {
 181             firstUsage = callerToUsages.computeIfAbsent(caller, k -> new HashSet<>()).add(u);
 182         }
 183 
 184         // log message if first usage
 185         if (firstUsage) {
 186             String msg = msgSupplier.get();
 187             if (PRINT_STACK_TRACE) {
 188                 synchronized (OUTPUT_LOCK) {
 189                     System.err.println(msg);
 190                     stack.forEach(f -> System.err.println("\tat " + f));
 191                 }
 192             } else {
 193                 System.err.println(msg);
 194             }
 195         }
 196     }
 197 
 198     private static class Usage {
 199         private final String what;
 200         private final int stack;
 201         Usage(String what, int stack) {
 202             this.what = what;
 203             this.stack = stack;
 204         }
 205         @Override
 206         public int hashCode() {
 207             return what.hashCode() ^ stack;
 208         }
 209         @Override
 210         public boolean equals(Object ob) {
 211             if (ob instanceof Usage) {
 212                 Usage that = (Usage)ob;
 213                 return what.equals(that.what) && stack == (that.stack);
 214             } else {
 215                 return false;
 216             }
 217         }
 218     }
 219 
 220     /**
 221      * Returns true if the stack frame is for a class in java.base.
 222      */
 223     private boolean isJavaBase(StackWalker.StackFrame frame) {
 224         Module caller = frame.getDeclaringClass().getModule();
 225         return "java.base".equals(caller.getName());
 226     }
 227 
 228     /**
 229      * Computes a hash code for the give stack frames. The hash code is based
 230      * on the class, method name, and BCI.
 231      */
 232     private int hash(List<StackWalker.StackFrame> stack) {
 233         int hash = 0;
 234         for (StackWalker.StackFrame frame : stack) {
 235             hash = (31 * hash) + Objects.hash(frame.getDeclaringClass(),
 236                                               frame.getMethodName(),
 237                                               frame.getByteCodeIndex());
 238         }
 239         return hash;
 240     }
 241 
 242     // system-wide IllegalAccessLogger
 243     private static volatile IllegalAccessLogger logger;
 244 
 245     /**
 246      * Sets the system-wide IllegalAccessLogger
 247      */
 248     public static void setIllegalAccessLogger(IllegalAccessLogger l) {
 249         if (l.exported.isEmpty() && l.opened.isEmpty()) {
 250             logger = null;
 251         } else {
 252             logger = l;
 253         }
 254     }
 255 
 256     /**
 257      * Returns the system-wide IllegalAccessLogger or {@code null} if there is
 258      * no logger.
 259      */
 260     public static IllegalAccessLogger illegalAccessLogger() {
 261         return logger;
 262     }
 263 
 264     /**
 265      * A builder for IllegalAccessLogger objects.
 266      */
 267     public static class Builder {
 268         private Map<Module, Map<String, String>> exported;
 269         private Map<Module, Map<String, String>> opened;
 270 
 271         public Builder() { }
 272 
 273         public Builder(Map<Module, Map<String, String>> exported,
 274                        Map<Module, Map<String, String>> opened) {
 275             this.exported = deepCopy(exported);
 276             this.opened = deepCopy(opened);
 277         }
 278 
 279         public void logAccessToExportedPackage(Module m, String pn, String how) {
 280             if (!m.isExported(pn)) {
 281                 if (exported == null)
 282                     exported = new HashMap<>();
 283                 exported.computeIfAbsent(m, k -> new HashMap<>()).putIfAbsent(pn, how);
 284             }
 285         }
 286 
 287         public void logAccessToOpenPackage(Module m, String pn, String how) {
 288             // opens implies exported at run-time.
 289             logAccessToExportedPackage(m, pn, how);
 290 
 291             if (!m.isOpen(pn)) {
 292                 if (opened == null)
 293                     opened = new HashMap<>();
 294                 opened.computeIfAbsent(m, k -> new HashMap<>()).putIfAbsent(pn, how);
 295             }
 296         }
 297 
 298         /**
 299          * Builds the logger.
 300          */
 301         public IllegalAccessLogger build() {
 302             return new IllegalAccessLogger(exported, opened);
 303         }
 304     }
 305 
 306 
 307     static Map<Module, Map<String, String>> deepCopy(Map<Module, Map<String, String>> map) {
 308         if (map == null || map.isEmpty()) {
 309             return new HashMap<>();
 310         } else {
 311             Map<Module, Map<String, String>> newMap = new HashMap<>();
 312             for (Map.Entry<Module, Map<String, String>> e : map.entrySet()) {
 313                 newMap.put(e.getKey(), new HashMap<>(e.getValue()));
 314             }
 315             return newMap;
 316         }
 317     }
 318 }