Skip to content

fix(wlroots): share one virtual keyboard across clients - #491

Open
0xFlo wants to merge 2 commits into
feschber:mainfrom
0xFlo:fix/shared-virtual-keyboard
Open

fix(wlroots): share one virtual keyboard across clients#491
0xFlo wants to merge 2 commits into
feschber:mainfrom
0xFlo:fix/shared-virtual-keyboard

Conversation

@0xFlo

@0xFlo 0xFlo commented Aug 30, 2026

Copy link
Copy Markdown

Fixes the file descriptor exhaustion behind #478.

Cause

State::add_client creates a virtual keyboard per emulation client and hands the compositor a keymap descriptor:

let keyboard: Vk = self.vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
if let Some((format, fd, size)) = self.keymap.as_ref() {
    keyboard.keymap(*format, fd.as_fd(), *size);

It is the same descriptor on every call. Wayland caps descriptors per sendmsg (MAX_FDS_OUT), so under client-creation churn they queue faster than the compositor drains them, and the kernel eventually refuses the send. too_many_unix_fds() compares the user's total in-flight SCM_RIGHTS count against the sender's RLIMIT_NOFILE, which is why this shows up on an otherwise idle desktop: a distro with the traditional 1024 soft limit is already close to the ceiling before lan-mouse adds anything.

Reproduction

Added as an example so it can be re-run: cargo run --release -p input-emulation --example keymap_fd_churn. It creates N emulation clients and samples /proc/self/fd. --control holds the client count at one and sends the same number of events instead.

On Arch, Hyprland 0.56.2, at ulimit -n 2048:

FAILED at iteration 896
error: wayland error: `Io error: Too many references: cannot splice (os error 109)`

That is ETOOMANYREFS, the error in #478. Control mode stays flat at 12 descriptors and completes clean, which is what identifies client creation rather than the event path as the source.

Note the threshold is environment dependent. The ambient in-flight count on this machine sits around 1024 to 1152, so a limit below that fails at iteration 0 regardless of this bug, and a limit far above it never trips. 1536 and 2048 both discriminate here.

After

completed 2000 iterations without error
open fds: 12 (baseline 12)

Interleaved A/B at ulimit -n 2048, alternating binaries so ambient pressure is not mistaken for the effect:

before after
round 1 FAILED at iteration 896, errno 109 2000 iterations, 12 fds
round 2 FAILED at iteration 919, errno 109 2000 iterations, 12 fds

WAYLAND_DEBUG=1 over a 2000 client run confirms it at the protocol level: create_virtual_keyboard 1, zwp_virtual_keyboard_v1.keymap 1, create_virtual_pointer 2000.

Why sharing the keyboard is safe

  • Every virtual keyboard was already created on the same wl_seat, so wlroots merged their key and modifier streams into one focused surface. N objects never gave clients independent keyboard state, only independent handles onto the same seat.
  • Per client modifier tracking is untouched. XMods stays in VirtualInput, one per client.
  • Key release on destroy does not depend on keyboard.destroy(). InputEmulation::destroy calls release_keys first, which emits explicit releases and zeroed modifiers before the backend's destroy runs.
  • Creation stays lazy rather than moving into new(), so the keyboard appears on the seat at the same moment as before, and never appears if no client is created.

Testing

Verified end to end between two Arch machines running Hyprland over a LAN, layer-shell capture and wlroots emulation on both: cursor crossing, text entry, and Shift and Ctrl held while typing, with no stuck or dropped modifiers on either side.

Adjacent, not addressed here

Dispatch<WlSeat> calls seat.get_keyboard(...) on every Capabilities event and never releases the wl_keyboard, so repeated capability announcements would leak keyboard objects and keymap descriptors. It looks unreachable today because nothing dispatches the queue after new(), so I have left it alone rather than change it without a reproduction.

@feschber

Copy link
Copy Markdown
Owner

Yeah this is probably a good idea. The current design predates the connection based model, where now every connection has its own virtual keyboard instead of every remote device, as was previously the case.

0xFlo added 2 commits August 31, 2026 13:14
add_client sent a keymap fd per emulation client, and it is the same fd every
time, so under client churn the queue grew past MAX_FDS_OUT and sendmsg failed
with ETOOMANYREFS. One lazily created keyboard sends it once. The pointer stays
per client because it carries no descriptor.
Creates N emulation clients while sampling open descriptors, with a control
mode that holds the client count at one to distinguish client creation from
the event path. Motion is always zero valued so it never moves the cursor.
@feschber
feschber force-pushed the fix/shared-virtual-keyboard branch from fba0341 to b420da1 Compare August 31, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants