Skip to content

8353950: Clipboard interaction on Windows is unstable #24614

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

Closed
wants to merge 7 commits into from

Conversation

matthiasblaesing
Copy link

@matthiasblaesing matthiasblaesing commented Apr 13, 2025

  • Introduce a lock into WClipboard that protects the code between
    openClipboard/closeClipboard invocations.
    The native side does not allow to open the clipboard multiple
    times or share the opened clipboard between multiple threads.

  • Remove of need to call openClipboard/closeClipboard from
    getClipboardFormats by using the win32 call
    GetUpdatedClipboardFormats

  • Prevent a race-condition by not registering the connection
    between java and native side of clipboard multiple time, but
    just at construction time.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issues

  • JDK-8353950: Clipboard interaction on Windows is unstable (Bug - P4)
  • JDK-8332271: Reading data from the clipboard from multiple threads crashes the JVM (Bug - P3)

Reviewers

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/24614/head:pull/24614
$ git checkout pull/24614

Update a local copy of the PR:
$ git checkout pull/24614
$ git pull https://git.openjdk.org/jdk.git pull/24614/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 24614

View PR using the GUI difftool:
$ git pr show -t 24614

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/24614.diff

Using Webrev

Link to Webrev Comment

For investigation/testing issue JDK-8353950 it is necessary to create a
non-crashing baseline. Testing JDK-8353950 was done with a fastdebug
build of the JDK. The build was modified so that instead of blockingly
showing a messagebox, only CLI output was generated when an assertion
was hit. This is necessary, so that the reproducer runs with higher
concurrency.

The call to MessageBoxA was commented out for this in awt_Debug.cpp.

Reproducer Code
===============

```java

import java.awt.Toolkit;
import java.awt.datatransfer.FlavorEvent;
import java.awt.datatransfer.FlavorListener;
import java.awt.datatransfer.StringSelection;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.SwingUtilities;

public class Clipboardreproducer2 {

    private static final System.Logger LOG = System.getLogger(Clipboardreproducer2.class.getName());

    public static void main(String[] args) throws Exception {

        AtomicBoolean end = new AtomicBoolean(false);

        SwingUtilities.invokeLater(() -> {
            Toolkit.getDefaultToolkit().getSystemClipboard().addFlavorListener(new FlavorListener() {
                @OverRide
                public void flavorsChanged(FlavorEvent e) {
                    LOG.log(System.Logger.Level.INFO, "Flavor Changed: {0} // {1}", e, SwingUtilities.isEventDispatchThread());
                }
            });
        });

        Runnable copy = new Runnable() {
            @OverRide
            public void run() {
                try {
                    Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection("Test"), null);
//                    LOG.log(System.Logger.Level.INFO, "Copy 1");
                } catch (IllegalStateException ex) {
                    LOG.log(System.Logger.Level.ERROR, "Failed to invoke copy (1)", ex);
                }
                if(! end.get()) {
                    SwingUtilities.invokeLater(this);
                } else {
                    System.out.println("Exitting");
                }
            }
        };

        SwingUtilities.invokeLater(copy);

        Thread.sleep(60 * 1000);

        LOG.log(System.Logger.Level.INFO, "DONE");

        end.set(true);

        Thread.sleep(500);

        LOG.log(System.Logger.Level.INFO, "END");

        System.exit(0);

    }

}
```

Crash message
=============

```
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (c:\temp\jdk\src\hotspot\share\runtime/jniHandles.inline.hpp:81), pid=3816, tid=5224
#  assert(external_guard || result != nullptr) failed: Invalid JNI handle
#
# JRE version: OpenJDK Runtime Environment (25.0) (fastdebug build 25-internal-adhoc.Matthias.jdk)
# Java VM: OpenJDK 64-Bit Server VM (fastdebug 25-internal-adhoc.Matthias.jdk, mixed mode, tiered, compressed oops, compressed class ptrs, g1 gc, windows-amd64)
# Problematic frame:
# V  [jvm.dll+0x5160eb]  JNIHandles::resolve_impl<0,0>+0x27b
#
# Core dump will be written. Default location: c:\Temp\hs_err_pid3816.mdmp
#
# An error report file with more information is saved as:
# c:\Temp\hs_err_pid3816.log
[16.331s][warning][os] Loading hsdis library failed
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
#
```

The code from JDK-8332271 was also tested, but that did not yield a
visible result. It crashes, but the only data recorded is an entry in
the eventlog of windows.

Analysis
========

Looking through the code in `awt_Clipboard.cpp` there is a possible race
condition. Each call to openClipboard with a non-null caller will reset
the value of AwtClipboard::theCurrentClipboard. That variable holds a
reference to the java peer of the native clipboard.

The race is (line number refer to state in commit
1d7138f):

Thread 1: Application code calls Clipboard#setContents. On windows the
code now flows through WClipboard#openClipboard and will run to
awt_Clipboard.cpp line 145 (this is in the native implementatio of
openClipboard. Just before DeleteGlobalRef.

Thread 2: The windows event loop gets a WM_CLIPBOARDUPDATE message and
calls into AwtClipboard::WmClipboardUpdate (awt_Clipboard.cpp line 60).
The code runs through the non-null check and just before the
`CallVoidMethod` call.

Thread 1: Continues and executes DeleteGlobalRef.

Thread 2: Continues exection with:
`env->CallVoidMethod(theCurrentClipboard, lostSelectionOwnershipMID)`.
But at this point theCurrentClipboard does not hold a valid object
handle anymore and crashes the VM.

Proposed solution
=================

The code already makes assumptions, that there is only one AwtToolkit
(see calls to AwtToolkit::GetInstance). In addition
WToolkit#getSystemClipboard ensures, that only one WClipboard will
be created for this.

Based on this, this change drops the ability to pass in a new Clipboard
on each call to `openClipboard`. Instead the registration of the
java side of the clipboard with the native side is moved to construction
time of the WClipboard object. `openClipboard` is modified, so that it
verifies the assumption asserting that a new owner matches the initial
owner.
The focus of this change is the windows port of the AWT. openClipboard
and closeClipboard are only implemented there.

The general idea here is, that the section between openClipboard and
closeClipboard must be considered a critical section, that must not be
accessed concurrently.

The fix idea here is to use a ReentrantLock to protect the native side.
openClipboard now check if the lock can be locked (tryLock) if that
fails, an IllegalStateException is raised.

This is defined behavior for the accessors from client code (see for
example the documentation for Clipboard#setContents and
Clipboard#getContents.

The remaining problem is the upcall from the WToolkit. The message loop
handler has to react to the WM_CLIPBOARDUPDATE. When that message is
received an upcall to WClipboard#handleContentsChanged is invoked. That
in turn will make a downcall to the native getClipboardFormats. That
method used EnumClipboardFormats to enumerate the formats in the
clipboard. The problem is, that EnumClipboardFormats need the clipboard
to be opened (as implemented in openClipboard). 

This is problematic as the upcall from native should not lead to an
exception. Even if the exception is caught, it will result in flavor
listeners not being called.

Since Windows Vista / Windows Server 2008 GetUpdatedClipboardFormats
can be used, which does not need an open clipboard and thus would be
safe to be called from the upcall.

Testing:

- fastdebug build does not hit asserts anymore for the reproducer code
  (see previous commit)
- reproducer code for JDK-8332271 does not crash anymore. Code was
  run for multiple minutes and no crash was observed
- tests from test/jdk/java/awt/Clipboard were run and tests succeeded
  in release and fastdebug configuration
@bridgekeeper
Copy link

bridgekeeper bot commented Apr 13, 2025

👋 Welcome back mblaesing! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Apr 13, 2025

@matthiasblaesing This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8353950: Clipboard interaction on Windows is unstable
8332271: Reading data from the clipboard from multiple threads crashes the JVM

Reviewed-by: abhiscxk, dnguyen

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 1014 new commits pushed to the master branch:

As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

As you do not have Committer status in this project an existing Committer must agree to sponsor your change. Possible candidates are the reviewers of this PR (@DamonGuy, @kumarabhi006) but any other Committer may sponsor as well.

➡️ To flag this PR as ready for integration with the above commit message, type /integrate in a new comment. (Afterwards, your sponsor types /sponsor in a new comment to perform the integration).

@openjdk openjdk bot added the rfr Pull request is ready for review label Apr 13, 2025
@openjdk
Copy link

openjdk bot commented Apr 13, 2025

@matthiasblaesing The following label will be automatically applied to this pull request:

  • client

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@matthiasblaesing
Copy link
Author

Some further comments:

  • More information than provided in the PR summary can be found in the two commits, which represent the steps I took to to fix this.
  • From my testing this change also fixes JDK-8332271
  • Testing:
    • fastdebug build does not hit asserts anymore for the reproducer code (See e893b36 / issue report)
    • reproducer code for JDK-8332271 does not crash anymore. Code was run for multiple minutes and no crash was observed
    • tests from test/jdk/java/awt/Clipboard were run and tests succeeded in release and fastdebug configuration

@mlbridge
Copy link

mlbridge bot commented Apr 13, 2025

Webrevs

@Override
public native void closeClipboard();
public void closeClipboard() {
if(clipboardLocked.isLocked()) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if(clipboardLocked.isLocked()) {
if (clipboardLocked.isLocked()) {

@ky0n
Copy link

ky0n commented Apr 14, 2025

copyright year update is missing

@ExE-Boss
Copy link

@matthiasblaesing
You need to put the following in a comment or at the end of the PR body to mark JDK‑8332271 as fixed by this PR1:

/issue add JDK-8332271

Footnotes

  1. https://wiki.openjdk.org/display/SKARA/Pull+Request+Commands#PullRequestCommands-%2Fissue

@openjdk
Copy link

openjdk bot commented Apr 17, 2025

@ExE-Boss Only the author (@matthiasblaesing) is allowed to issue the /issue command.

@matthiasblaesing
Copy link
Author

/issue add JDK-8332271

@openjdk
Copy link

openjdk bot commented Apr 17, 2025

@matthiasblaesing
Adding additional issue to issue list: 8332271: Reading data from the clipboard from multiple threads crashes the JVM.

@matthiasblaesing
Copy link
Author

It would be great to get a review on this. While the issues are "only" P-3 and P-4, the issues leave a very bad image for java on windows. Copy-and-Paste should just work and not fail randomly and maybe even crash the VM.

The issue is serious enough for NetBeans, that it will ship a java agent to work around this problem, but that will not implement this change here and is clearly not an option in the long run and thus this needs a real fix.

@mrserb
Copy link
Member

mrserb commented May 13, 2025

is it possible to create a jtreg test for this issue?

@matthiasblaesing
Copy link
Author

@mrserb added the reproducer and validated, that it works:

  • fastdebug build: Before this change assertion is hit quickly, this will not be reported as a testfailure as the assertion handler displays a message and only blocks further execution of that thread, after the change no assertion is hit.
  • release build: Before this change the JVM crashes and that is reported as a test failure, after the change the code runs through.

The test was run via:

CONF=windows-x86_64-server-release make test TEST="test/jdk/java/awt/Clipboard/ConcurrentClipboardAccessTest.java"
# and
CONF=windows-x86_64-server-fastdebug make test TEST="test/jdk/java/awt/Clipboard/ConcurrentClipboardAccessTest.java"

@matthiasblaesing
Copy link
Author

@mrserb would you mind having another look at this?

long[] formats = getClipboardFormats();
checkChange(formats);
} catch (Throwable ex) {
System.getLogger(WClipboard.class.getName()).log(Level.WARNING, "Failed to process handleContentsChanged", ex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not saying not to necessarily change this, but what are we hoping to see with logging this warning here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here is to not silently swallow exceptions. This code is called from AwtToolkit::WndProc, which from my reading will not report exceptions/throwables. Pushing the exception to the system logger gives a chance to detect problems here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable but loggers don't seem to be used in src files as far as I can tell. They are used in tests to help detect issues as you mentioned, and I can find plenty of occurrences of loggers used in the test directory of the open JDK but I don't see any in src. Makes me think there's an alternative way to handle this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dropped the log level to DEBUG that way in normal operation this will not visible (and shut not affect performance). I think this might be a way to address the concern.

Comment on lines +207 to -208
openClipboard(null);

try {
openClipboard(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand why this is better? Seems odd that the try/finally block has closeClipboard in the finally block but the openClipboard was moved out of the try.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before the change the call order was inconsistent:

  • ClipboardTransferable: one call, openClipboard is called before the try-block
  • WClipboard: one call with openClipboard before the try-block, one call with openClipboard as first statement in the try-block
  • SunClipboard: three calls with openClipboard as first statement inside the try-block

I unified these so that in all cases openClipboard is called as the last statement before the try-block. The assumption is, that only if openClipboard succeeds (does not raise an exception), calling closeClipboard makes sense.

Comment on lines 24 to 29
/*
@test
@bug 8332271
@summary tests that concurrent access to the clipboard does not crash the JVM
@run main ConcurrentClipboardAccessTest
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been brought to my attention that this test needs to be headful since we're dealing with clipboard behavior.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, thanks for catching this. Pushed an updated version.

@matthiasblaesing
Copy link
Author

@DamonGuy thanks for your check. I updated the PR and added replies. If more changes/informations are required, I'm willing to help.

@@ -109,13 +115,32 @@ protected void clearNativeContext() {}
* @throws IllegalStateException if the clipboard has not been opened
*/
@Override
public native void openClipboard(SunClipboard newOwner) throws IllegalStateException;
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this comment accidentally deleted?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored the comment and adjusted the comment on openClipboard to better address the required calling sequence.

@Override
public void run() {
final Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
while (true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this cause the test to run until timeout if the thread isn't interrupted?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point. Adjusted test to use daemon threads for the concurrent access tests. I reran the tests on the baseline JDK and after the changes to verify the findings from #24614 (comment). Did not notice this as the jtreg harness indeed seems to terminate the JVM and not report that as a failure, tested now also manually (without jtreg).

@key headful
@run main ConcurrentClipboardAccessTest
*/
import java.awt.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expand imports

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

- reduce log level in WClipboard#handleContentsChanged to DEBUG, so that
  in normal operation it will not be visible
- restore comment on WClipboard#closeClipboard
- adjust comment on WClipboard#openClipboard
- Ensure ConcurrentClipboardAccessTest shutsdown on its own when not
  run in an environment with an external timeout handling.
- Added finishing message to ConcurrentClipboardAccessTest, so that
  correct termination can be determined visually if run manually
@matthiasblaesing
Copy link
Author

@alisenchung @DamonGuy thank you both for comments, pushed an update, that hopefully addresses your concerns.

Copy link
Contributor

@DamonGuy DamonGuy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks fair to me now. But, you still need a Reviewer to look at this. Maybe @kumarabhi006 ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test passed even without the fix on macOS.
Is the fix applicable only on Windows ?

If yes, then I think you should restrict the test to run only on required platforms.

@matthiasblaesing
Copy link
Author

@kumarabhi006 my thinking was, that the JVM should not crash on any platform when clipboard is accessed concurrently, but you are right, that the crash is observed windows only. Only that platform (see implementation class WClipboard) requires the openClipboard, closeClipboard calls. For all other platforms these calls are no-ops.

Pushed an update adding the test requirement to the test.

Copy link
Contributor

@kumarabhi006 kumarabhi006 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.
Verified the test case with the fix and don't see any assertions hit on Windows 11.

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Jun 6, 2025
@matthiasblaesing
Copy link
Author

/integrate

@openjdk openjdk bot added the sponsor Pull request is ready to be sponsored label Jun 9, 2025
@openjdk
Copy link

openjdk bot commented Jun 9, 2025

@matthiasblaesing
Your change (at version b930745) is now ready to be sponsored by a Committer.

@matthiasblaesing
Copy link
Author

@DamonGuy @kumarabhi006 would be one of you kind enough to act as a sponsor for this change? I have an author status, but not a committer status.

Before issuing /integate I created a local test branch by merging master from 2-3 days ago into a copy of this branch. That merged cleanly. I reran the application level reproducer with the JDK image from that build and JDK-24. The problem on the application level was reproducible with JDK-24, but not anymore with this changeset in.

Thank you!

@sendaoYan
Copy link
Member

/sponsor

@openjdk
Copy link

openjdk bot commented Jun 10, 2025

Going to push as commit 92be782.
Since your change was applied there have been 1018 commits pushed to the master branch:

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label Jun 10, 2025
@openjdk openjdk bot closed this Jun 10, 2025
@openjdk openjdk bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review sponsor Pull request is ready to be sponsored labels Jun 10, 2025
@openjdk
Copy link

openjdk bot commented Jun 10, 2025

@sendaoYan @matthiasblaesing Pushed as commit 92be782.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
client [email protected] integrated Pull request has been integrated
Development

Successfully merging this pull request may close these issues.

8 participants