Skip to content

Commit 74c5bae

Browse files
jonahgrahamakurtakov
authored andcommitted
[GTK3] Defer disposing Callbacks while GTK still has pointer to them
The calls to gtk_clipboard_set_with_owner (in ClipboardProxy.setData) means the GtkClipboard (in C side) has function pointers saved to the getFunc + clearFunc callbacks. Therefore, when we dispose ClipboardProxy we cannot dispose the callbacks until we know that GtkClipboard doesn't have a pointer to these callbacks. GtkClipboard clears these pointers in [clipboard_unset ](https://gitlab.gnome.org/GNOME/gtk/-/blob/716458e86a222f43e64f7a4feda37749f3469ee4/gtk/gtkclipboard.c#L755) and notifies us that the pointers are no longer stored by calling [clearFunc ](https://gitlab.gnome.org/GNOME/gtk/-/blob/716458e86a222f43e64f7a4feda37749f3469ee4/gtk/gtkclipboard.c#L782) Therefore, after disposing ClipboardProxy we need to defer disposing the callbacks until clearFunc has been called. We know if we have been called sufficiently (for both clipboards that could have a handle stored) when both clipboards no longer have data stored. If we don't defer the disposal it causes SIGSEGV or other undefined behavior. Fixes #2675
1 parent fe1ede8 commit 74c5bae

File tree

3 files changed

+208
-12
lines changed

3 files changed

+208
-12
lines changed

bundles/org.eclipse.swt/Eclipse SWT Drag and Drop/gtk/org/eclipse/swt/dnd/ClipboardProxy.java

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class ClipboardProxy {
3434

3535
long clipboardOwner = GTK3.gtk_window_new(GTK.GTK_WINDOW_TOPLEVEL);
3636

37+
/**
38+
* display == null means that the display has been disposed.
39+
*/
3740
Display display;
3841
Clipboard activeClipboard = null;
3942
Clipboard activePrimaryClipboard = null;
@@ -78,7 +81,48 @@ void gtk_gdk_clipboard_clear(long clipboard) {
7881
GTK3.gtk_clipboard_clear(clipboard);
7982
}
8083

84+
/**
85+
* The calls to gtk_clipboard_set_with_owner (in
86+
* {@link #setData(Clipboard, Object[], Transfer[], int)}) means the
87+
* GtkClipboard (in C side) has function pointers saved to the getFunc +
88+
* clearFunc callbacks.
89+
*
90+
* Therefore, when we dispose {@link ClipboardProxy} we cannot dispose the
91+
* callbacks until we know that GtkClipboard doesn't have a pointer to these
92+
* callbacks. GtkClipboard clears these pointers in clipboard_unset
93+
* (https://gitlab.gnome.org/GNOME/gtk/-/blob/716458e86a222f43e64f7a4feda37749f3469ee4/gtk/gtkclipboard.c#L755)
94+
* and notifies us that the pointers are no longer stored by calling clearFunc
95+
* (https://gitlab.gnome.org/GNOME/gtk/-/blob/716458e86a222f43e64f7a4feda37749f3469ee4/gtk/gtkclipboard.c#L782)
96+
*
97+
* Therefore, after disposing ClipboardProxy we need to defer disposing the
98+
* callbacks until clearFunc has been called.
99+
*
100+
* We know if we have been called sufficiently (for both clipboards that could
101+
* have a handle stored) when both clipboards no longer have data stored.
102+
*
103+
* If we don't defer the disposal it causes SIGSEGV or other undefined behavior.
104+
*
105+
* Fix for https://github.com/eclipse-platform/eclipse.platform.swt/issues/2675
106+
*/
107+
private void finishDispose() {
108+
if (display == null) {
109+
if (clipboardDataTypes == null && primaryClipboardDataTypes == null) {
110+
if (getFunc != null ) getFunc.dispose();
111+
getFunc = null;
112+
if (clearFunc != null) clearFunc.dispose();
113+
clearFunc = null;
114+
if (clipboardOwner != 0) {
115+
GTK3.gtk_widget_destroy(clipboardOwner);
116+
}
117+
clipboardOwner = 0;
118+
}
119+
}
120+
}
121+
81122
long clearFunc(long clipboard,long user_data_or_owner){
123+
if (display == null) {
124+
System.err.println("***WARNING: Attempt to access SWT clipboard after disposing SWT Display.");
125+
}
82126
if (clipboard == Clipboard.GTKCLIPBOARD) {
83127
activeClipboard = null;
84128
clipboardData = null;
@@ -89,6 +133,7 @@ long clearFunc(long clipboard,long user_data_or_owner){
89133
primaryClipboardData = null;
90134
primaryClipboardDataTypes = null;
91135
}
136+
finishDispose();
92137
return 1;
93138
}
94139

@@ -101,25 +146,18 @@ void dispose () {
101146
GTK3.gtk_clipboard_store(Clipboard.GTKPRIMARYCLIPBOARD);
102147
}
103148
display = null;
104-
if (getFunc != null ) getFunc.dispose();
105-
getFunc = null;
106-
if (clearFunc != null) clearFunc.dispose();
107-
clearFunc = null;
108-
clipboardData = null;
109-
clipboardDataTypes = null;
110-
primaryClipboardData = null;
111-
primaryClipboardDataTypes = null;
112-
if (clipboardOwner != 0) {
113-
GTK3.gtk_widget_destroy(clipboardOwner);
114-
}
115-
clipboardOwner = 0;
149+
finishDispose();
116150
}
117151

118152
/**
119153
* This function provides the data to the clipboard on request.
120154
* When this clipboard is disposed, the data will no longer be available.
121155
*/
122156
long getFunc(long clipboard, long selection_data, long info, long user_data_or_owner){
157+
if (display == null) {
158+
System.err.println("***WARNING: Attempt to access SWT clipboard after disposing SWT Display.");
159+
return 0;
160+
}
123161
if (selection_data == 0) return 0;
124162
long target = GTK3.gtk_selection_data_get_target(selection_data);
125163
TransferData tdata = new TransferData();

tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,28 @@ public final static boolean isWayland() {
149149
throw new IllegalStateException("unreachable");
150150
}
151151

152+
/**
153+
* Return whether running on GTK3. This is dynamically set at runtime and cannot
154+
* be accessed before the corresponding property is initialized in OS.
155+
*
156+
* <strong>Note:</strong> this method still must be called after the static
157+
* block of OS is run.
158+
*/
159+
public final static boolean isGTK3() {
160+
if (!isGTK) {
161+
return false;
162+
}
163+
164+
String version = System.getProperty("org.eclipse.swt.internal.gtk.version", "");
165+
if (version.startsWith("3")) {
166+
return true;
167+
} else if (!version.isBlank()) {
168+
return false;
169+
}
170+
fail("org.eclipse.swt.internal.gtk.version System property is not set yet. Create a new Display (or otherwise access OS) before calling isGTK4");
171+
throw new IllegalStateException("unreachable");
172+
}
173+
152174
/**
153175
* Return whether running on GTK4. This is dynamically set at runtime and cannot
154176
* be accessed before the corresponding property is initialized in OS.

tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919
import java.util.Arrays;
2020
import java.util.concurrent.CompletableFuture;
2121

22+
import org.eclipse.swt.SWT;
2223
import org.eclipse.swt.dnd.DND;
2324
import org.eclipse.swt.dnd.RTFTransfer;
2425
import org.eclipse.swt.dnd.TextTransfer;
2526
import org.eclipse.swt.dnd.Transfer;
2627
import org.eclipse.swt.dnd.TransferData;
28+
import org.eclipse.swt.widgets.Shell;
29+
import org.eclipse.swt.widgets.Text;
2730
import org.junit.jupiter.api.BeforeEach;
2831
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
2932
import org.junit.jupiter.api.Order;
@@ -243,4 +246,137 @@ public void test_getAvailableTypes(int clipboardId) throws Exception {
243246
availableTypes = clipboard.getAvailableTypes(clipboardId);
244247
assertTrue(Arrays.stream(availableTypes).anyMatch(rtfTransfer::isSupportedType));
245248
}
249+
250+
/**
251+
* tear down and start again without clearing off the clipboard
252+
*/
253+
private void tearDownAndStartAgain() {
254+
// we can't call tearDown because we don't want to clear the clipboard
255+
clipboard.dispose();
256+
clipboard = null;
257+
shell.dispose();
258+
shell = null;
259+
SwtTestUtil.processEvents();
260+
261+
// tearDown doesn't dispose on each test, but the error we are checking
262+
// for happens when Display is disposed (causing ClipboardProxy to be disposed)
263+
display.dispose();
264+
display = null;
265+
266+
// start back up
267+
setUp();
268+
}
269+
270+
private void checkStderrForGTK3Warning(String stderr) {
271+
if (SwtTestUtil.isGTK3()) {
272+
if ("***WARNING: Attempt to access SWT clipboard after disposing SWT Display.\n".equals(stderr)) {
273+
// test passed (VM didn't SIGSEGV) and we exercise the problematic code causing
274+
// https://github.com/eclipse-platform/eclipse.platform.swt/issues/2675
275+
} else if (stderr.isEmpty()) {
276+
assumeTrue(false, """
277+
test passed (VM didn't SIGSEGV) but we didn't exercise the problematic code causing
278+
https://github.com/eclipse-platform/eclipse.platform.swt/issues/2675
279+
""");
280+
} else {
281+
// test passed (VM didn't SIGSEGV) but we don't know why there is other stderr
282+
// output, log it to the test results for further investigation.
283+
assertEquals("", stderr);
284+
}
285+
} else {
286+
assertEquals("", stderr);
287+
}
288+
}
289+
290+
private Text setContentsOnClipboardAndDisposeDisplay() throws InterruptedException {
291+
openAndFocusShell(false);
292+
293+
String helloWorld = getUniqueTestString();
294+
clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer });
295+
assertEquals(helloWorld, clipboard.getContents(textTransfer));
296+
297+
tearDownAndStartAgain();
298+
shell = new Shell(display);
299+
Text text = new Text(shell, SWT.SINGLE);
300+
String textBoxHello = getUniqueTestString();
301+
text.setText(textBoxHello);
302+
text.setSelection(0, textBoxHello.length());
303+
304+
return text;
305+
}
306+
307+
/**
308+
* Regression test for
309+
* https://github.com/eclipse-platform/eclipse.platform.swt/issues/2675
310+
*/
311+
@Test
312+
public void test_AfterNewDisplay_TextCopy() throws Exception {
313+
Text text = setContentsOnClipboardAndDisposeDisplay();
314+
SwtTestUtil.openShell(shell);
315+
316+
/*
317+
* on GTK3 this Text.copy would trigger SIGSEGV on GTK calling disposed callback
318+
* ClipboardProxy.clearFunc
319+
*/
320+
String stderr = SwtTestUtil.runWithCapturedStderr(() -> text.copy());
321+
checkStderrForGTK3Warning(stderr);
322+
323+
// After the test, clear out the partially disposed clipboard by taking
324+
// ownership and then clearing it
325+
clipboard.setContents(new Object[] { getUniqueTestString() }, new Transfer[] { textTransfer });
326+
clipboard.clearContents();
327+
}
328+
329+
/**
330+
* Regression test for
331+
* https://github.com/eclipse-platform/eclipse.platform.swt/issues/2675
332+
*/
333+
@Test
334+
public void test_AfterNewDisplay_LocalSetContents() throws Exception {
335+
setContentsOnClipboardAndDisposeDisplay();
336+
SwtTestUtil.openShell(shell);
337+
338+
/*
339+
* on GTK3 this setContents would lead to undefined behavior because when the
340+
* ClipboardProxy called bind on the new ClipboardProxy.clearFunc there was a
341+
* pretty good chance it would bind to the same entry in the callback table. But
342+
* depending on exact order of operations that table entry could be empty,
343+
* leading to SIGSEGV.
344+
*/
345+
String stderr = SwtTestUtil.runWithCapturedStderr(
346+
() -> clipboard.setContents(new Object[] { getUniqueTestString() }, new Transfer[] { textTransfer }));
347+
checkStderrForGTK3Warning(stderr);
348+
}
349+
350+
/**
351+
* Regression test for
352+
* https://github.com/eclipse-platform/eclipse.platform.swt/issues/2675
353+
*/
354+
@Test
355+
public void test_AfterNewDisplay_RemoteGetContents() throws Exception {
356+
setContentsOnClipboardAndDisposeDisplay();
357+
openAndFocusRemote();
358+
/*
359+
* on GTK3 this getting from the remote would trigger SIGSEGV on GTK calling
360+
* disposed callback ClipboardProxy.getFunc
361+
* https://github.com/eclipse-platform/eclipse.platform.swt/issues/2675
362+
*
363+
* Caveat: What is available on the clipboard after Display dispose is very
364+
* platform and configuration dependent and no guarantees in the SWT API exist.
365+
* Therefore the return value of remote.getStringContents() is not checked here.
366+
*/
367+
String stderr = SwtTestUtil
368+
.runWithCapturedStderr(() -> SwtTestUtil.runOperationInThread(() -> remote.getStringContents()));
369+
checkStderrForGTK3Warning(stderr);
370+
371+
/*
372+
* On GTK3 At this point the old ClipboardProxy has not been disposed because
373+
* this process still owns the clipboard from the original setContents done at
374+
* the beginning of the test. Therefore set some new contents so that the old
375+
* contents can be cleared.
376+
*/
377+
SwtTestUtil.openShell(shell);
378+
stderr = SwtTestUtil.runWithCapturedStderr(
379+
() -> clipboard.setContents(new Object[] { getUniqueTestString() }, new Transfer[] { textTransfer }));
380+
checkStderrForGTK3Warning(stderr);
381+
}
246382
}

0 commit comments

Comments
 (0)