8314263: Signed jars triggering Logger finder recursion and StackOverflowError

Co-authored-by: Daniel Fuchs <dfuchs@openjdk.org>
Reviewed-by: dfuchs
This commit is contained in:
Sean Coffey 2023-08-30 12:54:57 +00:00
parent 6701eba736
commit 7daae1fb42
12 changed files with 829 additions and 29 deletions

View File

@ -68,6 +68,7 @@ import java.util.function.Supplier;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import jdk.internal.logger.LoggerFinderLoader.TemporaryLoggerFinder;
import jdk.internal.misc.CarrierThreadLocal;
import jdk.internal.misc.Unsafe;
import jdk.internal.util.StaticProperty;
@ -1766,13 +1767,16 @@ public final class System {
// We do not need to synchronize: LoggerFinderLoader will
// always return the same instance, so if we don't have it,
// just fetch it again.
if (service == null) {
LoggerFinder finder = service;
if (finder == null) {
PrivilegedAction<LoggerFinder> pa =
() -> LoggerFinderLoader.getLoggerFinder();
service = AccessController.doPrivileged(pa, null,
finder = AccessController.doPrivileged(pa, null,
LOGGERFINDER_PERMISSION);
if (finder instanceof TemporaryLoggerFinder) return finder;
service = finder;
}
return service;
return finder;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2023, 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
@ -38,7 +38,6 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.lang.System.LoggerFinder;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
@ -227,9 +226,19 @@ public final class BootstrapLogger implements Logger, PlatformLogger.Bridge,
// The accessor in which this logger is temporarily set.
final LazyLoggerAccessor holder;
// tests whether the logger is invoked by the loading thread before
// the LoggerFinder is loaded; can be null;
final BooleanSupplier isLoadingThread;
BootstrapLogger(LazyLoggerAccessor holder) {
// returns true if the logger is invoked by the loading thread before the
// LoggerFinder service is loaded
boolean isLoadingThread() {
return isLoadingThread != null && isLoadingThread.getAsBoolean();
}
BootstrapLogger(LazyLoggerAccessor holder, BooleanSupplier isLoadingThread) {
this.holder = holder;
this.isLoadingThread = isLoadingThread;
}
// Temporary data object storing log events
@ -505,14 +514,15 @@ public final class BootstrapLogger implements Logger, PlatformLogger.Bridge,
static void log(LogEvent log, PlatformLogger.Bridge logger) {
final SecurityManager sm = System.getSecurityManager();
if (sm == null || log.acc == null) {
log.log(logger);
BootstrapExecutors.submit(() -> log.log(logger));
} else {
// not sure we can actually use lambda here. We may need to create
// an anonymous class. Although if we reach here, then it means
// the VM is booted.
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
log.log(logger); return null;
}, log.acc);
BootstrapExecutors.submit(() ->
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
log.log(logger); return null;
}, log.acc));
}
}
@ -559,8 +569,9 @@ public final class BootstrapLogger implements Logger, PlatformLogger.Bridge,
* @return true if the VM is still bootstrapping.
*/
boolean checkBootstrapping() {
if (isBooted()) {
if (isBooted() && !isLoadingThread()) {
BootstrapExecutors.flush();
holder.getConcreteLogger(this);
return false;
}
return true;
@ -935,10 +946,16 @@ public final class BootstrapLogger implements Logger, PlatformLogger.Bridge,
// - the logging backend is a custom backend
// - the logging backend is JUL, there is no custom config,
// and the LogManager has not been initialized yet.
public static synchronized boolean useLazyLoggers() {
return !BootstrapLogger.isBooted()
|| DetectBackend.detectedBackend == LoggingBackend.CUSTOM
|| useSurrogateLoggers();
public static boolean useLazyLoggers() {
// Note: avoid triggering the initialization of the DetectBackend class
// while holding the BootstrapLogger class monitor
if (!BootstrapLogger.isBooted() ||
DetectBackend.detectedBackend == LoggingBackend.CUSTOM) {
return true;
}
synchronized (BootstrapLogger.class) {
return useSurrogateLoggers();
}
}
// Called by LazyLoggerAccessor. This method will determine whether
@ -946,9 +963,9 @@ public final class BootstrapLogger implements Logger, PlatformLogger.Bridge,
// a SurrogateLogger (if JUL is the default backend and there
// is no custom JUL configuration and LogManager is not yet initialized),
// or a logger returned by the loaded LoggerFinder (all other cases).
static Logger getLogger(LazyLoggerAccessor accessor) {
if (!BootstrapLogger.isBooted()) {
return new BootstrapLogger(accessor);
static Logger getLogger(LazyLoggerAccessor accessor, BooleanSupplier isLoading) {
if (!BootstrapLogger.isBooted() || isLoading != null && isLoading.getAsBoolean()) {
return new BootstrapLogger(accessor, isLoading);
} else {
if (useSurrogateLoggers()) {
// JUL is the default backend, there is no custom configuration,
@ -964,6 +981,12 @@ public final class BootstrapLogger implements Logger, PlatformLogger.Bridge,
}
}
// trigger class initialization outside of holding lock
static void ensureBackendDetected() {
assert VM.isBooted() : "VM is not booted";
// triggers detection of the backend
var backend = DetectBackend.detectedBackend;
}
// If the backend is JUL, and there is no custom configuration, and
// nobody has attempted to call LogManager.getLogManager() yet, then

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2023, 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
@ -32,6 +32,9 @@ import java.lang.System.LoggerFinder;
import java.lang.System.Logger;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.function.BooleanSupplier;
import jdk.internal.logger.LoggerFinderLoader.TemporaryLoggerFinder;
import jdk.internal.misc.VM;
import sun.util.logging.PlatformLogger;
@ -110,6 +113,9 @@ public final class LazyLoggers {
// We need to pass the actual caller module when creating the logger.
private final WeakReference<Module> moduleRef;
// whether this is the loading thread, can be null
private final BooleanSupplier isLoadingThread;
// The name of the logger that will be created lazyly
final String name;
// The plain logger SPI object - null until it is accessed for the
@ -122,16 +128,24 @@ public final class LazyLoggers {
private LazyLoggerAccessor(String name,
LazyLoggerFactories<? extends Logger> factories,
Module module) {
this(Objects.requireNonNull(name), Objects.requireNonNull(factories),
Objects.requireNonNull(module), null);
this(name, factories, module, null);
}
private LazyLoggerAccessor(String name,
LazyLoggerFactories<? extends Logger> factories,
Module module, Void unused) {
Module module, BooleanSupplier isLoading) {
this(Objects.requireNonNull(name), Objects.requireNonNull(factories),
Objects.requireNonNull(module), isLoading, null);
}
private LazyLoggerAccessor(String name,
LazyLoggerFactories<? extends Logger> factories,
Module module, BooleanSupplier isLoading, Void unused) {
this.name = name;
this.factories = factories;
this.moduleRef = new WeakReference<>(module);
this.isLoadingThread = isLoading;
}
/**
@ -162,7 +176,7 @@ public final class LazyLoggers {
// BootstrapLogger has the logic to decide whether to invoke the
// SPI or use a temporary (BootstrapLogger or SimpleConsoleLogger)
// logger.
wrapped = BootstrapLogger.getLogger(this);
wrapped = BootstrapLogger.getLogger(this, isLoadingThread);
synchronized(this) {
// if w has already been in between, simply drop 'wrapped'.
setWrappedIfNotSet(wrapped);
@ -194,7 +208,7 @@ public final class LazyLoggers {
// BootstrapLogger has the logic to decide whether to invoke the
// SPI or use a temporary (BootstrapLogger or SimpleConsoleLogger)
// logger.
final Logger wrapped = BootstrapLogger.getLogger(this);
final Logger wrapped = BootstrapLogger.getLogger(this, isLoadingThread);
synchronized(this) {
// if w has already been set, simply drop 'wrapped'.
setWrappedIfNotSet(wrapped);
@ -282,10 +296,10 @@ public final class LazyLoggers {
* Creates a new lazy logger accessor for the named logger. The given
* factories will be use when it becomes necessary to actually create
* the logger.
* @param <T> An interface that extends {@link Logger}.
* @param name The logger name.
* @param factories The factories that should be used to create the
* wrapped logger.
* @param module The module for which the logger is being created
* @return A new LazyLoggerAccessor.
*/
public static LazyLoggerAccessor makeAccessor(String name,
@ -340,6 +354,7 @@ public final class LazyLoggers {
prov = sm == null ? LoggerFinder.getLoggerFinder() :
AccessController.doPrivileged(
(PrivilegedAction<LoggerFinder>)LoggerFinder::getLoggerFinder);
if (prov instanceof TemporaryLoggerFinder) return prov;
provider = prov;
}
return prov;
@ -359,7 +374,6 @@ public final class LazyLoggers {
new LazyLoggerFactories<>(loggerSupplier);
// A concrete implementation of Logger that delegates to a System.Logger,
// but only creates the System.Logger instance lazily when it's used for
// the first time.
@ -377,6 +391,11 @@ public final class LazyLoggers {
}
}
static Logger makeLazyLogger(String name, Module module, BooleanSupplier isLoading) {
final LazyLoggerAccessor holder = new LazyLoggerAccessor(name, factories, module, isLoading);
return new JdkLazyLogger(holder, null);
}
/**
* Gets a logger from the LoggerFinder. Creates the actual concrete
* logger.

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2023, 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
@ -25,6 +25,8 @@
package jdk.internal.logger;
import java.io.FilePermission;
import java.lang.System.Logger;
import java.lang.System.LoggerFinder;
import java.security.AccessController;
import java.security.Permission;
import java.security.PrivilegedAction;
@ -32,6 +34,9 @@ import java.util.Iterator;
import java.util.Locale;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.function.BooleanSupplier;
import jdk.internal.vm.annotation.Stable;
import sun.security.util.SecurityConstants;
import sun.security.action.GetBooleanAction;
import sun.security.action.GetPropertyAction;
@ -65,13 +70,28 @@ public final class LoggerFinderLoader {
throw new InternalError("LoggerFinderLoader cannot be instantiated");
}
// record the loadingThread while loading the backend
static volatile Thread loadingThread;
// Return the loaded LoggerFinder, or load it if not already loaded.
private static System.LoggerFinder service() {
if (service != null) return service;
// ensure backend is detected before attempting to load the finder
BootstrapLogger.ensureBackendDetected();
synchronized(lock) {
if (service != null) return service;
service = loadLoggerFinder();
Thread currentThread = Thread.currentThread();
if (loadingThread == currentThread) {
// recursive attempt to load the backend while loading the backend
// use a temporary logger finder that returns special BootstrapLogger
// which will wait until loading is finished
return TemporaryLoggerFinder.INSTANCE;
}
loadingThread = currentThread;
try {
service = loadLoggerFinder();
} finally {
loadingThread = null;
}
}
// Since the LoggerFinder is already loaded - we can stop using
// temporary loggers.
@ -79,6 +99,12 @@ public final class LoggerFinderLoader {
return service;
}
// returns true if called by the thread that loads the LoggerFinder, while
// loading the LoggerFinder.
static boolean isLoadingThread() {
return loadingThread != null && loadingThread == Thread.currentThread();
}
// Get configuration error policy
private static ErrorPolicy configurationErrorPolicy() {
String errorPolicy =
@ -117,6 +143,34 @@ public final class LoggerFinderLoader {
return iterator;
}
public static final class TemporaryLoggerFinder extends LoggerFinder {
private TemporaryLoggerFinder() {}
@Stable
private LoggerFinder loadedService;
private static final BooleanSupplier isLoadingThread = new BooleanSupplier() {
@Override
public boolean getAsBoolean() {
return LoggerFinderLoader.isLoadingThread();
}
};
private static final TemporaryLoggerFinder INSTANCE = new TemporaryLoggerFinder();
@Override
public Logger getLogger(String name, Module module) {
if (loadedService == null) {
loadedService = service;
if (loadedService == null) {
return LazyLoggers.makeLazyLogger(name, module, isLoadingThread);
}
}
assert loadedService != null;
assert !LoggerFinderLoader.isLoadingThread();
assert loadedService != this;
return LazyLoggers.getLogger(name, module);
}
}
// Loads the LoggerFinder using ServiceLoader. If no LoggerFinder
// is found returns the default (possibly JUL based) implementation
private static System.LoggerFinder loadLoggerFinder() {

View File

@ -0,0 +1 @@
loggerfinder.SimpleLoggerFinder

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) 2023, 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.
*
* 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.
*/
/*
* @test
* @bug 8314263
* @summary Creating a logger while loading the Logger finder
* triggers recursion and StackOverflowError
* @modules java.base/sun.util.logging
* @library /test/lib
* @compile RecursiveLoadingTest.java SimpleLoggerFinder.java
* @run main/othervm PlatformRecursiveLoadingTest
*/
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.logging.LogRecord;
import sun.util.logging.PlatformLogger;
public class PlatformRecursiveLoadingTest {
/**
* This test triggers recursion by calling `System.getLogger` in the class init and constructor
* of a custom LoggerFinder. Without the fix, this is expected to throw
* java.lang.NoClassDefFoundError: Could not initialize class jdk.internal.logger.LoggerFinderLoader$ErrorPolicy
* caused by: java.lang.StackOverflowError
*/
public static void main(String[] args) throws Throwable {
PlatformLogger.getLogger("main").info("in main");
List<Object> logs = loggerfinder.SimpleLoggerFinder.LOGS;
logs.stream().map(SimpleLogRecord::of).forEach(System.out::println);
logs.stream().map(SimpleLogRecord::of).forEach(SimpleLogRecord::check);
assertEquals(String.valueOf(logs.size()), String.valueOf(3));
}
static List<Object> asList(Object[] params) {
return params == null ? null : Arrays.asList(params);
}
record SimpleLogRecord(String message, Instant instant, String loggerName,
java.util.logging.Level level, List<Object> params,
String resourceBundleName, long seqNumber,
String sourceClassName, String methodName, Throwable thrown) {
SimpleLogRecord(LogRecord record) {
this(record.getMessage(), record.getInstant(), record.getLoggerName(), record.getLevel(),
asList(record.getParameters()), record.getResourceBundleName(), record.getSequenceNumber(),
record.getSourceClassName(), record.getSourceMethodName(), record.getThrown());
}
static SimpleLogRecord of(Object o) {
return (o instanceof LogRecord record) ? new SimpleLogRecord(record) : null;
}
static SimpleLogRecord check(SimpleLogRecord record) {
if (record.loggerName.equals("dummy")) {
assertEquals(record.sourceClassName, "jdk.internal.logger.BootstrapLogger$LogEvent");
assertEquals(record.methodName(), "log");
}
if (record.loggerName.equals("main")) {
assertEquals(record.sourceClassName, PlatformRecursiveLoadingTest.class.getName());
assertEquals(record.methodName, "main");
}
return record;
}
}
private static void assertEquals(String received, String expected) {
if (!expected.equals(received)) {
throw new RuntimeException("Received: " + received);
}
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) 2023, 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.
*
* 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.
*/
/*
* @test
* @bug 8314263
* @summary Creating a logger while loading the Logger finder
* triggers recursion and StackOverflowError
* @compile RecursiveLoadingTest.java SimpleLoggerFinder.java
* @run main/othervm RecursiveLoadingTest
*/
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.logging.LogRecord;
public class RecursiveLoadingTest {
/**
* This test triggers recursion by calling `System.getLogger` in the class init and constructor
* of a custom LoggerFinder. Without the fix, this is expected to throw
* java.lang.NoClassDefFoundError: Could not initialize class jdk.internal.logger.LoggerFinderLoader$ErrorPolicy
* caused by: java.lang.StackOverflowError
*/
public static void main(String[] args) throws Throwable {
System.getLogger("main").log(System.Logger.Level.INFO, "in main");
List<Object> logs = loggerfinder.SimpleLoggerFinder.LOGS;
logs.stream().map(SimpleLogRecord::of).forEach(System.out::println);
logs.stream().map(SimpleLogRecord::of).forEach(SimpleLogRecord::check);
assertEquals(String.valueOf(logs.size()), String.valueOf(3));
}
static List<Object> asList(Object[] params) {
return params == null ? null : Arrays.asList(params);
}
record SimpleLogRecord(String message, Instant instant, String loggerName,
java.util.logging.Level level, List<Object> params,
String resourceBundleName, long seqNumber,
String sourceClassName, String methodName, Throwable thrown) {
SimpleLogRecord(LogRecord record) {
this(record.getMessage(), record.getInstant(), record.getLoggerName(), record.getLevel(),
asList(record.getParameters()), record.getResourceBundleName(), record.getSequenceNumber(),
record.getSourceClassName(), record.getSourceMethodName(), record.getThrown());
}
static SimpleLogRecord of(Object o) {
return (o instanceof LogRecord record) ? new SimpleLogRecord(record) : null;
}
static SimpleLogRecord check(SimpleLogRecord record) {
if (record.loggerName.equals("dummy")) {
assertEquals(record.sourceClassName, "jdk.internal.logger.BootstrapLogger$LogEvent");
assertEquals(record.methodName(), "log");
}
if (record.loggerName.equals("main")) {
assertEquals(record.sourceClassName, RecursiveLoadingTest.class.getName());
assertEquals(record.methodName, "main");
}
return record;
}
}
private static void assertEquals(String received, String expected) {
if (!expected.equals(received)) {
throw new RuntimeException("Received: " + received);
}
}
}

View File

@ -0,0 +1,132 @@
/*
* Copyright (c) 2023, 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.
*
* 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 loggerfinder;
import java.lang.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
public class SimpleLoggerFinder extends System.LoggerFinder {
public static final CopyOnWriteArrayList<Object> LOGS = new CopyOnWriteArrayList<>();
static {
try {
long sleep = new Random().nextLong(1000L) + 1L;
// simulate a slow load service
Thread.sleep(sleep);
System.getLogger("dummy")
.log(System.Logger.Level.INFO,
"Logger finder service load sleep value: " + sleep);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private final Map<String, SimpleLogger> loggers = new ConcurrentHashMap<>();
public SimpleLoggerFinder() {
System.getLogger("dummy")
.log(System.Logger.Level.INFO,
"Logger finder service created");
}
@Override
public System.Logger getLogger(String name, Module module) {
return loggers.computeIfAbsent(name, SimpleLogger::new);
}
private static class SimpleLogger implements System.Logger {
private final java.util.logging.Logger logger;
private static final class SimpleHandler extends Handler {
@Override
public void publish(LogRecord record) {
LOGS.add(record);
}
@Override public void flush() { }
@Override public void close() { }
}
public SimpleLogger(String name) {
logger = Logger.getLogger(name);
logger.addHandler(new SimpleHandler());
}
@Override
public String getName() {
return logger.getName();
}
java.util.logging.Level level(Level level) {
return switch (level) {
case ALL -> java.util.logging.Level.ALL;
case DEBUG -> java.util.logging.Level.FINE;
case TRACE -> java.util.logging.Level.FINER;
case INFO -> java.util.logging.Level.INFO;
case WARNING -> java.util.logging.Level.WARNING;
case ERROR -> java.util.logging.Level.SEVERE;
case OFF -> java.util.logging.Level.OFF;
};
}
@Override
public boolean isLoggable(Level level) {
return logger.isLoggable(level(level));
}
@Override
public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
var julLevel = level(level);
if (!logger.isLoggable(julLevel)) return;
if (bundle != null) {
logger.logrb(julLevel, bundle, msg, thrown);
} else {
logger.log(julLevel, msg, thrown);
}
}
@Override
public void log(Level level, ResourceBundle bundle, String format, Object... params) {
var julLevel = level(level);
if (!logger.isLoggable(julLevel)) return;
if (params == null) {
if (bundle == null) {
logger.log(julLevel, format);
} else {
logger.logrb(julLevel, bundle, format);
}
} else {
if (bundle == null) {
logger.log(julLevel, format, params);
} else {
logger.logrb(julLevel, bundle, format, params);
}
}
}
}
}

View File

@ -0,0 +1,297 @@
/*
* Copyright (c) 2023, 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.
*
* 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.
*/
/*
* @test
* @bug 8314263
* @summary Signed jars triggering Logger finder recursion and StackOverflowError
* @library /test/lib
* @build jdk.test.lib.compiler.CompilerUtils
* jdk.test.lib.process.*
* jdk.test.lib.util.JarUtils
* jdk.test.lib.JDKToolLauncher
* @compile SignedLoggerFinderTest.java SimpleLoggerFinder.java
* @run main SignedLoggerFinderTest init
* @run main SignedLoggerFinderTest init sign
*/
import java.io.File;
import java.nio.file.*;
import java.security.*;
import java.util.*;
import java.util.function.*;
import java.util.jar.*;
import jdk.test.lib.JDKToolFinder;
import jdk.test.lib.JDKToolLauncher;
import jdk.test.lib.Utils;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.util.JarUtils;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Arrays.asList;
public class SignedLoggerFinderTest {
/**
* This test triggers recursion in the broken JDK. The error can
* manifest in a few different ways.
* One error seen is "java.lang.NoClassDefFoundError:
* Could not initialize class jdk.internal.logger.LoggerFinderLoader$ErrorPolicy"
*
* The original reported error was a StackOverflow (also seen in different iterations
* of this run). Running test in signed and unsigned jar mode for sanity coverage.
* The current bug only manifests when jars are signed.
*/
private static boolean init = false;
private static boolean signJars = false;
private static boolean mutliThreadLoad = false;
private static volatile boolean testComplete = false;
private static final String KEYSTORE = "8314263.keystore";
private static final String ALIAS = "JavaTest";
private static final String STOREPASS = "changeit";
private static final String KEYPASS = "changeit";
private static final String DNAME = "CN=sample";
private static final String CUSTOM_LOGGER_FINDER_NAME =
"loggerfinder.SimpleLoggerFinder";
private static final String CUSTOM_LOGGER_NAME =
"loggerfinder.SimpleLoggerFinder$SimpleLogger";
private static final String INTERNAL_LOGGER_FINDER_NAME =
"sun.util.logging.internal.LoggingProviderImpl";
private static final String INTERNAL_LOGGER_NAME =
"sun.util.logging.internal.LoggingProviderImpl$JULWrapper";
private static final Path jarPath1 =
Path.of(System.getProperty("test.classes", "."), "SimpleLoggerFinder.jar");
private static final Path jarPath2 =
Path.of(System.getProperty("test.classes", "."), "SimpleLoggerFinder2.jar");
public static void main(String[] args) throws Throwable {
init = args.length >=1 && args[0].equals("init");
signJars = args.length >=2 && args[1].equals("sign");
// init only passed in by jtreg test run, initialize the environment
// for the subsequent test run
if (init) {
initialize();
launchTest(false, false);
launchTest(false, true);
launchTest(true, false);
launchTest(true, true);
} else {
// set up complete. Run the code to trigger the recursion
// We're in the JVM launched by ProcessTools.executeCommand
boolean multiThreadLoad = Boolean.getBoolean("multiThreadLoad");
boolean withCustomLoggerFinder = Boolean.getBoolean("withCustomLoggerFinder");
if (multiThreadLoad) {
long sleep = new Random().nextLong(100L) + 1L;
System.out.println("multi thread load sleep value: " + sleep);
new Thread(runnableWithSleep(
() -> System.getLogger("logger" + System.currentTimeMillis()),
sleep, "System.getLogger type: ")).start();
new Thread(runnableWithSleep(
() -> System.LoggerFinder.getLoggerFinder(),
sleep, "System.getLoggerFinder type: ")).start();
}
if (withCustomLoggerFinder) {
JarFile jf = new JarFile(jarPath1.toString(), true);
jf.getInputStream(jf.getJarEntry("loggerfinder/SimpleLoggerFinder.class"));
JarFile jf2 = new JarFile(jarPath2.toString(), true);
jf2.getInputStream(jf.getJarEntry("loggerfinder/SimpleLoggerFinder.class"));
} else {
// some other call to prod LoggerFinder loading
System.getLogger("random" + System.currentTimeMillis());
System.LoggerFinder.getLoggerFinder();
}
Security.setProperty("test_1", "test");
// some extra sanity checks
if (withCustomLoggerFinder) {
assertEquals(System.LoggerFinder.getLoggerFinder().getClass().getName(),
CUSTOM_LOGGER_FINDER_NAME);
System.Logger testLogger = System.getLogger("jdk.event.security");
assertEquals(testLogger.getClass().getName(), CUSTOM_LOGGER_NAME);
} else {
assertEquals(System.LoggerFinder.getLoggerFinder().getClass().getName(),
INTERNAL_LOGGER_FINDER_NAME);
System.Logger testLogger = System.getLogger("jdk.event.security");
assertEquals(testLogger.getClass().getName(), INTERNAL_LOGGER_NAME);
}
testComplete = true;
// LoggerFinder should be initialized, trigger a simple log call
Security.setProperty("test_2", "test");
}
}
// helper to create the inner test. Run config variations with the LoggerFinder jars
// on the classpath and with other threads running System.Logger calls during load
private static void launchTest(boolean multiThreadLoad, boolean withCustomLoggerFinder) {
List<String> cmds = new ArrayList<>();
cmds.add(JDKToolFinder.getJDKTool("java"));
cmds.addAll(asList(Utils.getTestJavaOpts()));
if (withCustomLoggerFinder) {
cmds.addAll(List.of("-classpath",
System.getProperty("test.classes") + File.pathSeparator +
jarPath1.toString() + File.pathSeparator + jarPath2.toString(),
"-Dtest.classes=" + System.getProperty("test.classes")));
} else {
cmds.addAll(List.of("-classpath",
System.getProperty("test.classes")));
}
cmds.addAll(List.of(
// following debug property seems useful to tickle the issue
"-Dsun.misc.URLClassPath.debug=true",
// console logger level to capture event output
"-Djdk.system.logger.level=DEBUG",
// useful for debug purposes
"-Djdk.logger.finder.error=DEBUG",
// enable logging to verify correct output
"-Djava.util.logging.config.file=" +
Path.of(System.getProperty("test.src", "."), "logging.properties")));
if (multiThreadLoad) {
cmds.add("-DmultiThreadLoad=true");
}
if (withCustomLoggerFinder) {
cmds.add("-DwithCustomLoggerFinder=true");
}
cmds.addAll(List.of(
"SignedLoggerFinderTest",
"no-init"));
try {
OutputAnalyzer outputAnalyzer = ProcessTools.executeCommand(cmds.stream()
.filter(t -> !t.isEmpty())
.toArray(String[]::new))
.shouldHaveExitValue(0);
if (withCustomLoggerFinder) {
outputAnalyzer
.shouldContain("TEST LOGGER: [test_1, test]")
.shouldContain("TEST LOGGER: [test_2, test]");
} else {
outputAnalyzer
.shouldContain("SecurityPropertyModification: key:test_1")
.shouldContain("SecurityPropertyModification: key:test_2");
}
if (withCustomLoggerFinder && signJars) {
// X509 cert generated during verification of signed jar file
outputAnalyzer
.shouldContain(DNAME);
}
} catch (Throwable t) {
throw new RuntimeException("Unexpected fail.", t);
}
}
private static Runnable runnableWithSleep(Supplier s, long sleep, String desc) {
return () -> {
while(!testComplete) {
System.out.println(desc + s.get().getClass().getName());
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
}
private static void initialize() throws Throwable {
if (signJars) {
genKey();
}
Path classes = Paths.get(System.getProperty("test.classes", ""));
JarUtils.createJarFile(jarPath1,
classes,
classes.resolve("loggerfinder/SimpleLoggerFinder.class"),
classes.resolve("loggerfinder/SimpleLoggerFinder$SimpleLogger.class"));
JarUtils.updateJarFile(jarPath1, Path.of(System.getProperty("test.src")),
Path.of("META-INF", "services", "java.lang.System$LoggerFinder"));
if (signJars) {
signJar(jarPath1.toString());
}
// multiple signed jars with services to have ServiceLoader iteration
Files.copy(jarPath1, jarPath2, REPLACE_EXISTING);
}
private static void genKey() throws Throwable {
String keytool = JDKToolFinder.getJDKTool("keytool");
Files.deleteIfExists(Paths.get(KEYSTORE));
ProcessTools.executeCommand(keytool,
"-J-Duser.language=en",
"-J-Duser.country=US",
"-genkey",
"-keyalg", "rsa",
"-alias", ALIAS,
"-keystore", KEYSTORE,
"-keypass", KEYPASS,
"-dname", DNAME,
"-storepass", STOREPASS
).shouldHaveExitValue(0);
}
private static OutputAnalyzer signJar(String jarName) throws Throwable {
List<String> args = new ArrayList<>();
args.add("-verbose");
args.add(jarName);
args.add(ALIAS);
return jarsigner(args);
}
private static OutputAnalyzer jarsigner(List<String> extra)
throws Throwable {
JDKToolLauncher launcher = JDKToolLauncher.createUsingTestJDK("jarsigner")
.addVMArg("-Duser.language=en")
.addVMArg("-Duser.country=US")
.addToolArg("-keystore")
.addToolArg(KEYSTORE)
.addToolArg("-storepass")
.addToolArg(STOREPASS)
.addToolArg("-keypass")
.addToolArg(KEYPASS);
for (String s : extra) {
if (s.startsWith("-J")) {
launcher.addVMArg(s.substring(2));
} else {
launcher.addToolArg(s);
}
}
return ProcessTools.executeCommand(launcher.getCommand());
}
private static void assertEquals(String received, String expected) {
if (!expected.equals(received)) {
throw new RuntimeException("Received: " + received);
}
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2023, 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.
*
* 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 loggerfinder;
import java.lang.*;
import java.util.*;
public class SimpleLoggerFinder extends System.LoggerFinder {
static {
try {
long sleep = new Random().nextLong(1000L) + 1L;
System.out.println("Logger finder service load sleep value: " + sleep);
// simulate a slow load service
Thread.sleep(sleep);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public System.Logger getLogger(String name, Module module) {
return new SimpleLogger(name);
}
private static class SimpleLogger implements System.Logger {
private final String name;
public SimpleLogger(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public boolean isLoggable(Level level) {
return true;
}
@Override
public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
System.out.println("TEST LOGGER: " + msg);
}
@Override
public void log(Level level, ResourceBundle bundle, String format, Object... params) {
System.out.println("TEST LOGGER: " + Arrays.asList(params));
}
}
}

View File

@ -0,0 +1,14 @@
############################################################
# Configuration file for log testing
#
############################################################
handlers= java.util.logging.ConsoleHandler
.level= FINE
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
jdk.event.security.level = FINE