From feb0e1c74d13c44c4c0b8c8db360039846bc9617 Mon Sep 17 00:00:00 2001 From: Tudor Brindus Date: Sat, 10 Oct 2020 22:51:50 -0400 Subject: [PATCH] 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. --- xwayland/selection/outgoing.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/xwayland/selection/outgoing.c b/xwayland/selection/outgoing.c index 33aa225b..943e4d1a 100644 --- a/xwayland/selection/outgoing.c +++ b/xwayland/selection/outgoing.c @@ -55,16 +55,27 @@ static int xwm_selection_flush_source_data( static void xwm_selection_transfer_start_outgoing( struct wlr_xwm_selection_transfer *transfer); +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, + outgoing_link); + } + + return first; +} + static void xwm_selection_transfer_destroy_outgoing( struct wlr_xwm_selection_transfer *transfer) { + struct wlr_xwm_selection *selection = transfer->selection; + bool was_first = transfer == xwm_selection_transfer_get_first(selection); wl_list_remove(&transfer->outgoing_link); - // Start next queued transfer - struct wlr_xwm_selection_transfer *first = NULL; - if (!wl_list_empty(&transfer->selection->outgoing)) { - first = wl_container_of(transfer->selection->outgoing.prev, first, - outgoing_link); - xwm_selection_transfer_start_outgoing(first); + // Start next queued transfer if we just removed the active one. + if (was_first && !wl_list_empty(&selection->outgoing)) { + xwm_selection_transfer_start_outgoing( + xwm_selection_transfer_get_first(selection)); } xwm_selection_transfer_remove_source(transfer);