From: Neill Kapron <nkapron@google.com>
To: gregkh@linuxfoundation.org, corbet@lwn.net, skhan@linuxfoundation.org
Cc: linux-usb@vger.kernel.org, linux-doc@vger.kernel.org,
linux-kernel@vger.kernel.org, kernel-team@android.com,
Neill Kapron <nkapron@google.com>
Subject: [PATCH v2 4/4] usb: gadget: f_fs: Introduce rw_proxy file descriptors
Date: Fri, 19 Jun 2026 04:06:06 +0000 [thread overview]
Message-ID: <20260619040609.4010746-5-nkapron@google.com> (raw)
In-Reply-To: <20260619040609.4010746-1-nkapron@google.com>
Currently, FunctionFS exposes each USB endpoint as a separate,
unidirectional file descriptor (e.g., `ep1` for IN, `ep2` for OUT).
While this mirrors the underlying hardware structure, it forces
userspace daemons implementing bidirectional protocols to manage
multiple file descriptors. When dealing with legacy protocols which
require exposing a single, bi-directional fd to userspace, this becomes
problematic.
This patch introduces the `FUNCTIONFS_RW_PROXY_EPS` UAPI flag. When
passed in the descriptor header during initialization, FunctionFS
provisions a "rw_proxy" bidirectional file descriptor (e.g., `ep1_rw`)
alongside every pair of IN/OUT endpoints.
Implementation details:
- RW proxy files act as a pure VFS alias, proxying operations
directly to the base ffs_epfile instances. A `read()` proxies to
the OUT endpoint's file, and a `write()` proxies to the IN file.
- Because operations are proxied natively, they reuse the underlying
base endpoint's lock (`epfile->mutex`) and tracking state. This
serializes concurrent I/O, preventing buffer corruption or races
even if userspace mixes transfers across both the rw_proxy and base files
while allowing full-duplex synchronous operations to occur concurrently
without serializing on a single lock.
- Control operations (like IOCTLs) and intentional stalls (via
reverse-direction I/O) must still be issued on the base endpoints, as the
rw_proxy returns `-ENOTTY` for IOCTLs and cannot trigger stalls.
Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Neill Kapron <nkapron@google.com>
---
Documentation/usb/functionfs.rst | 56 +++++++++++++++++++
drivers/usb/gadget/function/f_fs.c | 87 ++++++++++++++++++++++++-----
drivers/usb/gadget/function/u_fs.h | 8 ++-
include/uapi/linux/usb/functionfs.h | 1 +
4 files changed, 136 insertions(+), 16 deletions(-)
diff --git a/Documentation/usb/functionfs.rst b/Documentation/usb/functionfs.rst
index 582e53549d5b..b189cf5626ba 100644
--- a/Documentation/usb/functionfs.rst
+++ b/Documentation/usb/functionfs.rst
@@ -96,6 +96,58 @@ One such IOCTL is:
* ``-ENODEV``: The FunctionFS instance is not active.
* ``-EINVAL``: The endpoint is not an IN endpoint.
* ``-EFAULT``: Invalid user space pointer for the argument.
+
+RW Proxy Endpoints
+==================
+
+If the ``FUNCTIONFS_RW_PROXY_EPS`` flag is passed in the descriptor header
+(requires ``FUNCTIONFS_DESCRIPTORS_MAGIC_V2``), FunctionFS will provision a
+bidirectional rw_proxy file descriptor (e.g., "ep1_rw") alongside each pair
+of IN and OUT endpoints. The rw_proxy file aliases the underlying hardware
+endpoints, allowing userspace to use a single file descriptor for both reading
+(OUT) and writing (IN).
+
+This flag requires the total number of hardware endpoints to be an even number.
+FunctionFS will automatically walk the provided endpoints and group them into
+adjacent pairs (e.g., ep1 and ep2 form the first pair, ep3 and ep4 form the
+second pair). Each pair must consist of exactly one IN endpoint and one OUT
+endpoint.
+
+For each valid pair, a rw_proxy file is created and named after the first
+endpoint in the pair with a "_rw" suffix. For example, if ep1 and ep2 are
+paired, a rw_proxy file named "ep1_rw" is created. If ep3 and ep4 are paired,
+"ep3_rw" is created.
+
+If the ``FUNCTIONFS_VIRTUAL_ADDR`` flag is also enabled, the endpoints will be
+named using their physical endpoint address in hexadecimal instead of their
+index. RW proxy files will inherit this naming convention. For example, if the
+first endpoint of a pair maps to address 0x02, the rw_proxy file will be
+named "ep02_rw".
+
+When this flag is enabled, userspace has the choice of performing data transfers
+via the single rw_proxy file descriptor or the two base file descriptors. The
+rw_proxy file descriptor acts as a pure VFS alias that proxies all operations
+directly to the underlying base file descriptors.
+
+Because it is a pure proxy, there are no data races or buffer corruptions if
+userspace uses both the rw_proxy endpoint and the base endpoints concurrently.
+The native mutexes of the base endpoints perfectly serialize all concurrent
+transfers. However, userspace should generally pick one method and stick to it
+to avoid interleaving its own data stream.
+
+- **IOCTLs (Clear Halt, etc.):** RW proxy endpoints do not support IOCTLs and
+ will return ``-ENOTTY``. To clear a host-initiated halt, userspace must issue
+ the ``FUNCTIONFS_CLEAR_HALT`` ioctl directly on the corresponding base
+ endpoint file descriptor.
+- **Intentional Stalls:** The traditional mechanism for intentionally halting an
+ endpoint by issuing a reverse-direction data operation (e.g., attempting to
+ read from an IN endpoint) continues to work, but it must be issued on the
+ base endpoint. RW proxy endpoints cannot be used to trigger a stall because
+ they are fully bidirectional.
+
+Note that DMABUF data transfers (``FUNCTIONFS_DMABUF_TRANSFER``) are unsupported
+via the rw_proxy endpoint because it does not support IOCTLs. If DMABUF
+transfers are required, users must use the standard base endpoints.
DMABUF interface
================
@@ -103,6 +155,10 @@ FunctionFS additionally supports a DMABUF based interface, where the
userspace can attach DMABUF objects (externally created) to an endpoint,
and subsequently use them for data transfers.
+Note: The DMABUF interface is unsupported on rw_proxy endpoints. See
+the RW Proxy Endpoints section for details on using DMABUF alongside
+the ``FUNCTIONFS_RW_PROXY_EPS`` flag.
+
A userspace application can then use this interface to share DMABUF
objects between several interfaces, allowing it to transfer data in a
zero-copy fashion, for instance between IIO and the USB stack.
diff --git a/drivers/usb/gadget/function/f_fs.c b/drivers/usb/gadget/function/f_fs.c
index 07aba722dd5b..c5647febf3ea 100644
--- a/drivers/usb/gadget/function/f_fs.c
+++ b/drivers/usb/gadget/function/f_fs.c
@@ -159,7 +159,9 @@ struct ffs_epfile {
struct mutex mutex;
struct ffs_data *ffs;
- struct ffs_ep *ep; /* P: ffs->eps_lock */
+ struct ffs_ep *ep; /* P: ffs->eps_lock */
+ struct ffs_epfile *epfile_in; /* P: ffs->eps_lock */
+ struct ffs_epfile *epfile_out; /* P: ffs->eps_lock */
/*
* Buffer for holding data from partial reads which may happen since
@@ -219,12 +221,13 @@ struct ffs_epfile {
struct ffs_buffer *read_buffer;
#define READ_BUFFER_DROP ((struct ffs_buffer *)ERR_PTR(-ESHUTDOWN))
- char name[5];
+ char name[8];
unsigned char in; /* P: ffs->eps_lock */
unsigned char isoc; /* P: ffs->eps_lock */
u8 zlp_enabled; /* P: ffs->eps_lock */
+ bool is_rw_proxy;
/* Protects dmabufs */
struct mutex dmabufs_mutex;
@@ -978,9 +981,8 @@ static ssize_t __ffs_epfile_read_data(struct ffs_epfile *epfile,
return ret;
}
-static struct ffs_ep *ffs_epfile_wait_ep(struct file *file)
+static struct ffs_ep *ffs_epfile_wait_ep(struct ffs_epfile *epfile, struct file *file)
{
- struct ffs_epfile *epfile = file->private_data;
struct ffs_ep *ep;
int ret;
@@ -1007,17 +1009,22 @@ static ssize_t ffs_epfile_io(struct file *file, struct ffs_io_data *io_data)
char *data = NULL;
ssize_t ret, data_len = -EINVAL;
int halt;
+ bool is_rw_proxy = epfile->is_rw_proxy;
/* Are we still active? */
if (WARN_ON(epfile->ffs->state != FFS_ACTIVE))
return -ENODEV;
- ep = ffs_epfile_wait_ep(file);
+ /* Proxy to base endpoint if rw_proxy */
+ if (is_rw_proxy)
+ epfile = io_data->read ? epfile->epfile_out : epfile->epfile_in;
+
+ ep = ffs_epfile_wait_ep(epfile, file);
if (IS_ERR(ep))
return PTR_ERR(ep);
/* Do we halt? */
- halt = (!io_data->read == !epfile->in);
+ halt = is_rw_proxy ? 0 : (!io_data->read == !epfile->in);
if (halt && epfile->isoc)
return -EINVAL;
@@ -1115,7 +1122,7 @@ static ssize_t ffs_epfile_io(struct file *file, struct ffs_io_data *io_data)
req->num_sgs = 0;
}
- req->zero = epfile->zlp_enabled;
+ req->zero = !io_data->read ? epfile->zlp_enabled : 0;
req->length = data_len;
io_data->buf = data;
@@ -1168,7 +1175,7 @@ static ssize_t ffs_epfile_io(struct file *file, struct ffs_io_data *io_data)
req->num_sgs = 0;
}
- req->zero = epfile->zlp_enabled;
+ req->zero = !io_data->read ? epfile->zlp_enabled : 0;
req->length = data_len;
io_data->buf = data;
@@ -1646,7 +1653,7 @@ static int ffs_dmabuf_transfer(struct file *file,
priv = attach->importer_priv;
- ep = ffs_epfile_wait_ep(file);
+ ep = ffs_epfile_wait_ep(epfile, file);
if (IS_ERR(ep)) {
ret = PTR_ERR(ep);
goto err_attachment_put;
@@ -1764,6 +1771,9 @@ static long ffs_epfile_ioctl(struct file *file, unsigned code,
if (WARN_ON(epfile->ffs->state != FFS_ACTIVE))
return -ENODEV;
+ if (epfile->is_rw_proxy)
+ return -ENOTTY;
+
switch (code) {
case FUNCTIONFS_DMABUF_ATTACH:
{
@@ -1814,7 +1824,7 @@ static long ffs_epfile_ioctl(struct file *file, unsigned code,
}
/* Wait for endpoint to be enabled */
- ep = ffs_epfile_wait_ep(file);
+ ep = ffs_epfile_wait_ep(epfile, file);
if (IS_ERR(ep))
return PTR_ERR(ep);
@@ -2212,7 +2222,7 @@ static void ffs_data_closed(struct ffs_data *ffs)
if (epfiles)
ffs_epfiles_destroy(ffs->sb, epfiles,
- ffs->eps_count);
+ ffs->epfiles_count);
if (ffs->setup_state == FFS_SETUP_PENDING)
__ffs_ep0_stall(ffs);
@@ -2270,7 +2280,7 @@ static void ffs_data_clear(struct ffs_data *ffs)
* copy of epfile will save us from use-after-free.
*/
if (epfiles) {
- ffs_epfiles_destroy(ffs->sb, epfiles, ffs->eps_count);
+ ffs_epfiles_destroy(ffs->sb, epfiles, ffs->epfiles_count);
ffs->epfiles = NULL;
}
@@ -2368,11 +2378,16 @@ static void functionfs_unbind(struct ffs_data *ffs)
static int ffs_epfiles_create(struct ffs_data *ffs)
{
struct ffs_epfile *epfile, *epfiles;
- unsigned i, count;
+ unsigned int i, count, epfiles_count;
int err;
count = ffs->eps_count;
- epfiles = kzalloc_objs(*epfiles, count);
+ epfiles_count = count;
+ if (ffs->user_flags & FUNCTIONFS_RW_PROXY_EPS)
+ epfiles_count += count / 2;
+ ffs->epfiles_count = epfiles_count;
+
+ epfiles = kzalloc_objs(*epfiles, epfiles_count);
if (!epfiles)
return -ENOMEM;
@@ -2395,6 +2410,32 @@ static int ffs_epfiles_create(struct ffs_data *ffs)
}
}
+ if (ffs->user_flags & FUNCTIONFS_RW_PROXY_EPS) {
+ struct ffs_epfile *comp = epfiles + count;
+
+ for (i = 0; i < count; i += 2, ++comp) {
+ struct ffs_epfile *ep1 = &epfiles[i];
+ struct ffs_epfile *ep2 = &epfiles[i + 1];
+ bool ep1_in = ffs->eps_addrmap[i + 1] & USB_ENDPOINT_DIR_MASK;
+
+ comp->ffs = ffs;
+ comp->is_rw_proxy = true;
+ comp->epfile_in = ep1_in ? ep1 : ep2;
+ comp->epfile_out = ep1_in ? ep2 : ep1;
+ mutex_init(&comp->mutex);
+ mutex_init(&comp->dmabufs_mutex);
+ INIT_LIST_HEAD(&comp->dmabufs);
+ snprintf(comp->name, sizeof(comp->name), "%s_rw",
+ epfiles[i].name);
+ err = ffs_sb_create_file(ffs->sb, comp->name,
+ comp, &ffs_epfile_operations);
+ if (err) {
+ ffs_epfiles_destroy(ffs->sb, epfiles, count + (i / 2));
+ return err;
+ }
+ }
+ }
+
ffs->epfiles = epfiles;
return 0;
}
@@ -2972,7 +3013,8 @@ static int __ffs_data_got_descs(struct ffs_data *ffs,
FUNCTIONFS_VIRTUAL_ADDR |
FUNCTIONFS_EVENTFD |
FUNCTIONFS_ALL_CTRL_RECIP |
- FUNCTIONFS_CONFIG0_SETUP)) {
+ FUNCTIONFS_CONFIG0_SETUP |
+ FUNCTIONFS_RW_PROXY_EPS)) {
ret = -ENOSYS;
goto error;
}
@@ -3060,6 +3102,21 @@ static int __ffs_data_got_descs(struct ffs_data *ffs,
goto error;
}
+ if (ffs->user_flags & FUNCTIONFS_RW_PROXY_EPS) {
+ if (ffs->eps_count % 2) {
+ ret = -EINVAL;
+ goto error;
+ }
+
+ for (i = 1; i < ffs->eps_count; i += 2) {
+ if ((ffs->eps_addrmap[i] & USB_ENDPOINT_DIR_MASK) ==
+ (ffs->eps_addrmap[i + 1] & USB_ENDPOINT_DIR_MASK)) {
+ ret = -EINVAL;
+ goto error;
+ }
+ }
+ }
+
ffs->raw_descs_data = _data;
ffs->raw_descs = raw_descs;
ffs->raw_descs_length = data - raw_descs;
diff --git a/drivers/usb/gadget/function/u_fs.h b/drivers/usb/gadget/function/u_fs.h
index 6a80182aadd7..c280c495fbd2 100644
--- a/drivers/usb/gadget/function/u_fs.h
+++ b/drivers/usb/gadget/function/u_fs.h
@@ -252,8 +252,14 @@ struct ffs_data {
unsigned short strings_count;
unsigned short interfaces_count;
+
+ /*
+ * eps_count tracks the number of underlying hardware endpoints.
+ * epfiles_count tracks the total number of VFS endpoint files.
+ * When companion endpoints are active, epfiles_count > eps_count.
+ */
unsigned short eps_count;
- unsigned short _pad1;
+ unsigned short epfiles_count;
/* filled by __ffs_data_got_strings() */
/* ids in stringtabs are set in functionfs_bind() */
diff --git a/include/uapi/linux/usb/functionfs.h b/include/uapi/linux/usb/functionfs.h
index 06134dca4e8a..290308c9dd92 100644
--- a/include/uapi/linux/usb/functionfs.h
+++ b/include/uapi/linux/usb/functionfs.h
@@ -25,6 +25,7 @@ enum functionfs_flags {
FUNCTIONFS_EVENTFD = 32,
FUNCTIONFS_ALL_CTRL_RECIP = 64,
FUNCTIONFS_CONFIG0_SETUP = 128,
+ FUNCTIONFS_RW_PROXY_EPS = 256,
};
/* Descriptor of an non-audio endpoint */
--
2.54.0.1136.gdb2ca164c4-goog
prev parent reply other threads:[~2026-06-19 4:06 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-19 4:06 [PATCH v2 0/4] usb: gadget: f_fs: Add R/W proxy EPs and ZLP support Neill Kapron
2026-06-19 4:06 ` [PATCH v2 1/4] usb: gadget: f_fs: Initialize epfile->in early to fix endpoint direction checks Neill Kapron
2026-06-19 4:06 ` [PATCH v2 2/4] usb: gadget: f_fs: Tie read_buffer lifetime to ffs_epfile Neill Kapron
2026-06-19 4:06 ` [PATCH v2 3/4] usb: gadget: f_fs: Add zero-length packet ioctl Neill Kapron
2026-06-19 4:06 ` Neill Kapron [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260619040609.4010746-5-nkapron@google.com \
--to=nkapron@google.com \
--cc=corbet@lwn.net \
--cc=gregkh@linuxfoundation.org \
--cc=kernel-team@android.com \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-usb@vger.kernel.org \
--cc=skhan@linuxfoundation.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox