From nobody Sat Jun 20 05:53:24 2026 Received: from mail-qk1-f182.google.com (mail-qk1-f182.google.com [209.85.222.182]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3A8012877F4 for ; Sun, 19 Apr 2026 16:12:36 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.222.182 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776615157; cv=none; b=IQxBnCPFW5Tfou3/3+WjGBhF0mgjIpCCYjMBUJtKYLMoWTxnGFrdL8qCn3OHsLFk7P1imAChFWl6PpZ0wsNFmAwRPEugm6xTjf+Cljbe0QORnmjxe79E9DCJVo1iT9kGKNn9Wm80RYyPvD+mbRkEFhVVVNzL+AAeXLC7GLaPw5c= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776615157; c=relaxed/simple; bh=6TROq5XU8fpGmkxwOEqRCkJZI5Po9h42BuGo6+mvmk0=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=lXRdBN6APVMSPhoQo2UfDogQPVK+B5sJQo4vuDbS8PIt1Ybm+tYp1aoniQf5GImohBATkoll+63Sacw4TsxQBPxCjx2b8JzpyserJXkMAfE/0TXCXD4I1UmgW3qrsMQziD8ppK7i5v26MiAc0M4YNq2qYotuxph5DKCvNUFuQfQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=HznOBMEh; arc=none smtp.client-ip=209.85.222.182 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="HznOBMEh" Received: by mail-qk1-f182.google.com with SMTP id af79cd13be357-8eab809593cso33091185a.3 for ; Sun, 19 Apr 2026 09:12:36 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1776615155; x=1777219955; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=B17bAyfAm7uzzttww80XtUu6/K1MgSIE6Bs+vc9UHtw=; b=HznOBMEhx/c230S9LKk1dua6oCbdfZpcCccoeUKOaEVRf+kWwKC8e5NIWDogOxSw5l iOnY1WtdoqurKtj6UNUtoAi1zw9PClQR8VNf/0FZzC2u/3JGHfPcCkUEsAzoARkKhI0z SlKXcSc34ZpjHxZkm+Bvp24wAauzmD9os+zzAFiNldPe3JEYVip6P82RLkfWpqLWInHY GlwrhsHihDQVbQgstrFh9id76UP5Ust3PjNkLa0AnyiN7R+rkBoyMM0Nhf9AG+yNSVxp yEkD6nLdCjJDp9c42mJ+YgaLWApO0tsWzjkTaYlzcr/+Jv3+niEMJ1mkrSnOD4vXO68H zPYw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776615155; x=1777219955; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=B17bAyfAm7uzzttww80XtUu6/K1MgSIE6Bs+vc9UHtw=; b=isXa6SS/hDdPECoIL4cLeOG1ZN1ifPBeETF3kKQ9plH/aMQJeybfLgLa/SJwmmN652 4NiQUzjeSgrdnj9EoefvuKRL0pGa6ZaWb1EKlTJKUUUXnAjjo1pO7o7UsYKdV/Yku+4m ily3E9IBMXzV0rjdkTnLhBPta/pLlFyflQcKNi7PFSH9sNVBXl3OorzEE1kLhfHO3jBW rnfjdjnLEe4psgIGBrtIOcW3uE5AWCEC62Jn6O0Bgab4ctiUtoEkCRjTwEKZCHm9y2zs NcF76V70smAhXQVpiqu+2CZPz0NX9Yctfz0dE9MhI7Ys40siyD6MDHgjxh1Z0gaWvPwc ml0Q== X-Forwarded-Encrypted: i=1; AFNElJ/d9nsysg7VVOmqW3s3sBPmrfRUQOv/Sl7CCsjkLX/jgegJDwC48mBCQlgJuPUdTu5y8E+CG23N/gNVNh4=@vger.kernel.org X-Gm-Message-State: AOJu0YxrOBXHKzWPqP5UkYB87w05knoEn2CspCxtFsgXcqTm3M7tEo1S f9ZK9ZJObz8/VtggVq4oxRrnaaOtqcJ6DZlQrzlo5UpLPbt7du2JSNDW X-Gm-Gg: AeBDieuntu60fDh8Di/ciVYH0iyOpO7z5MCnbhvjqeQ5VeK6WOCf1+mwE0LgXaQIW1x hc5UUT8+clBsJuhIX2nZN5Qw+I8l24nRwTQIxbR2f8JyaXU23R0krkRaN3c4vN9Ic8dhV0FKu0G L19/CsxP7UbpBFVbLcXKOmmbveJ4X4MA5CuoiD7kb/B38Zlj3zShWfAhGW9GECxo38JoHQ1d2nL Fuod30kozcDoM5QpMZZjd/yACBca7pFXCkSO6um/KpMvb7NbKGuI9Kzq93iLR8onA8EoVf1G6MR GoVwsXnG3XAdJgF4izqiLlDl345LSwf2P6pgfdbqjL3G5fqyF/wJClEUSs5raRLV/E9jNcY0OoV CnGjEHhTPmPMIq72c6xomXHD0jwhj3rsbEI/BQBscIHXs0wDdLCixd+tfLIfu2Uf8nI9ymmzV3D crMGJr+4b/hS0v4ZYC6xw4fLuTi2gAnbGlNHV4cmb2CRxA2jJxeXm71Th035OpRvXQVlJ8BDNso KjwSuySiBQ3FU90XMLPqXiOOKYWBDg= X-Received: by 2002:a05:620a:2844:b0:8b2:1ee9:dcfb with SMTP id af79cd13be357-8e78f82cf5emr1427279685a.8.1776615155112; Sun, 19 Apr 2026 09:12:35 -0700 (PDT) Received: from server0 (c-68-48-65-54.hsd1.mi.comcast.net. [68.48.65.54]) by smtp.gmail.com with ESMTPSA id af79cd13be357-8e7d8edb795sm598271385a.25.2026.04.19.09.12.33 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 19 Apr 2026 09:12:34 -0700 (PDT) From: Michael Bommarito To: Greg Kroah-Hartman Cc: Al Viro , Sam Day , Christian Brauner , Ingo Rohloff , Paul Cercueil , Sumit Semwal , Christian Koenig , Simona Vetter , Kees Cook , linux-usb@vger.kernel.org, linux-kernel@vger.kernel.org, linux-media@vger.kernel.org, linaro-mm-sig@lists.linaro.org, dri-devel@lists.freedesktop.org Subject: [PATCH] usb: gadget: f_fs: serialize DMABUF cancel against request completion Date: Sun, 19 Apr 2026 12:12:27 -0400 Message-ID: <20260419161227.1587668-1-michael.bommarito@gmail.com> X-Mailer: git-send-email 2.53.0 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" ffs_epfile_dmabuf_io_complete() calls usb_ep_free_request() on the completed request but leaves priv->req, the back-pointer that ffs_dmabuf_transfer() set on submission, pointing at the freed memory. A later FUNCTIONFS_DMABUF_DETACH ioctl or ffs_epfile_release() on the close path still sees priv->req non-NULL under ffs->eps_lock: if (priv->ep && priv->req) usb_ep_dequeue(priv->ep, priv->req); so usb_ep_dequeue() is called on a freed usb_request. On dummy_hcd the dequeue path only walks a live queue and pointer-compares, so the freed pointer reads without faulting and KASAN requires an explicit check at the FunctionFS call site to surface the use-after-free. On SG-capable in-tree UDCs the dequeue path dereferences the supplied request immediately: * chipidea's ep_dequeue() does container_of(req, struct ci_hw_req, req) and reads hwreq->req.status before acquiring its own lock. * cdnsp's cdnsp_gadget_ep_dequeue() reads request->status first. The narrower option of clearing priv->req via cmpxchg() in the completion does not close the race: the completion runs without eps_lock, so a cancel path holding eps_lock can still observe priv->req non-NULL, race a concurrent completion that clears and frees, and pass the freed pointer to usb_ep_dequeue(). A slightly longer fix that moves the free into the cleanup work is needed. Same class of lifetime race as the recent usbip-vudc timer fix [1]. Take eps_lock in the sole place that mutates priv->req from the callback direction by moving usb_ep_free_request() out of the completion into ffs_dmabuf_cleanup(), the existing work handler scheduled by ffs_dmabuf_signal_done() on ffs->io_completion_wq. Clear priv->req there under eps_lock before freeing, and only clear if priv->req still names our request (a subsequent ffs_dmabuf_transfer() on the same attachment may have queued a new one). This keeps the existing dummy_hcd sync-dequeue invariant: the completion callback is still invoked by the UDC without eps_lock held (dummy_hcd drops its own lock before calling the callback), and the callback now takes no f_fs lock at all. Serialization against the cancel path happens in cleanup, which runs from the workqueue with no f_fs lock held on entry. The priv ref count protects the containing ffs_dmabuf_priv: ffs_dmabuf_transfer() takes a ref via ffs_dmabuf_get(), cleanup drops it via ffs_dmabuf_put(), so priv stays live for the cleanup even after the cancel path's list_del + ffs_dmabuf_put. The ffs_dmabuf_transfer() error path no longer frees usb_req inline: fence->req and fence->ep are set before usb_ep_queue(), so ffs_dmabuf_cleanup() (scheduled by the error-path ffs_dmabuf_signal_done()) owns the free regardless of whether the queue succeeded. Reproduced under KASAN on both detach and close paths against dummy_hcd with an observability hook (kasan_check_byte(priv->req) immediately before usb_ep_dequeue) at the two FunctionFS cancel sites to surface the stale-pointer access; the hook is not part of this patch. The KASAN allocator / free stacks in the captured splats identify the same request: alloc in dummy_alloc_request, free in dummy_timer, fault reached from ffs_epfile_release (close) and from the FUNCTIONFS_DMABUF_DETACH ioctl (detach). With the patch applied, both paths are silent under the same hook. The bug is reached from the FunctionFS device node, which in real deployments is owned by the privileged gadget daemon (adbd, UMS, composite gadget services, etc.); it is not reachable from unprivileged userspace or from a USB host on the cable. FunctionFS mounts default to GLOBAL_ROOT_UID, but the filesystem supports uid=3D, gid=3D, and fmode=3D delegation to a non-root gadget daemon, so on real deployments the attacker may be a less-privileged service rather than root. Fixes: 7b07a2a7ca02 ("usb: gadget: functionfs: Add DMABUF import interface") Link: https://lore.kernel.org/all/20260417163552.807548-1-michael.bommarito= @gmail.com/ [1] Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-7 Signed-off-by: Michael Bommarito --- drivers/usb/gadget/function/f_fs.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/drivers/usb/gadget/function/f_fs.c b/drivers/usb/gadget/functi= on/f_fs.c index 815639506520..75912ce6ab55 100644 --- a/drivers/usb/gadget/function/f_fs.c +++ b/drivers/usb/gadget/function/f_fs.c @@ -150,6 +150,8 @@ struct ffs_dma_fence { struct dma_fence base; struct ffs_dmabuf_priv *priv; struct work_struct work; + struct usb_ep *ep; + struct usb_request *req; }; =20 struct ffs_epfile { @@ -1385,6 +1387,21 @@ static void ffs_dmabuf_cleanup(struct work_struct *w= ork) struct ffs_dmabuf_priv *priv =3D dma_fence->priv; struct dma_buf_attachment *attach =3D priv->attach; struct dma_fence *fence =3D &dma_fence->base; + struct usb_request *req =3D dma_fence->req; + struct usb_ep *ep =3D dma_fence->ep; + + /* + * eps_lock pairs with the cancel paths so they cannot pass a freed + * req to usb_ep_dequeue(). Only clear if priv->req still names ours; + * a re-queue on the same attachment may have taken that slot. + */ + spin_lock_irq(&priv->ffs->eps_lock); + if (priv->req =3D=3D req) + priv->req =3D NULL; + spin_unlock_irq(&priv->ffs->eps_lock); + + if (ep && req) + usb_ep_free_request(ep, req); =20 ffs_dmabuf_put(attach); dma_fence_put(fence); @@ -1414,8 +1431,8 @@ static void ffs_epfile_dmabuf_io_complete(struct usb_= ep *ep, struct usb_request *req) { pr_vdebug("FFS: DMABUF transfer complete, status=3D%d\n", req->status); + /* req is freed by ffs_dmabuf_cleanup() under eps_lock. */ ffs_dmabuf_signal_done(req->context, req->status); - usb_ep_free_request(ep, req); } =20 static const char *ffs_dmabuf_get_driver_name(struct dma_fence *fence) @@ -1699,6 +1716,10 @@ static int ffs_dmabuf_transfer(struct file *file, usb_req->context =3D fence; usb_req->complete =3D ffs_epfile_dmabuf_io_complete; =20 + /* ffs_dmabuf_cleanup() frees usb_req via these two fields. */ + fence->req =3D usb_req; + fence->ep =3D ep->ep; + cookie =3D dma_fence_begin_signalling(); ret =3D usb_ep_queue(ep->ep, usb_req, GFP_ATOMIC); dma_fence_end_signalling(cookie); @@ -1708,7 +1729,6 @@ static int ffs_dmabuf_transfer(struct file *file, } else { pr_warn("FFS: Failed to queue DMABUF: %d\n", ret); ffs_dmabuf_signal_done(fence, ret); - usb_ep_free_request(ep->ep, usb_req); } =20 spin_unlock_irq(&epfile->ffs->eps_lock); --=20 2.53.0