You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

485 lines
15 KiB

#include <assert.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wayland-util.h>
#include <wlr/types/wlr_data_device.h>
#include <wlr/types/wlr_primary_selection.h>
#include <wlr/util/log.h>
#include <xcb/xfixes.h>
#include "xwayland/selection.h"
#include "xwayland/xwm.h"
static void xwm_selection_send_notify(struct wlr_xwm *xwm,
xcb_selection_request_event_t *req, bool success) {
xcb_selection_notify_event_t selection_notify = {
.response_type = XCB_SELECTION_NOTIFY,
.sequence = 0,
.time = req->time,
.requestor = req->requestor,
.selection = req->selection,
.target = req->target,
.property = success ? req->property : XCB_ATOM_NONE,
};
wlr_log(WLR_DEBUG, "SendEvent destination=%" PRIu32 " SelectionNotify(31) time=%" PRIu32
" requestor=%" PRIu32 " selection=%" PRIu32 " target=%" PRIu32 " property=%" PRIu32,
req->requestor, req->time, req->requestor, req->selection, req->target,
selection_notify.property);
xcb_send_event(xwm->xcb_conn,
0, // propagate
req->requestor,
XCB_EVENT_MASK_NO_EVENT,
(const char *)&selection_notify);
xcb_flush(xwm->xcb_conn);
}
static int xwm_selection_flush_source_data(
struct wlr_xwm_selection_transfer *transfer) {
xcb_change_property(transfer->selection->xwm->xcb_conn,
XCB_PROP_MODE_REPLACE,
transfer->request.requestor,
transfer->request.property,
transfer->request.target,
8, // format
transfer->source_data.size,
transfer->source_data.data);
xcb_flush(transfer->selection->xwm->xcb_conn);
transfer->property_set = true;
size_t length = transfer->source_data.size;
transfer->source_data.size = 0;
return length;
}
static void xwm_selection_transfer_start_outgoing(
struct wlr_xwm_selection_transfer *transfer);
xwayland: fix use-after-free in selection handling Fixes #2425. wlroots can only handle one outgoing transfer at a time, so it keeps a list of pending selections. The head of the list is the currently-active selection, and when that transfer completes and is destroyed, the next one is started. The trouble is when you have a transfer to some app that is misbehaving. fcitx is one such application. With really large transfers, fcitx will hang and never wake up again. So, you can end up with a transfer list that looks like this: | T1: started | T2: pending | T3: pending | T4: pending | The file descriptor for transfer T1 is registered in libwayland's epoll loop. The rest are waiting in wlroots' list. As a user, you want your clipboard back, so you `pkill fcitx`. Now Xwayland sends `XCB_DESTROY_NOTIFY` to let us know to give up. We clean up T4 first. Due to a bug in wlroots code, we register the (fd, transfer data pointer) pair for T1 with libwayland *again*, despite it already being registered. We do this 2 more times as we remove T3 and T2. Finally, we remove T1 and `free` all the memory associated with it, before `close`-ing its transfer file descriptor. However, we still have 3 copies of T1's file descriptor left in the epoll loop, since we erroneously added them as part of removing T2/3/4. When we `close` the file descriptor as part of T1's teardown, we actually cause the epoll loop to wake up the next time around, saying "this file descriptor has activity!" (it was closed, so `read`-ing would normally return 0 to let us know of EOF). But instead of returning 0, it returns -1 with `EBADF`, because the file descriptor has already been closed. And finally, as part of error-handling this, we access the transfer pointer, which was `free`'d. And we crash.
4 years ago
static struct wlr_xwm_selection_transfer *xwm_selection_transfer_get_first(
struct wlr_xwm_selection *selection) {
struct wlr_xwm_selection_transfer *first = NULL;
if (!wl_list_empty(&selection->outgoing)) {
first = wl_container_of(selection->outgoing.prev, first, link);
xwayland: fix use-after-free in selection handling Fixes #2425. wlroots can only handle one outgoing transfer at a time, so it keeps a list of pending selections. The head of the list is the currently-active selection, and when that transfer completes and is destroyed, the next one is started. The trouble is when you have a transfer to some app that is misbehaving. fcitx is one such application. With really large transfers, fcitx will hang and never wake up again. So, you can end up with a transfer list that looks like this: | T1: started | T2: pending | T3: pending | T4: pending | The file descriptor for transfer T1 is registered in libwayland's epoll loop. The rest are waiting in wlroots' list. As a user, you want your clipboard back, so you `pkill fcitx`. Now Xwayland sends `XCB_DESTROY_NOTIFY` to let us know to give up. We clean up T4 first. Due to a bug in wlroots code, we register the (fd, transfer data pointer) pair for T1 with libwayland *again*, despite it already being registered. We do this 2 more times as we remove T3 and T2. Finally, we remove T1 and `free` all the memory associated with it, before `close`-ing its transfer file descriptor. However, we still have 3 copies of T1's file descriptor left in the epoll loop, since we erroneously added them as part of removing T2/3/4. When we `close` the file descriptor as part of T1's teardown, we actually cause the epoll loop to wake up the next time around, saying "this file descriptor has activity!" (it was closed, so `read`-ing would normally return 0 to let us know of EOF). But instead of returning 0, it returns -1 with `EBADF`, because the file descriptor has already been closed. And finally, as part of error-handling this, we access the transfer pointer, which was `free`'d. And we crash.
4 years ago
}
return first;
}
void xwm_selection_transfer_destroy_outgoing(
struct wlr_xwm_selection_transfer *transfer) {
xwayland: fix use-after-free in selection handling Fixes #2425. wlroots can only handle one outgoing transfer at a time, so it keeps a list of pending selections. The head of the list is the currently-active selection, and when that transfer completes and is destroyed, the next one is started. The trouble is when you have a transfer to some app that is misbehaving. fcitx is one such application. With really large transfers, fcitx will hang and never wake up again. So, you can end up with a transfer list that looks like this: | T1: started | T2: pending | T3: pending | T4: pending | The file descriptor for transfer T1 is registered in libwayland's epoll loop. The rest are waiting in wlroots' list. As a user, you want your clipboard back, so you `pkill fcitx`. Now Xwayland sends `XCB_DESTROY_NOTIFY` to let us know to give up. We clean up T4 first. Due to a bug in wlroots code, we register the (fd, transfer data pointer) pair for T1 with libwayland *again*, despite it already being registered. We do this 2 more times as we remove T3 and T2. Finally, we remove T1 and `free` all the memory associated with it, before `close`-ing its transfer file descriptor. However, we still have 3 copies of T1's file descriptor left in the epoll loop, since we erroneously added them as part of removing T2/3/4. When we `close` the file descriptor as part of T1's teardown, we actually cause the epoll loop to wake up the next time around, saying "this file descriptor has activity!" (it was closed, so `read`-ing would normally return 0 to let us know of EOF). But instead of returning 0, it returns -1 with `EBADF`, because the file descriptor has already been closed. And finally, as part of error-handling this, we access the transfer pointer, which was `free`'d. And we crash.
4 years ago
struct wlr_xwm_selection *selection = transfer->selection;
bool was_first = transfer == xwm_selection_transfer_get_first(selection);
wl_list_remove(&transfer->link);
wlr_log(WLR_DEBUG, "Destroying transfer %p", transfer);
xwayland: fix use-after-free in selection handling Fixes #2425. wlroots can only handle one outgoing transfer at a time, so it keeps a list of pending selections. The head of the list is the currently-active selection, and when that transfer completes and is destroyed, the next one is started. The trouble is when you have a transfer to some app that is misbehaving. fcitx is one such application. With really large transfers, fcitx will hang and never wake up again. So, you can end up with a transfer list that looks like this: | T1: started | T2: pending | T3: pending | T4: pending | The file descriptor for transfer T1 is registered in libwayland's epoll loop. The rest are waiting in wlroots' list. As a user, you want your clipboard back, so you `pkill fcitx`. Now Xwayland sends `XCB_DESTROY_NOTIFY` to let us know to give up. We clean up T4 first. Due to a bug in wlroots code, we register the (fd, transfer data pointer) pair for T1 with libwayland *again*, despite it already being registered. We do this 2 more times as we remove T3 and T2. Finally, we remove T1 and `free` all the memory associated with it, before `close`-ing its transfer file descriptor. However, we still have 3 copies of T1's file descriptor left in the epoll loop, since we erroneously added them as part of removing T2/3/4. When we `close` the file descriptor as part of T1's teardown, we actually cause the epoll loop to wake up the next time around, saying "this file descriptor has activity!" (it was closed, so `read`-ing would normally return 0 to let us know of EOF). But instead of returning 0, it returns -1 with `EBADF`, because the file descriptor has already been closed. And finally, as part of error-handling this, we access the transfer pointer, which was `free`'d. And we crash.
4 years ago
// Start next queued transfer if we just removed the active one.
if (was_first && !wl_list_empty(&selection->outgoing)) {
wlr_log(WLR_DEBUG, "Destroyed transfer was active, starting next");
xwayland: fix use-after-free in selection handling Fixes #2425. wlroots can only handle one outgoing transfer at a time, so it keeps a list of pending selections. The head of the list is the currently-active selection, and when that transfer completes and is destroyed, the next one is started. The trouble is when you have a transfer to some app that is misbehaving. fcitx is one such application. With really large transfers, fcitx will hang and never wake up again. So, you can end up with a transfer list that looks like this: | T1: started | T2: pending | T3: pending | T4: pending | The file descriptor for transfer T1 is registered in libwayland's epoll loop. The rest are waiting in wlroots' list. As a user, you want your clipboard back, so you `pkill fcitx`. Now Xwayland sends `XCB_DESTROY_NOTIFY` to let us know to give up. We clean up T4 first. Due to a bug in wlroots code, we register the (fd, transfer data pointer) pair for T1 with libwayland *again*, despite it already being registered. We do this 2 more times as we remove T3 and T2. Finally, we remove T1 and `free` all the memory associated with it, before `close`-ing its transfer file descriptor. However, we still have 3 copies of T1's file descriptor left in the epoll loop, since we erroneously added them as part of removing T2/3/4. When we `close` the file descriptor as part of T1's teardown, we actually cause the epoll loop to wake up the next time around, saying "this file descriptor has activity!" (it was closed, so `read`-ing would normally return 0 to let us know of EOF). But instead of returning 0, it returns -1 with `EBADF`, because the file descriptor has already been closed. And finally, as part of error-handling this, we access the transfer pointer, which was `free`'d. And we crash.
4 years ago
xwm_selection_transfer_start_outgoing(
xwm_selection_transfer_get_first(selection));
}
xwm_selection_transfer_remove_event_source(transfer);
xwm_selection_transfer_close_wl_client_fd(transfer);
wl_array_release(&transfer->source_data);
free(transfer);
}
static int xwm_data_source_read(int fd, uint32_t mask, void *data) {
struct wlr_xwm_selection_transfer *transfer = data;
struct wlr_xwm *xwm = transfer->selection->xwm;
void *p;
size_t current = transfer->source_data.size;
if (transfer->source_data.size < INCR_CHUNK_SIZE) {
p = wl_array_add(&transfer->source_data, INCR_CHUNK_SIZE);
if (p == NULL) {
wlr_log(WLR_ERROR, "Could not allocate selection source_data");
goto error_out;
}
} else {
p = (char *)transfer->source_data.data + transfer->source_data.size;
}
size_t available = transfer->source_data.alloc - current;
ssize_t len = read(fd, p, available);
if (len == -1) {
wlr_log_errno(WLR_ERROR, "read error from data source");
goto error_out;
}
wlr_log(WLR_DEBUG, "read %zd bytes (available %zu, mask 0x%x)", len,
available, mask);
transfer->source_data.size = current + len;
if (transfer->source_data.size >= INCR_CHUNK_SIZE) {
if (!transfer->incr) {
wlr_log(WLR_DEBUG, "got %zu bytes, starting incr",
transfer->source_data.size);
size_t incr_chunk_size = INCR_CHUNK_SIZE;
xcb_change_property(xwm->xcb_conn,
XCB_PROP_MODE_REPLACE,
transfer->request.requestor,
transfer->request.property,
xwm->atoms[INCR],
32, /* format */
1, &incr_chunk_size);
transfer->incr = true;
transfer->property_set = true;
transfer->flush_property_on_delete = true;
xwm_selection_transfer_remove_event_source(transfer);
xwm_selection_send_notify(xwm, &transfer->request, true);
} else if (transfer->property_set) {
wlr_log(WLR_DEBUG, "got %zu bytes, waiting for property delete",
transfer->source_data.size);
transfer->flush_property_on_delete = true;
xwm_selection_transfer_remove_event_source(transfer);
} else {
wlr_log(WLR_DEBUG, "got %zu bytes, property deleted, setting new "
"property", transfer->source_data.size);
xwm_selection_flush_source_data(transfer);
}
} else if (len == 0 && !transfer->incr) {
wlr_log(WLR_DEBUG, "non-incr transfer complete");
xwm_selection_flush_source_data(transfer);
xwm_selection_send_notify(xwm, &transfer->request, true);
xwm_selection_transfer_destroy_outgoing(transfer);
} else if (len == 0 && transfer->incr) {
wlr_log(WLR_DEBUG, "incr transfer complete");
transfer->flush_property_on_delete = true;
if (transfer->property_set) {
wlr_log(WLR_DEBUG, "got %zu bytes, waiting for property delete",
transfer->source_data.size);
} else {
wlr_log(WLR_DEBUG, "got %zu bytes, property deleted, setting new "
"property", transfer->source_data.size);
xwm_selection_flush_source_data(transfer);
}
xwm_selection_transfer_remove_event_source(transfer);
xwm_selection_transfer_close_wl_client_fd(transfer);
} else {
wlr_log(WLR_DEBUG, "nothing happened, buffered the bytes");
}
return 1;
error_out:
xwm_selection_send_notify(xwm, &transfer->request, false);
xwm_selection_transfer_destroy_outgoing(transfer);
return 0;
}
void xwm_send_incr_chunk(struct wlr_xwm_selection_transfer *transfer) {
wlr_log(WLR_DEBUG, "property deleted");
transfer->property_set = false;
if (transfer->flush_property_on_delete) {
wlr_log(WLR_DEBUG, "setting new property, %zu bytes",
transfer->source_data.size);
transfer->flush_property_on_delete = false;
int length = xwm_selection_flush_source_data(transfer);
if (transfer->wl_client_fd >= 0) {
xwm_selection_transfer_start_outgoing(transfer);
} else if (length > 0) {
/* Transfer is all done, but queue a flush for
* the delete of the last chunk so we can set
* the 0 sized property to signal the end of
* the transfer. */
transfer->flush_property_on_delete = true;
wl_array_release(&transfer->source_data);
wl_array_init(&transfer->source_data);
} else {
xwm_selection_transfer_destroy_outgoing(transfer);
}
}
}
static void xwm_selection_source_send(struct wlr_xwm_selection *selection,
const char *mime_type, int32_t fd) {
if (selection == &selection->xwm->clipboard_selection) {
struct wlr_data_source *source =
selection->xwm->seat->selection_source;
if (source != NULL) {
wlr_data_source_send(source, mime_type, fd);
return;
}
} else if (selection == &selection->xwm->primary_selection) {
struct wlr_primary_selection_source *source =
selection->xwm->seat->primary_selection_source;
if (source != NULL) {
wlr_primary_selection_source_send(source, mime_type, fd);
return;
}
} else if (selection == &selection->xwm->dnd_selection) {
struct wlr_data_source *source =
selection->xwm->seat->drag_source;
if (source != NULL) {
wlr_data_source_send(source, mime_type, fd);
return;
}
}
wlr_log(WLR_DEBUG, "not sending selection: no selection source available");
}
static void xwm_selection_transfer_start_outgoing(
struct wlr_xwm_selection_transfer *transfer) {
struct wlr_xwm *xwm = transfer->selection->xwm;
struct wl_event_loop *loop =
wl_display_get_event_loop(xwm->xwayland->wl_display);
wlr_log(WLR_DEBUG, "Starting transfer %p", transfer);
assert(transfer == xwm_selection_transfer_get_first(transfer->selection));
transfer->event_source = wl_event_loop_add_fd(loop, transfer->wl_client_fd,
WL_EVENT_READABLE, xwm_data_source_read, transfer);
}
static struct wl_array *xwm_selection_source_get_mime_types(
struct wlr_xwm_selection *selection) {
if (selection == &selection->xwm->clipboard_selection) {
struct wlr_data_source *source =
selection->xwm->seat->selection_source;
if (source != NULL) {
return &source->mime_types;
}
} else if (selection == &selection->xwm->primary_selection) {
struct wlr_primary_selection_source *source =
selection->xwm->seat->primary_selection_source;
if (source != NULL) {
return &source->mime_types;
}
} else if (selection == &selection->xwm->dnd_selection) {
struct wlr_data_source *source =
selection->xwm->seat->drag_source;
if (source != NULL) {
return &source->mime_types;
}
}
return NULL;
}
/**
* Read the Wayland selection and send it to an Xwayland client.
*/
static bool xwm_selection_send_data(struct wlr_xwm_selection *selection,
xcb_selection_request_event_t *req, const char *mime_type) {
// Check MIME type
struct wl_array *mime_types =
xwm_selection_source_get_mime_types(selection);
if (mime_types == NULL) {
wlr_log(WLR_ERROR, "not sending selection: no MIME type list available");
return false;
}
bool found = false;
char **mime_type_ptr;
wl_array_for_each(mime_type_ptr, mime_types) {
char *t = *mime_type_ptr;
if (strcmp(t, mime_type) == 0) {
found = true;
break;
}
}
if (!found) {
wlr_log(WLR_ERROR, "not sending selection: "
"requested an unsupported MIME type %s", mime_type);
return false;
}
struct wlr_xwm_selection_transfer *transfer =
calloc(1, sizeof(struct wlr_xwm_selection_transfer));
if (transfer == NULL) {
wlr_log(WLR_ERROR, "Allocation failed");
return false;
}
xwm_selection_transfer_init(transfer, selection);
transfer->request = *req;
wl_array_init(&transfer->source_data);
int p[2];
if (pipe(p) == -1) {
wlr_log_errno(WLR_ERROR, "pipe() failed");
return false;
}
fcntl(p[0], F_SETFD, FD_CLOEXEC);
fcntl(p[0], F_SETFL, O_NONBLOCK);
fcntl(p[1], F_SETFD, FD_CLOEXEC);
fcntl(p[1], F_SETFL, O_NONBLOCK);
transfer->wl_client_fd = p[0];
wlr_log(WLR_DEBUG, "Sending Wayland selection %u to Xwayland window with "
"MIME type %s, target %u, transfer %p", req->target, mime_type,
req->target, transfer);
xwm_selection_source_send(selection, mime_type, p[1]);
// It seems that if we ever try to reply to a selection request after
// another has been sent by the same requestor, the requestor never reads
// from it. It appears to only ever read from the latest, so purge stale
// transfers to prevent clipboard hangs.
struct wlr_xwm_selection_transfer *outgoing, *tmp;
wl_list_for_each_safe(outgoing, tmp, &selection->outgoing, link) {
if (transfer->request.requestor == outgoing->request.requestor) {
wlr_log(WLR_DEBUG, "Destroying stale transfer %p", outgoing);
xwm_selection_send_notify(selection->xwm, &outgoing->request, false);
xwm_selection_transfer_destroy_outgoing(outgoing);
}
}
wl_list_insert(&selection->outgoing, &transfer->link);
// We can only handle one transfer at a time
if (wl_list_length(&selection->outgoing) == 1) {
wlr_log(WLR_DEBUG, "No transfer active, starting %p now", transfer);
xwm_selection_transfer_start_outgoing(transfer);
} else {
struct wlr_xwm_selection_transfer *outgoing;
wl_list_for_each(outgoing, &selection->outgoing, link) {
wlr_log(WLR_DEBUG, "Transfer %p still queued", outgoing);
}
}
return true;
}
static void xwm_selection_send_targets(struct wlr_xwm_selection *selection,
xcb_selection_request_event_t *req) {
struct wlr_xwm *xwm = selection->xwm;
struct wl_array *mime_types =
xwm_selection_source_get_mime_types(selection);
if (mime_types == NULL) {
wlr_log(WLR_ERROR, "not sending selection targets: "
"no selection source available");
xwm_selection_send_notify(selection->xwm, req, false);
return;
}
size_t n = 2 + mime_types->size / sizeof(char *);
xcb_atom_t targets[n];
targets[0] = xwm->atoms[TIMESTAMP];
targets[1] = xwm->atoms[TARGETS];
size_t i = 0;
char **mime_type_ptr;
wl_array_for_each(mime_type_ptr, mime_types) {
char *mime_type = *mime_type_ptr;
targets[2+i] = xwm_mime_type_to_atom(xwm, mime_type);
++i;
}
xcb_change_property(xwm->xcb_conn,
XCB_PROP_MODE_REPLACE,
req->requestor,
req->property,
XCB_ATOM_ATOM,
32, // format
n, targets);
xwm_selection_send_notify(selection->xwm, req, true);
}
static void xwm_selection_send_timestamp(struct wlr_xwm_selection *selection,
xcb_selection_request_event_t *req) {
xcb_change_property(selection->xwm->xcb_conn,
XCB_PROP_MODE_REPLACE,
req->requestor,
req->property,
XCB_ATOM_INTEGER,
32, // format
1, &selection->timestamp);
xwm_selection_send_notify(selection->xwm, req, true);
}
void xwm_handle_selection_request(struct wlr_xwm *xwm,
xcb_selection_request_event_t *req) {
wlr_log(WLR_DEBUG, "XCB_SELECTION_REQUEST (time=%u owner=%u, requestor=%u "
"selection=%u, target=%u, property=%u)",
req->time, req->owner, req->requestor, req->selection, req->target,
req->property);
if (req->selection == xwm->atoms[CLIPBOARD_MANAGER]) {
// The wlroots clipboard should already have grabbed the first target,
// so just send selection notify now. This isn't synchronized with the
// clipboard finishing getting the data, so there's a race here.
xwm_selection_send_notify(xwm, req, true);
return;
}
struct wlr_xwm_selection *selection =
xwm_get_selection(xwm, req->selection);
if (selection == NULL) {
wlr_log(WLR_DEBUG, "received selection request for unknown selection");
goto fail_notify_requestor;
}
if (selection->window != req->owner) {
wlr_log(WLR_DEBUG, "received selection request with invalid owner");
goto fail_notify_requestor;
}
// No xwayland surface focused, deny access to clipboard
if (xwm->focus_surface == NULL && xwm->drag_focus == NULL) {
char *selection_name = xwm_get_atom_name(xwm, selection->atom);
wlr_log(WLR_DEBUG, "denying read access to selection %u (%s): "
"no xwayland surface focused", selection->atom, selection_name);
free(selection_name);
goto fail_notify_requestor;
}
if (req->target == xwm->atoms[TARGETS]) {
xwm_selection_send_targets(selection, req);
} else if (req->target == xwm->atoms[TIMESTAMP]) {
xwm_selection_send_timestamp(selection, req);
} else if (req->target == xwm->atoms[DELETE]) {
xwm_selection_send_notify(selection->xwm, req, true);
} else {
// Send data
char *mime_type = xwm_mime_type_from_atom(xwm, req->target);
if (mime_type == NULL) {
wlr_log(WLR_ERROR, "ignoring selection request: unknown atom %u",
req->target);
goto fail_notify_requestor;
}
bool send_success = xwm_selection_send_data(selection, req, mime_type);
free(mime_type);
if (!send_success) {
goto fail_notify_requestor;
}
}
return;
fail_notify_requestor:
// Something went wrong, and there won't be any data being sent to the
// requestor, so let them know.
xwm_selection_send_notify(xwm, req, false);
}
void xwm_handle_selection_destroy_notify(struct wlr_xwm *xwm,
xcb_destroy_notify_event_t *event) {
struct wlr_xwm_selection *selections[] = {
&xwm->clipboard_selection,
&xwm->primary_selection,
&xwm->dnd_selection,
};
for (size_t i = 0; i < sizeof(selections)/sizeof(selections[0]); ++i) {
struct wlr_xwm_selection *selection = selections[i];
struct wlr_xwm_selection_transfer *outgoing, *tmp;
wl_list_for_each_safe(outgoing, tmp, &selection->outgoing, link) {
if (event->window == outgoing->request.requestor) {
xwm_selection_transfer_destroy_outgoing(outgoing);
}
}
}
}