Fix segfault in multithreaded context

Dropping libwayland-client's queue in multithreaded context is safe if
and only if there're no alive proxies on it, otherwise it could result
in use-after-free. For more see [1] and [2].

[1] - https://gitlab.freedesktop.org/wayland/wayland/-/issues/13
[2] - https://github.com/Smithay/wayland-rs/issues/359
This commit is contained in:
Kirill Chibisov 2020-12-12 18:28:29 +03:00 • committed by GitHub
parent 276aa84cf5
commit f6f5b98235
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 42 deletions

View file

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- Segfault when dropping clipboard in multithreaded context while main queue is still running
## 0.6.1 -- 2020-10-13 ## 0.6.1 -- 2020-10-13
- Crash when failing to write to a clipboard - Crash when failing to write to a clipboard

View file

@ -223,7 +223,7 @@ fn process_keyboard_event(event: KeyboardEvent, dispatch_data: &mut DispatchData
let contents = dispatch_data let contents = dispatch_data
.clipboard .clipboard
.load() .load()
.unwrap_or_else(|_| String::from("Failed to load primary selection")); .unwrap_or_else(|_| String::from("Failed to load selection"));
println!("Paste: {}", contents); println!("Paste: {}", contents);
} }
// Copy primary. // Copy primary.

View file

@ -1,47 +1,68 @@
use std::collections::VecDeque;
use std::slice::IterMut;
use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_seat::WlSeat;
use super::seat_data::SeatData;
/// Data to track latest seat and serial for clipboard requests. /// Data to track latest seat and serial for clipboard requests.
#[derive(Default)]
pub struct ClipboardDispatchData { pub struct ClipboardDispatchData {
observed_seats: Vec<(WlSeat, u32)>, /// Seats that our application encountered. The first seat is the latest one we've encountered.
last_pos: usize, observed_seats: VecDeque<(WlSeat, u32)>,
/// All the seats that were advertised.
seats: Vec<SeatData>,
} }
impl ClipboardDispatchData { impl ClipboardDispatchData {
/// Builds new `ClipboardDispatchData` with all fields equal to `None`. /// Builds new `ClipboardDispatchData` with all fields equal to `None`.
pub fn new() -> Self { pub fn new(seats: Vec<SeatData>) -> Self {
Self::default() Self { observed_seats: Default::default(), seats }
}
/// Returns the requested seat's data or adds a new one.
pub fn get_seat_data_or_add(&mut self, seat: WlSeat) -> &mut SeatData {
let pos = self.seats.iter().position(|st| st.seat == seat);
let index = pos.unwrap_or_else(|| {
self.seats.push(SeatData::new(seat, None, None));
self.seats.len() - 1
});
&mut self.seats[index]
}
pub fn seats(&mut self) -> IterMut<'_, SeatData> {
self.seats.iter_mut()
} }
/// Set the last observed seat. /// Set the last observed seat.
pub fn set_last_seat(&mut self, seat: WlSeat, serial: u32) { pub fn set_last_observed_seat(&mut self, seat: WlSeat, serial: u32) {
let pos = self.observed_seats.iter().position(|st| st.0 == seat); let pos = self.observed_seats.iter().position(|st| st.0 == seat);
match pos { let (seat, serial) = match pos {
Some(pos) => { Some(pos) => {
// Update serial and set the last data we've seen. // We just found that `pos` we're going to remove, so unwrapping is safe.
self.observed_seats[pos].1 = serial; self.observed_seats.remove(pos).unwrap()
self.last_pos = pos;
} }
None => { None => (seat, serial),
// Add new seat and mark it as last. };
self.last_pos = self.observed_seats.len();
self.observed_seats.push((seat, serial)); // Add seat to front, thus it'll be the latest observed one.
} self.observed_seats.push_front((seat, serial));
}
} }
/// Remove the given seat from the observer seats. /// Remove the given seat from the observed seats.
pub fn remove_seat(&mut self, seat: WlSeat) { pub fn remove_observed_seat(&mut self, seat: WlSeat) {
let pos = self.observed_seats.iter().position(|st| st.0 == seat); let pos = match self.observed_seats.iter().position(|st| st.0 == seat) {
Some(pos) => pos,
None => return,
};
if let Some(pos) = pos { // Remove the seat data.
// Remove the seat data. self.observed_seats.remove(pos);
self.observed_seats.remove(pos);
}
} }
/// Return the last observed seat and the serial. /// Return the last observed seat and the serial.
pub fn last_seat(&self) -> Option<&(WlSeat, u32)> { pub fn last_observed_seat(&self) -> Option<&(WlSeat, u32)> {
self.observed_seats.get(self.last_pos) self.observed_seats.front()
} }
} }

View file

@ -104,10 +104,10 @@ pub fn pointer_handler(seat: WlSeat, event: PointerEvent, mut dispatch_data: Dis
}; };
match event { match event {
PointerEvent::Enter { serial, .. } => { PointerEvent::Enter { serial, .. } => {
dispatch_data.set_last_seat(seat, serial); dispatch_data.set_last_observed_seat(seat, serial);
} }
PointerEvent::Button { serial, .. } => { PointerEvent::Button { serial, .. } => {
dispatch_data.set_last_seat(seat, serial); dispatch_data.set_last_observed_seat(seat, serial);
} }
_ => {} _ => {}
} }
@ -121,13 +121,13 @@ pub fn keyboard_handler(seat: WlSeat, event: KeyboardEvent, mut dispatch_data: D
}; };
match event { match event {
KeyboardEvent::Enter { serial, .. } => { KeyboardEvent::Enter { serial, .. } => {
dispatch_data.set_last_seat(seat, serial); dispatch_data.set_last_observed_seat(seat, serial);
} }
KeyboardEvent::Key { serial, .. } => { KeyboardEvent::Key { serial, .. } => {
dispatch_data.set_last_seat(seat, serial); dispatch_data.set_last_observed_seat(seat, serial);
} }
KeyboardEvent::Leave { .. } => { KeyboardEvent::Leave { .. } => {
dispatch_data.remove_seat(seat); dispatch_data.remove_observed_seat(seat);
} }
KeyboardEvent::Keymap { fd, .. } => { KeyboardEvent::Keymap { fd, .. } => {
// Prevent fd leaking. // Prevent fd leaking.

View file

@ -143,15 +143,13 @@ fn worker_impl(display: Display, request_rx: Receiver<Command>, reply_tx: Sender
} }
// Listen for seats. // Listen for seats.
let _listener = env.listen_for_seats(move |seat, seat_data, _| { let listener = env.listen_for_seats(move |seat, seat_data, mut dispatch_data| {
let detached_seat = seat.detach(); let dispatch_data = match dispatch_data.get::<ClipboardDispatchData>() {
let pos = seats.iter().position(|st| st.seat == detached_seat); Some(dispatch_data) => dispatch_data,
let index = pos.unwrap_or_else(|| { None => return,
seats.push(SeatData::new(detached_seat, None, None)); };
seats.len() - 1
});
let seat_resources = &mut seats[index]; let seat_resources = dispatch_data.get_seat_data_or_add(seat.detach());
if seat_data.has_keyboard && !seat_data.defunct { if seat_data.has_keyboard && !seat_data.defunct {
if seat_resources.keyboard.is_none() { if seat_resources.keyboard.is_none() {
@ -194,7 +192,7 @@ fn worker_impl(display: Display, request_rx: Receiver<Command>, reply_tx: Sender
// Flush the display. // Flush the display.
let _ = queue.display().flush(); let _ = queue.display().flush();
let mut dispatch_data = ClipboardDispatchData::new(); let mut dispatch_data = ClipboardDispatchData::new(seats);
// Setup sleep amount tracker. // Setup sleep amount tracker.
let mut sa_tracker = SleepAmountTracker::new(MAX_TIME_TO_SLEEP, MAX_WARM_WAKEUPS); let mut sa_tracker = SleepAmountTracker::new(MAX_TIME_TO_SLEEP, MAX_WARM_WAKEUPS);
@ -219,7 +217,7 @@ fn worker_impl(display: Display, request_rx: Receiver<Command>, reply_tx: Sender
} }
// Get latest observed seat and serial. // Get latest observed seat and serial.
let (seat, serial) = match dispatch_data.last_seat() { let (seat, serial) = match dispatch_data.last_observed_seat() {
Some(data) => data, Some(data) => data,
None => { None => {
handlers::reply_error(&reply_tx, "no focus on a seat."); handlers::reply_error(&reply_tx, "no focus on a seat.");
@ -284,7 +282,7 @@ fn worker_impl(display: Display, request_rx: Receiver<Command>, reply_tx: Sender
// If some application is trying to spam us when there're no seats, it's likely that // If some application is trying to spam us when there're no seats, it's likely that
// someone is trying to paste from us. // someone is trying to paste from us.
if dispatch_data.last_seat().is_none() && pending_events != 0 { if dispatch_data.last_observed_seat().is_none() && pending_events != 0 {
sa_tracker.reset_sleep(); sa_tracker.reset_sleep();
} else { } else {
// Time for thread to sleep. // Time for thread to sleep.
@ -296,4 +294,29 @@ fn worker_impl(display: Display, request_rx: Receiver<Command>, reply_tx: Sender
sa_tracker.increase_sleep(); sa_tracker.increase_sleep();
} }
} }
// While everything inside this block is safe, the logic is generally unsafe, since we must
// drop every proxy on the current `queue`, since dropping it in multithreaded context
// could result in use-after-free in libwayland-client.
//
// For more see https://gitlab.freedesktop.org/wayland/wayland/-/issues/13.
#[allow(unused_unsafe)]
unsafe {
for seat in dispatch_data.seats() {
if let Some(pointer) = seat.pointer.take() {
if pointer.as_ref().version() >= 3 {
pointer.release();
}
}
if let Some(keyboard) = seat.keyboard.take() {
if keyboard.as_ref().version() >= 3 {
keyboard.release();
}
}
}
std::mem::drop(listener);
let _ = queue.sync_roundtrip(&mut dispatch_data, |_, _, _| unimplemented!());
let _ = queue.display().flush();
}
} }