Skip to content

Commit ebcb9a8

Browse files
david-beaumontStuart Marks
authored andcommitted
8349206: j.u.l.Handler classes create deadlock risk via synchronized publish() method
Reviewed-by: dfuchs, smarks
1 parent d894b78 commit ebcb9a8

File tree

8 files changed

+445
-23
lines changed

8 files changed

+445
-23
lines changed

src/java.logging/share/classes/java/util/logging/ConsoleHandler.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ public ConsoleHandler() {
8787
* The logging request was made initially to a {@code Logger} object,
8888
* which initialized the {@code LogRecord} and forwarded it here.
8989
*
90+
* @implSpec This method is not synchronized, and subclasses must not define
91+
* overridden {@code publish()} methods to be {@code synchronized} if they
92+
* call {@code super.publish()} or format user arguments. See the
93+
* {@linkplain Handler##threadSafety discussion in java.util.logging.Handler}
94+
* for more information.
95+
*
9096
* @param record description of the log event. A null record is
9197
* silently ignored and is not published
9298
*/

src/java.logging/share/classes/java/util/logging/FileHandler.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2000, 2025, 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
@@ -739,11 +739,14 @@ private synchronized void rotate() {
739739
* silently ignored and is not published
740740
*/
741741
@Override
742-
public synchronized void publish(LogRecord record) {
743-
if (!isLoggable(record)) {
744-
return;
745-
}
742+
public void publish(LogRecord record) {
746743
super.publish(record);
744+
}
745+
746+
@Override
747+
void synchronousPostWriteHook() {
748+
// no need to synchronize here, this method is called from within a
749+
// synchronized block.
747750
flush();
748751
if (limit > 0 && (meter.written >= limit || meter.written < 0)) {
749752
rotate();

src/java.logging/share/classes/java/util/logging/Handler.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2000, 2025, 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
@@ -43,10 +43,25 @@
4343
* and {@code Level}. See the specific documentation for each concrete
4444
* {@code Handler} class.
4545
*
46+
* <h2><a id=threadSafety>Thread Safety and Deadlock Risk in Handlers</a></h2>
47+
*
48+
* Implementations of {@code Handler} should be thread-safe. Handlers are
49+
* expected to be invoked concurrently from arbitrary threads. However,
50+
* over-use of synchronization may result in unwanted thread contention,
51+
* performance issues or even deadlocking.
52+
* <p>
53+
* In particular, subclasses should avoid acquiring locks around code which
54+
* calls back to arbitrary user-supplied objects, especially during log record
55+
* formatting. Holding a lock around any such callbacks creates a deadlock risk
56+
* between logging code and user code.
57+
* <p>
58+
* As such, general purpose {@code Handler} subclasses should not synchronize
59+
* their {@link #publish(LogRecord)} methods, or call {@code super.publish()}
60+
* while holding locks, since these are typically expected to need to process
61+
* and format user-supplied arguments.
4662
*
4763
* @since 1.4
4864
*/
49-
5065
public abstract class Handler {
5166
private static final int offValue = Level.OFF.intValue();
5267

@@ -123,6 +138,10 @@ protected Handler() { }
123138
* <p>
124139
* The {@code Handler} is responsible for formatting the message, when and
125140
* if necessary. The formatting should include localization.
141+
* <p>
142+
* @apiNote To avoid the risk of deadlock, implementations of this method
143+
* should avoid holding any locks while calling out to application code,
144+
* such as the formatting of {@code LogRecord}.
126145
*
127146
* @param record description of the log event. A null record is
128147
* silently ignored and is not published

src/java.logging/share/classes/java/util/logging/SocketHandler.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2000, 2025, 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
@@ -168,14 +168,17 @@ public synchronized void close() {
168168
/**
169169
* Format and publish a {@code LogRecord}.
170170
*
171+
* @implSpec This method is not synchronized, and subclasses must not define
172+
* overridden {@code publish()} methods to be {@code synchronized} if they
173+
* call {@code super.publish()} or format user arguments. See the
174+
* {@linkplain Handler##threadSafety discussion in java.util.logging.Handler}
175+
* for more information.
176+
*
171177
* @param record description of the log event. A null record is
172178
* silently ignored and is not published
173179
*/
174180
@Override
175-
public synchronized void publish(LogRecord record) {
176-
if (!isLoggable(record)) {
177-
return;
178-
}
181+
public void publish(LogRecord record) {
179182
super.publish(record);
180183
flush();
181184
}

src/java.logging/share/classes/java/util/logging/StreamHandler.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2000, 2025, 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
@@ -180,17 +180,31 @@ public synchronized void setEncoding(String encoding)
180180
* {@code OutputStream}, the {@code Formatter}'s "head" string is
181181
* written to the stream before the {@code LogRecord} is written.
182182
*
183+
* @implSpec This method avoids acquiring locks during {@code LogRecord}
184+
* formatting, but {@code this} instance is synchronized when writing to the
185+
* output stream. To avoid deadlock risk, subclasses must not hold locks
186+
* while calling {@code super.publish()}. Specifically, subclasses must
187+
* not define the overridden {@code publish()} method to be
188+
* {@code synchronized} if they call {@code super.publish()}.
189+
*
183190
* @param record description of the log event. A null record is
184191
* silently ignored and is not published
185192
*/
186193
@Override
187-
public synchronized void publish(LogRecord record) {
188-
if (!isLoggable(record)) {
194+
public void publish(LogRecord record) {
195+
if (!isLoggable(record)) {
189196
return;
190197
}
198+
// Read once for consistency (whether in or outside the locked region
199+
// is not important).
200+
Formatter formatter = getFormatter();
201+
// JDK-8349206: To avoid deadlock risk, it is essential that the handler
202+
// is not locked while formatting the log record. Methods such as
203+
// reportError() and isLoggable() are defined to be thread safe, so we
204+
// can restrict locking to just writing the message.
191205
String msg;
192206
try {
193-
msg = getFormatter().format(record);
207+
msg = formatter.format(record);
194208
} catch (Exception ex) {
195209
// We don't want to throw an exception here, but we
196210
// report the exception to any registered ErrorManager.
@@ -199,19 +213,33 @@ public synchronized void publish(LogRecord record) {
199213
}
200214

201215
try {
202-
Writer writer = this.writer;
203-
if (!doneHeader) {
204-
writer.write(getFormatter().getHead(this));
205-
doneHeader = true;
216+
synchronized(this) {
217+
Writer writer = this.writer;
218+
if (!doneHeader) {
219+
writer.write(formatter.getHead(this));
220+
doneHeader = true;
221+
}
222+
writer.write(msg);
223+
synchronousPostWriteHook();
206224
}
207-
writer.write(msg);
208225
} catch (Exception ex) {
209226
// We don't want to throw an exception here, but we
210227
// report the exception to any registered ErrorManager.
211228
reportError(null, ex, ErrorManager.WRITE_FAILURE);
212229
}
213230
}
214231

232+
/**
233+
* Overridden by other handlers in this package to facilitate synchronous
234+
* post-write behaviour. If other handlers need similar functionality, it
235+
* might be feasible to make this method protected (see JDK-8349206), but
236+
* please find a better name if you do ;).
237+
*/
238+
void synchronousPostWriteHook() {
239+
// Empty by default. We could do:
240+
// assert Thread.holdsLock(this);
241+
// but this is already covered by unit tests.
242+
}
215243

216244
/**
217245
* Check if this {@code Handler} would actually log a given {@code LogRecord}.
@@ -249,15 +277,17 @@ public synchronized void flush() {
249277
}
250278
}
251279

280+
// Called synchronously with "this" handler instance locked.
252281
private void flushAndClose() {
253282
Writer writer = this.writer;
254283
if (writer != null) {
284+
Formatter formatter = getFormatter();
255285
try {
256286
if (!doneHeader) {
257-
writer.write(getFormatter().getHead(this));
287+
writer.write(formatter.getHead(this));
258288
doneHeader = true;
259289
}
260-
writer.write(getFormatter().getTail(this));
290+
writer.write(formatter.getTail(this));
261291
writer.flush();
262292
writer.close();
263293
} catch (Exception ex) {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright (c) 2025, 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 8349206
27+
* @summary j.u.l.Handler classes create deadlock risk via synchronized publish() method.
28+
* @modules java.base/sun.util.logging
29+
* java.logging
30+
* @build java.logging/java.util.logging.TestStreamHandler
31+
* @run main/othervm StreamHandlerLockingTest
32+
*/
33+
34+
import java.io.ByteArrayOutputStream;
35+
import java.io.IOException;
36+
import java.util.logging.Formatter;
37+
import java.util.logging.Handler;
38+
import java.util.logging.Level;
39+
import java.util.logging.LogRecord;
40+
import java.util.logging.TestStreamHandler;
41+
42+
public class StreamHandlerLockingTest {
43+
static class TestFormatter extends Formatter {
44+
final Handler handler;
45+
46+
TestFormatter(Handler handler) {
47+
this.handler = handler;
48+
}
49+
50+
@Override
51+
public String format(LogRecord record) {
52+
if (Thread.holdsLock(handler)) {
53+
throw new AssertionError("format() was called with handler locked (bad).");
54+
}
55+
return record.getMessage() + "\n";
56+
}
57+
58+
@Override
59+
public String getHead(Handler h) {
60+
// This is currently true, and not easy to make unsynchronized.
61+
if (!Thread.holdsLock(handler)) {
62+
throw new AssertionError("getHead() expected to be called with handler locked.");
63+
}
64+
return "--HEAD--\n";
65+
}
66+
67+
@Override
68+
public String getTail(Handler h) {
69+
// This is currently true, and not easy to make unsynchronized.
70+
if (!Thread.holdsLock(handler)) {
71+
throw new AssertionError("getTail() expected to be called with handler locked.");
72+
}
73+
return "--TAIL--\n";
74+
}
75+
}
76+
77+
private static final String EXPECTED_LOG =
78+
String.join("\n","--HEAD--", "Hello World", "Some more logging...", "And we're done!", "--TAIL--", "");
79+
80+
public static void main(String[] args) throws IOException {
81+
ByteArrayOutputStream out = new ByteArrayOutputStream();
82+
TestStreamHandler handler = new TestStreamHandler(out);
83+
TestFormatter formatter = new TestFormatter(handler);
84+
handler.setFormatter(formatter);
85+
86+
handler.publish(log("Hello World"));
87+
handler.publish(log("Some more logging..."));
88+
handler.publish(log("And we're done!"));
89+
handler.close();
90+
91+
// Post write callback should have happened once per publish call (with lock held).
92+
if (handler.callbackCount != 3) {
93+
throw new AssertionError("Unexpected callback count: " + handler.callbackCount);
94+
}
95+
96+
String logged = out.toString("UTF-8");
97+
if (!EXPECTED_LOG.equals(logged)) {
98+
throw new AssertionError("Unexpected log contents: " + logged);
99+
}
100+
}
101+
102+
static LogRecord log(String msg) {
103+
return new LogRecord(Level.INFO, msg);
104+
}
105+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) 2025, 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 java.util.logging;
25+
26+
import java.io.OutputStream;
27+
import java.io.UnsupportedEncodingException;
28+
29+
/**
30+
* A trivial UTF-8 stream handler subclass class to capture whether the
31+
* (package protected) post-write callback method is synchronized.
32+
*/
33+
public class TestStreamHandler extends StreamHandler {
34+
35+
public int callbackCount = 0;
36+
37+
public TestStreamHandler(OutputStream out) {
38+
setOutputStream(out);
39+
try {
40+
setEncoding("UTF-8");
41+
} catch (UnsupportedEncodingException e) {
42+
throw new RuntimeException(e);
43+
}
44+
}
45+
46+
@Override
47+
void synchronousPostWriteHook() {
48+
if (!Thread.holdsLock(this)) {
49+
throw new AssertionError(
50+
String.format("Post write callback [index=%d] was invoked without handler locked.", callbackCount));
51+
}
52+
callbackCount++;
53+
}
54+
}

0 commit comments

Comments
 (0)