From 6530142e2231cb3d88b1ba899bed09b46af42cc5 Mon Sep 17 00:00:00 2001 From: Adam Ford Date: Mon, 29 Jun 2026 08:39:13 -0500 Subject: [PATCH] x11bridge: refcount GEM handles for multi-plane DRI3 buffers A DRI3 PixmapFromBuffers for a multi-plane buffer (or any buffer whose planes share one backing object) passes several dma-buf fds that all resolve to the same virtgpu GEM handle, since drm_prime_fd_to_handle deduplicates imports of the same object. vgpu_id_from_prime creates one GemHandleFinalizer per fd, so the bridge calls drm_gem_close on that handle once per fd. The first close frees it; each redundant close returns EINVAL (ENOENT on some drivers), which propagated out of process_socket and tore down the whole X11 client. Refcount the live finalizers per handle: vgpu_id_from_prime increments on import and finalize decrements, closing only when the last one runs. This makes the redundant close impossible rather than swallowing its error, so genuine drm_gem_close errors are still propagated. The Intel iris native context hands DRI3 a multi-plane CCS-aux buffer and was affected: every GL client (glmark2 and others) died with "XIO: fatal IO error 95" on the first frame, now they run to completion. Tested on an Intel Raptor Lake-U iGPU (8086:a721), host iGPU bound to both i915 and xe. AMD (Strix Point) and Mali (MediaTek MT8196) do not pass multi-plane buffers this way and were unaffected. The fix is host-driver-independent, as the offending close is against the guest virtio-gpu device. Signed-off-by: Adam Ford --- crates/muvm/src/guest/bridge/common.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/muvm/src/guest/bridge/common.rs b/crates/muvm/src/guest/bridge/common.rs index 04b047b..b8c7e7f 100644 --- a/crates/muvm/src/guest/bridge/common.rs +++ b/crates/muvm/src/guest/bridge/common.rs @@ -479,6 +479,9 @@ pub struct Client<'a, P: ProtocolHandler> { debug_loop: DebugLoop, pub send_queue: VecDeque, pub sub_poll: SubPoll<'a, P>, + /// Live GemHandleFinalizers per GEM handle. Multi-plane / same-BO DRI3 buffers + /// dedup several fds to one handle, so we refcount and close only on the last. + gem_handle_refs: HashMap, } #[derive(Debug)] @@ -493,6 +496,16 @@ pub struct GemHandleFinalizer(u32); impl GemHandleFinalizer { pub fn finalize(self, client: &mut Client) -> Result<()> { + // Several fds can dedup to one handle (multi-plane / same-BO buffers), so a + // finalizer per fd would close it more than once -- EINVAL/ENOENT, tearing + // down the client. Close only on the last reference; real errors then matter. + if let Some(refs) = client.gem_handle_refs.get_mut(&self.0) { + if *refs > 1 { + *refs -= 1; + return Ok(()); + } + client.gem_handle_refs.remove(&self.0); + } // SAFETY: we own self.0 unsafe { let close = DrmGemClose::new(self.0); @@ -520,6 +533,7 @@ impl<'a, P: ProtocolHandler> Client<'a, P> { debug_loop: DebugLoop::new(), send_queue: VecDeque::new(), sub_poll, + gem_handle_refs: HashMap::new(), })); { let mut borrow = this.borrow_mut(); @@ -709,6 +723,11 @@ impl<'a, P: ProtocolHandler> Client<'a, P> { unsafe { drm_virtgpu_resource_info(self.gpu_ctx.fd.as_raw_fd() as c_int, &mut res_info)?; } + // Count a live reference; only the last finalizer for this handle closes it. + self.gem_handle_refs + .entry(to_handle.handle) + .and_modify(|refcount| *refcount += 1) + .or_insert(1); Ok(( CrossDomainResource { identifier: res_info.res_handle,