Linux Documentation
 help / color / mirror / Atom feed
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 3/3] usb: gadget: f_fs: Introduce rw_proxy file descriptors
Date: Sun, 14 Jun 2026 18:10:02 +0000	[thread overview]
Message-ID: <20260614181006.3648010-4-nkapron@google.com> (raw)
In-Reply-To: <20260614181006.3648010-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 read or write operations, preventing buffer
  corruption and race conditions even if userspace mixes transfers across
  both the rw_proxy and base files. This approach allows 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  | 109 +++++++++++++++++++++++-----
 drivers/usb/gadget/function/u_fs.h  |   8 +-
 include/uapi/linux/usb/functionfs.h |   1 +
 4 files changed, 156 insertions(+), 18 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 4c1bafb3eef5..0ccfdcfb1810 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,17 +221,20 @@ struct ffs_epfile {
 	struct ffs_buffer		*read_buffer;
 #define READ_BUFFER_DROP ((struct ffs_buffer *)ERR_PTR(-ESHUTDOWN))
 
-	char				name[5];
+	char				name[10];
 
 	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;
 	struct list_head		dmabufs; /* P: dmabufs_mutex */
 	atomic_t			seqno;
+
+	int				opened_count; /* P: ffs->eps_lock */
 };
 
 struct ffs_buffer {
@@ -978,9 +983,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 +1011,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 +1124,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 +1177,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;
@@ -1225,6 +1234,12 @@ ffs_epfile_open(struct inode *inode, struct file *file)
 		spin_unlock_irq(&ffs->eps_lock);
 		return -ENODEV;
 	}
+	if (epfile->is_rw_proxy) {
+		epfile->epfile_in->opened_count++;
+		epfile->epfile_out->opened_count++;
+	} else {
+		epfile->opened_count++;
+	}
 	ffs->opened++;
 	spin_unlock_irq(&ffs->eps_lock);
 
@@ -1378,8 +1393,18 @@ ffs_epfile_release(struct inode *inode, struct file *file)
 
 	mutex_unlock(&epfile->dmabufs_mutex);
 
-	__ffs_epfile_read_buffer_free(epfile);
-	ffs_data_closed(epfile->ffs);
+	spin_lock_irq(&ffs->eps_lock);
+	if (epfile->is_rw_proxy) {
+		epfile->epfile_in->opened_count--;
+		if (--epfile->epfile_out->opened_count == 0)
+			__ffs_epfile_read_buffer_free(epfile->epfile_out);
+	} else {
+		if (--epfile->opened_count == 0)
+			__ffs_epfile_read_buffer_free(epfile);
+	}
+	spin_unlock_irq(&ffs->eps_lock);
+
+	ffs_data_closed(ffs);
 
 	return 0;
 }
@@ -1647,7 +1672,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 +1790,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 +1843,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 +2241,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 +2299,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 +2397,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;
 
@@ -2396,6 +2429,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 +3031,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 +3120,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


  parent reply	other threads:[~2026-06-14 18:10 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-14 18:09 [PATCH 0/3] usb: gadget: f_fs: Add R/W proxy EPs and ZLP support Neill Kapron
2026-06-14 18:10 ` [PATCH 1/3] usb: gadget: f_fs: Initialize epfile->in early to fix endpoint direction checks Neill Kapron
2026-06-15  2:30   ` Greg KH
2026-06-14 18:10 ` [PATCH 2/3] usb: gadget: f_fs: Add zero-length packet ioctl Neill Kapron
2026-06-14 18:10 ` Neill Kapron [this message]
2026-06-15  2:35   ` [PATCH 3/3] usb: gadget: f_fs: Introduce rw_proxy file descriptors Greg KH
2026-06-15  2:30 ` [PATCH 0/3] usb: gadget: f_fs: Add R/W proxy EPs and ZLP support Greg KH

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=20260614181006.3648010-4-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