client-allocated clipboard-transfer buffers lead to DOS attacks on the sender
The core and the primary-selection protocol currently mandate that clipboard-transfer file descriptors are allocated by the recipient. Unless very careful programming is applied on the sender-side, this can lead to the sender ceasing to operate normally. I have confirmed that a malicious but unprivileged client can completely lock up sway, plasma, and gnome using this vulnerability.
Let's assume that the attacker has allocated a pipe for the data transfer. At some point the sender has to perform a write
-like operation on the write-end of the pipe. If the classic write(2)
system call is used, then the sender must do one of two things to prevent this call from blocking indefinitely:
- Call
poll(2)
before the write call to ensure that at least 1 byte can be written. - Set the file descriptor to
O_NONBLOCK
before callingwrite(2)
.
Neither of those are sufficient. If 2 is applied, then the attacker can remove the O_NONBLOCK
flag in a loop so that the write(2)
call in the sender operates on a blocking file description.
If 1 is applied, then the attacker can call read(2)
and write(2)
themselves in a loop so that the write(2)
in the sender will likely operate on a full pipe.
sway, plasma, and gnome are currently vulnerable to this attack in their xwayland code. If a selection was performed in an xwayland client, an unprivileged pure-wayland attacker that gains the keyboard focus can request this selection to be transferred to it. In this case the sender is the compositor itself and the compositor locks up because it is blocked in a write(2)
call.
I know of two ways to work around this attack:
- Fork a new process to perform the transfer, this process can be killed after a timeout if it becomes unresponsive.
- Use the linux-specific io_uring set of system calls. These system calls can perform asynchronous operations even on blocking file descriptors and such operations can be cancelled.
As even the compositors themselves are vulnerable, clients cannot be expected to deal with this. In a future protocol version, these pipes should be allocated by the compositor itself to ensure that neither side can gain access to the other end of the pipe.
The following rust code demonstrates the attack:
eprintln!("got selection");
let (rx, tx) = uapi::pipe().unwrap();
unsafe {
c::fcntl(tx.raw(), c::F_SETPIPE_SZ, 4096);
}
let mut buf = [0u8; 4096];
uapi::write(tx.raw(), &mut buf[..]).unwrap();
offer.receive(connhandle, "text/plain".to_string(), tx.raw());
thread::spawn(move || {
loop {
let n = uapi::read(rx.raw(), &mut buf[..]).unwrap();
eprintln!("read {} bytes", n.len());
uapi::write(tx.raw(), &mut buf[..1]).unwrap();
let fl = uapi::fcntl_getfl(tx.raw()).unwrap();
if fl != c::O_WRONLY {
uapi::fcntl_setfl(tx.raw(), c::O_WRONLY).unwrap();
}
}
});
In your X application you might have to copy a significant chunk of data to trigger the lockup. Using a slightly modified program, the lockup can be triggered in sway and plasma even if only a single byte has been copied.