usb: gadget: inode: fix use-after-free of struct ep_data
PATCH 1/1 usb: gadget: inode: fix use-after-free of struct ep_data
gadgetfs_unbind() -> destroy_ep_files() frees each struct ep_data via put_ep(), but the inode still points at that ep_data through ->i_private with no reference of its own. A concurrent openat() on an endpoint file can therefore reach ep_open() and dereference the freed ep_data (the mutex fast path), even while the inode outlives it.
Give the inode its own reference: take get_ep() when the endpoint inode is created and drop it again from a new gadgetfs_evict_inode(), so an ep_data can never be freed while any inode still points at it. This is also safe against dcache pruning, since the inode reference keeps the count >= 1 until the endpoint is removed from dev->epfiles.
--- drivers/usb/gadget/legacy/inode.c | 13 +++++++++++++ 1 file changed, 13 insertions(+)
drivers/usb/gadget/legacy/inode.c +13 -0
@@ -1626,6 +1626,8 @@ static int activate_ep_files (struct dev_data *dev)if (!data->req) goto enomem1; +/* The inode keeps this ep_data alive via ->i_private. */ +get_ep (data); err =gadgetfs_create_file (dev->sb, data->name, data, &ep_io_operations);if (err)
@@ -2015,9 +2017,20 @@ static int gadgetfs_create_file (struct super_block *sb, char const *name,return 0 ; } +static void gadgetfs_evict_inode (struct inode *inode) +{ +/* EP inodes hold the reference on their ep_data via ->i_private. */ +if (inode->i_fop == &ep_io_operations) +put_ep (inode->i_private); + +truncate_inode_pages_final (&inode->i_data); +clear_inode (inode); +} +static const struct super_operations gadget_fs_operations = { .statfs = simple_statfs, .drop_inode = inode_just_drop, + .evict_inode = gadgetfs_evict_inode, }; static int
Signature · 1 line
2.51.0
Quoted 12 lines · Adriano Cordova
gadgetfs_unbind()->destroy_ep_files()frees each structep_dataviaput_ep(), but the inode still points at thatep_datathrough ->i_private with no reference of its own. A concurrentopenat()on an endpoint file can therefore reachep_open()and dereference the freedep_data(the mutex fast path), even while the inode outlives it.Give the inode its own reference: take
get_ep()when the endpoint inode is created and drop it again from a newgadgetfs_evict_inode(), so anep_datacan never be freed while any inode still points at it. This is also safe against dcache pruning, since the inode reference keeps the count >= 1 until the endpoint is removed fromdev->epfiles.
Quoted 5 lines
Reported-by [email protected]Tested-by [email protected]Signed-off-by Adriano Cordova <[email protected]>---
A similar, but not identical, patch to fix this problem was submitted some time ago:
https://lore.kernel.org/linux-usb/[email protected]/
Alan Stern
Quoted 41 lines
drivers/usb/gadget/legacy/inode.c | 13 +++++++++++++ 1 file changed, 13 insertions(+)drivers/usb/gadget/legacy/inode.c +13
-0@@ -1626,6 +1626,8 @@ static int activate_ep_files (struct dev_data *dev)if (!data->req) goto enomem1; +/* The inode keeps this ep_data alive via ->i_private. */ +get_ep (data); err =gadgetfs_create_file (dev->sb, data->name, data, &ep_io_operations);if (err)@@ -2015,9 +2017,20 @@ static int gadgetfs_create_file (struct super_block *sb, char const *name,return 0 ; } +static void gadgetfs_evict_inode (struct inode *inode) +{ +/* EP inodes hold the reference on their ep_data via ->i_private. */ +if (inode->i_fop == &ep_io_operations) +put_ep (inode->i_private); + +truncate_inode_pages_final (&inode->i_data); +clear_inode (inode); +} +static const struct super_operations gadget_fs_operations = { .statfs = simple_statfs, .drop_inode = inode_just_drop, + .evict_inode = gadgetfs_evict_inode, }; static intSignature · 1 line
2.51.0
A message that is not in the archive (sent off-list or not yet received): CAMGU2hUOyoWAG-HVCvchqqc4R4uQ7QHaJ_GKGK+hUWw-=Z2deQ@mail.gmail.com
Quoted 6 lines · Adriano Córdova
Hi Alan,
Right. But that patch makes the inode the sole lifetime owner of the
ep_data(deletes the list call to 'put_ep'), butep_datais also linked in the listdev->epfilesand that reference can outlive the inode (it doesn't happen now I think, but only incidentally).
Okay, I see.
Quoted 2 lines
So in the patch in here I decided to go with a refcount approach, just to be safe.
The patch description could be improved just a little...
Quoted 9 lines
Quoted 6 lines · Alan Stern
Quoted 5 lines · Adriano Cordova
gadgetfs_unbind()->destroy_ep_files()frees each structep_dataviaput_ep(), but the inode still points at thatep_datathrough ->i_private with no reference of its own. A concurrentopenat()on an endpoint file can therefore reachep_open()and dereference the freedep_data(the mutex fast path), even while the inode outlives it.
You could mention something about the inode's lifetime and what causes it to be released (and when). This would make it clearer to people like me how a concurrent open() can race with inode destruction. But I agree, you're right that the inode's pointer should be counted in the ep_data's refcount.
Quoted 5 lines
Quoted 5 lines
Quoted 5 lines
Give the inode its own reference: take
get_ep()when the endpoint inode is created and drop it again from a newgadgetfs_evict_inode(), so anep_datacan never be freed while any inode still points at it. This is also safe against dcache pruning, since the inode reference keeps the count >= 1 until the endpoint is removed fromdev->epfiles.
Now it all makes sense.
Your patch doesn't handle the case where the gadgetfs_make_inode() call in gadgetfs_create_file() fails. If that happens, the refcount will remain elevated because there is no inode to evict. I'll leave it to you to figure out the best solution.
Alan Stern
Quoted 26 lines · Alan Stern
Quoted 6 lines · Adriano Córdova
Hi Alan,
Right. But that patch makes the inode the sole lifetime owner of the
ep_data(deletes the list call to 'put_ep'), butep_datais also linked in the listdev->epfilesand that reference can outlive the inode (it doesn't happen now I think, but only incidentally).Okay, I see.
Quoted 2 lines
So in the patch in here I decided to go with a refcount approach, just to be safe.
The patch description could be improved just a little...
Quoted 9 lines
Quoted 6 lines · Alan Stern
Quoted 5 lines · Adriano Cordova
gadgetfs_unbind()->destroy_ep_files()frees each structep_dataviaput_ep(), but the inode still points at thatep_datathrough ->i_private with no reference of its own. A concurrentopenat()on an endpoint file can therefore reachep_open()and dereference the freedep_data(the mutex fast path), even while the inode outlives it.
Quoted 5 lines
You could mention something about the inode's lifetime and what causes it to be released (and when). This would make it clearer to people like me how a concurrent
open()can race with inode destruction. But I agree, you're right that the inode's pointer should be counted in the ep_data's refcount.
Ok, I'll send a v2
Quoted 8 lines
Quoted 7 lines
Quoted 7 lines
Quoted 7 lines
Give the inode its own reference: take
get_ep()when the endpoint inode is created and drop it again from a newgadgetfs_evict_inode(), so anep_datacan never be freed while any inode still points at it. This is also safe against dcache pruning, since the inode reference keeps the count >= 1 until the endpoint is removed fromdev->epfiles.
Quoted 6 lines
Now it all makes sense.
Your patch doesn't handle the case where the
gadgetfs_make_inode()call ingadgetfs_create_file()fails. If that happens, the refcount will remain elevated because there is no inode to evict. I'll leave it to you to figure out the best solution.
If gadgetfs_make_inode() call in gadgetfs_create_file() fails, it returns -ENOMEM and the error path of activate_ep_files() (where the new inc to the refcount is) kfrees the object directly, bypassing the refcount.
Quoted 3 lines
Alan Stern
Best,
Adriano Cordova
Quoted 11 lines · Adriano Córdova
Quoted 4 lines · Alan Stern
Your patch doesn't handle the case where the
gadgetfs_make_inode()call ingadgetfs_create_file()fails. If that happens, the refcount will remain elevated because there is no inode to evict. I'll leave it to you to figure out the best solution.If
gadgetfs_make_inode()call ingadgetfs_create_file()fails, it returns-ENOMEMand the error path ofactivate_ep_files()(where the new inc to the refcount is) kfrees the object directly, bypassing the refcount.
Oh, yes. I hadn't noticed that.
One last thing: Can a similar race between open and remove for the main (the ep0_operations) file cause problems?
Alan Stern
I think the main CHIP has a dev_data instead of ep_data
and it belongs to the mount.
Quoted 2 lines · Adriano Córdova
I think the main CHIP has a
dev_datainstead ofep_data
and it belongs to the mount.
Sure, but that doesn't answer the question. Is it subject to a similar problem caused by a similar race?
Alan Stern