Skip to content

Commit 7daae1f

Browse files
coffeysdfuch
andcommittedAug 30, 2023
8314263: Signed jars triggering Logger finder recursion and StackOverflowError
Co-authored-by: Daniel Fuchs <dfuchs@openjdk.org> Reviewed-by: dfuchs
1 parent 6701eba commit 7daae1f

File tree

12 files changed

+828
-28
lines changed

12 files changed

+828
-28
lines changed
 

‎src/java.base/share/classes/java/lang/System.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import java.util.concurrent.ConcurrentHashMap;
6969
import java.util.stream.Stream;
7070

71+
import jdk.internal.logger.LoggerFinderLoader.TemporaryLoggerFinder;
7172
import jdk.internal.misc.CarrierThreadLocal;
7273
import jdk.internal.misc.Unsafe;
7374
import jdk.internal.util.StaticProperty;
@@ -1766,13 +1767,16 @@ static LoggerFinder accessProvider() {
17661767
// We do not need to synchronize: LoggerFinderLoader will
17671768
// always return the same instance, so if we don't have it,
17681769
// just fetch it again.
1769-
if (service == null) {
1770+
LoggerFinder finder = service;
1771+
if (finder == null) {
17701772
PrivilegedAction<LoggerFinder> pa =
17711773
() -> LoggerFinderLoader.getLoggerFinder();
1772-
service = AccessController.doPrivileged(pa, null,
1774+
finder = AccessController.doPrivileged(pa, null,
17731775
LOGGERFINDER_PERMISSION);
1776+
if (finder instanceof TemporaryLoggerFinder) return finder;
1777+
service = finder;
17741778
}
1775-
return service;
1779+
return finder;
17761780
}
17771781

17781782
}

‎src/java.base/share/classes/jdk/internal/logger/BootstrapLogger.java

+38-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -38,7 +38,6 @@
3838
import java.util.function.Supplier;
3939
import java.lang.System.LoggerFinder;
4040
import java.lang.System.Logger;
41-
import java.lang.System.Logger.Level;
4241
import java.lang.ref.WeakReference;
4342
import java.util.Objects;
4443
import java.util.concurrent.ExecutionException;
@@ -227,9 +226,19 @@ static void flush() {
227226

228227
// The accessor in which this logger is temporarily set.
229228
final LazyLoggerAccessor holder;
229+
// tests whether the logger is invoked by the loading thread before
230+
// the LoggerFinder is loaded; can be null;
231+
final BooleanSupplier isLoadingThread;
232+
233+
// returns true if the logger is invoked by the loading thread before the
234+
// LoggerFinder service is loaded
235+
boolean isLoadingThread() {
236+
return isLoadingThread != null && isLoadingThread.getAsBoolean();
237+
}
230238

231-
BootstrapLogger(LazyLoggerAccessor holder) {
239+
BootstrapLogger(LazyLoggerAccessor holder, BooleanSupplier isLoadingThread) {
232240
this.holder = holder;
241+
this.isLoadingThread = isLoadingThread;
233242
}
234243

235244
// Temporary data object storing log events
@@ -505,14 +514,15 @@ static LogEvent valueOf(BootstrapLogger bootstrap, PlatformLogger.Level level,
505514
static void log(LogEvent log, PlatformLogger.Bridge logger) {
506515
final SecurityManager sm = System.getSecurityManager();
507516
if (sm == null || log.acc == null) {
508-
log.log(logger);
517+
BootstrapExecutors.submit(() -> log.log(logger));
509518
} else {
510519
// not sure we can actually use lambda here. We may need to create
511520
// an anonymous class. Although if we reach here, then it means
512521
// the VM is booted.
513-
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
514-
log.log(logger); return null;
515-
}, log.acc);
522+
BootstrapExecutors.submit(() ->
523+
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
524+
log.log(logger); return null;
525+
}, log.acc));
516526
}
517527
}
518528

@@ -559,8 +569,9 @@ public String getName() {
559569
* @return true if the VM is still bootstrapping.
560570
*/
561571
boolean checkBootstrapping() {
562-
if (isBooted()) {
572+
if (isBooted() && !isLoadingThread()) {
563573
BootstrapExecutors.flush();
574+
holder.getConcreteLogger(this);
564575
return false;
565576
}
566577
return true;
@@ -935,20 +946,26 @@ private static boolean useSurrogateLoggers() {
935946
// - the logging backend is a custom backend
936947
// - the logging backend is JUL, there is no custom config,
937948
// and the LogManager has not been initialized yet.
938-
public static synchronized boolean useLazyLoggers() {
939-
return !BootstrapLogger.isBooted()
940-
|| DetectBackend.detectedBackend == LoggingBackend.CUSTOM
941-
|| useSurrogateLoggers();
949+
public static boolean useLazyLoggers() {
950+
// Note: avoid triggering the initialization of the DetectBackend class
951+
// while holding the BootstrapLogger class monitor
952+
if (!BootstrapLogger.isBooted() ||
953+
DetectBackend.detectedBackend == LoggingBackend.CUSTOM) {
954+
return true;
955+
}
956+
synchronized (BootstrapLogger.class) {
957+
return useSurrogateLoggers();
958+
}
942959
}
943960

944961
// Called by LazyLoggerAccessor. This method will determine whether
945962
// to create a BootstrapLogger (if the VM is not yet booted),
946963
// a SurrogateLogger (if JUL is the default backend and there
947964
// is no custom JUL configuration and LogManager is not yet initialized),
948965
// or a logger returned by the loaded LoggerFinder (all other cases).
949-
static Logger getLogger(LazyLoggerAccessor accessor) {
950-
if (!BootstrapLogger.isBooted()) {
951-
return new BootstrapLogger(accessor);
966+
static Logger getLogger(LazyLoggerAccessor accessor, BooleanSupplier isLoading) {
967+
if (!BootstrapLogger.isBooted() || isLoading != null && isLoading.getAsBoolean()) {
968+
return new BootstrapLogger(accessor, isLoading);
952969
} else {
953970
if (useSurrogateLoggers()) {
954971
// JUL is the default backend, there is no custom configuration,
@@ -964,6 +981,12 @@ static Logger getLogger(LazyLoggerAccessor accessor) {
964981
}
965982
}
966983

984+
// trigger class initialization outside of holding lock
985+
static void ensureBackendDetected() {
986+
assert VM.isBooted() : "VM is not booted";
987+
// triggers detection of the backend
988+
var backend = DetectBackend.detectedBackend;
989+
}
967990

968991
// If the backend is JUL, and there is no custom configuration, and
969992
// nobody has attempted to call LogManager.getLogManager() yet, then

‎src/java.base/share/classes/jdk/internal/logger/LazyLoggers.java

+26-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -32,6 +32,9 @@
3232
import java.lang.System.Logger;
3333
import java.lang.ref.WeakReference;
3434
import java.util.Objects;
35+
import java.util.function.BooleanSupplier;
36+
37+
import jdk.internal.logger.LoggerFinderLoader.TemporaryLoggerFinder;
3538
import jdk.internal.misc.VM;
3639
import sun.util.logging.PlatformLogger;
3740

@@ -110,6 +113,9 @@ static final class LazyLoggerAccessor implements LoggerAccessor {
110113
// We need to pass the actual caller module when creating the logger.
111114
private final WeakReference<Module> moduleRef;
112115

116+
// whether this is the loading thread, can be null
117+
private final BooleanSupplier isLoadingThread;
118+
113119
// The name of the logger that will be created lazyly
114120
final String name;
115121
// The plain logger SPI object - null until it is accessed for the
@@ -122,16 +128,24 @@ static final class LazyLoggerAccessor implements LoggerAccessor {
122128
private LazyLoggerAccessor(String name,
123129
LazyLoggerFactories<? extends Logger> factories,
124130
Module module) {
131+
this(name, factories, module, null);
132+
}
133+
134+
private LazyLoggerAccessor(String name,
135+
LazyLoggerFactories<? extends Logger> factories,
136+
Module module, BooleanSupplier isLoading) {
137+
125138
this(Objects.requireNonNull(name), Objects.requireNonNull(factories),
126-
Objects.requireNonNull(module), null);
139+
Objects.requireNonNull(module), isLoading, null);
127140
}
128141

129142
private LazyLoggerAccessor(String name,
130143
LazyLoggerFactories<? extends Logger> factories,
131-
Module module, Void unused) {
144+
Module module, BooleanSupplier isLoading, Void unused) {
132145
this.name = name;
133146
this.factories = factories;
134147
this.moduleRef = new WeakReference<>(module);
148+
this.isLoadingThread = isLoading;
135149
}
136150

137151
/**
@@ -162,7 +176,7 @@ public Logger wrapped() {
162176
// BootstrapLogger has the logic to decide whether to invoke the
163177
// SPI or use a temporary (BootstrapLogger or SimpleConsoleLogger)
164178
// logger.
165-
wrapped = BootstrapLogger.getLogger(this);
179+
wrapped = BootstrapLogger.getLogger(this, isLoadingThread);
166180
synchronized(this) {
167181
// if w has already been in between, simply drop 'wrapped'.
168182
setWrappedIfNotSet(wrapped);
@@ -194,7 +208,7 @@ public PlatformLogger.Bridge platform() {
194208
// BootstrapLogger has the logic to decide whether to invoke the
195209
// SPI or use a temporary (BootstrapLogger or SimpleConsoleLogger)
196210
// logger.
197-
final Logger wrapped = BootstrapLogger.getLogger(this);
211+
final Logger wrapped = BootstrapLogger.getLogger(this, isLoadingThread);
198212
synchronized(this) {
199213
// if w has already been set, simply drop 'wrapped'.
200214
setWrappedIfNotSet(wrapped);
@@ -282,10 +296,10 @@ Logger createLogger() {
282296
* Creates a new lazy logger accessor for the named logger. The given
283297
* factories will be use when it becomes necessary to actually create
284298
* the logger.
285-
* @param <T> An interface that extends {@link Logger}.
286299
* @param name The logger name.
287300
* @param factories The factories that should be used to create the
288301
* wrapped logger.
302+
* @param module The module for which the logger is being created
289303
* @return A new LazyLoggerAccessor.
290304
*/
291305
public static LazyLoggerAccessor makeAccessor(String name,
@@ -340,6 +354,7 @@ private static LoggerFinder accessLoggerFinder() {
340354
prov = sm == null ? LoggerFinder.getLoggerFinder() :
341355
AccessController.doPrivileged(
342356
(PrivilegedAction<LoggerFinder>)LoggerFinder::getLoggerFinder);
357+
if (prov instanceof TemporaryLoggerFinder) return prov;
343358
provider = prov;
344359
}
345360
return prov;
@@ -359,7 +374,6 @@ public Logger apply(String name, Module module) {
359374
new LazyLoggerFactories<>(loggerSupplier);
360375

361376

362-
363377
// A concrete implementation of Logger that delegates to a System.Logger,
364378
// but only creates the System.Logger instance lazily when it's used for
365379
// the first time.
@@ -377,6 +391,11 @@ private JdkLazyLogger(LazyLoggerAccessor holder, Void unused) {
377391
}
378392
}
379393

394+
static Logger makeLazyLogger(String name, Module module, BooleanSupplier isLoading) {
395+
final LazyLoggerAccessor holder = new LazyLoggerAccessor(name, factories, module, isLoading);
396+
return new JdkLazyLogger(holder, null);
397+
}
398+
380399
/**
381400
* Gets a logger from the LoggerFinder. Creates the actual concrete
382401
* logger.

‎src/java.base/share/classes/jdk/internal/logger/LoggerFinderLoader.java

+57-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -25,13 +25,18 @@
2525
package jdk.internal.logger;
2626

2727
import java.io.FilePermission;
28+
import java.lang.System.Logger;
29+
import java.lang.System.LoggerFinder;
2830
import java.security.AccessController;
2931
import java.security.Permission;
3032
import java.security.PrivilegedAction;
3133
import java.util.Iterator;
3234
import java.util.Locale;
3335
import java.util.ServiceConfigurationError;
3436
import java.util.ServiceLoader;
37+
import java.util.function.BooleanSupplier;
38+
39+
import jdk.internal.vm.annotation.Stable;
3540
import sun.security.util.SecurityConstants;
3641
import sun.security.action.GetBooleanAction;
3742
import sun.security.action.GetPropertyAction;
@@ -65,20 +70,41 @@ private LoggerFinderLoader() {
6570
throw new InternalError("LoggerFinderLoader cannot be instantiated");
6671
}
6772

68-
73+
// record the loadingThread while loading the backend
74+
static volatile Thread loadingThread;
6975
// Return the loaded LoggerFinder, or load it if not already loaded.
7076
private static System.LoggerFinder service() {
7177
if (service != null) return service;
78+
// ensure backend is detected before attempting to load the finder
79+
BootstrapLogger.ensureBackendDetected();
7280
synchronized(lock) {
7381
if (service != null) return service;
74-
service = loadLoggerFinder();
82+
Thread currentThread = Thread.currentThread();
83+
if (loadingThread == currentThread) {
84+
// recursive attempt to load the backend while loading the backend
85+
// use a temporary logger finder that returns special BootstrapLogger
86+
// which will wait until loading is finished
87+
return TemporaryLoggerFinder.INSTANCE;
88+
}
89+
loadingThread = currentThread;
90+
try {
91+
service = loadLoggerFinder();
92+
} finally {
93+
loadingThread = null;
94+
}
7595
}
7696
// Since the LoggerFinder is already loaded - we can stop using
7797
// temporary loggers.
7898
BootstrapLogger.redirectTemporaryLoggers();
7999
return service;
80100
}
81101

102+
// returns true if called by the thread that loads the LoggerFinder, while
103+
// loading the LoggerFinder.
104+
static boolean isLoadingThread() {
105+
return loadingThread != null && loadingThread == Thread.currentThread();
106+
}
107+
82108
// Get configuration error policy
83109
private static ErrorPolicy configurationErrorPolicy() {
84110
String errorPolicy =
@@ -117,6 +143,34 @@ private static Iterator<System.LoggerFinder> findLoggerFinderProviders() {
117143
return iterator;
118144
}
119145

146+
public static final class TemporaryLoggerFinder extends LoggerFinder {
147+
private TemporaryLoggerFinder() {}
148+
@Stable
149+
private LoggerFinder loadedService;
150+
151+
private static final BooleanSupplier isLoadingThread = new BooleanSupplier() {
152+
@Override
153+
public boolean getAsBoolean() {
154+
return LoggerFinderLoader.isLoadingThread();
155+
}
156+
};
157+
private static final TemporaryLoggerFinder INSTANCE = new TemporaryLoggerFinder();
158+
159+
@Override
160+
public Logger getLogger(String name, Module module) {
161+
if (loadedService == null) {
162+
loadedService = service;
163+
if (loadedService == null) {
164+
return LazyLoggers.makeLazyLogger(name, module, isLoadingThread);
165+
}
166+
}
167+
assert loadedService != null;
168+
assert !LoggerFinderLoader.isLoadingThread();
169+
assert loadedService != this;
170+
return LazyLoggers.getLogger(name, module);
171+
}
172+
}
173+
120174
// Loads the LoggerFinder using ServiceLoader. If no LoggerFinder
121175
// is found returns the default (possibly JUL based) implementation
122176
private static System.LoggerFinder loadLoggerFinder() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
loggerfinder.SimpleLoggerFinder
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (c) 2023, 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.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8314263
27+
* @summary Creating a logger while loading the Logger finder
28+
* triggers recursion and StackOverflowError
29+
* @modules java.base/sun.util.logging
30+
* @library /test/lib
31+
* @compile RecursiveLoadingTest.java SimpleLoggerFinder.java
32+
* @run main/othervm PlatformRecursiveLoadingTest
33+
*/
34+
35+
36+
import java.time.Instant;
37+
import java.util.Arrays;
38+
import java.util.List;
39+
import java.util.logging.LogRecord;
40+
41+
import sun.util.logging.PlatformLogger;
42+
43+
public class PlatformRecursiveLoadingTest {
44+
45+
/**
46+
* This test triggers recursion by calling `System.getLogger` in the class init and constructor
47+
* of a custom LoggerFinder. Without the fix, this is expected to throw
48+
* java.lang.NoClassDefFoundError: Could not initialize class jdk.internal.logger.LoggerFinderLoader$ErrorPolicy
49+
* caused by: java.lang.StackOverflowError
50+
*/
51+
public static void main(String[] args) throws Throwable {
52+
PlatformLogger.getLogger("main").info("in main");
53+
List<Object> logs = loggerfinder.SimpleLoggerFinder.LOGS;
54+
logs.stream().map(SimpleLogRecord::of).forEach(System.out::println);
55+
logs.stream().map(SimpleLogRecord::of).forEach(SimpleLogRecord::check);
56+
assertEquals(String.valueOf(logs.size()), String.valueOf(3));
57+
}
58+
59+
static List<Object> asList(Object[] params) {
60+
return params == null ? null : Arrays.asList(params);
61+
}
62+
63+
record SimpleLogRecord(String message, Instant instant, String loggerName,
64+
java.util.logging.Level level, List<Object> params,
65+
String resourceBundleName, long seqNumber,
66+
String sourceClassName, String methodName, Throwable thrown) {
67+
SimpleLogRecord(LogRecord record) {
68+
this(record.getMessage(), record.getInstant(), record.getLoggerName(), record.getLevel(),
69+
asList(record.getParameters()), record.getResourceBundleName(), record.getSequenceNumber(),
70+
record.getSourceClassName(), record.getSourceMethodName(), record.getThrown());
71+
}
72+
static SimpleLogRecord of(Object o) {
73+
return (o instanceof LogRecord record) ? new SimpleLogRecord(record) : null;
74+
}
75+
static SimpleLogRecord check(SimpleLogRecord record) {
76+
if (record.loggerName.equals("dummy")) {
77+
assertEquals(record.sourceClassName, "jdk.internal.logger.BootstrapLogger$LogEvent");
78+
assertEquals(record.methodName(), "log");
79+
}
80+
if (record.loggerName.equals("main")) {
81+
assertEquals(record.sourceClassName, PlatformRecursiveLoadingTest.class.getName());
82+
assertEquals(record.methodName, "main");
83+
}
84+
return record;
85+
}
86+
}
87+
88+
private static void assertEquals(String received, String expected) {
89+
if (!expected.equals(received)) {
90+
throw new RuntimeException("Received: " + received);
91+
}
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (c) 2023, 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.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8314263
27+
* @summary Creating a logger while loading the Logger finder
28+
* triggers recursion and StackOverflowError
29+
* @compile RecursiveLoadingTest.java SimpleLoggerFinder.java
30+
* @run main/othervm RecursiveLoadingTest
31+
*/
32+
33+
import java.time.Instant;
34+
import java.util.Arrays;
35+
import java.util.List;
36+
import java.util.logging.LogRecord;
37+
38+
public class RecursiveLoadingTest {
39+
40+
/**
41+
* This test triggers recursion by calling `System.getLogger` in the class init and constructor
42+
* of a custom LoggerFinder. Without the fix, this is expected to throw
43+
* java.lang.NoClassDefFoundError: Could not initialize class jdk.internal.logger.LoggerFinderLoader$ErrorPolicy
44+
* caused by: java.lang.StackOverflowError
45+
*/
46+
public static void main(String[] args) throws Throwable {
47+
System.getLogger("main").log(System.Logger.Level.INFO, "in main");
48+
List<Object> logs = loggerfinder.SimpleLoggerFinder.LOGS;
49+
logs.stream().map(SimpleLogRecord::of).forEach(System.out::println);
50+
logs.stream().map(SimpleLogRecord::of).forEach(SimpleLogRecord::check);
51+
assertEquals(String.valueOf(logs.size()), String.valueOf(3));
52+
}
53+
54+
static List<Object> asList(Object[] params) {
55+
return params == null ? null : Arrays.asList(params);
56+
}
57+
58+
record SimpleLogRecord(String message, Instant instant, String loggerName,
59+
java.util.logging.Level level, List<Object> params,
60+
String resourceBundleName, long seqNumber,
61+
String sourceClassName, String methodName, Throwable thrown) {
62+
SimpleLogRecord(LogRecord record) {
63+
this(record.getMessage(), record.getInstant(), record.getLoggerName(), record.getLevel(),
64+
asList(record.getParameters()), record.getResourceBundleName(), record.getSequenceNumber(),
65+
record.getSourceClassName(), record.getSourceMethodName(), record.getThrown());
66+
}
67+
static SimpleLogRecord of(Object o) {
68+
return (o instanceof LogRecord record) ? new SimpleLogRecord(record) : null;
69+
}
70+
static SimpleLogRecord check(SimpleLogRecord record) {
71+
if (record.loggerName.equals("dummy")) {
72+
assertEquals(record.sourceClassName, "jdk.internal.logger.BootstrapLogger$LogEvent");
73+
assertEquals(record.methodName(), "log");
74+
}
75+
if (record.loggerName.equals("main")) {
76+
assertEquals(record.sourceClassName, RecursiveLoadingTest.class.getName());
77+
assertEquals(record.methodName, "main");
78+
}
79+
return record;
80+
}
81+
}
82+
83+
private static void assertEquals(String received, String expected) {
84+
if (!expected.equals(received)) {
85+
throw new RuntimeException("Received: " + received);
86+
}
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright (c) 2023, 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.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
package loggerfinder;
25+
26+
import java.lang.*;
27+
import java.util.*;
28+
import java.util.concurrent.CopyOnWriteArrayList;
29+
import java.util.concurrent.ConcurrentHashMap;
30+
import java.util.logging.Handler;
31+
import java.util.logging.LogRecord;
32+
import java.util.logging.Logger;
33+
34+
public class SimpleLoggerFinder extends System.LoggerFinder {
35+
36+
public static final CopyOnWriteArrayList<Object> LOGS = new CopyOnWriteArrayList<>();
37+
static {
38+
try {
39+
long sleep = new Random().nextLong(1000L) + 1L;
40+
// simulate a slow load service
41+
Thread.sleep(sleep);
42+
System.getLogger("dummy")
43+
.log(System.Logger.Level.INFO,
44+
"Logger finder service load sleep value: " + sleep);
45+
} catch (InterruptedException e) {
46+
throw new RuntimeException(e);
47+
}
48+
}
49+
50+
private final Map<String, SimpleLogger> loggers = new ConcurrentHashMap<>();
51+
public SimpleLoggerFinder() {
52+
System.getLogger("dummy")
53+
.log(System.Logger.Level.INFO,
54+
"Logger finder service created");
55+
}
56+
57+
@Override
58+
public System.Logger getLogger(String name, Module module) {
59+
return loggers.computeIfAbsent(name, SimpleLogger::new);
60+
}
61+
62+
private static class SimpleLogger implements System.Logger {
63+
private final java.util.logging.Logger logger;
64+
65+
private static final class SimpleHandler extends Handler {
66+
@Override
67+
public void publish(LogRecord record) {
68+
LOGS.add(record);
69+
}
70+
@Override public void flush() { }
71+
@Override public void close() { }
72+
}
73+
74+
public SimpleLogger(String name) {
75+
logger = Logger.getLogger(name);
76+
logger.addHandler(new SimpleHandler());
77+
}
78+
79+
@Override
80+
public String getName() {
81+
return logger.getName();
82+
}
83+
84+
java.util.logging.Level level(Level level) {
85+
return switch (level) {
86+
case ALL -> java.util.logging.Level.ALL;
87+
case DEBUG -> java.util.logging.Level.FINE;
88+
case TRACE -> java.util.logging.Level.FINER;
89+
case INFO -> java.util.logging.Level.INFO;
90+
case WARNING -> java.util.logging.Level.WARNING;
91+
case ERROR -> java.util.logging.Level.SEVERE;
92+
case OFF -> java.util.logging.Level.OFF;
93+
};
94+
}
95+
96+
@Override
97+
public boolean isLoggable(Level level) {
98+
return logger.isLoggable(level(level));
99+
}
100+
101+
@Override
102+
public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
103+
var julLevel = level(level);
104+
if (!logger.isLoggable(julLevel)) return;
105+
if (bundle != null) {
106+
logger.logrb(julLevel, bundle, msg, thrown);
107+
} else {
108+
logger.log(julLevel, msg, thrown);
109+
}
110+
}
111+
112+
@Override
113+
public void log(Level level, ResourceBundle bundle, String format, Object... params) {
114+
var julLevel = level(level);
115+
if (!logger.isLoggable(julLevel)) return;
116+
if (params == null) {
117+
if (bundle == null) {
118+
logger.log(julLevel, format);
119+
} else {
120+
logger.logrb(julLevel, bundle, format);
121+
}
122+
} else {
123+
if (bundle == null) {
124+
logger.log(julLevel, format, params);
125+
} else {
126+
logger.logrb(julLevel, bundle, format, params);
127+
}
128+
}
129+
}
130+
}
131+
}
132+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
loggerfinder.SimpleLoggerFinder
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*
2+
* Copyright (c) 2023, 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.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8314263
27+
* @summary Signed jars triggering Logger finder recursion and StackOverflowError
28+
* @library /test/lib
29+
* @build jdk.test.lib.compiler.CompilerUtils
30+
* jdk.test.lib.process.*
31+
* jdk.test.lib.util.JarUtils
32+
* jdk.test.lib.JDKToolLauncher
33+
* @compile SignedLoggerFinderTest.java SimpleLoggerFinder.java
34+
* @run main SignedLoggerFinderTest init
35+
* @run main SignedLoggerFinderTest init sign
36+
*/
37+
38+
import java.io.File;
39+
import java.nio.file.*;
40+
import java.security.*;
41+
import java.util.*;
42+
import java.util.function.*;
43+
import java.util.jar.*;
44+
45+
import jdk.test.lib.JDKToolFinder;
46+
import jdk.test.lib.JDKToolLauncher;
47+
import jdk.test.lib.Utils;
48+
import jdk.test.lib.process.OutputAnalyzer;
49+
import jdk.test.lib.process.ProcessTools;
50+
import jdk.test.lib.util.JarUtils;
51+
52+
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
53+
import static java.util.Arrays.asList;
54+
55+
public class SignedLoggerFinderTest {
56+
57+
/**
58+
* This test triggers recursion in the broken JDK. The error can
59+
* manifest in a few different ways.
60+
* One error seen is "java.lang.NoClassDefFoundError:
61+
* Could not initialize class jdk.internal.logger.LoggerFinderLoader$ErrorPolicy"
62+
*
63+
* The original reported error was a StackOverflow (also seen in different iterations
64+
* of this run). Running test in signed and unsigned jar mode for sanity coverage.
65+
* The current bug only manifests when jars are signed.
66+
*/
67+
68+
private static boolean init = false;
69+
private static boolean signJars = false;
70+
private static boolean mutliThreadLoad = false;
71+
private static volatile boolean testComplete = false;
72+
73+
private static final String KEYSTORE = "8314263.keystore";
74+
private static final String ALIAS = "JavaTest";
75+
private static final String STOREPASS = "changeit";
76+
private static final String KEYPASS = "changeit";
77+
private static final String DNAME = "CN=sample";
78+
private static final String CUSTOM_LOGGER_FINDER_NAME =
79+
"loggerfinder.SimpleLoggerFinder";
80+
private static final String CUSTOM_LOGGER_NAME =
81+
"loggerfinder.SimpleLoggerFinder$SimpleLogger";
82+
private static final String INTERNAL_LOGGER_FINDER_NAME =
83+
"sun.util.logging.internal.LoggingProviderImpl";
84+
private static final String INTERNAL_LOGGER_NAME =
85+
"sun.util.logging.internal.LoggingProviderImpl$JULWrapper";
86+
private static final Path jarPath1 =
87+
Path.of(System.getProperty("test.classes", "."), "SimpleLoggerFinder.jar");
88+
private static final Path jarPath2 =
89+
Path.of(System.getProperty("test.classes", "."), "SimpleLoggerFinder2.jar");
90+
91+
public static void main(String[] args) throws Throwable {
92+
init = args.length >=1 && args[0].equals("init");
93+
signJars = args.length >=2 && args[1].equals("sign");
94+
95+
// init only passed in by jtreg test run, initialize the environment
96+
// for the subsequent test run
97+
if (init) {
98+
initialize();
99+
launchTest(false, false);
100+
launchTest(false, true);
101+
launchTest(true, false);
102+
launchTest(true, true);
103+
104+
} else {
105+
// set up complete. Run the code to trigger the recursion
106+
// We're in the JVM launched by ProcessTools.executeCommand
107+
boolean multiThreadLoad = Boolean.getBoolean("multiThreadLoad");
108+
boolean withCustomLoggerFinder = Boolean.getBoolean("withCustomLoggerFinder");
109+
110+
if (multiThreadLoad) {
111+
long sleep = new Random().nextLong(100L) + 1L;
112+
System.out.println("multi thread load sleep value: " + sleep);
113+
new Thread(runnableWithSleep(
114+
() -> System.getLogger("logger" + System.currentTimeMillis()),
115+
sleep, "System.getLogger type: ")).start();
116+
new Thread(runnableWithSleep(
117+
() -> System.LoggerFinder.getLoggerFinder(),
118+
sleep, "System.getLoggerFinder type: ")).start();
119+
}
120+
121+
if (withCustomLoggerFinder) {
122+
JarFile jf = new JarFile(jarPath1.toString(), true);
123+
jf.getInputStream(jf.getJarEntry("loggerfinder/SimpleLoggerFinder.class"));
124+
JarFile jf2 = new JarFile(jarPath2.toString(), true);
125+
jf2.getInputStream(jf.getJarEntry("loggerfinder/SimpleLoggerFinder.class"));
126+
} else {
127+
// some other call to prod LoggerFinder loading
128+
System.getLogger("random" + System.currentTimeMillis());
129+
System.LoggerFinder.getLoggerFinder();
130+
}
131+
Security.setProperty("test_1", "test");
132+
133+
// some extra sanity checks
134+
if (withCustomLoggerFinder) {
135+
assertEquals(System.LoggerFinder.getLoggerFinder().getClass().getName(),
136+
CUSTOM_LOGGER_FINDER_NAME);
137+
System.Logger testLogger = System.getLogger("jdk.event.security");
138+
assertEquals(testLogger.getClass().getName(), CUSTOM_LOGGER_NAME);
139+
} else {
140+
assertEquals(System.LoggerFinder.getLoggerFinder().getClass().getName(),
141+
INTERNAL_LOGGER_FINDER_NAME);
142+
System.Logger testLogger = System.getLogger("jdk.event.security");
143+
assertEquals(testLogger.getClass().getName(), INTERNAL_LOGGER_NAME);
144+
}
145+
testComplete = true;
146+
147+
// LoggerFinder should be initialized, trigger a simple log call
148+
Security.setProperty("test_2", "test");
149+
}
150+
}
151+
152+
// helper to create the inner test. Run config variations with the LoggerFinder jars
153+
// on the classpath and with other threads running System.Logger calls during load
154+
private static void launchTest(boolean multiThreadLoad, boolean withCustomLoggerFinder) {
155+
List<String> cmds = new ArrayList<>();
156+
cmds.add(JDKToolFinder.getJDKTool("java"));
157+
cmds.addAll(asList(Utils.getTestJavaOpts()));
158+
if (withCustomLoggerFinder) {
159+
cmds.addAll(List.of("-classpath",
160+
System.getProperty("test.classes") + File.pathSeparator +
161+
jarPath1.toString() + File.pathSeparator + jarPath2.toString(),
162+
"-Dtest.classes=" + System.getProperty("test.classes")));
163+
} else {
164+
cmds.addAll(List.of("-classpath",
165+
System.getProperty("test.classes")));
166+
}
167+
cmds.addAll(List.of(
168+
// following debug property seems useful to tickle the issue
169+
"-Dsun.misc.URLClassPath.debug=true",
170+
// console logger level to capture event output
171+
"-Djdk.system.logger.level=DEBUG",
172+
// useful for debug purposes
173+
"-Djdk.logger.finder.error=DEBUG",
174+
// enable logging to verify correct output
175+
"-Djava.util.logging.config.file=" +
176+
Path.of(System.getProperty("test.src", "."), "logging.properties")));
177+
if (multiThreadLoad) {
178+
cmds.add("-DmultiThreadLoad=true");
179+
}
180+
if (withCustomLoggerFinder) {
181+
cmds.add("-DwithCustomLoggerFinder=true");
182+
}
183+
cmds.addAll(List.of(
184+
"SignedLoggerFinderTest",
185+
"no-init"));
186+
187+
try {
188+
OutputAnalyzer outputAnalyzer = ProcessTools.executeCommand(cmds.stream()
189+
.filter(t -> !t.isEmpty())
190+
.toArray(String[]::new))
191+
.shouldHaveExitValue(0);
192+
if (withCustomLoggerFinder) {
193+
outputAnalyzer
194+
.shouldContain("TEST LOGGER: [test_1, test]")
195+
.shouldContain("TEST LOGGER: [test_2, test]");
196+
} else {
197+
outputAnalyzer
198+
.shouldContain("SecurityPropertyModification: key:test_1")
199+
.shouldContain("SecurityPropertyModification: key:test_2");
200+
}
201+
if (withCustomLoggerFinder && signJars) {
202+
// X509 cert generated during verification of signed jar file
203+
outputAnalyzer
204+
.shouldContain(DNAME);
205+
}
206+
207+
} catch (Throwable t) {
208+
throw new RuntimeException("Unexpected fail.", t);
209+
}
210+
}
211+
212+
private static Runnable runnableWithSleep(Supplier s, long sleep, String desc) {
213+
return () -> {
214+
while(!testComplete) {
215+
System.out.println(desc + s.get().getClass().getName());
216+
try {
217+
Thread.sleep(sleep);
218+
} catch (InterruptedException e) {
219+
throw new RuntimeException(e);
220+
}
221+
}
222+
};
223+
}
224+
225+
private static void initialize() throws Throwable {
226+
if (signJars) {
227+
genKey();
228+
}
229+
230+
Path classes = Paths.get(System.getProperty("test.classes", ""));
231+
JarUtils.createJarFile(jarPath1,
232+
classes,
233+
classes.resolve("loggerfinder/SimpleLoggerFinder.class"),
234+
classes.resolve("loggerfinder/SimpleLoggerFinder$SimpleLogger.class"));
235+
236+
JarUtils.updateJarFile(jarPath1, Path.of(System.getProperty("test.src")),
237+
Path.of("META-INF", "services", "java.lang.System$LoggerFinder"));
238+
if (signJars) {
239+
signJar(jarPath1.toString());
240+
}
241+
// multiple signed jars with services to have ServiceLoader iteration
242+
Files.copy(jarPath1, jarPath2, REPLACE_EXISTING);
243+
}
244+
245+
private static void genKey() throws Throwable {
246+
String keytool = JDKToolFinder.getJDKTool("keytool");
247+
Files.deleteIfExists(Paths.get(KEYSTORE));
248+
ProcessTools.executeCommand(keytool,
249+
"-J-Duser.language=en",
250+
"-J-Duser.country=US",
251+
"-genkey",
252+
"-keyalg", "rsa",
253+
"-alias", ALIAS,
254+
"-keystore", KEYSTORE,
255+
"-keypass", KEYPASS,
256+
"-dname", DNAME,
257+
"-storepass", STOREPASS
258+
).shouldHaveExitValue(0);
259+
}
260+
261+
262+
private static OutputAnalyzer signJar(String jarName) throws Throwable {
263+
List<String> args = new ArrayList<>();
264+
args.add("-verbose");
265+
args.add(jarName);
266+
args.add(ALIAS);
267+
268+
return jarsigner(args);
269+
}
270+
271+
private static OutputAnalyzer jarsigner(List<String> extra)
272+
throws Throwable {
273+
JDKToolLauncher launcher = JDKToolLauncher.createUsingTestJDK("jarsigner")
274+
.addVMArg("-Duser.language=en")
275+
.addVMArg("-Duser.country=US")
276+
.addToolArg("-keystore")
277+
.addToolArg(KEYSTORE)
278+
.addToolArg("-storepass")
279+
.addToolArg(STOREPASS)
280+
.addToolArg("-keypass")
281+
.addToolArg(KEYPASS);
282+
for (String s : extra) {
283+
if (s.startsWith("-J")) {
284+
launcher.addVMArg(s.substring(2));
285+
} else {
286+
launcher.addToolArg(s);
287+
}
288+
}
289+
return ProcessTools.executeCommand(launcher.getCommand());
290+
}
291+
292+
private static void assertEquals(String received, String expected) {
293+
if (!expected.equals(received)) {
294+
throw new RuntimeException("Received: " + received);
295+
}
296+
}
297+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2023, 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.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
package loggerfinder;
25+
26+
import java.lang.*;
27+
import java.util.*;
28+
29+
public class SimpleLoggerFinder extends System.LoggerFinder {
30+
31+
static {
32+
try {
33+
long sleep = new Random().nextLong(1000L) + 1L;
34+
System.out.println("Logger finder service load sleep value: " + sleep);
35+
// simulate a slow load service
36+
Thread.sleep(sleep);
37+
} catch (InterruptedException e) {
38+
throw new RuntimeException(e);
39+
}
40+
}
41+
@Override
42+
public System.Logger getLogger(String name, Module module) {
43+
return new SimpleLogger(name);
44+
}
45+
46+
private static class SimpleLogger implements System.Logger {
47+
private final String name;
48+
49+
public SimpleLogger(String name) {
50+
this.name = name;
51+
}
52+
53+
@Override
54+
public String getName() {
55+
return name;
56+
}
57+
58+
@Override
59+
public boolean isLoggable(Level level) {
60+
return true;
61+
}
62+
63+
@Override
64+
public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {
65+
System.out.println("TEST LOGGER: " + msg);
66+
}
67+
68+
@Override
69+
public void log(Level level, ResourceBundle bundle, String format, Object... params) {
70+
System.out.println("TEST LOGGER: " + Arrays.asList(params));
71+
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
############################################################
2+
# Configuration file for log testing
3+
#
4+
############################################################
5+
6+
handlers= java.util.logging.ConsoleHandler
7+
8+
.level= FINE
9+
10+
java.util.logging.ConsoleHandler.level = FINE
11+
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
12+
13+
jdk.event.security.level = FINE
14+

0 commit comments

Comments
 (0)
Please sign in to comment.