1 /*
   2  * Copyright (c) 2017, 2018, 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.io.PrintStream;
  29 import java.lang.invoke.MethodHandles;
  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.LinkedHashMap;
  37 import java.util.List;
  38 import java.util.Map;
  39 import java.util.Objects;
  40 import java.util.Set;
  41 import java.util.StringJoiner;
  42 import java.util.WeakHashMap;
  43 import java.util.function.Supplier;
  44 import java.util.stream.Collectors;
  45 import static java.util.Collections.*;
  46 
  47 import jdk.internal.access.JavaLangAccess;
  48 import jdk.internal.access.SharedSecrets;
  49 
  50 /**
  51  * Supports logging of access to members of exported and concealed packages
  52  * that are opened to code in unnamed modules for illegal access.
  53  */
  54 
  55 public final class IllegalAccessLogger {
  56 
  57     /**
  58      * Logger modes
  59      */
  60     public static enum Mode {
  61         /**
  62          * Prints a warning when an illegal access succeeds and then
  63          * discards the logger so that there is no further output.
  64          */
  65         ONESHOT,
  66         /**
  67          * Print warnings when illegal access succeeds
  68          */
  69         WARN,
  70         /**
  71          * Prints warnings and a stack trace when illegal access succeeds
  72          */
  73         DEBUG,
  74     }
  75 
  76     /**
  77      * A builder for IllegalAccessLogger objects.
  78      */
  79     public static class Builder {
  80         private final Mode mode;
  81         private final PrintStream warningStream;
  82         private final Map<Module, Set<String>> moduleToConcealedPackages;
  83         private final Map<Module, Set<String>> moduleToExportedPackages;
  84         private boolean complete;
  85 
  86         private void ensureNotComplete() {
  87             if (complete) throw new IllegalStateException();
  88         }
  89 
  90         /**
  91          * Creates a builder.
  92          */
  93         public Builder(Mode mode, PrintStream warningStream) {
  94             this.mode = mode;
  95             this.warningStream = warningStream;
  96             this.moduleToConcealedPackages = new HashMap<>();
  97             this.moduleToExportedPackages = new HashMap<>();
  98         }
  99 
 100         /**
 101          * Adding logging of reflective-access to any member of a type in
 102          * otherwise concealed packages.
 103          */
 104         public Builder logAccessToConcealedPackages(Module m, Set<String> packages) {
 105             ensureNotComplete();
 106             moduleToConcealedPackages.put(m, unmodifiableSet(packages));
 107             return this;
 108         }
 109 
 110         /**
 111          * Adding logging of reflective-access to non-public members/types in
 112          * otherwise exported (not open) packages.
 113          */
 114         public Builder logAccessToExportedPackages(Module m, Set<String> packages) {
 115             ensureNotComplete();
 116             moduleToExportedPackages.put(m, unmodifiableSet(packages));
 117             return this;
 118         }
 119 
 120         /**
 121          * Builds the IllegalAccessLogger and sets it as the system-wise logger.
 122          */
 123         public void complete() {
 124             Map<Module, Set<String>> map1 = unmodifiableMap(moduleToConcealedPackages);
 125             Map<Module, Set<String>> map2 = unmodifiableMap(moduleToExportedPackages);
 126             logger = new IllegalAccessLogger(mode, warningStream, map1, map2);
 127             complete = true;
 128         }
 129     }
 130 
 131     // need access to java.lang.Module
 132     private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
 133 
 134     // system-wide IllegalAccessLogger
 135     private static volatile IllegalAccessLogger logger;
 136 
 137     // logger mode
 138     private final Mode mode;
 139 
 140     // the print stream to send the warnings
 141     private final PrintStream warningStream;
 142 
 143     // module -> packages open for illegal access
 144     private final Map<Module, Set<String>> moduleToConcealedPackages;
 145     private final Map<Module, Set<String>> moduleToExportedPackages;
 146 
 147     // caller -> usages
 148     private final Map<Class<?>, Usages> callerToUsages = new WeakHashMap<>();
 149 
 150     private IllegalAccessLogger(Mode mode,
 151                                 PrintStream warningStream,
 152                                 Map<Module, Set<String>> moduleToConcealedPackages,
 153                                 Map<Module, Set<String>> moduleToExportedPackages)
 154     {
 155         this.mode = mode;
 156         this.warningStream = warningStream;
 157         this.moduleToConcealedPackages = moduleToConcealedPackages;
 158         this.moduleToExportedPackages = moduleToExportedPackages;
 159     }
 160 
 161     /**
 162      * Returns the system-wide IllegalAccessLogger or {@code null} if there is
 163      * no logger.
 164      */
 165     public static IllegalAccessLogger illegalAccessLogger() {
 166         return logger;
 167     }
 168 
 169     /**
 170      * Returns true if the module exports a concealed package for illegal
 171      * access.
 172      */
 173     public boolean isExportedForIllegalAccess(Module module, String pn) {
 174         Set<String> packages = moduleToConcealedPackages.get(module);
 175         if (packages != null && packages.contains(pn))
 176             return true;
 177         return false;
 178     }
 179 
 180     /**
 181      * Returns true if the module opens a concealed or exported package for
 182      * illegal access.
 183      */
 184     public boolean isOpenForIllegalAccess(Module module, String pn) {
 185         if (isExportedForIllegalAccess(module, pn))
 186             return true;
 187         Set<String> packages = moduleToExportedPackages.get(module);
 188         if (packages != null && packages.contains(pn))
 189             return true;
 190         return false;
 191     }
 192 
 193     /**
 194      * Logs access to the member of a target class by a caller class if the class
 195      * is in a package that is exported for illegal access.
 196      *
 197      * The {@code whatSupplier} supplies the message that describes the member.
 198      */
 199     public void logIfExportedForIllegalAccess(Class<?> caller,
 200                                               Class<?> target,
 201                                               Supplier<String> whatSupplier) {
 202         Module targetModule = target.getModule();
 203         String targetPackage = target.getPackageName();
 204         if (isExportedForIllegalAccess(targetModule, targetPackage)) {
 205             Module callerModule = caller.getModule();
 206             if (!JLA.isReflectivelyExported(targetModule, targetPackage, callerModule)) {
 207                 log(caller, whatSupplier.get());
 208             }
 209         }
 210     }
 211 
 212     /**
 213      * Logs access to the member of a target class by a caller class if the class
 214      * is in a package that is opened for illegal access.
 215      *
 216      * The {@code what} parameter supplies the message that describes the member.
 217      */
 218     public void logIfOpenedForIllegalAccess(Class<?> caller,
 219                                             Class<?> target,
 220                                             Supplier<String> whatSupplier) {
 221         Module targetModule = target.getModule();
 222         String targetPackage = target.getPackageName();
 223         if (isOpenForIllegalAccess(targetModule, targetPackage)) {
 224             Module callerModule = caller.getModule();
 225             if (!JLA.isReflectivelyOpened(targetModule, targetPackage, callerModule)) {
 226                 log(caller, whatSupplier.get());
 227             }
 228         }
 229     }
 230 
 231     /**
 232      * Logs access by caller lookup if the target class is in a package that is
 233      * opened for illegal access.
 234      */
 235     public void logIfOpenedForIllegalAccess(MethodHandles.Lookup caller, Class<?> target) {
 236         Module targetModule = target.getModule();
 237         String targetPackage = target.getPackageName();
 238         if (isOpenForIllegalAccess(targetModule, targetPackage)) {
 239             Class<?> callerClass = caller.lookupClass();
 240             Module callerModule = callerClass.getModule();
 241             if (!JLA.isReflectivelyOpened(targetModule, targetPackage, callerModule)) {
 242                 URL url = codeSource(callerClass);
 243                 final String source;
 244                 if (url == null) {
 245                     source = callerClass.getName();
 246                 } else {
 247                     source = callerClass.getName() + " (" + url + ")";
 248                 }
 249                 log(callerClass, target.getName(), () ->
 250                     "WARNING: Illegal reflective access using Lookup on " + source
 251                     + " to " + target);
 252             }
 253         }
 254     }
 255 
 256     /**
 257      * Logs access by a caller class. The {@code what} parameter describes
 258      * the member being accessed.
 259      */
 260     private void log(Class<?> caller, String what) {
 261         log(caller, what, () -> {
 262             URL url = codeSource(caller);
 263             String source = caller.getName();
 264             if (url != null)
 265                 source += " (" + url + ")";
 266             return "WARNING: Illegal reflective access by " + source + " to " + what;
 267         });
 268     }
 269 
 270     /**
 271      * Log access by a caller. The {@code what} parameter describes the class or
 272      * member that is being accessed. The {@code msgSupplier} supplies the log
 273      * message.
 274      *
 275      * To reduce output, this method only logs the access if it hasn't been seen
 276      * previously. "Seen previously" is implemented as a map of caller class -> Usage,
 277      * where a Usage is the "what" and a hash of the stack trace. The map has weak
 278      * keys so it can be expunged when the caller is GC'ed/unloaded.
 279      */
 280     private void log(Class<?> caller, String what, Supplier<String> msgSupplier) {
 281         if (mode == Mode.ONESHOT) {
 282             synchronized (IllegalAccessLogger.class) {
 283                 // discard the system wide logger
 284                 if (logger == null)
 285                     return;
 286                 logger = null;
 287             }
 288             warningStream.println(loudWarning(caller, msgSupplier));
 289             return;
 290         }
 291 
 292         // stack trace without the top-most frames in java.base
 293         List<StackWalker.StackFrame> stack = StackWalkerHolder.INSTANCE.walk(s ->
 294             s.dropWhile(this::isJavaBase)
 295              .limit(32)
 296              .collect(Collectors.toList())
 297         );
 298 
 299         // record usage if this is the first (or not recently recorded)
 300         Usage u = new Usage(what, hash(stack));
 301         boolean added;
 302         synchronized (this) {
 303             added = callerToUsages.computeIfAbsent(caller, k -> new Usages()).add(u);
 304         }
 305 
 306         // print warning if this is the first (or not a recent) usage
 307         if (added) {
 308             String msg = msgSupplier.get();
 309             if (mode == Mode.DEBUG) {
 310                 StringBuilder sb = new StringBuilder(msg);
 311                 stack.forEach(f ->
 312                     sb.append(System.lineSeparator()).append("\tat " + f)
 313                 );
 314                 msg = sb.toString();
 315             }
 316             warningStream.println(msg);
 317         }
 318     }
 319 
 320     /**
 321      * Returns the code source for the given class or null if there is no code source
 322      */
 323     private URL codeSource(Class<?> clazz) {
 324         PrivilegedAction<ProtectionDomain> pa = clazz::getProtectionDomain;
 325         CodeSource cs = AccessController.doPrivileged(pa).getCodeSource();
 326         return (cs != null) ? cs.getLocation() : null;
 327     }
 328 
 329     private String loudWarning(Class<?> caller,  Supplier<String> msgSupplier) {
 330         StringJoiner sj = new StringJoiner(System.lineSeparator());
 331         sj.add("WARNING: An illegal reflective access operation has occurred");
 332         sj.add(msgSupplier.get());
 333         sj.add("WARNING: Please consider reporting this to the maintainers of "
 334                 + caller.getName());
 335         sj.add("WARNING: Use --illegal-access=warn to enable warnings of further"
 336                 + " illegal reflective access operations");
 337         sj.add("WARNING: All illegal access operations will be denied in a"
 338                 + " future release");
 339         return sj.toString();
 340     }
 341 
 342     private static class StackWalkerHolder {
 343         static final StackWalker INSTANCE;
 344         static {
 345             PrivilegedAction<StackWalker> pa = () ->
 346                 StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
 347             INSTANCE = AccessController.doPrivileged(pa);
 348         }
 349     }
 350 
 351     /**
 352      * Returns true if the stack frame is for a class in java.base.
 353      */
 354     private boolean isJavaBase(StackWalker.StackFrame frame) {
 355         Module caller = frame.getDeclaringClass().getModule();
 356         return "java.base".equals(caller.getName());
 357     }
 358 
 359     /**
 360      * Computes a hash code for the give stack frames. The hash code is based
 361      * on the class, method name, and BCI.
 362      */
 363     private int hash(List<StackWalker.StackFrame> stack) {
 364         int hash = 0;
 365         for (StackWalker.StackFrame frame : stack) {
 366             hash = (31 * hash) + Objects.hash(frame.getDeclaringClass(),
 367                                               frame.getMethodName(),
 368                                               frame.getByteCodeIndex());
 369         }
 370         return hash;
 371     }
 372 
 373     private static class Usage {
 374         private final String what;
 375         private final int stack;
 376         Usage(String what, int stack) {
 377             this.what = what;
 378             this.stack = stack;
 379         }
 380         @Override
 381         public int hashCode() {
 382             return what.hashCode() ^ stack;
 383         }
 384         @Override
 385         public boolean equals(Object ob) {
 386             if (ob instanceof Usage) {
 387                 Usage that = (Usage)ob;
 388                 return what.equals(that.what) && stack == (that.stack);
 389             } else {
 390                 return false;
 391             }
 392         }
 393     }
 394 
 395     @SuppressWarnings("serial")
 396     private static class Usages extends LinkedHashMap<Usage, Boolean> {
 397         Usages() { }
 398         boolean add(Usage u) {
 399             return (putIfAbsent(u, Boolean.TRUE) == null);
 400         }
 401         @Override
 402         protected boolean removeEldestEntry(Map.Entry<Usage, Boolean> oldest) {
 403             // prevent map growing too big, say where a utility class
 404             // is used by generated code to do illegal access
 405             return size() > 16;
 406         }
 407     }
 408 }