summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAl Viro <viro@zeniv.linux.org.uk>2026-01-31 18:24:41 -0500
committerAl Viro <viro@zeniv.linux.org.uk>2026-02-05 13:53:12 -0500
commit2005aabe94eaab8608879d98afb901bc99bc3a31 (patch)
tree25a3c36351581ca100594b0ee14b89977b9f9c0b
parent351ea48ae880b1673abcf232947c577183fdf712 (diff)
functionfs: use spinlock for FFS_DEACTIVATED/FFS_CLOSING transitions
When all files are closed, functionfs needs ffs_data_reset() to be done before any further opens are allowed. During that time we have ffs->state set to FFS_CLOSING; that makes ->open() fail with -EBUSY. Once ffs_data_reset() is done, it switches state (to FFS_READ_DESCRIPTORS) indicating that opening that thing is allowed again. There's a couple of additional twists: * mounting with -o no_disconnect delays ffs_data_reset() from doing that at the final ->release() to the first subsequent open(). That's indicated by ffs->state set to FFS_DEACTIVATED; if open() sees that, it immediately switches to FFS_CLOSING and proceeds with doing ffs_data_reset() before returning to userland. * a couple of usb callbacks need to force the delayed transition; unfortunately, they are done in locking environment that does not allow blocking and ffs_data_reset() can block. As the result, if these callbacks see FFS_DEACTIVATED, they change state to FFS_CLOSING and use schedule_work() to get ffs_data_reset() executed asynchronously. Unfortunately, the locking is rather insufficient. A fix attempted in e5bf5ee26663 ("functionfs: fix the open/removal races") had closed a bunch of UAF, but it didn't do anything to the callbacks, lacked barriers in transition from FFS_CLOSING to FFS_READ_DESCRIPTORS _and_ it had been too heavy-handed in open()/open() serialization - I've used ffs->mutex for that, and it's being held over actual IO on ep0, complete with copy_from_user(), etc. Even more unfortunately, the userland side is apparently racy enough to have the resulting timing changes (no failures, just a delayed return of open(2)) disrupt the things quite badly. Userland bugs or not, it's a clear regression that needs to be dealt with. Solution is to use a spinlock for serializing these state checks and transitions - unlike ffs->mutex it can be taken in these callbacks and it doesn't disrupt the timings in open(). We could introduce a new spinlock, but it's easier to use the one that is already there (ffs->eps_lock) instead - the locking environment is safe for it in all affected places. Since now it is held over all places that alter or check the open count (ffs->opened), there's no need to keep that atomic_t - int would serve just fine and it's simpler that way. Fixes: e5bf5ee26663 ("functionfs: fix the open/removal races") Fixes: 18d6b32fca38 ("usb: gadget: f_fs: add "no_disconnect" mode") # v4.0 Tested-by: Samuel Wu <wusamuel@google.com> Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
-rw-r--r--drivers/usb/gadget/function/f_fs.c108
-rw-r--r--drivers/usb/gadget/function/u_fs.h2
2 files changed, 53 insertions, 57 deletions
diff --git a/drivers/usb/gadget/function/f_fs.c b/drivers/usb/gadget/function/f_fs.c
index 05c6750702b6..fa467a40949d 100644
--- a/drivers/usb/gadget/function/f_fs.c
+++ b/drivers/usb/gadget/function/f_fs.c
@@ -59,7 +59,6 @@ static struct ffs_data *__must_check ffs_data_new(const char *dev_name)
__attribute__((malloc));
/* Opened counter handling. */
-static void ffs_data_opened(struct ffs_data *ffs);
static void ffs_data_closed(struct ffs_data *ffs);
/* Called with ffs->mutex held; take over ownership of data. */
@@ -636,23 +635,25 @@ done_mutex:
return ret;
}
+
+static void ffs_data_reset(struct ffs_data *ffs);
+
static int ffs_ep0_open(struct inode *inode, struct file *file)
{
struct ffs_data *ffs = inode->i_sb->s_fs_info;
- int ret;
- /* Acquire mutex */
- ret = ffs_mutex_lock(&ffs->mutex, file->f_flags & O_NONBLOCK);
- if (ret < 0)
- return ret;
-
- ffs_data_opened(ffs);
+ spin_lock_irq(&ffs->eps_lock);
if (ffs->state == FFS_CLOSING) {
- ffs_data_closed(ffs);
- mutex_unlock(&ffs->mutex);
+ spin_unlock_irq(&ffs->eps_lock);
return -EBUSY;
}
- mutex_unlock(&ffs->mutex);
+ if (!ffs->opened++ && ffs->state == FFS_DEACTIVATED) {
+ ffs->state = FFS_CLOSING;
+ spin_unlock_irq(&ffs->eps_lock);
+ ffs_data_reset(ffs);
+ } else {
+ spin_unlock_irq(&ffs->eps_lock);
+ }
file->private_data = ffs;
return stream_open(inode, file);
@@ -1202,15 +1203,10 @@ ffs_epfile_open(struct inode *inode, struct file *file)
{
struct ffs_data *ffs = inode->i_sb->s_fs_info;
struct ffs_epfile *epfile;
- int ret;
-
- /* Acquire mutex */
- ret = ffs_mutex_lock(&ffs->mutex, file->f_flags & O_NONBLOCK);
- if (ret < 0)
- return ret;
- if (!atomic_inc_not_zero(&ffs->opened)) {
- mutex_unlock(&ffs->mutex);
+ spin_lock_irq(&ffs->eps_lock);
+ if (!ffs->opened) {
+ spin_unlock_irq(&ffs->eps_lock);
return -ENODEV;
}
/*
@@ -1220,11 +1216,11 @@ ffs_epfile_open(struct inode *inode, struct file *file)
*/
epfile = smp_load_acquire(&inode->i_private);
if (unlikely(ffs->state != FFS_ACTIVE || !epfile)) {
- mutex_unlock(&ffs->mutex);
- ffs_data_closed(ffs);
+ spin_unlock_irq(&ffs->eps_lock);
return -ENODEV;
}
- mutex_unlock(&ffs->mutex);
+ ffs->opened++;
+ spin_unlock_irq(&ffs->eps_lock);
file->private_data = epfile;
return stream_open(inode, file);
@@ -2092,8 +2088,6 @@ static int ffs_fs_init_fs_context(struct fs_context *fc)
return 0;
}
-static void ffs_data_reset(struct ffs_data *ffs);
-
static void
ffs_fs_kill_sb(struct super_block *sb)
{
@@ -2150,15 +2144,6 @@ static void ffs_data_get(struct ffs_data *ffs)
refcount_inc(&ffs->ref);
}
-static void ffs_data_opened(struct ffs_data *ffs)
-{
- if (atomic_add_return(1, &ffs->opened) == 1 &&
- ffs->state == FFS_DEACTIVATED) {
- ffs->state = FFS_CLOSING;
- ffs_data_reset(ffs);
- }
-}
-
static void ffs_data_put(struct ffs_data *ffs)
{
if (refcount_dec_and_test(&ffs->ref)) {
@@ -2176,28 +2161,29 @@ static void ffs_data_put(struct ffs_data *ffs)
static void ffs_data_closed(struct ffs_data *ffs)
{
- if (atomic_dec_and_test(&ffs->opened)) {
- if (ffs->no_disconnect) {
- struct ffs_epfile *epfiles;
- unsigned long flags;
-
- ffs->state = FFS_DEACTIVATED;
- spin_lock_irqsave(&ffs->eps_lock, flags);
- epfiles = ffs->epfiles;
- ffs->epfiles = NULL;
- spin_unlock_irqrestore(&ffs->eps_lock,
- flags);
-
- if (epfiles)
- ffs_epfiles_destroy(ffs->sb, epfiles,
- ffs->eps_count);
-
- if (ffs->setup_state == FFS_SETUP_PENDING)
- __ffs_ep0_stall(ffs);
- } else {
- ffs->state = FFS_CLOSING;
- ffs_data_reset(ffs);
- }
+ spin_lock_irq(&ffs->eps_lock);
+ if (--ffs->opened) { // not the last opener?
+ spin_unlock_irq(&ffs->eps_lock);
+ return;
+ }
+ if (ffs->no_disconnect) {
+ struct ffs_epfile *epfiles;
+
+ ffs->state = FFS_DEACTIVATED;
+ epfiles = ffs->epfiles;
+ ffs->epfiles = NULL;
+ spin_unlock_irq(&ffs->eps_lock);
+
+ if (epfiles)
+ ffs_epfiles_destroy(ffs->sb, epfiles,
+ ffs->eps_count);
+
+ if (ffs->setup_state == FFS_SETUP_PENDING)
+ __ffs_ep0_stall(ffs);
+ } else {
+ ffs->state = FFS_CLOSING;
+ spin_unlock_irq(&ffs->eps_lock);
+ ffs_data_reset(ffs);
}
}
@@ -2214,7 +2200,7 @@ static struct ffs_data *ffs_data_new(const char *dev_name)
}
refcount_set(&ffs->ref, 1);
- atomic_set(&ffs->opened, 0);
+ ffs->opened = 0;
ffs->state = FFS_READ_DESCRIPTORS;
mutex_init(&ffs->mutex);
spin_lock_init(&ffs->eps_lock);
@@ -2266,6 +2252,7 @@ static void ffs_data_reset(struct ffs_data *ffs)
{
ffs_data_clear(ffs);
+ spin_lock_irq(&ffs->eps_lock);
ffs->raw_descs_data = NULL;
ffs->raw_descs = NULL;
ffs->raw_strings = NULL;
@@ -2289,6 +2276,7 @@ static void ffs_data_reset(struct ffs_data *ffs)
ffs->ms_os_descs_ext_prop_count = 0;
ffs->ms_os_descs_ext_prop_name_len = 0;
ffs->ms_os_descs_ext_prop_data_len = 0;
+ spin_unlock_irq(&ffs->eps_lock);
}
@@ -3756,6 +3744,7 @@ static int ffs_func_set_alt(struct usb_function *f,
{
struct ffs_function *func = ffs_func_from_usb(f);
struct ffs_data *ffs = func->ffs;
+ unsigned long flags;
int ret = 0, intf;
if (alt > MAX_ALT_SETTINGS)
@@ -3768,12 +3757,15 @@ static int ffs_func_set_alt(struct usb_function *f,
if (ffs->func)
ffs_func_eps_disable(ffs->func);
+ spin_lock_irqsave(&ffs->eps_lock, flags);
if (ffs->state == FFS_DEACTIVATED) {
ffs->state = FFS_CLOSING;
+ spin_unlock_irqrestore(&ffs->eps_lock, flags);
INIT_WORK(&ffs->reset_work, ffs_reset_work);
schedule_work(&ffs->reset_work);
return -ENODEV;
}
+ spin_unlock_irqrestore(&ffs->eps_lock, flags);
if (ffs->state != FFS_ACTIVE)
return -ENODEV;
@@ -3791,16 +3783,20 @@ static void ffs_func_disable(struct usb_function *f)
{
struct ffs_function *func = ffs_func_from_usb(f);
struct ffs_data *ffs = func->ffs;
+ unsigned long flags;
if (ffs->func)
ffs_func_eps_disable(ffs->func);
+ spin_lock_irqsave(&ffs->eps_lock, flags);
if (ffs->state == FFS_DEACTIVATED) {
ffs->state = FFS_CLOSING;
+ spin_unlock_irqrestore(&ffs->eps_lock, flags);
INIT_WORK(&ffs->reset_work, ffs_reset_work);
schedule_work(&ffs->reset_work);
return;
}
+ spin_unlock_irqrestore(&ffs->eps_lock, flags);
if (ffs->state == FFS_ACTIVE) {
ffs->func = NULL;
diff --git a/drivers/usb/gadget/function/u_fs.h b/drivers/usb/gadget/function/u_fs.h
index 4b3365f23fd7..6a80182aadd7 100644
--- a/drivers/usb/gadget/function/u_fs.h
+++ b/drivers/usb/gadget/function/u_fs.h
@@ -176,7 +176,7 @@ struct ffs_data {
/* reference counter */
refcount_t ref;
/* how many files are opened (EP0 and others) */
- atomic_t opened;
+ int opened;
/* EP0 state */
enum ffs_state state;