CVE-2026-63894
Publication date:
19/07/2026
In the Linux kernel, the following vulnerability has been resolved:<br />
<br />
usb: gadget: f_fs: serialize DMABUF cancel against request completion<br />
<br />
ffs_epfile_dmabuf_io_complete() calls usb_ep_free_request() on the<br />
completed request but leaves priv->req, the back-pointer that<br />
ffs_dmabuf_transfer() set on submission, pointing at the freed<br />
memory. A later FUNCTIONFS_DMABUF_DETACH ioctl or<br />
ffs_epfile_release() on the close path still sees priv->req<br />
non-NULL under ffs->eps_lock:<br />
<br />
if (priv->ep && priv->req)<br />
usb_ep_dequeue(priv->ep, priv->req);<br />
<br />
so usb_ep_dequeue() is called on a freed usb_request.<br />
<br />
On dummy_hcd the dequeue path only walks a live queue and<br />
pointer-compares, so the freed pointer reads without faulting and<br />
KASAN requires an explicit check at the FunctionFS call site to<br />
surface the use-after-free. On SG-capable in-tree UDCs the<br />
dequeue path dereferences the supplied request immediately:<br />
<br />
* chipidea&#39;s ep_dequeue() does<br />
container_of(req, struct ci_hw_req, req) and reads<br />
hwreq->req.status before acquiring its own lock.<br />
* cdnsp&#39;s cdnsp_gadget_ep_dequeue() reads request->status first.<br />
<br />
The narrower option of clearing priv->req via cmpxchg() in the<br />
completion does not close the race: the completion runs without<br />
eps_lock, so a cancel path holding eps_lock can still observe<br />
priv->req non-NULL, race a concurrent completion that clears and<br />
frees, and pass the freed pointer to usb_ep_dequeue(). A slightly<br />
longer fix that moves the free into the cleanup work is needed.<br />
<br />
Same class of lifetime race as the recent usbip-vudc timer fix [1].<br />
<br />
Take eps_lock in the sole place that mutates priv->req from the<br />
callback direction by moving usb_ep_free_request() out of the<br />
completion into ffs_dmabuf_cleanup(), the existing work handler<br />
scheduled by ffs_dmabuf_signal_done() on<br />
ffs->io_completion_wq. Clear priv->req there under eps_lock<br />
before freeing, and only clear if priv->req still names our<br />
request (a subsequent ffs_dmabuf_transfer() on the same<br />
attachment may have queued a new one).<br />
<br />
This keeps the existing dummy_hcd sync-dequeue invariant: the<br />
completion callback is still invoked by the UDC without<br />
eps_lock held (dummy_hcd drops its own lock before calling the<br />
callback), and the callback now takes no f_fs lock at all.<br />
Serialization against the cancel path happens in cleanup, which<br />
runs from the workqueue with no f_fs lock held on entry.<br />
<br />
The priv ref count protects the containing ffs_dmabuf_priv:<br />
ffs_dmabuf_transfer() takes a ref via ffs_dmabuf_get(), cleanup<br />
drops it via ffs_dmabuf_put(), so priv stays live for the<br />
cleanup even after the cancel path&#39;s list_del + ffs_dmabuf_put.<br />
<br />
The ffs_dmabuf_transfer() error path no longer frees usb_req<br />
inline: fence->req and fence->ep are set before usb_ep_queue(),<br />
so ffs_dmabuf_cleanup() (scheduled by the error-path<br />
ffs_dmabuf_signal_done()) owns the free regardless of whether<br />
the queue succeeded.<br />
<br />
Reproduced under KASAN on both detach and close paths against<br />
dummy_hcd with an observability hook<br />
(kasan_check_byte(priv->req) immediately before usb_ep_dequeue)<br />
at the two FunctionFS cancel sites to surface the stale-pointer<br />
access; the hook is not part of this patch. The KASAN<br />
allocator / free stacks in the captured splats identify the<br />
same request: alloc in dummy_alloc_request, free in<br />
dummy_timer, fault reached from ffs_epfile_release (close) and<br />
from the FUNCTIONFS_DMABUF_DETACH ioctl (detach). With the<br />
patch applied, both paths are silent under the same hook.<br />
<br />
The bug is reached from the FunctionFS device node, which in<br />
real deployments is owned by the privileged gadget daemon<br />
(adbd, UMS, composite gadget services, etc.); it is not<br />
reachable from unprivileged userspace or from a USB host on the<br />
cable. FunctionFS mounts default to GLOBAL_ROOT_UID, but the<br />
filesystem supports uid=, gid=, and fmode= delegation to a<br />
non-root gadget daemon, so on real deployments the attacker may<br />
be a less-privileged service rather than root.
Severity CVSS v4.0: Pending analysis
Last modification:
20/07/2026