-
Notifications
You must be signed in to change notification settings - Fork 6.1k
8349206: j.u.l.Handler classes create deadlock risk via synchronized publish() method #23491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
157594c
0ce8341
f857704
e897bdc
5ba71dc
c35e519
6beb0c8
601e88f
11ba770
0c6ec9a
01157cb
f425f3f
19e8472
35984b0
6051c69
147aeca
1bf2da4
43c7611
15cad6e
8db4c90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* | ||
* Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. | ||
* Copyright (c) 2000, 2025, 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 | ||
|
@@ -180,17 +180,31 @@ public synchronized void setEncoding(String encoding) | |
* {@code OutputStream}, the {@code Formatter}'s "head" string is | ||
* written to the stream before the {@code LogRecord} is written. | ||
* | ||
* @implSpec This method avoids acquiring locks during {@code LogRecord} | ||
* formatting, but {@code this} instance is synchronized when writing to the | ||
* output stream. To avoid deadlock risk, subclasses must not hold locks | ||
* while calling {@code super.publish()}. Specifically, subclasses must | ||
* not define the overridden {@code publish()} method to be | ||
* {@code synchronized} if they call {@code super.publish()}. | ||
* | ||
* @param record description of the log event. A null record is | ||
* silently ignored and is not published | ||
david-beaumont marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
@Override | ||
public synchronized void publish(LogRecord record) { | ||
if (!isLoggable(record)) { | ||
public void publish(LogRecord record) { | ||
if (!isLoggable(record)) { | ||
return; | ||
} | ||
// Read once for consistency (whether in or outside the locked region | ||
// is not important). | ||
Formatter formatter = getFormatter(); | ||
// JDK-8349206: To avoid deadlock risk, it is essential that the handler | ||
// is not locked while formatting the log record. Methods such as | ||
// reportError() and isLoggable() are defined to be thread safe, so we | ||
// can restrict locking to just writing the message. | ||
String msg; | ||
try { | ||
msg = getFormatter().format(record); | ||
msg = formatter.format(record); | ||
} catch (Exception ex) { | ||
// We don't want to throw an exception here, but we | ||
// report the exception to any registered ErrorManager. | ||
|
@@ -199,19 +213,33 @@ public synchronized void publish(LogRecord record) { | |
} | ||
|
||
try { | ||
Writer writer = this.writer; | ||
if (!doneHeader) { | ||
writer.write(getFormatter().getHead(this)); | ||
doneHeader = true; | ||
synchronized(this) { | ||
Writer writer = this.writer; | ||
if (!doneHeader) { | ||
writer.write(formatter.getHead(this)); | ||
doneHeader = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should getHead and or getTail also not be called while holding lock? I format a single record in: https://eclipse-ee4j.github.io/angus-mail/docs/api/org.eclipse.angus.mail/org/eclipse/angus/mail/util/logging/CollectorFormatter.html#getTail(java.util.logging.Handler) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well spotted :) Yes, it's a theoretical risk, but not at the same level as that of log record formatting. My original draft push of this PR had comments about lock expectations here, but I was asked to not change the JavaDoc on Formatter. The getHead() and getTail() methods could cause deadlock, but only because of code directly associated with their implementation. They don't have any access to a log record (and no reason to have access to log records), so they aren't going to be calling into completely arbitrary user code. It's also unlikely that a formatter implementation will do a lot of complex work in these methods since, semantically, they are called an arbitrary number of times (according to configuration) and at arbitrary times, so they really cannot meaningfully rely on user runtime data or much beyond things like timestamps and counters. So while, in theory, it could be an issue, it's an issue that can almost certainly be fixed by the author of the Formatter class itself. This is contrasted with the issue at hand, which is inherent in the handler code and cannot be fixed in any other reasonable way by the user of the logging library. I'd be happy to move the head/tail acquisition out of the locked regions if it were deemed a risk, but that's never something I've observed as an issue (I spent 10 years doing Java logging stuff and saw the publish() deadlock, but never issues with head/tail stuff). It's also hard to move it out, because tail writing happens in close(), called from flush(), both of which are much more expected to be synchronized, so you'd probably want to get and cache the tail() string when the file was opened. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As for the example you gave there, that is interesting. Keeping an unformatted log record around for any time after the log statement that created it has exited would be quite problematic (it prevents GC of arbitrary things). If I were doing some kind of summary tail entry, I'd pull, format (user args) and cache anything I needed out of the log record when I had it, but not keep it around. Then when the tail is written I'd just have what I need ready to go. But yes, right now, with or without this PR, that code looks like it's got a deadlock risk. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to update to have single call to getFormatter(). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've always leaned on:
That said, I'll probably create PRs for MailHandler and CollectorFormatter to optionally support deep cloning of the LogRecord via serialization before it is stored. Doing so would switch the params to string without tampering with the source record. MemoryHander could do the same and effectively become a non-caching (pushLevel=ALL) TransformHandler to decorate Handers that over synchronize. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd have to disagree with the points you make. The fact is that loggers are never expected to modify the passed parameters. To ask people to "disown" the parameters they pass to a logger requires that your best advice on how to write a log statement with mutable values must look something like:
as opposed to:
The former is, in my opinion, a pretty awful user experience, error prone, and (more to the point) something that almost nobody ever does in real code because, reasonably, they trust the internal logger classes not to be malicious. The comment about the record being owned by the logger means that it can't be cached and reused, or passed to different log statements etc. It doesn't give logging classes any freedom to modify the log statement parameters. The other issue with "defer processing to another thread", even if you do format in the original thread, is that either:
It's just really hard (maybe impossible) to create any general solution where a log record (as opposed to some custom semi-flattened form) can be safely used between threads without this being visible in some way to users. So my (personal) strong advice for Handler implementations is:
So, if you wanted to use something like However, if you are formatting everything in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Specifically, I'm referring to this, with awareness of the above described limitations:
How did you worked around this deadlock issue prior to this patch? That would be awesome information to add to the JIRA ticket for those that would like this patch but are unable to upgrade. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only workaround is to early format parameters, either at the call site or in the logger/handler. |
||
} | ||
writer.write(msg); | ||
synchronousPostWriteHook(); | ||
} | ||
writer.write(msg); | ||
} catch (Exception ex) { | ||
// We don't want to throw an exception here, but we | ||
// report the exception to any registered ErrorManager. | ||
reportError(null, ex, ErrorManager.WRITE_FAILURE); | ||
} | ||
} | ||
|
||
/** | ||
* Overridden by other handlers in this package to facilitate synchronous | ||
* post-write behaviour. If other handlers need similar functionality, it | ||
* might be feasible to make this method protected (see JDK-8349206), but | ||
* please find a better name if you do ;). | ||
*/ | ||
void synchronousPostWriteHook() { | ||
// Empty by default. We could do: | ||
// assert Thread.holdsLock(this); | ||
// but this is already covered by unit tests. | ||
} | ||
|
||
/** | ||
* Check if this {@code Handler} would actually log a given {@code LogRecord}. | ||
|
@@ -249,15 +277,17 @@ public synchronized void flush() { | |
} | ||
} | ||
|
||
// Called synchronously with "this" handler instance locked. | ||
private void flushAndClose() { | ||
Writer writer = this.writer; | ||
if (writer != null) { | ||
Formatter formatter = getFormatter(); | ||
try { | ||
if (!doneHeader) { | ||
writer.write(getFormatter().getHead(this)); | ||
writer.write(formatter.getHead(this)); | ||
doneHeader = true; | ||
} | ||
writer.write(getFormatter().getTail(this)); | ||
writer.write(formatter.getTail(this)); | ||
david-beaumont marked this conversation as resolved.
Show resolved
Hide resolved
|
||
writer.flush(); | ||
writer.close(); | ||
} catch (Exception ex) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* Copyright (c) 2025, 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 8349206 | ||
* @summary j.u.l.Handler classes create deadlock risk via synchronized publish() method. | ||
* @modules java.base/sun.util.logging | ||
* java.logging | ||
* @build java.logging/java.util.logging.TestStreamHandler | ||
* @run main/othervm StreamHandlerLockingTest | ||
*/ | ||
|
||
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
import java.util.logging.Formatter; | ||
import java.util.logging.Handler; | ||
import java.util.logging.Level; | ||
import java.util.logging.LogRecord; | ||
import java.util.logging.TestStreamHandler; | ||
|
||
public class StreamHandlerLockingTest { | ||
static class TestFormatter extends Formatter { | ||
final Handler handler; | ||
|
||
TestFormatter(Handler handler) { | ||
this.handler = handler; | ||
} | ||
|
||
@Override | ||
public String format(LogRecord record) { | ||
if (Thread.holdsLock(handler)) { | ||
throw new AssertionError("format() was called with handler locked (bad)."); | ||
} | ||
return record.getMessage() + "\n"; | ||
} | ||
|
||
@Override | ||
public String getHead(Handler h) { | ||
// This is currently true, and not easy to make unsynchronized. | ||
if (!Thread.holdsLock(handler)) { | ||
throw new AssertionError("getHead() expected to be called with handler locked."); | ||
} | ||
return "--HEAD--\n"; | ||
} | ||
|
||
@Override | ||
public String getTail(Handler h) { | ||
// This is currently true, and not easy to make unsynchronized. | ||
if (!Thread.holdsLock(handler)) { | ||
throw new AssertionError("getTail() expected to be called with handler locked."); | ||
} | ||
return "--TAIL--\n"; | ||
} | ||
} | ||
|
||
private static final String EXPECTED_LOG = | ||
String.join("\n","--HEAD--", "Hello World", "Some more logging...", "And we're done!", "--TAIL--", ""); | ||
|
||
public static void main(String[] args) throws IOException { | ||
ByteArrayOutputStream out = new ByteArrayOutputStream(); | ||
TestStreamHandler handler = new TestStreamHandler(out); | ||
TestFormatter formatter = new TestFormatter(handler); | ||
handler.setFormatter(formatter); | ||
|
||
handler.publish(log("Hello World")); | ||
handler.publish(log("Some more logging...")); | ||
handler.publish(log("And we're done!")); | ||
handler.close(); | ||
|
||
// Post write callback should have happened once per publish call (with lock held). | ||
if (handler.callbackCount != 3) { | ||
throw new AssertionError("Unexpected callback count: " + handler.callbackCount); | ||
} | ||
|
||
String logged = out.toString("UTF-8"); | ||
if (!EXPECTED_LOG.equals(logged)) { | ||
throw new AssertionError("Unexpected log contents: " + logged); | ||
} | ||
} | ||
|
||
static LogRecord log(String msg) { | ||
return new LogRecord(Level.INFO, msg); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* Copyright (c) 2025, 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 java.util.logging; | ||
|
||
import java.io.OutputStream; | ||
import java.io.UnsupportedEncodingException; | ||
|
||
/** | ||
* A trivial UTF-8 stream handler subclass class to capture whether the | ||
* (package protected) post-write callback method is synchronized. | ||
*/ | ||
public class TestStreamHandler extends StreamHandler { | ||
|
||
public int callbackCount = 0; | ||
|
||
public TestStreamHandler(OutputStream out) { | ||
setOutputStream(out); | ||
try { | ||
setEncoding("UTF-8"); | ||
} catch (UnsupportedEncodingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
@Override | ||
void synchronousPostWriteHook() { | ||
if (!Thread.holdsLock(this)) { | ||
throw new AssertionError( | ||
String.format("Post write callback [index=%d] was invoked without handler locked.", callbackCount)); | ||
} | ||
callbackCount++; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.