usb: gadget: f_fs: Introduce rw_proxy file descriptors

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>
Link: https://patch.msgid.link/20260619040609.4010746-5-nkapron@google.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Neill Kapron
2026-06-19 04:06:06 +00:00
committed by Greg Kroah-Hartman
parent c3249c462b
commit 0f0ea552cd
4 changed files with 136 additions and 16 deletions

View File

@@ -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.

View File

@@ -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;
@@ -1647,7 +1654,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;
@@ -1765,6 +1772,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:
{
@@ -1815,7 +1825,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);
@@ -2213,7 +2223,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);
@@ -2271,7 +2281,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;
}
@@ -2369,11 +2379,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;

View File

@@ -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() */

View File

@@ -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 */