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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1999, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1999, 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
Expand Down Expand Up @@ -204,8 +204,9 @@ public Object getData(DataFlavor flavor)
byte[] data = null;
Transferable localeTransferable = null;

openClipboard(null);

try {
openClipboard(null);
Comment on lines +207 to -208
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.


long[] formats = getClipboardFormats();
Long lFormat = DataTransferer.getInstance().
Expand Down Expand Up @@ -318,12 +319,7 @@ protected void lostOwnershipNow(final AppContext disposedContext) {
* @since 1.5
*/
protected long[] getClipboardFormatsOpenClose() {
try {
openClipboard(null);
return getClipboardFormats();
} finally {
closeClipboard();
}
return getClipboardFormats();
}

/**
Expand Down Expand Up @@ -356,15 +352,7 @@ public synchronized void addFlavorListener(FlavorListener listener) {
flavorListeners.add(listener);

if (numberOfFlavorListeners++ == 0) {
long[] currentFormats = null;
try {
openClipboard(null);
currentFormats = getClipboardFormats();
} catch (final IllegalStateException ignored) {
} finally {
closeClipboard();
}
this.currentFormats = currentFormats;
this.currentFormats = getClipboardFormats();

registerClipboardViewerChecked();
}
Expand Down
52 changes: 40 additions & 12 deletions src/java.desktop/windows/classes/sun/awt/windows/WClipboard.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1996, 2014, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1996, 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
Expand Down Expand Up @@ -29,7 +29,9 @@
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.lang.System.Logger.Level;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import sun.awt.datatransfer.DataTransferer;
import sun.awt.datatransfer.SunClipboard;
Expand All @@ -51,8 +53,12 @@ final class WClipboard extends SunClipboard {

private boolean isClipboardViewerRegistered;

private final ReentrantLock clipboardLocked = new ReentrantLock();

WClipboard() {
super("System");
// Register java side of the clipboard with the native side
registerClipboard();
}

@Override
Expand Down Expand Up @@ -104,18 +110,42 @@ protected void clearNativeContext() {}

/**
* Call the Win32 OpenClipboard function. If newOwner is non-null,
* we also call EmptyClipboard and take ownership.
* we also call EmptyClipboard and take ownership. If this method call
* succeeds, it must be followed by a call to {@link #closeClipboard()}.
*
* @throws IllegalStateException if the clipboard has not been opened
*/
@Override
public native void openClipboard(SunClipboard newOwner) throws IllegalStateException;
public void openClipboard(SunClipboard newOwner) throws IllegalStateException {
if (!clipboardLocked.tryLock()) {
throw new IllegalStateException("Failed to acquire clipboard lock");
}
try {
openClipboard0(newOwner);
} catch (IllegalStateException ex) {
clipboardLocked.unlock();
throw ex;
}
}

/**
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.

* Call the Win32 CloseClipboard function if we have clipboard ownership,
* does nothing if we have not ownership.
*/
@Override
public native void closeClipboard();
public void closeClipboard() {
if (clipboardLocked.isLocked()) {
try {
closeClipboard0();
} finally {
clipboardLocked.unlock();
}
}
}

private native void openClipboard0(SunClipboard newOwner) throws IllegalStateException;
private native void closeClipboard0();

/**
* Call the Win32 SetClipboardData function.
*/
Expand Down Expand Up @@ -157,16 +187,12 @@ private void handleContentsChanged() {
return;
}

long[] formats = null;
try {
openClipboard(null);
formats = getClipboardFormats();
} catch (IllegalStateException exc) {
// do nothing to handle the exception, call checkChange(null)
} finally {
closeClipboard();
long[] formats = getClipboardFormats();
checkChange(formats);
} catch (Throwable ex) {
System.getLogger(WClipboard.class.getName()).log(Level.DEBUG, "Failed to process handleContentsChanged", ex);
}
checkChange(formats);
}

/**
Expand Down Expand Up @@ -214,4 +240,6 @@ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorExcepti
}
};
}

private native void registerClipboard();
}
45 changes: 28 additions & 17 deletions src/java.desktop/windows/native/libawt/windows/awt_Clipboard.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1996, 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
Expand Down Expand Up @@ -69,9 +69,8 @@ void AwtClipboard::RegisterClipboardViewer(JNIEnv *env, jobject jclipboard) {
return;
}

if (theCurrentClipboard == NULL) {
theCurrentClipboard = env->NewGlobalRef(jclipboard);
}
DASSERT(AwtClipboard::theCurrentClipboard != NULL);
DASSERT(env->IsSameObject(AwtClipboard::theCurrentClipboard, jclipboard));

jclass cls = env->GetObjectClass(jclipboard);
AwtClipboard::handleContentsChangedMID =
Expand Down Expand Up @@ -128,11 +127,13 @@ Java_sun_awt_windows_WClipboard_init(JNIEnv *env, jclass cls)
* Signature: (Lsun/awt/windows/WClipboard;)V
*/
JNIEXPORT void JNICALL
Java_sun_awt_windows_WClipboard_openClipboard(JNIEnv *env, jobject self,
Java_sun_awt_windows_WClipboard_openClipboard0(JNIEnv *env, jobject self,
jobject newOwner)
{
TRY;

DASSERT(AwtClipboard::theCurrentClipboard != NULL);
DASSERT(newOwner == NULL || env->IsSameObject(AwtClipboard::theCurrentClipboard, newOwner));
DASSERT(::GetOpenClipboardWindow() != AwtToolkit::GetInstance().GetHWnd());

if (!::OpenClipboard(AwtToolkit::GetInstance().GetHWnd())) {
Expand All @@ -142,10 +143,6 @@ Java_sun_awt_windows_WClipboard_openClipboard(JNIEnv *env, jobject self,
}
if (newOwner != NULL) {
AwtClipboard::GetOwnership();
if (AwtClipboard::theCurrentClipboard != NULL) {
env->DeleteGlobalRef(AwtClipboard::theCurrentClipboard);
}
AwtClipboard::theCurrentClipboard = env->NewGlobalRef(newOwner);
}

CATCH_BAD_ALLOC;
Expand All @@ -157,7 +154,7 @@ Java_sun_awt_windows_WClipboard_openClipboard(JNIEnv *env, jobject self,
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_sun_awt_windows_WClipboard_closeClipboard(JNIEnv *env, jobject self)
Java_sun_awt_windows_WClipboard_closeClipboard0(JNIEnv *env, jobject self)
{
TRY;

Expand Down Expand Up @@ -297,23 +294,25 @@ Java_sun_awt_windows_WClipboard_getClipboardFormats
{
TRY;

DASSERT(::GetOpenClipboardWindow() == AwtToolkit::GetInstance().GetHWnd());
unsigned int cFormats = 128; // Allocate enough space to hold all
unsigned int pcFormatsOut = 0;
unsigned int lpuiFormats[128] = { 0 };

jsize nFormats = ::CountClipboardFormats();
jlongArray formats = env->NewLongArray(nFormats);
VERIFY(::GetUpdatedClipboardFormats(lpuiFormats, 128, &pcFormatsOut));

jlongArray formats = env->NewLongArray(pcFormatsOut);
if (formats == NULL) {
throw std::bad_alloc();
}
if (nFormats == 0) {
if (pcFormatsOut == 0) {
return formats;
}
jboolean isCopy;
jlong *lFormats = env->GetLongArrayElements(formats, &isCopy),
*saveFormats = lFormats;
UINT num = 0;

for (jsize i = 0; i < nFormats; i++, lFormats++) {
*lFormats = num = ::EnumClipboardFormats(num);
for (unsigned int i = 0; i < pcFormatsOut; i++, lFormats++) {
*lFormats = lpuiFormats[i];
}

env->ReleaseLongArrayElements(formats, saveFormats, 0);
Expand Down Expand Up @@ -478,4 +477,16 @@ Java_sun_awt_windows_WClipboard_getClipboardData
CATCH_BAD_ALLOC_RET(NULL);
}

/*
* Class: sun_awt_windows_WClipboard
* Method: registerClipboard
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_sun_awt_windows_WClipboard_registerClipboard(JNIEnv *env, jobject self)
{
DASSERT(AwtClipboard::theCurrentClipboard == NULL);
AwtClipboard::theCurrentClipboard = env->NewGlobalRef(self);
}

} /* extern "C" */
77 changes: 77 additions & 0 deletions test/jdk/java/awt/Clipboard/ConcurrentClipboardAccessTest.java
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.

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 8332271
@summary tests that concurrent access to the clipboard does not crash the JVM
@key headful
@requires (os.family == "windows")
@run main ConcurrentClipboardAccessTest
*/
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;

public class ConcurrentClipboardAccessTest {

public static void main(String[] args) {
Thread clipboardLoader1 = new Thread(new ClipboardLoader());
clipboardLoader1.setDaemon(true);
clipboardLoader1.start();
Thread clipboardLoader2 = new Thread(new ClipboardLoader());
clipboardLoader2.setDaemon(true);
clipboardLoader2.start();
long start = System.currentTimeMillis();
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
long now = System.currentTimeMillis();
if ((now - start) > (10L * 1000L)) {
break;
}
}
// Test is considered successful if the concurrent repeated reading
// from clipboard succeeds for the allotted time and the JVM does not
// crash.
System.out.println("Shutdown normally");
}

public static class ClipboardLoader implements Runnable {

@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).

try {
if (systemClipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
systemClipboard.getData(DataFlavor.stringFlavor);
}
} catch (Exception ignored) {
}
}
}
}
}