2023-10-01 02:20:17

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

We have been seeing two main stability issues that uvc gadget driver
runs into when stopping streams:
1. Attempting to queue usb_requests to a disabled usb_ep
2. use-after-free issue for inflight usb_requests

The three patches below fix the two issues above. Patch 1/3 fixes the
first issue, and Patch 2/3 and 3/3 fix the second issue.

Avichal Rakesh (3):
usb: gadget: uvc: prevent use of disabled endpoint
usb: gadget: uvc: Allocate uvc_requests one at a time
usb: gadget: uvc: Fix use-after-free for inflight usb_requests

drivers/usb/gadget/function/f_uvc.c | 11 +-
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 6 +-
drivers/usb/gadget/function/uvc_v4l2.c | 21 ++-
drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
5 files changed, 164 insertions(+), 65 deletions(-)

--
2.42.0.582.g8ccd20d70d-goog


2023-10-03 11:09:23

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

Hi

On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>We have been seeing two main stability issues that uvc gadget driver
>runs into when stopping streams:
> 1. Attempting to queue usb_requests to a disabled usb_ep
> 2. use-after-free issue for inflight usb_requests
>
>The three patches below fix the two issues above. Patch 1/3 fixes the
>first issue, and Patch 2/3 and 3/3 fix the second issue.
>
>Avichal Rakesh (3):
> usb: gadget: uvc: prevent use of disabled endpoint
> usb: gadget: uvc: Allocate uvc_requests one at a time
> usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>
> drivers/usb/gadget/function/f_uvc.c | 11 +-
> drivers/usb/gadget/function/f_uvc.h | 2 +-
> drivers/usb/gadget/function/uvc.h | 6 +-
> drivers/usb/gadget/function/uvc_v4l2.c | 21 ++-
> drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
> 5 files changed, 164 insertions(+), 65 deletions(-)

These patches are not applying on gregkh/usb-testing since
Greg did take my patches first. I have already rebased them.

In the updated version I the stack runs into the
following error, when enabling lockdep. Could you
try your version with lockdep enabled?

[ 41.278520] configfs-gadget.vz gadget.0: uvc: reset UVC
[ 47.156261] configfs-gadget.vz gadget.0: uvc: uvc_function_set_alt(2, 0)
[ 47.169177]
[ 47.170903] ============================================
[ 47.176857] WARNING: possible recursive locking detected
[ 47.182798] 6.5.0-20230919-1+ #19 Tainted: G C
[ 47.189323] --------------------------------------------
[ 47.195256] vzuvcd/412 is trying to acquire lock:
[ 47.200511] ffffff8009560928 (&video->req_lock){....}-{3:3}, at: uvc_video_complete+0x44/0x2e0
[ 47.210172]
[ 47.210172] but task is already holding lock:
[ 47.216687] ffffff8009560928 (&video->req_lock){....}-{3:3}, at: uvcg_video_enable+0x2d0/0x5c0
[ 47.226333]
[ 47.226333] other info that might help us debug this:
[ 47.233625] Possible unsafe locking scenario:
[ 47.233625]
[ 47.240242] CPU0
[ 47.242974] ----
[ 47.245709] lock(&video->req_lock);
[ 47.249802] lock(&video->req_lock);
[ 47.253897]
[ 47.253897] *** DEADLOCK ***
[ 47.253897]
[ 47.260511] May be due to missing lock nesting notation
[ 47.260511]


Regards,
Michael

--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (2.65 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-03 23:16:31

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

Thank you for testing the patch, Michael!

On 10/3/23 04:09, Michael Grzeschik wrote:
> Hi
>
> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>> We have been seeing two main stability issues that uvc gadget driver
>> runs into when stopping streams:
>> 1. Attempting to queue usb_requests to a disabled usb_ep
>> 2. use-after-free issue for inflight usb_requests
>>
>> The three patches below fix the two issues above. Patch 1/3 fixes the
>> first issue, and Patch 2/3 and 3/3 fix the second issue.
>>
>> Avichal Rakesh (3):
>>  usb: gadget: uvc: prevent use of disabled endpoint
>>  usb: gadget: uvc: Allocate uvc_requests one at a time
>>  usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>>
>> drivers/usb/gadget/function/f_uvc.c     |  11 +-
>> drivers/usb/gadget/function/f_uvc.h     |   2 +-
>> drivers/usb/gadget/function/uvc.h       |   6 +-
>> drivers/usb/gadget/function/uvc_v4l2.c  |  21 ++-
>> drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>> 5 files changed, 164 insertions(+), 65 deletions(-)
>
> These patches are not applying on gregkh/usb-testing since
> Greg did take my patches first. I have already rebased them.

Ah, I didn't realize Greg had picked up your changes in his tree.
Rebased the patches in V2.

Also want to point out that
https://lore.kernel.org/[email protected]/
may have introduced an issue when setting the uvc->state to
UVC_STATE_CONNECTED in uvcg_video_enable.

Effectively, uvc_video_enable can be called
1. because the host asks to halt the stream, or
2. if the gadget was disabled.

Setting uvc->state to CONNECTED is fine for (1). In (2), uvc_v4l2
sets uvc->state to DISCONNECTED before calling uvcg_video_enable.
In this case, your change would overwrite the value to CONNECTED
without any further checks.

>
> In the updated version I the stack runs into the
> following error, when enabling lockdep. Could you
> try your version with lockdep enabled?
>
> [   41.278520] configfs-gadget.vz gadget.0: uvc: reset UVC
> [   47.156261] configfs-gadget.vz gadget.0: uvc: uvc_function_set_alt(2, 0)
> [   47.169177]
> [   47.170903] ============================================
> [   47.176857] WARNING: possible recursive locking detected
> [   47.182798] 6.5.0-20230919-1+ #19 Tainted: G         C
> [   47.189323] --------------------------------------------
> [   47.195256] vzuvcd/412 is trying to acquire lock:
> [   47.200511] ffffff8009560928 (&video->req_lock){....}-{3:3}, at: uvc_video_complete+0x44/0x2e0
> [   47.210172]
> [   47.210172] but task is already holding lock:
> [   47.216687] ffffff8009560928 (&video->req_lock){....}-{3:3}, at: uvcg_video_enable+0x2d0/0x5c0
> [   47.226333]
> [   47.226333] other info that might help us debug this:
> [   47.233625]  Possible unsafe locking scenario:
> [   47.233625]
> [   47.240242]        CPU0
> [   47.242974]        ----
> [   47.245709]   lock(&video->req_lock);
> [   47.249802]   lock(&video->req_lock);
> [   47.253897]
> [   47.253897]  *** DEADLOCK ***
> [   47.253897]
> [   47.260511]  May be due to missing lock nesting notation
> [   47.260511]
>

Thank you for catching this. I couldn't repro the deadlock, but it looks
like DWC3 controller can call the complete callback directly from
usb_ep_dequeue for requests in its pending list. I am not sure if that
is up to spec, considering calling usb_ep_queue explicitly forbids calling
the complete callback from within. Regardless, it is easy enough to fix
by moving the dequeue calls to before we acquire req_lock. The semantics
of the rest of the patch is unchanged.

Uploading the fixed patches shortly.

Regards,
Avi.

2023-10-05 14:58:13

by Laurent Pinchart

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

On Tue, Oct 03, 2023 at 01:09:06PM +0200, Michael Grzeschik wrote:
> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
> > We have been seeing two main stability issues that uvc gadget driver
> > runs into when stopping streams:
> > 1. Attempting to queue usb_requests to a disabled usb_ep
> > 2. use-after-free issue for inflight usb_requests
> >
> > The three patches below fix the two issues above. Patch 1/3 fixes the
> > first issue, and Patch 2/3 and 3/3 fix the second issue.
> >
> > Avichal Rakesh (3):
> > usb: gadget: uvc: prevent use of disabled endpoint
> > usb: gadget: uvc: Allocate uvc_requests one at a time
> > usb: gadget: uvc: Fix use-after-free for inflight usb_requests
> >
> > drivers/usb/gadget/function/f_uvc.c | 11 +-
> > drivers/usb/gadget/function/f_uvc.h | 2 +-
> > drivers/usb/gadget/function/uvc.h | 6 +-
> > drivers/usb/gadget/function/uvc_v4l2.c | 21 ++-
> > drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
> > 5 files changed, 164 insertions(+), 65 deletions(-)
>
> These patches are not applying on gregkh/usb-testing since
> Greg did take my patches first. I have already rebased them.

I think they got merged too soon :-( We could fix things on top, but
there's very little time to do so for v6.7.

> In the updated version I the stack runs into the
> following error, when enabling lockdep. Could you
> try your version with lockdep enabled?
>
> [ 41.278520] configfs-gadget.vz gadget.0: uvc: reset UVC
> [ 47.156261] configfs-gadget.vz gadget.0: uvc: uvc_function_set_alt(2, 0)
> [ 47.169177]
> [ 47.170903] ============================================
> [ 47.176857] WARNING: possible recursive locking detected
> [ 47.182798] 6.5.0-20230919-1+ #19 Tainted: G C
> [ 47.189323] --------------------------------------------
> [ 47.195256] vzuvcd/412 is trying to acquire lock:
> [ 47.200511] ffffff8009560928 (&video->req_lock){....}-{3:3}, at: uvc_video_complete+0x44/0x2e0
> [ 47.210172]
> [ 47.210172] but task is already holding lock:
> [ 47.216687] ffffff8009560928 (&video->req_lock){....}-{3:3}, at: uvcg_video_enable+0x2d0/0x5c0
> [ 47.226333]
> [ 47.226333] other info that might help us debug this:
> [ 47.233625] Possible unsafe locking scenario:
> [ 47.233625]
> [ 47.240242] CPU0
> [ 47.242974] ----
> [ 47.245709] lock(&video->req_lock);
> [ 47.249802] lock(&video->req_lock);
> [ 47.253897]
> [ 47.253897] *** DEADLOCK ***
> [ 47.253897]
> [ 47.260511] May be due to missing lock nesting notation
> [ 47.260511]

--
Regards,

Laurent Pinchart

2023-10-05 15:49:14

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

Hi Laurent

On Thu, Oct 05, 2023 at 11:23:27AM +0300, Laurent Pinchart wrote:
>On Tue, Oct 03, 2023 at 01:09:06PM +0200, Michael Grzeschik wrote:
>> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>> > We have been seeing two main stability issues that uvc gadget driver
>> > runs into when stopping streams:
>> > 1. Attempting to queue usb_requests to a disabled usb_ep
>> > 2. use-after-free issue for inflight usb_requests
>> >
>> > The three patches below fix the two issues above. Patch 1/3 fixes the
>> > first issue, and Patch 2/3 and 3/3 fix the second issue.
>> >
>> > Avichal Rakesh (3):
>> > usb: gadget: uvc: prevent use of disabled endpoint
>> > usb: gadget: uvc: Allocate uvc_requests one at a time
>> > usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>> >
>> > drivers/usb/gadget/function/f_uvc.c | 11 +-
>> > drivers/usb/gadget/function/f_uvc.h | 2 +-
>> > drivers/usb/gadget/function/uvc.h | 6 +-
>> > drivers/usb/gadget/function/uvc_v4l2.c | 21 ++-
>> > drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>> > 5 files changed, 164 insertions(+), 65 deletions(-)
>>
>> These patches are not applying on gregkh/usb-testing since
>> Greg did take my patches first. I have already rebased them.
>
>I think they got merged too soon :-( We could fix things on top, but
>there's very little time to do so for v6.7.

Agreed. I was jumping from one workaround to another one, since this
is not easy to fix in a proper way. And still after this long discussion
with Avichal I don't think we are there yet.


So far the first two patches from Avichal look legit. But the overall
Use-After-Free fix is yet to be done properly.

The "abondoned" method he suggested is really bad to follow and will
add too much complexity and will be hard to debug.

IMHO it should be possible to introduce two cleanup pathes.

One path would be in the uvc_cleanup_requests that will cleanup the
requests that are actually not used in the controller and are registered
in the req_free list.

The second path would be the complete functions that are being run
from the controller and will ensure that the cleanup will really free
the requests from the controller after they were consumed.

What do you think?

Regards,
Michael

>> In the updated version I the stack runs into the
>> following error, when enabling lockdep. Could you
>> try your version with lockdep enabled?
>>
>> [ 41.278520] configfs-gadget.vz gadget.0: uvc: reset UVC
>> [ 47.156261] configfs-gadget.vz gadget.0: uvc: uvc_function_set_alt(2, 0)
>> [ 47.169177]
>> [ 47.170903] ============================================
>> [ 47.176857] WARNING: possible recursive locking detected
>> [ 47.182798] 6.5.0-20230919-1+ #19 Tainted: G C
>> [ 47.189323] --------------------------------------------
>> [ 47.195256] vzuvcd/412 is trying to acquire lock:
>> [ 47.200511] ffffff8009560928 (&video->req_lock){....}-{3:3}, at: uvc_video_complete+0x44/0x2e0
>> [ 47.210172]
>> [ 47.210172] but task is already holding lock:
>> [ 47.216687] ffffff8009560928 (&video->req_lock){....}-{3:3}, at: uvcg_video_enable+0x2d0/0x5c0
>> [ 47.226333]
>> [ 47.226333] other info that might help us debug this:
>> [ 47.233625] Possible unsafe locking scenario:
>> [ 47.233625]
>> [ 47.240242] CPU0
>> [ 47.242974] ----
>> [ 47.245709] lock(&video->req_lock);
>> [ 47.249802] lock(&video->req_lock);
>> [ 47.253897]
>> [ 47.253897] *** DEADLOCK ***
>> [ 47.253897]
>> [ 47.260511] May be due to missing lock nesting notation
>> [ 47.260511]
>
>--
>Regards,
>
>Laurent Pinchart
>

--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (3.98 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-05 16:21:08

by Greg Kroah-Hartman

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

On Tue, Oct 03, 2023 at 04:16:00PM -0700, Avichal Rakesh wrote:
> Thank you for testing the patch, Michael!
>
> On 10/3/23 04:09, Michael Grzeschik wrote:
> > Hi
> >
> > On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
> >> We have been seeing two main stability issues that uvc gadget driver
> >> runs into when stopping streams:
> >> 1. Attempting to queue usb_requests to a disabled usb_ep
> >> 2. use-after-free issue for inflight usb_requests
> >>
> >> The three patches below fix the two issues above. Patch 1/3 fixes the
> >> first issue, and Patch 2/3 and 3/3 fix the second issue.
> >>
> >> Avichal Rakesh (3):
> >> ?usb: gadget: uvc: prevent use of disabled endpoint
> >> ?usb: gadget: uvc: Allocate uvc_requests one at a time
> >> ?usb: gadget: uvc: Fix use-after-free for inflight usb_requests
> >>
> >> drivers/usb/gadget/function/f_uvc.c???? |? 11 +-
> >> drivers/usb/gadget/function/f_uvc.h???? |?? 2 +-
> >> drivers/usb/gadget/function/uvc.h?????? |?? 6 +-
> >> drivers/usb/gadget/function/uvc_v4l2.c? |? 21 ++-
> >> drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
> >> 5 files changed, 164 insertions(+), 65 deletions(-)
> >
> > These patches are not applying on gregkh/usb-testing since
> > Greg did take my patches first. I have already rebased them.
>
> Ah, I didn't realize Greg had picked up your changes in his tree.
> Rebased the patches in V2.

The "v2" series here is almost impossible to follow, sorry.

Please send it as a new thread, not as responses to the individual
commits, how am I supposed to pick them up that way?

And make it v3 please.

thanks,

greg k-h

2023-10-05 18:09:31

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v3 1/3] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. To be consistent with the actual streaming state, uvc->state
is now toggled between CONNECTED and STREAMING from the v4l2 event
callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT and reworded commit message.
v2 -> v3: Fix email threading goof-up

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 21 ++++++++++++++++++---
4 files changed, 25 insertions(+), 11 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index faa398109431..75c9f9a3f884 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep) {
+ usb_ep_disable(uvc->video.ep);
+ }
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..e7f9f13f14dc 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..3d3469883ed0 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,19 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0) {
+ return ret;
+ }
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +508,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ if (uvc->state == UVC_STATE_STREAMING) {
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ uvc->state = UVC_STATE_CONNECTED;
+ }
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +663,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
--
2.42.0.609.gbb76f46606-goog

2023-10-05 18:09:41

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v3 3/3] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
flag to uvc_request to mark it as 'abandoned'. When disabling the video
stream, instead of de-allocating all uvc_requests and usb_requests, the
gadget driver only de-allocates those usb_requests that are currently
owned by the gadget driver (as present in req_free). Other usb_requests
have their corresponding 'is_abandoned' flag tripped, and the
usb_requests complete handler takes care of freeing the usb_request and
its corresponding uvc_request.

This should ensure that uvc gadget driver never accidentally de-allocates
a usb_request that it doesn't own.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT, and fixed deadlock reported in
https://lore.kernel.org/all/[email protected]/
v2 -> v3: Fix email threading goof-up

drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_video.c | 118 ++++++++++++++++++++----
2 files changed, 102 insertions(+), 17 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 993694da0bbc..e69cfb7cced1 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -82,6 +82,7 @@ struct uvc_request {
u8 header[UVCG_REQUEST_HEADER_LEN];
struct uvc_buffer *last_buf;
struct list_head list;
+ bool is_abandoned;
};

struct uvc_video {
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 3c4d286d81c0..69521886d599 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -271,7 +271,21 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
struct uvc_video *video = ureq->video;
struct uvc_video_queue *queue = &video->queue;
struct uvc_device *uvc = video->uvc;
+ struct uvc_buffer *last_buf;
unsigned long flags;
+ bool is_abandoned;
+
+ spin_lock_irqsave(&video->req_lock, flags);
+ is_abandoned = ureq->is_abandoned;
+ last_buf = ureq->last_buf;
+ ureq->last_buf = NULL;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ if (is_abandoned) {
+ uvcg_dbg(&video->uvc->func, "Freeing abandoned usb_request\n");
+ uvc_video_free_request(ureq, ep);
+ return;
+ }

if (uvc->state == UVC_STATE_CONNECTED) {
usb_ep_free_request(video->ep, ureq->req);
@@ -300,15 +314,29 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
uvcg_queue_cancel(queue, 0);
}

- if (ureq->last_buf) {
- uvcg_complete_buffer(&video->queue, ureq->last_buf);
- ureq->last_buf = NULL;
+ if (last_buf) {
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ uvcg_complete_buffer(&video->queue, last_buf);
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
}

+ /*
+ * request might have been abandoned while being processed.
+ * do a last minute check before queueing the request back.
+ */
spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
+ is_abandoned = ureq->is_abandoned;
+ if (!is_abandoned)
+ list_add_tail(&req->list, &video->req_free);
spin_unlock_irqrestore(&video->req_lock, flags);

+ if (is_abandoned) {
+ uvcg_dbg(&video->uvc->func,
+ "usb_request abandoned mid-processing - freeing.\n");
+ uvc_video_free_request(ureq, ep);
+ return;
+ }
+
if (uvc->state == UVC_STATE_STREAMING)
queue_work(video->async_wq, &video->pump);
}
@@ -372,7 +400,6 @@ uvc_video_alloc_requests(struct uvc_video *video)
}

video->req_size = req_size;
-
return 0;

error:
@@ -504,13 +531,80 @@ static void uvcg_video_pump(struct work_struct *work)
return;
}

+/*
+ * Disable video stream. This ensures that any inflight usb requests are marked
+ * for clean up and all video buffers are dropped before returning.
+ */
+static void uvcg_video_disable(struct uvc_video *video)
+{
+ struct uvc_buffer *buf, *tmp_buf;
+ struct uvc_request *ureq, *temp;
+ struct list_head buf_list; /* track in-flight video buffers */
+ struct usb_request *req;
+ unsigned long flags;
+
+ INIT_LIST_HEAD(&buf_list);
+
+ cancel_work_sync(&video->pump);
+ uvcg_queue_cancel(&video->queue, 0);
+
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }
+
+ /**
+ * acquiring req_lock here should prevent any more complete
+ * callbacks from processing until we've abandoned the
+ * requests that are still in-flight and let the complete
+ * callback handle cleanup
+ */
+ spin_lock_irqsave(&video->req_lock, flags);
+ /* abandon all usb requests */
+ list_for_each_entry_safe(ureq, temp, &video->ureqs, list) {
+ ureq->is_abandoned = true;
+ if (ureq->last_buf) {
+ list_add(&ureq->last_buf->queue, &buf_list);
+ ureq->last_buf = NULL;
+ }
+ list_del_init(&ureq->list);
+ }
+ /*
+ * re-add uvc_requests currently owned by the gadget to
+ * video->ureqs to be deallocated. This effectively leaves
+ * video->ureqs with the requests that we currently own.
+ */
+ list_for_each_entry(req, &video->req_free, list) {
+ ureq = req->context;
+ list_add_tail(&ureq->list, &video->ureqs);
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ /*
+ * drop abandoned uvc_buffers, as the completion handler
+ * no longer will
+ */
+ if (!list_empty(&buf_list)) {
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ list_for_each_entry_safe(buf, tmp_buf,
+ &buf_list, queue) {
+ video->queue.flags |= UVC_QUEUE_DROP_INCOMPLETE;
+ uvcg_complete_buffer(&video->queue, buf);
+ list_del(&buf->queue);
+ }
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
+ }
+
+ uvc_video_free_requests(video);
+ uvcg_queue_enable(&video->queue, 0);
+}
+
/*
* Enable or disable the video stream.
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
struct uvc_device *uvc = video->uvc;
- struct uvc_request *ureq;
int ret;

if (video->ep == NULL) {
@@ -521,17 +615,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)

if (!enable) {
uvc->state = UVC_STATE_CONNECTED;
-
- cancel_work_sync(&video->pump);
- uvcg_queue_cancel(&video->queue, 0);
-
- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
- }
-
- uvc_video_free_requests(video);
- uvcg_queue_enable(&video->queue, 0);
+ uvcg_video_disable(video);
return 0;
}

--
2.42.0.609.gbb76f46606-goog

2023-10-05 18:11:19

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.



On 10/5/23 00:11, Greg Kroah-Hartman wrote:
> On Tue, Oct 03, 2023 at 04:16:00PM -0700, Avichal Rakesh wrote:
>> Thank you for testing the patch, Michael!
>>
>> On 10/3/23 04:09, Michael Grzeschik wrote:
>>> Hi
>>>
>>> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>>>> We have been seeing two main stability issues that uvc gadget driver
>>>> runs into when stopping streams:
>>>> 1. Attempting to queue usb_requests to a disabled usb_ep
>>>> 2. use-after-free issue for inflight usb_requests
>>>>
>>>> The three patches below fix the two issues above. Patch 1/3 fixes the
>>>> first issue, and Patch 2/3 and 3/3 fix the second issue.
>>>>
>>>> Avichal Rakesh (3):
>>>>  usb: gadget: uvc: prevent use of disabled endpoint
>>>>  usb: gadget: uvc: Allocate uvc_requests one at a time
>>>>  usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>>>>
>>>> drivers/usb/gadget/function/f_uvc.c     |  11 +-
>>>> drivers/usb/gadget/function/f_uvc.h     |   2 +-
>>>> drivers/usb/gadget/function/uvc.h       |   6 +-
>>>> drivers/usb/gadget/function/uvc_v4l2.c  |  21 ++-
>>>> drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>>>> 5 files changed, 164 insertions(+), 65 deletions(-)
>>>
>>> These patches are not applying on gregkh/usb-testing since
>>> Greg did take my patches first. I have already rebased them.
>>
>> Ah, I didn't realize Greg had picked up your changes in his tree.
>> Rebased the patches in V2.
>
> The "v2" series here is almost impossible to follow, sorry.
>
> Please send it as a new thread, not as responses to the individual
> commits, how am I supposed to pick them up that way?
>
> And make it v3 please.

Sent out v3 as a new thread. Sorry about that!

- Avi.

2023-10-05 18:30:50

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.



On 10/5/23 03:14, Michael Grzeschik wrote:
> Hi Laurent
>
> On Thu, Oct 05, 2023 at 11:23:27AM +0300, Laurent Pinchart wrote:
>> On Tue, Oct 03, 2023 at 01:09:06PM +0200, Michael Grzeschik wrote:
>>> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>>> > We have been seeing two main stability issues that uvc gadget driver
>>> > runs into when stopping streams:
>>> >  1. Attempting to queue usb_requests to a disabled usb_ep
>>> >  2. use-after-free issue for inflight usb_requests
>>> >
>>> > The three patches below fix the two issues above. Patch 1/3 fixes the
>>> > first issue, and Patch 2/3 and 3/3 fix the second issue.
>>> >
>>> > Avichal Rakesh (3):
>>> >   usb: gadget: uvc: prevent use of disabled endpoint
>>> >   usb: gadget: uvc: Allocate uvc_requests one at a time
>>> >   usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>>> >
>>> > drivers/usb/gadget/function/f_uvc.c     |  11 +-
>>> > drivers/usb/gadget/function/f_uvc.h     |   2 +-
>>> > drivers/usb/gadget/function/uvc.h       |   6 +-
>>> > drivers/usb/gadget/function/uvc_v4l2.c  |  21 ++-
>>> > drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>>> > 5 files changed, 164 insertions(+), 65 deletions(-)
>>>
>>> These patches are not applying on gregkh/usb-testing since
>>> Greg did take my patches first. I have already rebased them.
>>
>> I think they got merged too soon :-( We could fix things on top, but
>> there's very little time to do so for v6.7.
>
> Agreed. I was jumping from one workaround to another one, since this
> is not easy to fix in a proper way. And still after this long discussion
> with Avichal I don't think we are there yet.
>
>
> So far the first two patches from Avichal look legit. But the overall
> Use-After-Free fix is yet to be done properly.
>
> The "abondoned" method he suggested is really bad to follow and will
> add too much complexity and will be hard to debug.
>
> IMHO it should be possible to introduce two cleanup pathes.
>
> One path would be in the uvc_cleanup_requests that will cleanup the
> requests that are actually not used in the controller and are registered
> in the req_free list.
>
> The second path would be the complete functions that are being run
> from the controller and will ensure that the cleanup will really free
> the requests from the controller after they were consumed.
>
> What do you think?

I am not sure I follow. Patch 3/3 does exactly what you say here.
There are two cleanup paths:
1. uvcg_video_disable cleans up only the requests in req_free, and
2. complete handler cleans up the in-flight requests.

The "abandoned" flag is simply to let the completion handler know
which requests to clean up and which ones to re-queue back to
the gadget driver.

The other "complications" are around making sure we can trust
the values in an inherently racey situation. The reasoning
can admittedly be difficult to follow at a glance, which incidentally
is why I went with a simple to prove timed wait in the past
(https://lore.kernel.org/[email protected]).

I am not suggesting we go back to a timed wait, but please do look
at the patch and let me know which parts don't make sense, or are
difficult to understand. We can add more documentation about our
assumptions there, or if you have a way to do this that you
think is simpler to reason about, then please let me know and I'll
be more than happy to use that!

Regards,
Avi.

2023-10-05 22:05:48

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

Hi Avichal,

On Thu, Oct 05, 2023 at 11:30:32AM -0700, Avichal Rakesh wrote:
>On 10/5/23 03:14, Michael Grzeschik wrote:
>> On Thu, Oct 05, 2023 at 11:23:27AM +0300, Laurent Pinchart wrote:
>>> On Tue, Oct 03, 2023 at 01:09:06PM +0200, Michael Grzeschik wrote:
>>>> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>>>> > We have been seeing two main stability issues that uvc gadget driver
>>>> > runs into when stopping streams:
>>>> >? 1. Attempting to queue usb_requests to a disabled usb_ep
>>>> >? 2. use-after-free issue for inflight usb_requests
>>>> >
>>>> > The three patches below fix the two issues above. Patch 1/3 fixes the
>>>> > first issue, and Patch 2/3 and 3/3 fix the second issue.
>>>> >
>>>> > Avichal Rakesh (3):
>>>> >?? usb: gadget: uvc: prevent use of disabled endpoint
>>>> >?? usb: gadget: uvc: Allocate uvc_requests one at a time
>>>> >?? usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>>>> >
>>>> > drivers/usb/gadget/function/f_uvc.c???? |? 11 +-
>>>> > drivers/usb/gadget/function/f_uvc.h???? |?? 2 +-
>>>> > drivers/usb/gadget/function/uvc.h?????? |?? 6 +-
>>>> > drivers/usb/gadget/function/uvc_v4l2.c? |? 21 ++-
>>>> > drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>>>> > 5 files changed, 164 insertions(+), 65 deletions(-)
>>>>
>>>> These patches are not applying on gregkh/usb-testing since
>>>> Greg did take my patches first. I have already rebased them.
>>>
>>> I think they got merged too soon :-( We could fix things on top, but
>>> there's very little time to do so for v6.7.
>>
>> Agreed. I was jumping from one workaround to another one, since this
>> is not easy to fix in a proper way. And still after this long discussion
>> with Avichal I don't think we are there yet.
>>
>>
>> So far the first two patches from Avichal look legit. But the overall
>> Use-After-Free fix is yet to be done properly.
>>
>> The "abondoned" method he suggested is really bad to follow and will
>> add too much complexity and will be hard to debug.
>>
>> IMHO it should be possible to introduce two cleanup pathes.
>>
>> One path would be in the uvc_cleanup_requests that will cleanup the
>> requests that are actually not used in the controller and are registered
>> in the req_free list.
>>
>> The second path would be the complete functions that are being run
>> from the controller and will ensure that the cleanup will really free
>> the requests from the controller after they were consumed.
>>
>> What do you think?
>
>I am not sure I follow. Patch 3/3 does exactly what you say here.

Yes, it was just to summ up what the latest state of the idea was,
so Laurent does not read the whole thread in detail. Sorry for not
being clear enough about that.

>There are two cleanup paths:
> 1. uvcg_video_disable cleans up only the requests in req_free, and
> 2. complete handler cleans up the in-flight requests.
>
>The "abandoned" flag is simply to let the completion handler know
>which requests to clean up and which ones to re-queue back to
>the gadget driver.

What I don't get is, why in the case of shutdown there needs to
be something re-queued back to the gadget driver. There should not
need to be any sort of barrier flag for the requests. Just the
complete handler running past a barrier where it knows that the
whole device is stopped. So every call on complete should then clean
that exact request it is touching currently.

I don't know where the extra complexity comes from.

>The other "complications" are around making sure we can trust
>the values in an inherently racey situation. The reasoning
>can admittedly be difficult to follow at a glance, which incidentally
>is why I went with a simple to prove timed wait in the past
>(https://lore.kernel.org/[email protected]).
>
>I am not suggesting we go back to a timed wait, but please do look
>at the patch and let me know which parts don't make sense, or are
>difficult to understand. We can add more documentation about our
>assumptions there, or if you have a way to do this that you
>think is simpler to reason about, then please let me know and I'll
>be more than happy to use that!

I really try to spin my head around the idea of the is_abondoned flag
you are using. Unfortunatly for now I am out to debug the issues I see
with your series.

So I did try these patches you send. Yes the deadlock error is gone with
v3. But the linked list is still running into cases where
dwc3_gadget_giveback(complete) is touching requests that are already
freed.

[ 61.408715] ------------[ cut here ]------------
[ 61.413897] kernel BUG at lib/list_debug.c:56!
...
[ 61.590762] Call trace:
[ 61.596890] __list_del_entry_valid+0xb8/0xe8
[ 61.603408] dwc3_gadget_giveback+0x3c/0x1b0
[ 61.607594] dwc3_remove_requests.part.0+0xcc/0x100
[ 61.612948] __dwc3_gadget_ep_disable+0xbc/0x1b8
[ 61.621019] dwc3_gadget_ep_disable+0x48/0x100
[ 61.627925] usb_ep_disable+0x3c/0x138
[ 61.638230] uvc_function_setup_continue+0x3c/0x60
[ 61.645040] uvc_v4l2_streamoff+0x5c/0x80
[ 61.659812] v4l_streamoff+0x40/0x60
[ 61.668950] __video_do_ioctl+0x344/0x420
[ 61.679548] video_usercopy+0x1d0/0x788
[ 61.685677] video_ioctl2+0x40/0x70
[ 61.697439] v4l2_ioctl+0x68/0xa0
[ 61.709200] __arm64_sys_ioctl+0x304/0xda0
[ 61.720768] invoke_syscall.constprop.0+0x70/0x130

Regards,
Michael

--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (5.69 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-06 17:00:42

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.



On 10/5/23 15:05, Michael Grzeschik wrote:
> Hi Avichal,
>
> On Thu, Oct 05, 2023 at 11:30:32AM -0700, Avichal Rakesh wrote:
>> On 10/5/23 03:14, Michael Grzeschik wrote:
>>> On Thu, Oct 05, 2023 at 11:23:27AM +0300, Laurent Pinchart wrote:
>>>> On Tue, Oct 03, 2023 at 01:09:06PM +0200, Michael Grzeschik wrote:
>>>>> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>>>>> > We have been seeing two main stability issues that uvc gadget driver
>>>>> > runs into when stopping streams:
>>>>> >  1. Attempting to queue usb_requests to a disabled usb_ep
>>>>> >  2. use-after-free issue for inflight usb_requests
>>>>> >
>>>>> > The three patches below fix the two issues above. Patch 1/3 fixes the
>>>>> > first issue, and Patch 2/3 and 3/3 fix the second issue.
>>>>> >
>>>>> > Avichal Rakesh (3):
>>>>> >   usb: gadget: uvc: prevent use of disabled endpoint
>>>>> >   usb: gadget: uvc: Allocate uvc_requests one at a time
>>>>> >   usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>>>>> >
>>>>> > drivers/usb/gadget/function/f_uvc.c     |  11 +-
>>>>> > drivers/usb/gadget/function/f_uvc.h     |   2 +-
>>>>> > drivers/usb/gadget/function/uvc.h       |   6 +-
>>>>> > drivers/usb/gadget/function/uvc_v4l2.c  |  21 ++-
>>>>> > drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>>>>> > 5 files changed, 164 insertions(+), 65 deletions(-)
>>>>>
>>>>> These patches are not applying on gregkh/usb-testing since
>>>>> Greg did take my patches first. I have already rebased them.
>>>>
>>>> I think they got merged too soon :-( We could fix things on top, but
>>>> there's very little time to do so for v6.7.
>>>
>>> Agreed. I was jumping from one workaround to another one, since this
>>> is not easy to fix in a proper way. And still after this long discussion
>>> with Avichal I don't think we are there yet.
>>>
>>>
>>> So far the first two patches from Avichal look legit. But the overall
>>> Use-After-Free fix is yet to be done properly.
>>>
>>> The "abondoned" method he suggested is really bad to follow and will
>>> add too much complexity and will be hard to debug.
>>>
>>> IMHO it should be possible to introduce two cleanup pathes.
>>>
>>> One path would be in the uvc_cleanup_requests that will cleanup the
>>> requests that are actually not used in the controller and are registered
>>> in the req_free list.
>>>
>>> The second path would be the complete functions that are being run
>>> from the controller and will ensure that the cleanup will really free
>>> the requests from the controller after they were consumed.
>>>
>>> What do you think?
>>
>> I am not sure I follow. Patch 3/3 does exactly what you say here.
>
> Yes, it was just to summ up what the latest state of the idea was,
> so Laurent does not read the whole thread in detail. Sorry for not
> being clear enough about that.

Whoops! Sorry about the misunderstanding!

>
>> There are two cleanup paths:
>>  1. uvcg_video_disable cleans up only the requests in req_free, and
>>  2. complete handler cleans up the in-flight requests.
>>
>> The "abandoned" flag is simply to let the completion handler know
>> which requests to clean up and which ones to re-queue back to
>> the gadget driver.
>
> What I don't get is, why in the case of shutdown there needs to
> be something re-queued back to the gadget driver. There should not
> need to be any sort of barrier flag for the requests. Just the
> complete handler running past a barrier where it knows that the
> whole device is stopped. So every call on complete should then clean
> that exact request it is touching currently.
>
> I don't know where the extra complexity comes from.

A lot of this complexity comes from assuming a back to back
STREAMOFF -> STREAMON sequence is possible where the gadget driver
doesn't have the time to clean up all in-flight usb_requests.
However, looking through the usb gadget APIs again, and it
looks like usb_ep_disable enforces that all requests will
be sent back to the gadget driver before it returns.

So you're right:
With Patch 1/3 in place, I think we can just guard on uvc->state
alone, because control requests are blocked until usb_ep_disable
is finished anyway. I'll upload v4 with the "is_abandoned"
flag removed and the checks simplified once I've verified the
fix locally.

That should also remove any bookkeeping issues that may have
triggered the stack below.

Regards,
Avi.

>
>> The other "complications" are around making sure we can trust
>> the values in an inherently racey situation. The reasoning
>> can admittedly be difficult to follow at a glance, which incidentally
>> is why I went with a simple to prove timed wait in the past
>> (https://lore.kernel.org/[email protected]).
>>
>> I am not suggesting we go back to a timed wait, but please do look
>> at the patch and let me know which parts don't make sense, or are
>> difficult to understand. We can add more documentation about our
>> assumptions there, or if you have a way to do this that you
>> think is simpler to reason about, then please let me know and I'll
>> be more than happy to use that!
>
> I really try to spin my head around the idea of the is_abondoned flag
> you are using. Unfortunatly for now I am out to debug the issues I see
> with your series.
>
> So I did try these patches you send. Yes the deadlock error is gone with
> v3. But the linked list is still running into cases where
> dwc3_gadget_giveback(complete) is touching requests that are already
> freed.
>
> [   61.408715] ------------[ cut here ]------------
> [   61.413897] kernel BUG at lib/list_debug.c:56!
> ...
> [   61.590762] Call trace:
> [   61.596890]  __list_del_entry_valid+0xb8/0xe8
> [   61.603408]  dwc3_gadget_giveback+0x3c/0x1b0
> [   61.607594]  dwc3_remove_requests.part.0+0xcc/0x100
> [   61.612948]  __dwc3_gadget_ep_disable+0xbc/0x1b8
> [   61.621019]  dwc3_gadget_ep_disable+0x48/0x100
> [   61.627925]  usb_ep_disable+0x3c/0x138
> [   61.638230]  uvc_function_setup_continue+0x3c/0x60
> [   61.645040]  uvc_v4l2_streamoff+0x5c/0x80
> [   61.659812]  v4l_streamoff+0x40/0x60
> [   61.668950]  __video_do_ioctl+0x344/0x420
> [   61.679548]  video_usercopy+0x1d0/0x788
> [   61.685677]  video_ioctl2+0x40/0x70
> [   61.697439]  v4l2_ioctl+0x68/0xa0
> [   61.709200]  __arm64_sys_ioctl+0x304/0xda0
> [   61.720768]  invoke_syscall.constprop.0+0x70/0x130
>

2023-10-06 22:04:31

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v3 1/3] usb: gadget: uvc: prevent use of disabled endpoint

On Thu, Oct 05, 2023 at 11:08:12AM -0700, Avichal Rakesh wrote:
>Currently the set_alt callback immediately disables the endpoint and queues
>the v4l2 streamoff event. However, as the streamoff event is processed
>asynchronously, it is possible that the video_pump thread attempts to queue
>requests to an already disabled endpoint.
>
>This change moves disabling usb endpoint to the end of streamoff event
>callback. To be consistent with the actual streaming state, uvc->state
>is now toggled between CONNECTED and STREAMING from the v4l2 event
>callback only.
>
>Link: https://lore.kernel.org/[email protected]/
>Link: https://lore.kernel.org/[email protected]/
>Signed-off-by: Avichal Rakesh <[email protected]>
>---
>v1 -> v2: Rebased to ToT and reworded commit message.
>v2 -> v3: Fix email threading goof-up
>
> drivers/usb/gadget/function/f_uvc.c | 11 +++++------
> drivers/usb/gadget/function/f_uvc.h | 2 +-
> drivers/usb/gadget/function/uvc.h | 2 +-
> drivers/usb/gadget/function/uvc_v4l2.c | 21 ++++++++++++++++++---
> 4 files changed, 25 insertions(+), 11 deletions(-)
>
>diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
>index faa398109431..75c9f9a3f884 100644
>--- a/drivers/usb/gadget/function/f_uvc.c
>+++ b/drivers/usb/gadget/function/f_uvc.c
>@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
> return 0;
> }
>
>-void uvc_function_setup_continue(struct uvc_device *uvc)
>+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
> {
> struct usb_composite_dev *cdev = uvc->func.config->cdev;
>
>+ if (disable_ep && uvc->video.ep) {
>+ usb_ep_disable(uvc->video.ep);
>+ }

Could you drop the extra braces and add one spare line here.

> usb_composite_setup_continue(cdev);
> }
>
>@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
> if (uvc->state != UVC_STATE_STREAMING)
> return 0;
>
>- if (uvc->video.ep)
>- usb_ep_disable(uvc->video.ep);
>-
> memset(&v4l2_event, 0, sizeof(v4l2_event));
> v4l2_event.type = UVC_EVENT_STREAMOFF;
> v4l2_event_queue(&uvc->vdev, &v4l2_event);
>
>- uvc->state = UVC_STATE_CONNECTED;
>- return 0;
>+ return USB_GADGET_DELAYED_STATUS;
>
> case 1:
> if (uvc->state != UVC_STATE_CONNECTED)
>diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
>index 1db972d4beeb..e7f9f13f14dc 100644
>--- a/drivers/usb/gadget/function/f_uvc.h
>+++ b/drivers/usb/gadget/function/f_uvc.h
>@@ -11,7 +11,7 @@
>
> struct uvc_device;
>
>-void uvc_function_setup_continue(struct uvc_device *uvc);
>+void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);
>
> void uvc_function_connect(struct uvc_device *uvc);
>
>diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
>index 6751de8b63ad..989bc6b4e93d 100644
>--- a/drivers/usb/gadget/function/uvc.h
>+++ b/drivers/usb/gadget/function/uvc.h
>@@ -177,7 +177,7 @@ struct uvc_file_handle {
> * Functions
> */
>
>-extern void uvc_function_setup_continue(struct uvc_device *uvc);
>+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
> extern void uvc_function_connect(struct uvc_device *uvc);
> extern void uvc_function_disconnect(struct uvc_device *uvc);
>
>diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
>index 3f0a9795c0d4..3d3469883ed0 100644
>--- a/drivers/usb/gadget/function/uvc_v4l2.c
>+++ b/drivers/usb/gadget/function/uvc_v4l2.c
>@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
> * Complete the alternate setting selection setup phase now that
> * userspace is ready to provide video frames.
> */
>- uvc_function_setup_continue(uvc);
>+ uvc_function_setup_continue(uvc, 0);
> uvc->state = UVC_STATE_STREAMING;
>
> return 0;
>@@ -463,11 +463,19 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
> struct video_device *vdev = video_devdata(file);
> struct uvc_device *uvc = video_get_drvdata(vdev);
> struct uvc_video *video = &uvc->video;
>+ int ret = 0;
>
> if (type != video->queue.queue.type)
> return -EINVAL;
>
>- return uvcg_video_enable(video, 0);
>+ uvc->state = UVC_STATE_CONNECTED;
>+ ret = uvcg_video_enable(video, 0);
>+ if (ret < 0) {
>+ return ret;
>+ }

Please drop those extra braces.

>+
>+ uvc_function_setup_continue(uvc, 1);
>+ return 0;
> }
>
> static int
>@@ -500,6 +508,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
> static void uvc_v4l2_disable(struct uvc_device *uvc)
> {
> uvc_function_disconnect(uvc);
>+ if (uvc->state == UVC_STATE_STREAMING) {
>+ /*
>+ * Drop uvc->state to CONNECTED if it was streaming before.
>+ * This ensures that the usb_requests are no longer queued
>+ * to the controller.
>+ */
>+ uvc->state = UVC_STATE_CONNECTED;
>+ }

Could you write the comment above the check
and also remove the extra braces.

> uvcg_video_enable(&uvc->video, 0);
> uvcg_free_buffers(&uvc->video.queue);
> uvc->func_connected = false;
>@@ -647,4 +663,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
> .get_unmapped_area = uvcg_v4l2_get_unmapped_area,
> #endif
> };
>-
>--
>2.42.0.609.gbb76f46606-goog


With this you can add my:

Reviewed-by: <[email protected]>

Thanks

--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (5.75 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-06 22:53:38

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

On Fri, Oct 06, 2023 at 10:00:11AM -0700, Avichal Rakesh wrote:
>
>
>On 10/5/23 15:05, Michael Grzeschik wrote:
>> Hi Avichal,
>>
>> On Thu, Oct 05, 2023 at 11:30:32AM -0700, Avichal Rakesh wrote:
>>> On 10/5/23 03:14, Michael Grzeschik wrote:
>>>> On Thu, Oct 05, 2023 at 11:23:27AM +0300, Laurent Pinchart wrote:
>>>>> On Tue, Oct 03, 2023 at 01:09:06PM +0200, Michael Grzeschik wrote:
>>>>>> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>>>>>> > We have been seeing two main stability issues that uvc gadget driver
>>>>>> > runs into when stopping streams:
>>>>>> >? 1. Attempting to queue usb_requests to a disabled usb_ep
>>>>>> >? 2. use-after-free issue for inflight usb_requests
>>>>>> >
>>>>>> > The three patches below fix the two issues above. Patch 1/3 fixes the
>>>>>> > first issue, and Patch 2/3 and 3/3 fix the second issue.
>>>>>> >
>>>>>> > Avichal Rakesh (3):
>>>>>> >?? usb: gadget: uvc: prevent use of disabled endpoint
>>>>>> >?? usb: gadget: uvc: Allocate uvc_requests one at a time
>>>>>> >?? usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>>>>>> >
>>>>>> > drivers/usb/gadget/function/f_uvc.c???? |? 11 +-
>>>>>> > drivers/usb/gadget/function/f_uvc.h???? |?? 2 +-
>>>>>> > drivers/usb/gadget/function/uvc.h?????? |?? 6 +-
>>>>>> > drivers/usb/gadget/function/uvc_v4l2.c? |? 21 ++-
>>>>>> > drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>>>>>> > 5 files changed, 164 insertions(+), 65 deletions(-)
>>>>>>
>>>>>> These patches are not applying on gregkh/usb-testing since
>>>>>> Greg did take my patches first. I have already rebased them.
>>>>>
>>>>> I think they got merged too soon :-( We could fix things on top, but
>>>>> there's very little time to do so for v6.7.
>>>>
>>>> Agreed. I was jumping from one workaround to another one, since this
>>>> is not easy to fix in a proper way. And still after this long discussion
>>>> with Avichal I don't think we are there yet.
>>>>
>>>>
>>>> So far the first two patches from Avichal look legit. But the overall
>>>> Use-After-Free fix is yet to be done properly.
>>>>
>>>> The "abondoned" method he suggested is really bad to follow and will
>>>> add too much complexity and will be hard to debug.
>>>>
>>>> IMHO it should be possible to introduce two cleanup pathes.
>>>>
>>>> One path would be in the uvc_cleanup_requests that will cleanup the
>>>> requests that are actually not used in the controller and are registered
>>>> in the req_free list.
>>>>
>>>> The second path would be the complete functions that are being run
>>>> from the controller and will ensure that the cleanup will really free
>>>> the requests from the controller after they were consumed.
>>>>
>>>> What do you think?
>>>
>>> I am not sure I follow. Patch 3/3 does exactly what you say here.
>>
>> Yes, it was just to summ up what the latest state of the idea was,
>> so Laurent does not read the whole thread in detail. Sorry for not
>> being clear enough about that.
>
>Whoops! Sorry about the misunderstanding!
>
>>
>>> There are two cleanup paths:
>>> ?1. uvcg_video_disable cleans up only the requests in req_free, and
>>> ?2. complete handler cleans up the in-flight requests.
>>>
>>> The "abandoned" flag is simply to let the completion handler know
>>> which requests to clean up and which ones to re-queue back to
>>> the gadget driver.
>>
>> What I don't get is, why in the case of shutdown there needs to
>> be something re-queued back to the gadget driver. There should not
>> need to be any sort of barrier flag for the requests. Just the
>> complete handler running past a barrier where it knows that the
>> whole device is stopped. So every call on complete should then clean
>> that exact request it is touching currently.
>>
>> I don't know where the extra complexity comes from.
>
>A lot of this complexity comes from assuming a back to back
>STREAMOFF -> STREAMON sequence is possible where the gadget driver
>doesn't have the time to clean up all in-flight usb_requests.
>However, looking through the usb gadget APIs again, and it
>looks like usb_ep_disable enforces that all requests will
>be sent back to the gadget driver before it returns.

Great!

>So you're right:
>With Patch 1/3 in place, I think we can just guard on uvc->state
>alone, because control requests are blocked until usb_ep_disable
>is finished anyway. I'll upload v4 with the "is_abandoned"
>flag removed and the checks simplified once I've verified the
>fix locally.
>
>That should also remove any bookkeeping issues that may have
>triggered the stack below.

I am currious if we should handle -ECONNRESET and -ESHUTDOWN in more
detail in the completion handler and make sure that the request will not
be added into the req_free list from there.

Will review your v4 then.

>>> The other "complications" are around making sure we can trust
>>> the values in an inherently racey situation. The reasoning
>>> can admittedly be difficult to follow at a glance, which incidentally
>>> is why I went with a simple to prove timed wait in the past
>>> (https://lore.kernel.org/[email protected]).
>>>
>>> I am not suggesting we go back to a timed wait, but please do look
>>> at the patch and let me know which parts don't make sense, or are
>>> difficult to understand. We can add more documentation about our
>>> assumptions there, or if you have a way to do this that you
>>> think is simpler to reason about, then please let me know and I'll
>>> be more than happy to use that!
>>
>> I really try to spin my head around the idea of the is_abondoned flag
>> you are using. Unfortunatly for now I am out to debug the issues I see
>> with your series.
>>
>> So I did try these patches you send. Yes the deadlock error is gone with
>> v3. But the linked list is still running into cases where
>> dwc3_gadget_giveback(complete) is touching requests that are already
>> freed.
>>
>> [?? 61.408715] ------------[ cut here ]------------
>> [?? 61.413897] kernel BUG at lib/list_debug.c:56!
>> ...
>> [?? 61.590762] Call trace:
>> [?? 61.596890]? __list_del_entry_valid+0xb8/0xe8
>> [?? 61.603408]? dwc3_gadget_giveback+0x3c/0x1b0
>> [?? 61.607594]? dwc3_remove_requests.part.0+0xcc/0x100
>> [?? 61.612948]? __dwc3_gadget_ep_disable+0xbc/0x1b8
>> [?? 61.621019]? dwc3_gadget_ep_disable+0x48/0x100
>> [?? 61.627925]? usb_ep_disable+0x3c/0x138
>> [?? 61.638230]? uvc_function_setup_continue+0x3c/0x60
>> [?? 61.645040]? uvc_v4l2_streamoff+0x5c/0x80
>> [?? 61.659812]? v4l_streamoff+0x40/0x60
>> [?? 61.668950]? __video_do_ioctl+0x344/0x420
>> [?? 61.679548]? video_usercopy+0x1d0/0x788
>> [?? 61.685677]? video_ioctl2+0x40/0x70
>> [?? 61.697439]? v4l2_ioctl+0x68/0xa0
>> [?? 61.709200]? __arm64_sys_ioctl+0x304/0xda0
>> [?? 61.720768]? invoke_syscall.constprop.0+0x70/0x130
>>
>
>

--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (7.09 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-06 23:48:38

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.



On 10/6/23 15:53, Michael Grzeschik wrote:
> On Fri, Oct 06, 2023 at 10:00:11AM -0700, Avichal Rakesh wrote:
>>
>>
>> On 10/5/23 15:05, Michael Grzeschik wrote:
>>> Hi Avichal,
>>>
>>> On Thu, Oct 05, 2023 at 11:30:32AM -0700, Avichal Rakesh wrote:
>>>> On 10/5/23 03:14, Michael Grzeschik wrote:
>>>>> On Thu, Oct 05, 2023 at 11:23:27AM +0300, Laurent Pinchart wrote:
>>>>>> On Tue, Oct 03, 2023 at 01:09:06PM +0200, Michael Grzeschik wrote:
>>>>>>> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>>>>>>> > We have been seeing two main stability issues that uvc gadget driver
>>>>>>> > runs into when stopping streams:
>>>>>>> >  1. Attempting to queue usb_requests to a disabled usb_ep
>>>>>>> >  2. use-after-free issue for inflight usb_requests
>>>>>>> >
>>>>>>> > The three patches below fix the two issues above. Patch 1/3 fixes the
>>>>>>> > first issue, and Patch 2/3 and 3/3 fix the second issue.
>>>>>>> >
>>>>>>> > Avichal Rakesh (3):
>>>>>>> >   usb: gadget: uvc: prevent use of disabled endpoint
>>>>>>> >   usb: gadget: uvc: Allocate uvc_requests one at a time
>>>>>>> >   usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>>>>>>> >
>>>>>>> > drivers/usb/gadget/function/f_uvc.c     |  11 +-
>>>>>>> > drivers/usb/gadget/function/f_uvc.h     |   2 +-
>>>>>>> > drivers/usb/gadget/function/uvc.h       |   6 +-
>>>>>>> > drivers/usb/gadget/function/uvc_v4l2.c  |  21 ++-
>>>>>>> > drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>>>>>>> > 5 files changed, 164 insertions(+), 65 deletions(-)
>>>>>>>
>>>>>>> These patches are not applying on gregkh/usb-testing since
>>>>>>> Greg did take my patches first. I have already rebased them.
>>>>>>
>>>>>> I think they got merged too soon :-( We could fix things on top, but
>>>>>> there's very little time to do so for v6.7.
>>>>>
>>>>> Agreed. I was jumping from one workaround to another one, since this
>>>>> is not easy to fix in a proper way. And still after this long discussion
>>>>> with Avichal I don't think we are there yet.
>>>>>
>>>>>
>>>>> So far the first two patches from Avichal look legit. But the overall
>>>>> Use-After-Free fix is yet to be done properly.
>>>>>
>>>>> The "abondoned" method he suggested is really bad to follow and will
>>>>> add too much complexity and will be hard to debug.
>>>>>
>>>>> IMHO it should be possible to introduce two cleanup pathes.
>>>>>
>>>>> One path would be in the uvc_cleanup_requests that will cleanup the
>>>>> requests that are actually not used in the controller and are registered
>>>>> in the req_free list.
>>>>>
>>>>> The second path would be the complete functions that are being run
>>>>> from the controller and will ensure that the cleanup will really free
>>>>> the requests from the controller after they were consumed.
>>>>>
>>>>> What do you think?
>>>>
>>>> I am not sure I follow. Patch 3/3 does exactly what you say here.
>>>
>>> Yes, it was just to summ up what the latest state of the idea was,
>>> so Laurent does not read the whole thread in detail. Sorry for not
>>> being clear enough about that.
>>
>> Whoops! Sorry about the misunderstanding!
>>
>>>
>>>> There are two cleanup paths:
>>>>  1. uvcg_video_disable cleans up only the requests in req_free, and
>>>>  2. complete handler cleans up the in-flight requests.
>>>>
>>>> The "abandoned" flag is simply to let the completion handler know
>>>> which requests to clean up and which ones to re-queue back to
>>>> the gadget driver.
>>>
>>> What I don't get is, why in the case of shutdown there needs to
>>> be something re-queued back to the gadget driver. There should not
>>> need to be any sort of barrier flag for the requests. Just the
>>> complete handler running past a barrier where it knows that the
>>> whole device is stopped. So every call on complete should then clean
>>> that exact request it is touching currently.
>>>
>>> I don't know where the extra complexity comes from.
>>
>> A lot of this complexity comes from assuming a back to back
>> STREAMOFF -> STREAMON sequence is possible where the gadget driver
>> doesn't have the time to clean up all in-flight usb_requests.
>> However, looking through the usb gadget APIs again, and it
>> looks like  usb_ep_disable enforces that all requests will
>> be sent back to the gadget driver before it returns.
>
> Great!

Uhh...apologies, I will have to take this back. I've been
trying to use uvc->state as the condition for when completion
handler should clean up usb_requests, and I cannot figure
out a way to do so cleanly.

The fundamental problem with using uvc->state is that it is
not protected by any locks. So there is no real way to
assert that its value has not changed between reading
uvc->state and acting on it.

Naively we can write something like the following in the
completion handler:

void uvc_video_complete(...) {
if (uvc->state != UVC_EVENT_STREAMING) {
usb_ep_free_request(....);
} else {
// handle usb_request normally
}
}

But without any locks, there are no guarantees that
uvc->state didn't mutate immediately after the if
condition was checked, and the complete handler is
handling a request that it should've freed instead
or vice-versa. This argument would hold for any logic
we guard with uvc->state, making uvc->state effectively
useless as a check for freeing memory.

We can work around it by either
1. Locking uvc->state with some driver level lock
to ensure that we can trust the value of uvc->state
at least for a little while, or
2. Using some other barrier condition that is protected by
another lock

If we go with (1), we'd have to add a lock around every
and every write to uvc->state, which isn't terrible, but
would require more testing to ensure that it doesn't
create any new deadlocks.

For (2), with the realization that usb_ep_disable flushes
all requests, we can add a barrier in uvc_video, protected by
req_lock. That should simplify the logic a little bit and
will hopefully be easier to reason about.

I could of course be missing a simpler solution here,
and am happy to be wrong. So please let me know if you
have any other ideas on how to guarantee such a check.


>
>> So you're right:
>> With Patch 1/3 in place, I think we can just guard on uvc->state
>> alone, because control requests are blocked until usb_ep_disable
>> is finished anyway. I'll upload v4 with the "is_abandoned"
>> flag removed and the checks simplified once I've verified the
>> fix locally.
>>
>> That should also remove any bookkeeping issues that may have
>> triggered the stack below.
>
> I am currious if we should handle -ECONNRESET and -ESHUTDOWN in more
> detail in the completion handler and make sure that the request will not
> be added into the req_free list from there.
>
> Will review your v4 then.

Appreciate the reviews, thank you!

>
>>>> The other "complications" are around making sure we can trust
>>>> the values in an inherently racey situation. The reasoning
>>>> can admittedly be difficult to follow at a glance, which incidentally
>>>> is why I went with a simple to prove timed wait in the past
>>>> (https://lore.kernel.org/[email protected]).
>>>>
>>>> I am not suggesting we go back to a timed wait, but please do look
>>>> at the patch and let me know which parts don't make sense, or are
>>>> difficult to understand. We can add more documentation about our
>>>> assumptions there, or if you have a way to do this that you
>>>> think is simpler to reason about, then please let me know and I'll
>>>> be more than happy to use that!
>>>
>>> I really try to spin my head around the idea of the is_abondoned flag
>>> you are using. Unfortunatly for now I am out to debug the issues I see
>>> with your series.
>>>
>>> So I did try these patches you send. Yes the deadlock error is gone with
>>> v3. But the linked list is still running into cases where
>>> dwc3_gadget_giveback(complete) is touching requests that are already
>>> freed.
>>>
>>> [   61.408715] ------------[ cut here ]------------
>>> [   61.413897] kernel BUG at lib/list_debug.c:56!
>>> ...
>>> [   61.590762] Call trace:
>>> [   61.596890]  __list_del_entry_valid+0xb8/0xe8
>>> [   61.603408]  dwc3_gadget_giveback+0x3c/0x1b0
>>> [   61.607594]  dwc3_remove_requests.part.0+0xcc/0x100
>>> [   61.612948]  __dwc3_gadget_ep_disable+0xbc/0x1b8
>>> [   61.621019]  dwc3_gadget_ep_disable+0x48/0x100
>>> [   61.627925]  usb_ep_disable+0x3c/0x138
>>> [   61.638230]  uvc_function_setup_continue+0x3c/0x60
>>> [   61.645040]  uvc_v4l2_streamoff+0x5c/0x80
>>> [   61.659812]  v4l_streamoff+0x40/0x60
>>> [   61.668950]  __video_do_ioctl+0x344/0x420
>>> [   61.679548]  video_usercopy+0x1d0/0x788
>>> [   61.685677]  video_ioctl2+0x40/0x70
>>> [   61.697439]  v4l2_ioctl+0x68/0xa0
>>> [   61.709200]  __arm64_sys_ioctl+0x304/0xda0
>>> [   61.720768]  invoke_syscall.constprop.0+0x70/0x130

Just to confirm: this stack was with all 3 patches applied?
Want to make sure this won't happen with v4.



Regards,
Avi.

2023-10-08 19:48:29

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.

On Fri, Oct 06, 2023 at 04:48:19PM -0700, Avichal Rakesh wrote:
>On 10/6/23 15:53, Michael Grzeschik wrote:
>> On Fri, Oct 06, 2023 at 10:00:11AM -0700, Avichal Rakesh wrote:
>>>
>>>
>>> On 10/5/23 15:05, Michael Grzeschik wrote:
>>>> Hi Avichal,
>>>>
>>>> On Thu, Oct 05, 2023 at 11:30:32AM -0700, Avichal Rakesh wrote:
>>>>> On 10/5/23 03:14, Michael Grzeschik wrote:
>>>>>> On Thu, Oct 05, 2023 at 11:23:27AM +0300, Laurent Pinchart wrote:
>>>>>>> On Tue, Oct 03, 2023 at 01:09:06PM +0200, Michael Grzeschik wrote:
>>>>>>>> On Sat, Sep 30, 2023 at 11:48:18AM -0700, Avichal Rakesh wrote:
>>>>>>>> > We have been seeing two main stability issues that uvc gadget driver
>>>>>>>> > runs into when stopping streams:
>>>>>>>> >? 1. Attempting to queue usb_requests to a disabled usb_ep
>>>>>>>> >? 2. use-after-free issue for inflight usb_requests
>>>>>>>> >
>>>>>>>> > The three patches below fix the two issues above. Patch 1/3 fixes the
>>>>>>>> > first issue, and Patch 2/3 and 3/3 fix the second issue.
>>>>>>>> >
>>>>>>>> > Avichal Rakesh (3):
>>>>>>>> >?? usb: gadget: uvc: prevent use of disabled endpoint
>>>>>>>> >?? usb: gadget: uvc: Allocate uvc_requests one at a time
>>>>>>>> >?? usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>>>>>>>> >
>>>>>>>> > drivers/usb/gadget/function/f_uvc.c???? |? 11 +-
>>>>>>>> > drivers/usb/gadget/function/f_uvc.h???? |?? 2 +-
>>>>>>>> > drivers/usb/gadget/function/uvc.h?????? |?? 6 +-
>>>>>>>> > drivers/usb/gadget/function/uvc_v4l2.c? |? 21 ++-
>>>>>>>> > drivers/usb/gadget/function/uvc_video.c | 189 +++++++++++++++++-------
>>>>>>>> > 5 files changed, 164 insertions(+), 65 deletions(-)
>>>>>>>>
>>>>>>>> These patches are not applying on gregkh/usb-testing since
>>>>>>>> Greg did take my patches first. I have already rebased them.
>>>>>>>
>>>>>>> I think they got merged too soon :-( We could fix things on top, but
>>>>>>> there's very little time to do so for v6.7.
>>>>>>
>>>>>> Agreed. I was jumping from one workaround to another one, since this
>>>>>> is not easy to fix in a proper way. And still after this long discussion
>>>>>> with Avichal I don't think we are there yet.
>>>>>>
>>>>>>
>>>>>> So far the first two patches from Avichal look legit. But the overall
>>>>>> Use-After-Free fix is yet to be done properly.
>>>>>>
>>>>>> The "abondoned" method he suggested is really bad to follow and will
>>>>>> add too much complexity and will be hard to debug.
>>>>>>
>>>>>> IMHO it should be possible to introduce two cleanup pathes.
>>>>>>
>>>>>> One path would be in the uvc_cleanup_requests that will cleanup the
>>>>>> requests that are actually not used in the controller and are registered
>>>>>> in the req_free list.
>>>>>>
>>>>>> The second path would be the complete functions that are being run
>>>>>> from the controller and will ensure that the cleanup will really free
>>>>>> the requests from the controller after they were consumed.
>>>>>>
>>>>>> What do you think?
>>>>>
>>>>> I am not sure I follow. Patch 3/3 does exactly what you say here.
>>>>
>>>> Yes, it was just to summ up what the latest state of the idea was,
>>>> so Laurent does not read the whole thread in detail. Sorry for not
>>>> being clear enough about that.
>>>
>>> Whoops! Sorry about the misunderstanding!
>>>
>>>>
>>>>> There are two cleanup paths:
>>>>> ?1. uvcg_video_disable cleans up only the requests in req_free, and
>>>>> ?2. complete handler cleans up the in-flight requests.
>>>>>
>>>>> The "abandoned" flag is simply to let the completion handler know
>>>>> which requests to clean up and which ones to re-queue back to
>>>>> the gadget driver.
>>>>
>>>> What I don't get is, why in the case of shutdown there needs to
>>>> be something re-queued back to the gadget driver. There should not
>>>> need to be any sort of barrier flag for the requests. Just the
>>>> complete handler running past a barrier where it knows that the
>>>> whole device is stopped. So every call on complete should then clean
>>>> that exact request it is touching currently.
>>>>
>>>> I don't know where the extra complexity comes from.
>>>
>>> A lot of this complexity comes from assuming a back to back
>>> STREAMOFF -> STREAMON sequence is possible where the gadget driver
>>> doesn't have the time to clean up all in-flight usb_requests.
>>> However, looking through the usb gadget APIs again, and it
>>> looks like? usb_ep_disable enforces that all requests will
>>> be sent back to the gadget driver before it returns.
>>
>> Great!
>
>Uhh...apologies, I will have to take this back. I've been
>trying to use uvc->state as the condition for when completion
>handler should clean up usb_requests, and I cannot figure
>out a way to do so cleanly.
>
>The fundamental problem with using uvc->state is that it is
>not protected by any locks. So there is no real way to
>assert that its value has not changed between reading
>uvc->state and acting on it.
>
>Naively we can write something like the following in the
>completion handler:
>
>void uvc_video_complete(...) {
> if (uvc->state != UVC_EVENT_STREAMING) {
> usb_ep_free_request(....);
> } else {
> // handle usb_request normally
> }
>}
>
>But without any locks, there are no guarantees that
>uvc->state didn't mutate immediately after the if
>condition was checked, and the complete handler is
>handling a request that it should've freed instead
>or vice-versa. This argument would hold for any logic
>we guard with uvc->state, making uvc->state effectively
>useless as a check for freeing memory.

Yes, this makes total sense. Since the above condition was also part of
the wait_event patch you created in the first place, I bet this issue
was there aswell and was probably causing the issues I saw while testing
it.


>We can work around it by either
>1. Locking uvc->state with some driver level lock
> to ensure that we can trust the value of uvc->state
> at least for a little while, or
>2. Using some other barrier condition that is protected by
> another lock
>
>If we go with (1), we'd have to add a lock around every
>and every write to uvc->state, which isn't terrible, but
>would require more testing to ensure that it doesn't
>create any new deadlocks.
>
>For (2), with the realization that usb_ep_disable flushes
>all requests, we can add a barrier in uvc_video, protected by
>req_lock. That should simplify the logic a little bit and
>will hopefully be easier to reason about.
>
>I could of course be missing a simpler solution here,
>and am happy to be wrong. So please let me know if you
>have any other ideas on how to guarantee such a check.

For now, I have no better Idea. Idea (2) sounds like
a good compromise. But I will have to review that code
to really judge.

Thanks for the work!

Michael


--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (7.06 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-12 00:25:04

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v4 1/3] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. As the endpoint's state can no longer be used, video_pump is
now guarded by uvc->state as well. To be consistent with the actual
streaming state, uvc->state is now toggled between CONNECTED and STREAMING
from the v4l2 event callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Reviewed-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT and reworded commit message.
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 21 ++++++++++++++++++---
drivers/usb/gadget/function/uvc_video.c | 3 ++-
5 files changed, 27 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index faa398109431..ae08341961eb 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep)
+ usb_ep_disable(uvc->video.ep);
+
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..e7f9f13f14dc 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..c0d77564a204 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,19 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0) {
+ return ret;
+ }
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +508,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ if (uvc->state == UVC_STATE_STREAMING)
+ uvc->state = UVC_STATE_CONNECTED;
+
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +663,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 91af3b1ef0d4..c334802ac0a4 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
+ struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (video->ep->enabled) {
+ while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
/*
* Retrieve the first available USB request, protected by the
* request lock.
--
2.42.0.609.gbb76f46606-goog

2023-10-12 00:25:08

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v4 2/3] usb: gadget: uvc: Allocate uvc_requests one at a time

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This patch is 1 of 2 patches addressing the use-after-free issue.
Instead of bulk allocating all uvc_requests as an array, this patch
allocates uvc_requests one at a time, which should allows for similar
granularity when deallocating the uvc_requests. This patch has no
functional changes other than allocating each uvc_request separately,
and similarly freeing each of them separately.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT

drivers/usb/gadget/function/uvc.h | 3 +-
drivers/usb/gadget/function/uvc_video.c | 87 ++++++++++++++-----------
2 files changed, 50 insertions(+), 40 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 989bc6b4e93d..993694da0bbc 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -81,6 +81,7 @@ struct uvc_request {
struct sg_table sgt;
u8 header[UVCG_REQUEST_HEADER_LEN];
struct uvc_buffer *last_buf;
+ struct list_head list;
};

struct uvc_video {
@@ -102,7 +103,7 @@ struct uvc_video {

/* Requests */
unsigned int req_size;
- struct uvc_request *ureq;
+ struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
spinlock_t req_lock;

diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c334802ac0a4..b62b3de79153 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+static void
+uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
+{
+ sg_free_table(&ureq->sgt);
+ if (ureq->req && ep) {
+ usb_ep_free_request(ep, ureq->req);
+ ureq->req = NULL;
+ }
+
+ kfree(ureq->req_buffer);
+ ureq->req_buffer = NULL;
+
+ if (!list_empty(&ureq->list))
+ list_del_init(&ureq->list);
+
+ kfree(ureq);
+}
+
static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
{
int ret;
@@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
static int
uvc_video_free_requests(struct uvc_video *video)
{
- unsigned int i;
+ struct uvc_request *ureq, *temp;

- if (video->ureq) {
- for (i = 0; i < video->uvc_num_requests; ++i) {
- sg_free_table(&video->ureq[i].sgt);
-
- if (video->ureq[i].req) {
- usb_ep_free_request(video->ep, video->ureq[i].req);
- video->ureq[i].req = NULL;
- }
-
- if (video->ureq[i].req_buffer) {
- kfree(video->ureq[i].req_buffer);
- video->ureq[i].req_buffer = NULL;
- }
- }
-
- kfree(video->ureq);
- video->ureq = NULL;
- }
+ list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
+ uvc_video_free_request(ureq, video->ep);

+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
video->req_size = 0;
return 0;
@@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
static int
uvc_video_alloc_requests(struct uvc_video *video)
{
+ struct uvc_request *ureq;
unsigned int req_size;
unsigned int i;
int ret = -ENOMEM;
@@ -332,29 +336,32 @@ uvc_video_alloc_requests(struct uvc_video *video)
* max_t(unsigned int, video->ep->maxburst, 1)
* (video->ep->mult);

- video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
- if (video->ureq == NULL)
- return -ENOMEM;
+ INIT_LIST_HEAD(&video->ureqs);
+ for (i = 0; i < video->uvc_num_requests; i++) {
+ ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
+ if (ureq == NULL)
+ goto error;
+ INIT_LIST_HEAD(&ureq->list);
+ list_add_tail(&ureq->list, &video->ureqs);

- for (i = 0; i < video->uvc_num_requests; ++i) {
- video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
- if (video->ureq[i].req_buffer == NULL)
+ ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
+ if (ureq->req_buffer == NULL)
goto error;

- video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
- if (video->ureq[i].req == NULL)
+ ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
+ if (ureq->req == NULL)
goto error;

- video->ureq[i].req->buf = video->ureq[i].req_buffer;
- video->ureq[i].req->length = 0;
- video->ureq[i].req->complete = uvc_video_complete;
- video->ureq[i].req->context = &video->ureq[i];
- video->ureq[i].video = video;
- video->ureq[i].last_buf = NULL;
+ ureq->req->buf = ureq->req_buffer;
+ ureq->req->length = 0;
+ ureq->req->complete = uvc_video_complete;
+ ureq->req->context = ureq;
+ ureq->video = video;
+ ureq->last_buf = NULL;

- list_add_tail(&video->ureq[i].req->list, &video->req_free);
+ list_add_tail(&ureq->req->list, &video->req_free);
/* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
- sg_alloc_table(&video->ureq[i].sgt,
+ sg_alloc_table(&ureq->sgt,
DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
PAGE_SIZE) + 2, GFP_KERNEL);
}
@@ -489,8 +496,8 @@ static void uvcg_video_pump(struct work_struct *work)
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
- unsigned int i;
int ret;
+ struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -502,9 +509,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- for (i = 0; i < video->uvc_num_requests; ++i)
- if (video->ureq && video->ureq[i].req)
- usb_ep_dequeue(video->ep, video->ureq[i].req);
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

uvc_video_free_requests(video);
uvcg_queue_enable(&video->queue, 0);
@@ -536,6 +544,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
INIT_WORK(&video->pump, uvcg_video_pump);
--
2.42.0.609.gbb76f46606-goog

2023-10-12 00:25:36

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v4 3/3] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
flag to uvc_video to track when frames and requests should be flowing.
When disabling the video stream, the flag is tripped and, instead
of de-allocating all uvc_requests and usb_requests, the gadget
driver only de-allocates those usb_requests that are currently
owned by it (as present in req_free). Other usb_requests are left
untouched until their completion handler is called which takes care
of freeing the usb_request and its corresponding uvc_request.

Now that uvc_video does not depends on uvc->state, this patch removes
unnecessary upates to uvc->state that were made to accomodate uvc_video
logic. This should ensure that uvc gadget driver never accidentally
de-allocates a usb_request that it doesn't own.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT, and fixed deadlock reported in
https://lore.kernel.org/all/[email protected]/
v2 -> v3: Fix email threading goof-up
v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
as discussed in
https://lore.kernel.org/[email protected]/

drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_v4l2.c | 12 +-
drivers/usb/gadget/function/uvc_video.c | 156 +++++++++++++++++++-----
3 files changed, 128 insertions(+), 41 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 993694da0bbc..be0d012aa244 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -102,6 +102,7 @@ struct uvc_video {
unsigned int uvc_num_requests;

/* Requests */
+ bool is_enabled; /* tracks whether video stream is enabled */
unsigned int req_size;
struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index c0d77564a204..ded7d33c2a52 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;
+ uvc_function_setup_continue(uvc, 0);

return 0;
}
@@ -468,12 +468,12 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
if (type != video->queue.queue.type)
return -EINVAL;

- uvc->state = UVC_STATE_CONNECTED;
ret = uvcg_video_enable(video, 0);
if (ret < 0) {
return ret;
}

+ uvc->state = UVC_STATE_CONNECTED;
uvc_function_setup_continue(uvc, 1);
return 0;
}
@@ -508,14 +508,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
- /*
- * Drop uvc->state to CONNECTED if it was streaming before.
- * This ensures that the usb_requests are no longer queued
- * to the controller.
- */
- if (uvc->state == UVC_STATE_STREAMING)
- uvc->state = UVC_STATE_CONNECTED;
-
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index b62b3de79153..05b89b5b6c48 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+/**
+ * Must be called with req_lock held as it modifies the list ureq is held in
+ */
static void
uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
{
@@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
struct uvc_request *ureq = req->context;
struct uvc_video *video = ureq->video;
struct uvc_video_queue *queue = &video->queue;
- struct uvc_device *uvc = video->uvc;
+ struct uvc_buffer *last_buf = NULL;
unsigned long flags;

+ spin_lock_irqsave(&video->req_lock, flags);
+ if (!video->is_enabled) {
+ /*
+ * When is_enabled is false, uvc_video_disable ensures that
+ * in-flight uvc_buffers are returned, so we can safely
+ * call free_request without worrying about last_buf.
+ */
+ uvc_video_free_request(ureq, ep);
+ spin_unlock_irqrestore(&video->req_lock, flags);
+ return;
+ }
+
+ last_buf = ureq->last_buf;
+ ureq->last_buf = NULL;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
switch (req->status) {
case 0:
break;
@@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
uvcg_queue_cancel(queue, 0);
}

- if (ureq->last_buf) {
- uvcg_complete_buffer(&video->queue, ureq->last_buf);
- ureq->last_buf = NULL;
+ if (last_buf) {
+ spin_lock_irqsave(&queue->irqlock, flags);
+ uvcg_complete_buffer(&video->queue, last_buf);
+ spin_unlock_irqrestore(&queue->irqlock, flags);
}

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
- spin_unlock_irqrestore(&video->req_lock, flags);
-
- if (uvc->state == UVC_STATE_STREAMING)
+ /*
+ * Video stream might have been disabled while we were
+ * processing the current usb_request. So make sure
+ * we're still streaming before queueing the usb_request
+ * back to req_free
+ */
+ if (video->is_enabled) {
+ list_add_tail(&req->list, &video->req_free);
queue_work(video->async_wq, &video->pump);
+ } else {
+ uvc_video_free_request(ureq, ep);
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
}

static int
@@ -391,20 +419,22 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
- struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
+ while(true) {
+ if (!video->ep->enabled)
+ return;
+
/*
- * Retrieve the first available USB request, protected by the
- * request lock.
+ * Check is_enabled and retrieve the first available USB
+ * request, protected by the request lock.
*/
spin_lock_irqsave(&video->req_lock, flags);
- if (list_empty(&video->req_free)) {
+ if (!video->is_enabled || list_empty(&video->req_free)) {
spin_unlock_irqrestore(&video->req_lock, flags);
return;
}
@@ -486,9 +516,78 @@ static void uvcg_video_pump(struct work_struct *work)
return;

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
+ if (video->is_enabled)
+ list_add_tail(&req->list, &video->req_free);
+ else
+ uvc_video_free_request(req->context, video->ep);
+ spin_unlock_irqrestore(&video->req_lock, flags);
+}
+
+/*
+ * Disable video stream
+ */
+static int
+uvcg_video_disable(struct uvc_video *video) {
+ unsigned long flags;
+ struct list_head inflight_bufs;
+ struct usb_request *req, *temp;
+ struct uvc_buffer *buf, *btemp;
+ struct uvc_request *ureq, *utemp;
+
+ INIT_LIST_HEAD(&inflight_bufs);
+ spin_lock_irqsave(&video->req_lock, flags);
+ video->is_enabled = false;
+
+ /*
+ * Remove any in-flight buffers from the uvc_requests
+ * because we want to return them before cancelling the
+ * queue. This ensures that we aren't stuck waiting for
+ * all complete callbacks to come through before disabling
+ * vb2 queue.
+ */
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->last_buf) {
+ list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
+ ureq->last_buf = NULL;
+ }
+ }
spin_unlock_irqrestore(&video->req_lock, flags);
- return;
+
+ cancel_work_sync(&video->pump);
+ uvcg_queue_cancel(&video->queue, 0);
+
+ spin_lock_irqsave(&video->req_lock, flags);
+ /*
+ * Remove all uvc_reqeusts from from ureqs with list_del_init
+ * This lets uvc_video_free_request correctly identify
+ * if the uvc_request is attached to a list or not when freeing
+ * memory.
+ */
+ list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
+ list_del_init(&ureq->list);
+
+ list_for_each_entry_safe(req, temp, &video->req_free, list) {
+ list_del(&req->list);
+ uvc_video_free_request(req->context, video->ep);
+ }
+
+ INIT_LIST_HEAD(&video->ureqs);
+ INIT_LIST_HEAD(&video->req_free);
+ video->req_size = 0;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ /*
+ * Return all the video buffers before disabling the queue.
+ */
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
+ list_del(&buf->queue);
+ uvcg_complete_buffer(&video->queue, buf);
+ }
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
+
+ uvcg_queue_enable(&video->queue, 0);
+ return 0;
}

/*
@@ -497,28 +596,22 @@ static void uvcg_video_pump(struct work_struct *work)
int uvcg_video_enable(struct uvc_video *video, int enable)
{
int ret;
- struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
"Video enable failed, device is uninitialized.\n");
return -ENODEV;
}
-
- if (!enable) {
- cancel_work_sync(&video->pump);
- uvcg_queue_cancel(&video->queue, 0);
-
- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
- }
-
- uvc_video_free_requests(video);
- uvcg_queue_enable(&video->queue, 0);
- return 0;
- }
-
+ if (!enable)
+ return uvcg_video_disable(video);
+
+ /*
+ * Safe to access request related fields without req_lock because
+ * this is the only thread currently active, and no other
+ * request handling thread will become active until this function
+ * returns.
+ */
+ video->is_enabled = true;
if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;

@@ -544,6 +637,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ video->is_enabled = false;
INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
--
2.42.0.609.gbb76f46606-goog

2023-10-12 00:34:15

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v1 0/3] usb: gadget: uvc: stability fixes on STREAMOFF.



On 10/8/23 12:48, Michael Grzeschik wrote:
> On Fri, Oct 06, 2023 at 04:48:19PM -0700, Avichal Rakesh wrote:
>> On 10/6/23 15:53, Michael Grzeschik wrote:
>>> On Fri, Oct 06, 2023 at 10:00:11AM -0700, Avichal Rakesh wrote:
>>>>
>>>>
>>>> On 10/5/23 15:05, Michael Grzeschik wrote:
>>>>> Hi Avichal,
>>>>>
>>>>> On Thu, Oct 05, 2023 at 11:30:32AM -0700, Avichal Rakesh wrote:
>>>>>> On 10/5/23 03:14, Michael Grzeschik wrote:
>>>>> <snip>
>>>>> I don't know where the extra complexity comes from.
>>>>
>>>> A lot of this complexity comes from assuming a back to back
>>>> STREAMOFF -> STREAMON sequence is possible where the gadget driver
>>>> doesn't have the time to clean up all in-flight usb_requests.
>>>> However, looking through the usb gadget APIs again, and it
>>>> looks like  usb_ep_disable enforces that all requests will
>>>> be sent back to the gadget driver before it returns.
>>>
>>> Great!
>>
>> Uhh...apologies, I will have to take this back. I've been
>> trying to use uvc->state as the condition for when completion
>> handler should clean up usb_requests, and I cannot figure
>> out a way to do so cleanly.
>>
>> The fundamental problem with using uvc->state is that it is
>> not protected by any locks. So there is no real way to
>> assert that its value has not changed between reading
>> uvc->state and acting on it.
>>
>> Naively we can write something like the following in the
>> completion handler:
>>
>> void uvc_video_complete(...) {
>>    if (uvc->state != UVC_EVENT_STREAMING) {
>>        usb_ep_free_request(....);
>>    } else {
>>        // handle usb_request normally
>>    }
>> }
>>
>> But without any locks, there are no guarantees that
>> uvc->state didn't mutate immediately after the if
>> condition was checked, and the complete handler is
>> handling a request that it should've freed instead
>> or vice-versa. This argument would hold for any logic
>> we guard with uvc->state, making uvc->state effectively
>> useless as a check for freeing memory.
>
> Yes, this makes total sense. Since the above condition was also part of
> the wait_event patch you created in the first place, I bet this issue
> was there aswell and was probably causing the issues I saw while testing
> it>
>
>> We can work around it by either
>> 1. Locking uvc->state with some driver level lock
>>   to ensure that we can trust the value of uvc->state
>>   at least for a little while, or
>> 2. Using some other barrier condition that is protected by
>>   another lock
>>
>> If we go with (1), we'd have to add a lock around every
>> and every write to uvc->state, which isn't terrible, but
>> would require more testing to ensure that it doesn't
>> create any new deadlocks.
>>
>> For (2), with the realization that usb_ep_disable flushes
>> all requests, we can add a barrier in uvc_video, protected by
>> req_lock. That should simplify the logic a little bit and
>> will hopefully be easier to reason about.
>>
>> I could of course be missing a simpler solution here,
>> and am happy to be wrong. So please let me know if you
>> have any other ideas on how to guarantee such a check.
>
> For now, I have no better Idea. Idea (2) sounds like
> a good compromise. But I will have to review that code
> to really judge.
>

Sent out v4 patches with option (2). It simplifies the logic
decently because we no longer have to reason about per-request
consistency. uvc_video now tracks its own state of whether
requests should be flowing or not based on calls to
uvcg_video_enable. This state is protected, and is the source
of truth for queueing usb_requests.

The last bit of complexity left is around returning in-flight
video buffers. AFAICT it should now be protected, and in my
local testing I didn't notice any un-returned buffers, but
please to take a look and let me know if your testing
uncovers anything.

Thanks,
Avi.

2023-10-12 00:42:40

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v4 3/3] usb: gadget: uvc: Fix use-after-free for inflight usb_requests



On 10/11/23 17:24, Avichal Rakesh wrote:
> Currently, the uvc gadget driver allocates all uvc_requests as one array
> and deallocates them all when the video stream stops. This includes
> de-allocating all the usb_requests associated with those uvc_requests.
> This can lead to use-after-free issues if any of those de-allocated
> usb_requests were still owned by the usb controller.
>
> This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
> flag to uvc_video to track when frames and requests should be flowing.
> When disabling the video stream, the flag is tripped and, instead
> of de-allocating all uvc_requests and usb_requests, the gadget
> driver only de-allocates those usb_requests that are currently
> owned by it (as present in req_free). Other usb_requests are left
> untouched until their completion handler is called which takes care
> of freeing the usb_request and its corresponding uvc_request.
>
> Now that uvc_video does not depends on uvc->state, this patch removes
> unnecessary upates to uvc->state that were made to accomodate uvc_video
> logic. This should ensure that uvc gadget driver never accidentally
> de-allocates a usb_request that it doesn't own.
>
> Link: https://lore.kernel.org/[email protected]
> Suggested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>
> ---
> v1 -> v2: Rebased to ToT, and fixed deadlock reported in
> https://lore.kernel.org/all/[email protected]/
> v2 -> v3: Fix email threading goof-up
> v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
> as discussed in
> https://lore.kernel.org/[email protected]/
>
> drivers/usb/gadget/function/uvc.h | 1 +
> drivers/usb/gadget/function/uvc_v4l2.c | 12 +-
> drivers/usb/gadget/function/uvc_video.c | 156 +++++++++++++++++++-----
> 3 files changed, 128 insertions(+), 41 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h

Apologies, I realized I forgot to run checkpatch on this patch. Will fix
the lint issues in the next version. This patch is functionally okay, but
has 2 minor formatting issues. Feel free to review the patch, and I will
fix the formatting as I am addressing the comments.

- Avi.

2023-10-18 13:03:28

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v4 2/3] usb: gadget: uvc: Allocate uvc_requests one at a time

A short second review.

On Wed, Oct 11, 2023 at 05:24:50PM -0700, Avichal Rakesh wrote:
>Currently, the uvc gadget driver allocates all uvc_requests as one array
>and deallocates them all when the video stream stops. This includes
>de-allocating all the usb_requests associated with those uvc_requests.
>This can lead to use-after-free issues if any of those de-allocated
>usb_requests were still owned by the usb controller.
>
>This patch is 1 of 2 patches addressing the use-after-free issue.
>Instead of bulk allocating all uvc_requests as an array, this patch
>allocates uvc_requests one at a time, which should allows for similar
>granularity when deallocating the uvc_requests. This patch has no
>functional changes other than allocating each uvc_request separately,
>and similarly freeing each of them separately.
>
>Link: https://lore.kernel.org/[email protected]
>Suggested-by: Michael Grzeschik <[email protected]>
>Reviewed-by: Michael Grzeschik <[email protected]>
>Signed-off-by: Avichal Rakesh <[email protected]>
>---
>v1 -> v2: Rebased to ToT
>v2 -> v3: Fix email threading goof-up
>v3 -> v4: Address review comments & re-rebase to ToT
>
> drivers/usb/gadget/function/uvc.h | 3 +-
> drivers/usb/gadget/function/uvc_video.c | 87 ++++++++++++++-----------
> 2 files changed, 50 insertions(+), 40 deletions(-)
>
>diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
>index 989bc6b4e93d..993694da0bbc 100644
>--- a/drivers/usb/gadget/function/uvc.h
>+++ b/drivers/usb/gadget/function/uvc.h
>@@ -81,6 +81,7 @@ struct uvc_request {
> struct sg_table sgt;
> u8 header[UVCG_REQUEST_HEADER_LEN];
> struct uvc_buffer *last_buf;
>+ struct list_head list;
> };
>
> struct uvc_video {
>@@ -102,7 +103,7 @@ struct uvc_video {
>
> /* Requests */
> unsigned int req_size;
>- struct uvc_request *ureq;
>+ struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
> struct list_head req_free;
> spinlock_t req_lock;
>
>diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
>index c334802ac0a4..b62b3de79153 100644
>--- a/drivers/usb/gadget/function/uvc_video.c
>+++ b/drivers/usb/gadget/function/uvc_video.c
>@@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
> * Request handling
> */
>
>+static void
>+uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
>+{
>+ sg_free_table(&ureq->sgt);
>+ if (ureq->req && ep) {
>+ usb_ep_free_request(ep, ureq->req);
>+ ureq->req = NULL;
>+ }
>+
>+ kfree(ureq->req_buffer);
>+ ureq->req_buffer = NULL;
>+
>+ if (!list_empty(&ureq->list))
>+ list_del_init(&ureq->list);
>+
>+ kfree(ureq);
>+}
>+
> static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
> {
> int ret;
>@@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
> static int
> uvc_video_free_requests(struct uvc_video *video)
> {
>- unsigned int i;
>+ struct uvc_request *ureq, *temp;
>
>- if (video->ureq) {
>- for (i = 0; i < video->uvc_num_requests; ++i) {
>- sg_free_table(&video->ureq[i].sgt);
>-
>- if (video->ureq[i].req) {
>- usb_ep_free_request(video->ep, video->ureq[i].req);
>- video->ureq[i].req = NULL;
>- }
>-
>- if (video->ureq[i].req_buffer) {
>- kfree(video->ureq[i].req_buffer);
>- video->ureq[i].req_buffer = NULL;
>- }
>- }
>-
>- kfree(video->ureq);
>- video->ureq = NULL;
>- }
>+ list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
>+ uvc_video_free_request(ureq, video->ep);
>
>+ INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> video->req_size = 0;
> return 0;
>@@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
> static int
> uvc_video_alloc_requests(struct uvc_video *video)
> {
>+ struct uvc_request *ureq;
> unsigned int req_size;
> unsigned int i;
> int ret = -ENOMEM;
>@@ -332,29 +336,32 @@ uvc_video_alloc_requests(struct uvc_video *video)
> * max_t(unsigned int, video->ep->maxburst, 1)
> * (video->ep->mult);
>
>- video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
>- if (video->ureq == NULL)
>- return -ENOMEM;
>+ INIT_LIST_HEAD(&video->ureqs);
>+ for (i = 0; i < video->uvc_num_requests; i++) {
>+ ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
>+ if (ureq == NULL)
>+ goto error;
Please add an empty line here.
>+ INIT_LIST_HEAD(&ureq->list);

Please add an empty line here.

>+ list_add_tail(&ureq->list, &video->ureqs);
>
>- for (i = 0; i < video->uvc_num_requests; ++i) {
>- video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
>- if (video->ureq[i].req_buffer == NULL)
>+ ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
>+ if (ureq->req_buffer == NULL)
You could also use if (!ureq->req_buffer)

> goto error;
>
>- video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
>- if (video->ureq[i].req == NULL)
>+ ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
>+ if (ureq->req == NULL)
> goto error;
>
>- video->ureq[i].req->buf = video->ureq[i].req_buffer;
>- video->ureq[i].req->length = 0;
>- video->ureq[i].req->complete = uvc_video_complete;
>- video->ureq[i].req->context = &video->ureq[i];
>- video->ureq[i].video = video;
>- video->ureq[i].last_buf = NULL;
>+ ureq->req->buf = ureq->req_buffer;
>+ ureq->req->length = 0;
>+ ureq->req->complete = uvc_video_complete;
>+ ureq->req->context = ureq;
>+ ureq->video = video;
>+ ureq->last_buf = NULL;
>
>- list_add_tail(&video->ureq[i].req->list, &video->req_free);
>+ list_add_tail(&ureq->req->list, &video->req_free);
> /* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
>- sg_alloc_table(&video->ureq[i].sgt,
>+ sg_alloc_table(&ureq->sgt,
> DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
> PAGE_SIZE) + 2, GFP_KERNEL);
> }
>@@ -489,8 +496,8 @@ static void uvcg_video_pump(struct work_struct *work)
> */
> int uvcg_video_enable(struct uvc_video *video, int enable)
> {
>- unsigned int i;
> int ret;
>+ struct uvc_request *ureq;
>
> if (video->ep == NULL) {
> uvcg_info(&video->uvc->func,
>@@ -502,9 +509,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> cancel_work_sync(&video->pump);
> uvcg_queue_cancel(&video->queue, 0);
>
>- for (i = 0; i < video->uvc_num_requests; ++i)
>- if (video->ureq && video->ureq[i].req)
>- usb_ep_dequeue(video->ep, video->ureq[i].req);
>+ list_for_each_entry(ureq, &video->ureqs, list) {
>+ if (ureq->req)
>+ usb_ep_dequeue(video->ep, ureq->req);
>+ }
>
> uvc_video_free_requests(video);
> uvcg_queue_enable(&video->queue, 0);
>@@ -536,6 +544,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> */
> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
> {
>+ INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> spin_lock_init(&video->req_lock);
> INIT_WORK(&video->pump, uvcg_video_pump);
>--
>2.42.0.609.gbb76f46606-goog
>
>

--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (7.38 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-18 13:10:57

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v4 3/3] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

On Wed, Oct 11, 2023 at 05:24:51PM -0700, Avichal Rakesh wrote:
>Currently, the uvc gadget driver allocates all uvc_requests as one array
>and deallocates them all when the video stream stops. This includes
>de-allocating all the usb_requests associated with those uvc_requests.
>This can lead to use-after-free issues if any of those de-allocated
>usb_requests were still owned by the usb controller.
>
>This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
>flag to uvc_video to track when frames and requests should be flowing.
>When disabling the video stream, the flag is tripped and, instead
>of de-allocating all uvc_requests and usb_requests, the gadget
>driver only de-allocates those usb_requests that are currently
>owned by it (as present in req_free). Other usb_requests are left
>untouched until their completion handler is called which takes care
>of freeing the usb_request and its corresponding uvc_request.
>
>Now that uvc_video does not depends on uvc->state, this patch removes
>unnecessary upates to uvc->state that were made to accomodate uvc_video
>logic. This should ensure that uvc gadget driver never accidentally
>de-allocates a usb_request that it doesn't own.
>
>Link: https://lore.kernel.org/[email protected]
>Suggested-by: Michael Grzeschik <[email protected]>
>Signed-off-by: Avichal Rakesh <[email protected]>
>---
>v1 -> v2: Rebased to ToT, and fixed deadlock reported in
> https://lore.kernel.org/all/[email protected]/
>v2 -> v3: Fix email threading goof-up
>v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
> as discussed in
> https://lore.kernel.org/[email protected]/

I tested this and I no longer saw any use after free
errors anymore! :)

Here comes some more review:

> drivers/usb/gadget/function/uvc.h | 1 +
> drivers/usb/gadget/function/uvc_v4l2.c | 12 +-
> drivers/usb/gadget/function/uvc_video.c | 156 +++++++++++++++++++-----
> 3 files changed, 128 insertions(+), 41 deletions(-)
>
>diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
>index 993694da0bbc..be0d012aa244 100644
>--- a/drivers/usb/gadget/function/uvc.h
>+++ b/drivers/usb/gadget/function/uvc.h
>@@ -102,6 +102,7 @@ struct uvc_video {
> unsigned int uvc_num_requests;
>
> /* Requests */
>+ bool is_enabled; /* tracks whether video stream is enabled */
> unsigned int req_size;
> struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
> struct list_head req_free;
>diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
>index c0d77564a204..ded7d33c2a52 100644
>--- a/drivers/usb/gadget/function/uvc_v4l2.c
>+++ b/drivers/usb/gadget/function/uvc_v4l2.c
>@@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
> * Complete the alternate setting selection setup phase now that
> * userspace is ready to provide video frames.
> */
>- uvc_function_setup_continue(uvc, 0);
> uvc->state = UVC_STATE_STREAMING;
>+ uvc_function_setup_continue(uvc, 0);
>
> return 0;
> }
>@@ -468,12 +468,12 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
> if (type != video->queue.queue.type)
> return -EINVAL;
>
>- uvc->state = UVC_STATE_CONNECTED;
> ret = uvcg_video_enable(video, 0);
> if (ret < 0) {
> return ret;
> }
>
>+ uvc->state = UVC_STATE_CONNECTED;
> uvc_function_setup_continue(uvc, 1);
> return 0;
> }
>@@ -508,14 +508,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
> static void uvc_v4l2_disable(struct uvc_device *uvc)
> {
> uvc_function_disconnect(uvc);
>- /*
>- * Drop uvc->state to CONNECTED if it was streaming before.
>- * This ensures that the usb_requests are no longer queued
>- * to the controller.
>- */
>- if (uvc->state == UVC_STATE_STREAMING)
>- uvc->state = UVC_STATE_CONNECTED;
>-
> uvcg_video_enable(&uvc->video, 0);
> uvcg_free_buffers(&uvc->video.queue);
> uvc->func_connected = false;
>diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
>index b62b3de79153..05b89b5b6c48 100644
>--- a/drivers/usb/gadget/function/uvc_video.c
>+++ b/drivers/usb/gadget/function/uvc_video.c
>@@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
> * Request handling
> */
>
>+/**
>+ * Must be called with req_lock held as it modifies the list ureq is held in
>+ */
> static void
> uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
> {
>@@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
> struct uvc_request *ureq = req->context;
> struct uvc_video *video = ureq->video;
> struct uvc_video_queue *queue = &video->queue;
>- struct uvc_device *uvc = video->uvc;
>+ struct uvc_buffer *last_buf = NULL;
> unsigned long flags;
>
>+ spin_lock_irqsave(&video->req_lock, flags);
>+ if (!video->is_enabled) {
>+ /*
>+ * When is_enabled is false, uvc_video_disable ensures that
>+ * in-flight uvc_buffers are returned, so we can safely
>+ * call free_request without worrying about last_buf.
>+ */
>+ uvc_video_free_request(ureq, ep);
>+ spin_unlock_irqrestore(&video->req_lock, flags);
>+ return;
>+ }
>+
>+ last_buf = ureq->last_buf;
>+ ureq->last_buf = NULL;
>+ spin_unlock_irqrestore(&video->req_lock, flags);
>+
> switch (req->status) {
> case 0:
> break;
>@@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
> uvcg_queue_cancel(queue, 0);
> }
>
>- if (ureq->last_buf) {
>- uvcg_complete_buffer(&video->queue, ureq->last_buf);
>- ureq->last_buf = NULL;
>+ if (last_buf) {
>+ spin_lock_irqsave(&queue->irqlock, flags);
>+ uvcg_complete_buffer(&video->queue, last_buf);
>+ spin_unlock_irqrestore(&queue->irqlock, flags);
> }
>
> spin_lock_irqsave(&video->req_lock, flags);
>- list_add_tail(&req->list, &video->req_free);
>- spin_unlock_irqrestore(&video->req_lock, flags);
>-
>- if (uvc->state == UVC_STATE_STREAMING)
>+ /*
>+ * Video stream might have been disabled while we were
>+ * processing the current usb_request. So make sure
>+ * we're still streaming before queueing the usb_request
>+ * back to req_free
>+ */
>+ if (video->is_enabled) {
>+ list_add_tail(&req->list, &video->req_free);
> queue_work(video->async_wq, &video->pump);
>+ } else {
>+ uvc_video_free_request(ureq, ep);
>+ }
>+ spin_unlock_irqrestore(&video->req_lock, flags);
> }
>
> static int
>@@ -391,20 +419,22 @@ static void uvcg_video_pump(struct work_struct *work)
> struct uvc_video_queue *queue = &video->queue;
> /* video->max_payload_size is only set when using bulk transfer */
> bool is_bulk = video->max_payload_size;
>- struct uvc_device *uvc = video->uvc;
> struct usb_request *req = NULL;
> struct uvc_buffer *buf;
> unsigned long flags;
> bool buf_done;
> int ret;
>
>- while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
>+ while(true) {

Missing space after "while".

>+ if (!video->ep->enabled)
>+ return;
>+
> /*
>- * Retrieve the first available USB request, protected by the
>- * request lock.
>+ * Check is_enabled and retrieve the first available USB
>+ * request, protected by the request lock.
> */
> spin_lock_irqsave(&video->req_lock, flags);
>- if (list_empty(&video->req_free)) {
>+ if (!video->is_enabled || list_empty(&video->req_free)) {
> spin_unlock_irqrestore(&video->req_lock, flags);
> return;
> }
>@@ -486,9 +516,78 @@ static void uvcg_video_pump(struct work_struct *work)
> return;
>
> spin_lock_irqsave(&video->req_lock, flags);
>- list_add_tail(&req->list, &video->req_free);
>+ if (video->is_enabled)
>+ list_add_tail(&req->list, &video->req_free);
>+ else
>+ uvc_video_free_request(req->context, video->ep);
>+ spin_unlock_irqrestore(&video->req_lock, flags);
>+}
>+
>+/*
>+ * Disable video stream
>+ */
>+static int
>+uvcg_video_disable(struct uvc_video *video) {
>+ unsigned long flags;
>+ struct list_head inflight_bufs;
>+ struct usb_request *req, *temp;
>+ struct uvc_buffer *buf, *btemp;
>+ struct uvc_request *ureq, *utemp;
>+
>+ INIT_LIST_HEAD(&inflight_bufs);
>+ spin_lock_irqsave(&video->req_lock, flags);
>+ video->is_enabled = false;
>+
>+ /*
>+ * Remove any in-flight buffers from the uvc_requests
>+ * because we want to return them before cancelling the
>+ * queue. This ensures that we aren't stuck waiting for
>+ * all complete callbacks to come through before disabling
>+ * vb2 queue.
>+ */
>+ list_for_each_entry(ureq, &video->ureqs, list) {
>+ if (ureq->last_buf) {
>+ list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
>+ ureq->last_buf = NULL;
>+ }
>+ }
> spin_unlock_irqrestore(&video->req_lock, flags);
>- return;
>+
>+ cancel_work_sync(&video->pump);
>+ uvcg_queue_cancel(&video->queue, 0);
>+
>+ spin_lock_irqsave(&video->req_lock, flags);
>+ /*
>+ * Remove all uvc_reqeusts from from ureqs with list_del_init
>+ * This lets uvc_video_free_request correctly identify
>+ * if the uvc_request is attached to a list or not when freeing
>+ * memory.
>+ */
>+ list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
>+ list_del_init(&ureq->list);
>+
>+ list_for_each_entry_safe(req, temp, &video->req_free, list) {
>+ list_del(&req->list);
>+ uvc_video_free_request(req->context, video->ep);
>+ }
>+
>+ INIT_LIST_HEAD(&video->ureqs);
>+ INIT_LIST_HEAD(&video->req_free);
>+ video->req_size = 0;
>+ spin_unlock_irqrestore(&video->req_lock, flags);
>+
>+ /*
>+ * Return all the video buffers before disabling the queue.
>+ */
>+ spin_lock_irqsave(&video->queue.irqlock, flags);
>+ list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
>+ list_del(&buf->queue);
>+ uvcg_complete_buffer(&video->queue, buf);
>+ }
>+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
>+
>+ uvcg_queue_enable(&video->queue, 0);
>+ return 0;
> }
>
> /*
>@@ -497,28 +596,22 @@ static void uvcg_video_pump(struct work_struct *work)
> int uvcg_video_enable(struct uvc_video *video, int enable)
> {
> int ret;
>- struct uvc_request *ureq;
>
> if (video->ep == NULL) {
> uvcg_info(&video->uvc->func,
> "Video enable failed, device is uninitialized.\n");
> return -ENODEV;
> }
>-
>- if (!enable) {
>- cancel_work_sync(&video->pump);
>- uvcg_queue_cancel(&video->queue, 0);
>-
>- list_for_each_entry(ureq, &video->ureqs, list) {
>- if (ureq->req)
>- usb_ep_dequeue(video->ep, ureq->req);
>- }
>-
>- uvc_video_free_requests(video);
>- uvcg_queue_enable(&video->queue, 0);
>- return 0;
>- }
>-
>+ if (!enable)
>+ return uvcg_video_disable(video);

Could you refactor this code as it is to an separate
function and prepand this change as an extra patch
to this one? It would make the changes in the functions
more obvious and better to review.

>+
>+ /*
>+ * Safe to access request related fields without req_lock because
>+ * this is the only thread currently active, and no other
>+ * request handling thread will become active until this function
>+ * returns.
>+ */
>+ video->is_enabled = true;

Add an extra empty line.

> if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
> return ret;
>
>@@ -544,6 +637,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> */
> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
> {
>+ video->is_enabled = false;
> INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> spin_lock_init(&video->req_lock);
>--
>2.42.0.609.gbb76f46606-goog
>
>

Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>


--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (12.00 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-18 19:47:10

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v5 1/3] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. As the endpoint's state can no longer be used, video_pump is
now guarded by uvc->state as well. To be consistent with the actual
streaming state, uvc->state is now toggled between CONNECTED and STREAMING
from the v4l2 event callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT and reworded commit message.
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT
v4 -> v5: Add Reviewed-by & Tested-by

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
drivers/usb/gadget/function/uvc_video.c | 3 ++-
5 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index faa398109431..ae08341961eb 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep)
+ usb_ep_disable(uvc->video.ep);
+
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..e7f9f13f14dc 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..7cb8d027ff0c 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0)
+ return ret;
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ if (uvc->state == UVC_STATE_STREAMING)
+ uvc->state = UVC_STATE_CONNECTED;
+
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 91af3b1ef0d4..c334802ac0a4 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
+ struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (video->ep->enabled) {
+ while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
/*
* Retrieve the first available USB request, protected by the
* request lock.
--
2.42.0.758.gaed0368e0e-goog

2023-10-18 19:47:15

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v5 3/3] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
flag to uvc_video to track when frames and requests should be flowing.
When disabling the video stream, the flag is tripped and, instead
of de-allocating all uvc_requests and usb_requests, the gadget
driver only de-allocates those usb_requests that are currently
owned by it (as present in req_free). Other usb_requests are left
untouched until their completion handler is called which takes care
of freeing the usb_request and its corresponding uvc_request.

Now that uvc_video does not depends on uvc->state, this patch removes
unnecessary upates to uvc->state that were made to accommodate uvc_video
logic. This should ensure that uvc gadget driver never accidentally
de-allocates a usb_request that it doesn't own.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT, and fixed deadlock reported in
https://lore.kernel.org/all/[email protected]/
v2 -> v3: Fix email threading goof-up
v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
as discussed in
https://lore.kernel.org/[email protected]/
v4 -> v5: Address review comments. Add Reviewed-by & Tested-by.

drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_v4l2.c | 12 +-
drivers/usb/gadget/function/uvc_video.c | 154 +++++++++++++++++++-----
3 files changed, 128 insertions(+), 39 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 993694da0bbc..be0d012aa244 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -102,6 +102,7 @@ struct uvc_video {
unsigned int uvc_num_requests;

/* Requests */
+ bool is_enabled; /* tracks whether video stream is enabled */
unsigned int req_size;
struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 7cb8d027ff0c..f4d2e24835d4 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;
+ uvc_function_setup_continue(uvc, 0);

return 0;
}
@@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
if (type != video->queue.queue.type)
return -EINVAL;

- uvc->state = UVC_STATE_CONNECTED;
ret = uvcg_video_enable(video, 0);
if (ret < 0)
return ret;

+ uvc->state = UVC_STATE_CONNECTED;
uvc_function_setup_continue(uvc, 1);
return 0;
}
@@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
- /*
- * Drop uvc->state to CONNECTED if it was streaming before.
- * This ensures that the usb_requests are no longer queued
- * to the controller.
- */
- if (uvc->state == UVC_STATE_STREAMING)
- uvc->state = UVC_STATE_CONNECTED;
-
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c180866c8e34..3a5192f10f95 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+/**
+ * Must be called with req_lock held as it modifies the list ureq is held in
+ */
static void
uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
{
@@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
struct uvc_request *ureq = req->context;
struct uvc_video *video = ureq->video;
struct uvc_video_queue *queue = &video->queue;
- struct uvc_device *uvc = video->uvc;
+ struct uvc_buffer *last_buf = NULL;
unsigned long flags;

+ spin_lock_irqsave(&video->req_lock, flags);
+ if (!video->is_enabled) {
+ /*
+ * When is_enabled is false, uvc_video_disable ensures that
+ * in-flight uvc_buffers are returned, so we can safely
+ * call free_request without worrying about last_buf.
+ */
+ uvc_video_free_request(ureq, ep);
+ spin_unlock_irqrestore(&video->req_lock, flags);
+ return;
+ }
+
+ last_buf = ureq->last_buf;
+ ureq->last_buf = NULL;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
switch (req->status) {
case 0:
break;
@@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
uvcg_queue_cancel(queue, 0);
}

- if (ureq->last_buf) {
- uvcg_complete_buffer(&video->queue, ureq->last_buf);
- ureq->last_buf = NULL;
+ if (last_buf) {
+ spin_lock_irqsave(&queue->irqlock, flags);
+ uvcg_complete_buffer(&video->queue, last_buf);
+ spin_unlock_irqrestore(&queue->irqlock, flags);
}

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
- spin_unlock_irqrestore(&video->req_lock, flags);
-
- if (uvc->state == UVC_STATE_STREAMING)
+ /*
+ * Video stream might have been disabled while we were
+ * processing the current usb_request. So make sure
+ * we're still streaming before queueing the usb_request
+ * back to req_free
+ */
+ if (video->is_enabled) {
+ list_add_tail(&req->list, &video->req_free);
queue_work(video->async_wq, &video->pump);
+ } else {
+ uvc_video_free_request(ureq, ep);
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
}

static int
@@ -393,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
- struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
+ while (true) {
+ if (!video->ep->enabled)
+ return;
+
/*
- * Retrieve the first available USB request, protected by the
- * request lock.
+ * Check is_enabled and retrieve the first available USB
+ * request, protected by the request lock.
*/
spin_lock_irqsave(&video->req_lock, flags);
- if (list_empty(&video->req_free)) {
+ if (!video->is_enabled || list_empty(&video->req_free)) {
spin_unlock_irqrestore(&video->req_lock, flags);
return;
}
@@ -488,9 +518,79 @@ static void uvcg_video_pump(struct work_struct *work)
return;

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
+ if (video->is_enabled)
+ list_add_tail(&req->list, &video->req_free);
+ else
+ uvc_video_free_request(req->context, video->ep);
spin_unlock_irqrestore(&video->req_lock, flags);
- return;
+}
+
+/*
+ * Disable video stream
+ */
+static int
+uvcg_video_disable(struct uvc_video *video)
+{
+ unsigned long flags;
+ struct list_head inflight_bufs;
+ struct usb_request *req, *temp;
+ struct uvc_buffer *buf, *btemp;
+ struct uvc_request *ureq, *utemp;
+
+ INIT_LIST_HEAD(&inflight_bufs);
+ spin_lock_irqsave(&video->req_lock, flags);
+ video->is_enabled = false;
+
+ /*
+ * Remove any in-flight buffers from the uvc_requests
+ * because we want to return them before cancelling the
+ * queue. This ensures that we aren't stuck waiting for
+ * all complete callbacks to come through before disabling
+ * vb2 queue.
+ */
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->last_buf) {
+ list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
+ ureq->last_buf = NULL;
+ }
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ cancel_work_sync(&video->pump);
+ uvcg_queue_cancel(&video->queue, 0);
+
+ spin_lock_irqsave(&video->req_lock, flags);
+ /*
+ * Remove all uvc_reqeusts from ureqs with list_del_init
+ * This lets uvc_video_free_request correctly identify
+ * if the uvc_request is attached to a list or not when freeing
+ * memory.
+ */
+ list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
+ list_del_init(&ureq->list);
+
+ list_for_each_entry_safe(req, temp, &video->req_free, list) {
+ list_del(&req->list);
+ uvc_video_free_request(req->context, video->ep);
+ }
+
+ INIT_LIST_HEAD(&video->ureqs);
+ INIT_LIST_HEAD(&video->req_free);
+ video->req_size = 0;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ /*
+ * Return all the video buffers before disabling the queue.
+ */
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
+ list_del(&buf->queue);
+ uvcg_complete_buffer(&video->queue, buf);
+ }
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
+
+ uvcg_queue_enable(&video->queue, 0);
+ return 0;
}

/*
@@ -499,27 +599,22 @@ static void uvcg_video_pump(struct work_struct *work)
int uvcg_video_enable(struct uvc_video *video, int enable)
{
int ret;
- struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
"Video enable failed, device is uninitialized.\n");
return -ENODEV;
}
+ if (!enable)
+ return uvcg_video_disable(video);

- if (!enable) {
- cancel_work_sync(&video->pump);
- uvcg_queue_cancel(&video->queue, 0);
-
- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
- }
-
- uvc_video_free_requests(video);
- uvcg_queue_enable(&video->queue, 0);
- return 0;
- }
+ /*
+ * Safe to access request related fields without req_lock because
+ * this is the only thread currently active, and no other
+ * request handling thread will become active until this function
+ * returns.
+ */
+ video->is_enabled = true;

if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;
@@ -546,6 +641,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ video->is_enabled = false;
INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
--
2.42.0.758.gaed0368e0e-goog

2023-10-18 19:53:10

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v4 2/3] usb: gadget: uvc: Allocate uvc_requests one at a time



On 10/18/23 06:03, Michael Grzeschik wrote:
> A short second review.

Thank you for the review. Sent out a v5 with the comments
addressed!

>
> On Wed, Oct 11, 2023 at 05:24:50PM -0700, Avichal Rakesh wrote:
>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>> and deallocates them all when the video stream stops. This includes
>> de-allocating all the usb_requests associated with those uvc_requests.
>> This can lead to use-after-free issues if any of those de-allocated
>> usb_requests were still owned by the usb controller.
>>
>> This patch is 1 of 2 patches addressing the use-after-free issue.
>> Instead of bulk allocating all uvc_requests as an array, this patch
>> allocates uvc_requests one at a time, which should allows for similar
>> granularity when deallocating the uvc_requests. This patch has no
>> functional changes other than allocating each uvc_request separately,
>> and similarly freeing each of them separately.
>>
>> Link: https://lore.kernel.org/[email protected]
>> Suggested-by: Michael Grzeschik <[email protected]>
>> Reviewed-by: Michael Grzeschik <[email protected]>
>> Signed-off-by: Avichal Rakesh <[email protected]>
>> ---
>> v1 -> v2: Rebased to ToT
>> v2 -> v3: Fix email threading goof-up
>> v3 -> v4: Address review comments & re-rebase to ToT
>>
>> drivers/usb/gadget/function/uvc.h       |  3 +-
>> drivers/usb/gadget/function/uvc_video.c | 87 ++++++++++++++-----------
>> 2 files changed, 50 insertions(+), 40 deletions(-)
>> -        if (video->ureq[i].req_buffer == NULL)
>> +        ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
>> +        if (ureq->req_buffer == NULL)
> You could also use if (!ureq->req_buffer)

Keeping this as is because I prefer `== NULL` check for readability.
Didn't find any specific rules in the kernel codestyle, so sticking
with the more readable option (in my opinion).

>
>>             goto error;
>>
>> -        video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
>> -        if (video->ureq[i].req == NULL)
>> +        ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
>> +        if (ureq->req == NULL)
>>             goto error;
>>
>> -        video->ureq[i].req->buf = video->ureq[i].req_buffer;
>> -        video->ureq[i].req->length = 0;
>> -        video->ureq[i].req->complete = uvc_video_complete;
>> -        video->ureq[i].req->context = &video->ureq[i];
>> -        video->ureq[i].video = video;
>> -        video->ureq[i].last_buf = NULL;
>> +        ureq->req->buf = ureq->req_buffer;
>> +        ureq->req->length = 0;
>> +        ureq->req->complete = uvc_video_complete;
>> +        ureq->req->context = ureq;
>> +        ureq->video = video;
>> +        ureq->last_buf = NULL;
>>
>> -        list_add_tail(&video->ureq[i].req->list, &video->req_free);
>> +        list_add_tail(&ureq->req->list, &video->req_free);
>>         /* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
>> -        sg_alloc_table(&video->ureq[i].sgt,
>> +        sg_alloc_table(&ureq->sgt,
>>                    DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
>>                         PAGE_SIZE) + 2, GFP_KERNEL);
>>     }
>> @@ -489,8 +496,8 @@ static void uvcg_video_pump(struct work_struct *work)
>>  */
>> int uvcg_video_enable(struct uvc_video *video, int enable)
>> {
>> -    unsigned int i;
>>     int ret;
>> +    struct uvc_request *ureq;
>>
>>     if (video->ep == NULL) {
>>         uvcg_info(&video->uvc->func,
>> @@ -502,9 +509,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
>>         cancel_work_sync(&video->pump);
>>         uvcg_queue_cancel(&video->queue, 0);
>>
>> -        for (i = 0; i < video->uvc_num_requests; ++i)
>> -            if (video->ureq && video->ureq[i].req)
>> -                usb_ep_dequeue(video->ep, video->ureq[i].req);
>> +        list_for_each_entry(ureq, &video->ureqs, list) {
>> +            if (ureq->req)
>> +                usb_ep_dequeue(video->ep, ureq->req);
>> +        }
>>
>>         uvc_video_free_requests(video);
>>         uvcg_queue_enable(&video->queue, 0);
>> @@ -536,6 +544,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
>>  */
>> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
>> {
>> +    INIT_LIST_HEAD(&video->ureqs);
>>     INIT_LIST_HEAD(&video->req_free);
>>     spin_lock_init(&video->req_lock);
>>     INIT_WORK(&video->pump, uvcg_video_pump);
>> --
>> 2.42.0.609.gbb76f46606-goog
>>
>>
>

2023-10-18 21:51:31

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v4 3/3] usb: gadget: uvc: Fix use-after-free for inflight usb_requests



On 10/18/23 06:10, Michael Grzeschik wrote:
> On Wed, Oct 11, 2023 at 05:24:51PM -0700, Avichal Rakesh wrote:
>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>> and deallocates them all when the video stream stops. This includes
>> de-allocating all the usb_requests associated with those uvc_requests.
>> This can lead to use-after-free issues if any of those de-allocated
>> usb_requests were still owned by the usb controller.
>>
>> This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
>> flag to uvc_video to track when frames and requests should be flowing.
>> When disabling the video stream, the flag is tripped and, instead
>> of de-allocating all uvc_requests and usb_requests, the gadget
>> driver only de-allocates those usb_requests that are currently
>> owned by it (as present in req_free). Other usb_requests are left
>> untouched until their completion handler is called which takes care
>> of freeing the usb_request and its corresponding uvc_request.
>>
>> Now that uvc_video does not depends on uvc->state, this patch removes
>> unnecessary upates to uvc->state that were made to accomodate uvc_video
>> logic. This should ensure that uvc gadget driver never accidentally
>> de-allocates a usb_request that it doesn't own.
>>
>> Link: https://lore.kernel.org/[email protected]
>> Suggested-by: Michael Grzeschik <[email protected]>
>> Signed-off-by: Avichal Rakesh <[email protected]>
>> ---
>> v1 -> v2: Rebased to ToT, and fixed deadlock reported in
>>          https://lore.kernel.org/all/[email protected]/
>> v2 -> v3: Fix email threading goof-up
>> v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
>>          as discussed in
>>          https://lore.kernel.org/[email protected]/
>
> I tested this and I no longer saw any use after free
> errors anymore! :)

Yay! Glad to hear!

>
> Here comes some more review:
>
>> drivers/usb/gadget/function/uvc.h       |   1 +
>> drivers/usb/gadget/function/uvc_v4l2.c  |  12 +-
>> drivers/usb/gadget/function/uvc_video.c | 156 +++++++++++++++++++-----
>> 3 files changed, 128 insertions(+), 41 deletions(-)
>>

>> +
>> +/*
>> + * Disable video stream
>> + */
>> +static int
>> +uvcg_video_disable(struct uvc_video *video) {
>> +    unsigned long flags;
>> +    struct list_head inflight_bufs;
>> +    struct usb_request *req, *temp;
>> +    struct uvc_buffer *buf, *btemp;
>> +    struct uvc_request *ureq, *utemp;
>> +
>> +    INIT_LIST_HEAD(&inflight_bufs);
>> +    spin_lock_irqsave(&video->req_lock, flags);
>> +    video->is_enabled = false;
>> +
>> +    /*
>> +     * Remove any in-flight buffers from the uvc_requests
>> +     * because we want to return them before cancelling the
>> +     * queue. This ensures that we aren't stuck waiting for
>> +     * all complete callbacks to come through before disabling
>> +     * vb2 queue.
>> +     */
>> +    list_for_each_entry(ureq, &video->ureqs, list) {
>> +        if (ureq->last_buf) {
>> +            list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
>> +            ureq->last_buf = NULL;
>> +        }
>> +    }
>>     spin_unlock_irqrestore(&video->req_lock, flags);
>> -    return;
>> +
>> +    cancel_work_sync(&video->pump);
>> +    uvcg_queue_cancel(&video->queue, 0);
>> +
>> +    spin_lock_irqsave(&video->req_lock, flags);
>> +    /*
>> +     * Remove all uvc_reqeusts from from ureqs with list_del_init
>> +     * This lets uvc_video_free_request correctly identify
>> +     * if the uvc_request is attached to a list or not when freeing
>> +     * memory.
>> +     */
>> +    list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
>> +        list_del_init(&ureq->list);
>> +
>> +    list_for_each_entry_safe(req, temp, &video->req_free, list) {
>> +        list_del(&req->list);
>> +        uvc_video_free_request(req->context, video->ep);
>> +    }
>> +
>> +    INIT_LIST_HEAD(&video->ureqs);
>> +    INIT_LIST_HEAD(&video->req_free);
>> +    video->req_size = 0;
>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>> +
>> +    /*
>> +     * Return all the video buffers before disabling the queue.
>> +     */
>> +    spin_lock_irqsave(&video->queue.irqlock, flags);
>> +    list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
>> +        list_del(&buf->queue);
>> +        uvcg_complete_buffer(&video->queue, buf);
>> +    }
>> +    spin_unlock_irqrestore(&video->queue.irqlock, flags);
>> +
>> +    uvcg_queue_enable(&video->queue, 0);
>> +    return 0;
>> }
>>
>> /*
>> @@ -497,28 +596,22 @@ static void uvcg_video_pump(struct work_struct *work)
>> int uvcg_video_enable(struct uvc_video *video, int enable)
>> {
>>     int ret;
>> -    struct uvc_request *ureq;
>>
>>     if (video->ep == NULL) {
>>         uvcg_info(&video->uvc->func,
>>               "Video enable failed, device is uninitialized.\n");
>>         return -ENODEV;
>>     }
>> -
>> -    if (!enable) {
>> -        cancel_work_sync(&video->pump);
>> -        uvcg_queue_cancel(&video->queue, 0);
>> -
>> -        list_for_each_entry(ureq, &video->ureqs, list) {
>> -            if (ureq->req)
>> -                usb_ep_dequeue(video->ep, ureq->req);
>> -        }
>> -
>> -        uvc_video_free_requests(video);
>> -        uvcg_queue_enable(&video->queue, 0);
>> -        return 0;
>> -    }
>> -
>> +    if (!enable)
>> +        return uvcg_video_disable(video);
>
> Could you refactor this code as it is to an separate
> function and prepand this change as an extra patch
> to this one? It would make the changes in the functions
> more obvious and better to review.

Sure I can send a follow up patch, but I am curious why you think this
needs to be a separate function? Refactoring into a function would
have the functions structured something like:

uvcg_video_disable(video) {
// ...
// disable impl
// ...
}

uvcg_video_enable(video) {
// ...
// enable impl
// ...
}

uvcg_video_enable(video, enable) {
// ep test

if (!enable)
return uvcg_video_disable(video);

return uvc_video_enable(video);
}

instead of the current structure:

uvcg_video_disable(video) {
// ...
// disable impl
// ...
}

uvcg_video_enable(video, enable) {
// ep test

if (!enable)
return uvcg_video_disable(video);

// ...
// enable impl
// ...
}

I am not sure if one is more readable than the other.

>
>> +
>> +    /*
>> +     * Safe to access request related fields without req_lock because
>> +     * this is the only thread currently active, and no other
>> +     * request handling thread will become active until this function
>> +     * returns.
>> +     */
>> +    video->is_enabled = true;
>
> Add an extra empty line.
>
>>     if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
>>         return ret;
>>
>> @@ -544,6 +637,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
>>  */
>> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
>> {
>> +    video->is_enabled = false;
>>     INIT_LIST_HEAD(&video->ureqs);
>>     INIT_LIST_HEAD(&video->req_free);
>>     spin_lock_init(&video->req_lock);
>> --
>> 2.42.0.609.gbb76f46606-goog
>>
>>
>
> Reviewed-by: Michael Grzeschik <[email protected]>
> Tested-by: Michael Grzeschik <[email protected]>
>
>

2023-10-18 22:07:26

by Michael Grzeschik

[permalink] [raw]
Subject: Re: [PATCH v4 3/3] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

On Wed, Oct 18, 2023 at 02:50:08PM -0700, Avichal Rakesh wrote:
>
>
>On 10/18/23 06:10, Michael Grzeschik wrote:
>> On Wed, Oct 11, 2023 at 05:24:51PM -0700, Avichal Rakesh wrote:
>>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>>> and deallocates them all when the video stream stops. This includes
>>> de-allocating all the usb_requests associated with those uvc_requests.
>>> This can lead to use-after-free issues if any of those de-allocated
>>> usb_requests were still owned by the usb controller.
>>>
>>> This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
>>> flag to uvc_video to track when frames and requests should be flowing.
>>> When disabling the video stream, the flag is tripped and, instead
>>> of de-allocating all uvc_requests and usb_requests, the gadget
>>> driver only de-allocates those usb_requests that are currently
>>> owned by it (as present in req_free). Other usb_requests are left
>>> untouched until their completion handler is called which takes care
>>> of freeing the usb_request and its corresponding uvc_request.
>>>
>>> Now that uvc_video does not depends on uvc->state, this patch removes
>>> unnecessary upates to uvc->state that were made to accomodate uvc_video
>>> logic. This should ensure that uvc gadget driver never accidentally
>>> de-allocates a usb_request that it doesn't own.
>>>
>>> Link: https://lore.kernel.org/[email protected]
>>> Suggested-by: Michael Grzeschik <[email protected]>
>>> Signed-off-by: Avichal Rakesh <[email protected]>
>>> ---
>>> v1 -> v2: Rebased to ToT, and fixed deadlock reported in
>>> ???????? https://lore.kernel.org/all/[email protected]/
>>> v2 -> v3: Fix email threading goof-up
>>> v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
>>> ???????? as discussed in
>>> ???????? https://lore.kernel.org/[email protected]/
>>
>> I tested this and I no longer saw any use after free
>> errors anymore! :)
>
>Yay! Glad to hear!
>
>>
>> Here comes some more review:
>>
>>> drivers/usb/gadget/function/uvc.h?????? |?? 1 +
>>> drivers/usb/gadget/function/uvc_v4l2.c? |? 12 +-
>>> drivers/usb/gadget/function/uvc_video.c | 156 +++++++++++++++++++-----
>>> 3 files changed, 128 insertions(+), 41 deletions(-)
>>>
>
>>> +
>>> +/*
>>> + * Disable video stream
>>> + */
>>> +static int
>>> +uvcg_video_disable(struct uvc_video *video) {
>>> +??? unsigned long flags;
>>> +??? struct list_head inflight_bufs;
>>> +??? struct usb_request *req, *temp;
>>> +??? struct uvc_buffer *buf, *btemp;
>>> +??? struct uvc_request *ureq, *utemp;
>>> +
>>> +??? INIT_LIST_HEAD(&inflight_bufs);
>>> +??? spin_lock_irqsave(&video->req_lock, flags);
>>> +??? video->is_enabled = false;
>>> +
>>> +??? /*
>>> +???? * Remove any in-flight buffers from the uvc_requests
>>> +???? * because we want to return them before cancelling the
>>> +???? * queue. This ensures that we aren't stuck waiting for
>>> +???? * all complete callbacks to come through before disabling
>>> +???? * vb2 queue.
>>> +???? */
>>> +??? list_for_each_entry(ureq, &video->ureqs, list) {
>>> +??????? if (ureq->last_buf) {
>>> +??????????? list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
>>> +??????????? ureq->last_buf = NULL;
>>> +??????? }
>>> +??? }
>>> ????spin_unlock_irqrestore(&video->req_lock, flags);
>>> -??? return;
>>> +
>>> +??? cancel_work_sync(&video->pump);
>>> +??? uvcg_queue_cancel(&video->queue, 0);
>>> +
>>> +??? spin_lock_irqsave(&video->req_lock, flags);
>>> +??? /*
>>> +???? * Remove all uvc_reqeusts from from ureqs with list_del_init
>>> +???? * This lets uvc_video_free_request correctly identify
>>> +???? * if the uvc_request is attached to a list or not when freeing
>>> +???? * memory.
>>> +???? */
>>> +??? list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
>>> +??????? list_del_init(&ureq->list);
>>> +
>>> +??? list_for_each_entry_safe(req, temp, &video->req_free, list) {
>>> +??????? list_del(&req->list);
>>> +??????? uvc_video_free_request(req->context, video->ep);
>>> +??? }
>>> +
>>> +??? INIT_LIST_HEAD(&video->ureqs);
>>> +??? INIT_LIST_HEAD(&video->req_free);
>>> +??? video->req_size = 0;
>>> +??? spin_unlock_irqrestore(&video->req_lock, flags);
>>> +
>>> +??? /*
>>> +???? * Return all the video buffers before disabling the queue.
>>> +???? */
>>> +??? spin_lock_irqsave(&video->queue.irqlock, flags);
>>> +??? list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
>>> +??????? list_del(&buf->queue);
>>> +??????? uvcg_complete_buffer(&video->queue, buf);
>>> +??? }
>>> +??? spin_unlock_irqrestore(&video->queue.irqlock, flags);
>>> +
>>> +??? uvcg_queue_enable(&video->queue, 0);
>>> +??? return 0;
>>> }
>>>
>>> /*
>>> @@ -497,28 +596,22 @@ static void uvcg_video_pump(struct work_struct *work)
>>> int uvcg_video_enable(struct uvc_video *video, int enable)
>>> {
>>> ????int ret;
>>> -??? struct uvc_request *ureq;
>>>
>>> ????if (video->ep == NULL) {
>>> ??????? uvcg_info(&video->uvc->func,
>>> ????????????? "Video enable failed, device is uninitialized.\n");
>>> ??????? return -ENODEV;
>>> ????}
>>> -
>>> -??? if (!enable) {
>>> -??????? cancel_work_sync(&video->pump);
>>> -??????? uvcg_queue_cancel(&video->queue, 0);
>>> -
>>> -??????? list_for_each_entry(ureq, &video->ureqs, list) {
>>> -??????????? if (ureq->req)
>>> -??????????????? usb_ep_dequeue(video->ep, ureq->req);
>>> -??????? }
>>> -
>>> -??????? uvc_video_free_requests(video);
>>> -??????? uvcg_queue_enable(&video->queue, 0);
>>> -??????? return 0;
>>> -??? }
>>> -
>>> +??? if (!enable)
>>> +??????? return uvcg_video_disable(video);
>>
>> Could you refactor this code as it is to an separate
>> function and prepand this change as an extra patch
>> to this one? It would make the changes in the functions
>> more obvious and better to review.
>
>Sure I can send a follow up patch, but I am curious why you think this
>needs to be a separate function? Refactoring into a function would
>have the functions structured something like:
>
>uvcg_video_disable(video) {
> // ...
> // disable impl
> // ...
>}
>
>uvcg_video_enable(video) {
> // ...
> // enable impl
> // ...
>}
>
>uvcg_video_enable(video, enable) {
> // ep test
>
> if (!enable)
> return uvcg_video_disable(video);
>
> return uvc_video_enable(video);
>}
>
>instead of the current structure:
>
>uvcg_video_disable(video) {
> // ...
> // disable impl
> // ...
>}
>
>uvcg_video_enable(video, enable) {
> // ep test
>
> if (!enable)
> return uvcg_video_disable(video);
>
> // ...
> // enable impl
> // ...
>}
>
>I am not sure if one is more readable than the other.

I think you misunderstood. The second structure is all right.

What I did want you to do is as follows.

Lets look at your series:

patch 0/3
patch 1/3
patch 2/3

<--- add a patch here that does the refactoring of the separate
function uvcg_video_disable without changing the functional
content of it:

uvcg_video_disable(video) {
// ...
// disable impl
// ...
}

uvcg_video_enable(video, enable) {
// ep test

if (!enable)
return uvcg_video_disable(video);

// ...
// enable impl
// ...
}

patch 3/3

This way in the patch 3/3 the functional changes you introduce to the
uvcg_video_diable will get better to review.

Regards,
Michael

--
Pengutronix e.K. | |
Steuerwalder Str. 21 | http://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |


Attachments:
(No filename) (7.77 kB)
signature.asc (849.00 B)
Download all attachments

2023-10-19 18:53:45

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v6 0/4] usb: gadget: uvc: stability fixes on STREAMOFF.

We have been seeing two main stability issues that uvc gadget driver
runs into when stopping streams:
1. Attempting to queue usb_requests to a disabled usb_ep
2. use-after-free issue for inflight usb_requests

The four patches below fix the two issues above. Patch 1/4 fixes the
first issue, and Patch 2/4 and 4/4 fix the second issue. Patch 3/4
is only there to make the diff in 4/4 cleaner.

Avichal Rakesh (4):
usb: gadget: uvc: prevent use of disabled endpoint
usb: gadget: uvc: Allocate uvc_requests one at a time
usb: gadget: uvc: move video disable logic to its own function
usb: gadget: uvc: Fix use-after-free for inflight usb_requests

drivers/usb/gadget/function/f_uvc.c | 11 +-
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 6 +-
drivers/usb/gadget/function/uvc_v4l2.c | 12 +-
drivers/usb/gadget/function/uvc_video.c | 231 +++++++++++++++++-------
5 files changed, 189 insertions(+), 73 deletions(-)

--
2.42.0.758.gaed0368e0e-goog

2023-10-19 18:53:53

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v6 1/4] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. As the endpoint's state can no longer be used, video_pump is
now guarded by uvc->state as well. To be consistent with the actual
streaming state, uvc->state is now toggled between CONNECTED and STREAMING
from the v4l2 event callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
---
v1 -> v2: Rebased to ToT and reworded commit message.
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT
v4 -> v5: Add Reviewed-by & Tested-by
v5 -> v6: No change

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
drivers/usb/gadget/function/uvc_video.c | 3 ++-
5 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index faa398109431..ae08341961eb 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep)
+ usb_ep_disable(uvc->video.ep);
+
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..e7f9f13f14dc 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..7cb8d027ff0c 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0)
+ return ret;
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ if (uvc->state == UVC_STATE_STREAMING)
+ uvc->state = UVC_STATE_CONNECTED;
+
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 91af3b1ef0d4..c334802ac0a4 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
+ struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (video->ep->enabled) {
+ while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
/*
* Retrieve the first available USB request, protected by the
* request lock.
--
2.42.0.758.gaed0368e0e-goog

2023-10-19 18:53:59

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v6 3/4] usb: gadget: uvc: move video disable logic to its own function

This patch refactors the video disable logic in uvcg_video_enable
into its own separate function 'uvcg_video_disable'.

Signed-off-by: Avichal Rakesh <[email protected]>
---
v6: Introduced this patch to make the next one easier to review

drivers/usb/gadget/function/uvc_video.c | 37 +++++++++++++++----------
1 file changed, 23 insertions(+), 14 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c180866c8e34..80b8eaea2d39 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -493,13 +493,33 @@ static void uvcg_video_pump(struct work_struct *work)
return;
}

+/*
+ * Disable video stream
+ */
+static int
+uvcg_video_disable(struct uvc_video *video)
+{
+ struct uvc_request *ureq;
+
+ cancel_work_sync(&video->pump);
+ uvcg_queue_cancel(&video->queue, 0);
+
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }
+
+ uvc_video_free_requests(video);
+ uvcg_queue_enable(&video->queue, 0);
+ return 0;
+}
+
/*
* Enable or disable the video stream.
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
int ret;
- struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -507,19 +527,8 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
return -ENODEV;
}

- if (!enable) {
- cancel_work_sync(&video->pump);
- uvcg_queue_cancel(&video->queue, 0);
-
- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
- }
-
- uvc_video_free_requests(video);
- uvcg_queue_enable(&video->queue, 0);
- return 0;
- }
+ if (!enable)
+ return uvcg_video_disable(video);

if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;
--
2.42.0.758.gaed0368e0e-goog

2023-10-19 18:54:00

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v6 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This patch is 1 of 2 patches addressing the use-after-free issue.
Instead of bulk allocating all uvc_requests as an array, this patch
allocates uvc_requests one at a time, which should allows for similar
granularity when deallocating the uvc_requests. This patch has no
functional changes other than allocating each uvc_request separately,
and similarly freeing each of them separately.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT
v4 -> v5: Address more review comments. Add Reviewed-by & Tested-by.
v5 -> v6: No change

drivers/usb/gadget/function/uvc.h | 3 +-
drivers/usb/gadget/function/uvc_video.c | 89 ++++++++++++++-----------
2 files changed, 52 insertions(+), 40 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 989bc6b4e93d..993694da0bbc 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -81,6 +81,7 @@ struct uvc_request {
struct sg_table sgt;
u8 header[UVCG_REQUEST_HEADER_LEN];
struct uvc_buffer *last_buf;
+ struct list_head list;
};

struct uvc_video {
@@ -102,7 +103,7 @@ struct uvc_video {

/* Requests */
unsigned int req_size;
- struct uvc_request *ureq;
+ struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
spinlock_t req_lock;

diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c334802ac0a4..c180866c8e34 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+static void
+uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
+{
+ sg_free_table(&ureq->sgt);
+ if (ureq->req && ep) {
+ usb_ep_free_request(ep, ureq->req);
+ ureq->req = NULL;
+ }
+
+ kfree(ureq->req_buffer);
+ ureq->req_buffer = NULL;
+
+ if (!list_empty(&ureq->list))
+ list_del_init(&ureq->list);
+
+ kfree(ureq);
+}
+
static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
{
int ret;
@@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
static int
uvc_video_free_requests(struct uvc_video *video)
{
- unsigned int i;
-
- if (video->ureq) {
- for (i = 0; i < video->uvc_num_requests; ++i) {
- sg_free_table(&video->ureq[i].sgt);
+ struct uvc_request *ureq, *temp;

- if (video->ureq[i].req) {
- usb_ep_free_request(video->ep, video->ureq[i].req);
- video->ureq[i].req = NULL;
- }
-
- if (video->ureq[i].req_buffer) {
- kfree(video->ureq[i].req_buffer);
- video->ureq[i].req_buffer = NULL;
- }
- }
-
- kfree(video->ureq);
- video->ureq = NULL;
- }
+ list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
+ uvc_video_free_request(ureq, video->ep);

+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
video->req_size = 0;
return 0;
@@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
static int
uvc_video_alloc_requests(struct uvc_video *video)
{
+ struct uvc_request *ureq;
unsigned int req_size;
unsigned int i;
int ret = -ENOMEM;
@@ -332,29 +336,34 @@ uvc_video_alloc_requests(struct uvc_video *video)
* max_t(unsigned int, video->ep->maxburst, 1)
* (video->ep->mult);

- video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
- if (video->ureq == NULL)
- return -ENOMEM;
+ INIT_LIST_HEAD(&video->ureqs);
+ for (i = 0; i < video->uvc_num_requests; i++) {
+ ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
+ if (ureq == NULL)
+ goto error;
+
+ INIT_LIST_HEAD(&ureq->list);
+
+ list_add_tail(&ureq->list, &video->ureqs);

- for (i = 0; i < video->uvc_num_requests; ++i) {
- video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
- if (video->ureq[i].req_buffer == NULL)
+ ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
+ if (ureq->req_buffer == NULL)
goto error;

- video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
- if (video->ureq[i].req == NULL)
+ ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
+ if (ureq->req == NULL)
goto error;

- video->ureq[i].req->buf = video->ureq[i].req_buffer;
- video->ureq[i].req->length = 0;
- video->ureq[i].req->complete = uvc_video_complete;
- video->ureq[i].req->context = &video->ureq[i];
- video->ureq[i].video = video;
- video->ureq[i].last_buf = NULL;
+ ureq->req->buf = ureq->req_buffer;
+ ureq->req->length = 0;
+ ureq->req->complete = uvc_video_complete;
+ ureq->req->context = ureq;
+ ureq->video = video;
+ ureq->last_buf = NULL;

- list_add_tail(&video->ureq[i].req->list, &video->req_free);
+ list_add_tail(&ureq->req->list, &video->req_free);
/* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
- sg_alloc_table(&video->ureq[i].sgt,
+ sg_alloc_table(&ureq->sgt,
DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
PAGE_SIZE) + 2, GFP_KERNEL);
}
@@ -489,8 +498,8 @@ static void uvcg_video_pump(struct work_struct *work)
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
- unsigned int i;
int ret;
+ struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -502,9 +511,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- for (i = 0; i < video->uvc_num_requests; ++i)
- if (video->ureq && video->ureq[i].req)
- usb_ep_dequeue(video->ep, video->ureq[i].req);
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

uvc_video_free_requests(video);
uvcg_queue_enable(&video->queue, 0);
@@ -536,6 +546,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
INIT_WORK(&video->pump, uvcg_video_pump);
--
2.42.0.758.gaed0368e0e-goog

2023-10-19 18:54:07

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
flag to uvc_video to track when frames and requests should be flowing.
When disabling the video stream, the flag is tripped and, instead
of de-allocating all uvc_requests and usb_requests, the gadget
driver only de-allocates those usb_requests that are currently
owned by it (as present in req_free). Other usb_requests are left
untouched until their completion handler is called which takes care
of freeing the usb_request and its corresponding uvc_request.

Now that uvc_video does not depends on uvc->state, this patch removes
unnecessary upates to uvc->state that were made to accommodate uvc_video
logic. This should ensure that uvc gadget driver never accidentally
de-allocates a usb_request that it doesn't own.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT, and fixed deadlock reported in
https://lore.kernel.org/all/[email protected]/
v2 -> v3: Fix email threading goof-up
v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
as discussed in
https://lore.kernel.org/[email protected]/
v4 -> v5: Address review comments. Add Reviewed-by & Tested-by.
v5 -> v6: Added another patch before this one to make uvcg_video_disable
easier to review.

drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_v4l2.c | 12 +--
drivers/usb/gadget/function/uvc_video.c | 128 ++++++++++++++++++++----
3 files changed, 111 insertions(+), 30 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 993694da0bbc..be0d012aa244 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -102,6 +102,7 @@ struct uvc_video {
unsigned int uvc_num_requests;

/* Requests */
+ bool is_enabled; /* tracks whether video stream is enabled */
unsigned int req_size;
struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 7cb8d027ff0c..f4d2e24835d4 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;
+ uvc_function_setup_continue(uvc, 0);

return 0;
}
@@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
if (type != video->queue.queue.type)
return -EINVAL;

- uvc->state = UVC_STATE_CONNECTED;
ret = uvcg_video_enable(video, 0);
if (ret < 0)
return ret;

+ uvc->state = UVC_STATE_CONNECTED;
uvc_function_setup_continue(uvc, 1);
return 0;
}
@@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
- /*
- * Drop uvc->state to CONNECTED if it was streaming before.
- * This ensures that the usb_requests are no longer queued
- * to the controller.
- */
- if (uvc->state == UVC_STATE_STREAMING)
- uvc->state = UVC_STATE_CONNECTED;
-
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 80b8eaea2d39..41fb4f24e829 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+/**
+ * Must be called with req_lock held as it modifies the list ureq is held in
+ */
static void
uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
{
@@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
struct uvc_request *ureq = req->context;
struct uvc_video *video = ureq->video;
struct uvc_video_queue *queue = &video->queue;
- struct uvc_device *uvc = video->uvc;
+ struct uvc_buffer *last_buf = NULL;
unsigned long flags;

+ spin_lock_irqsave(&video->req_lock, flags);
+ if (!video->is_enabled) {
+ /*
+ * When is_enabled is false, uvc_video_disable ensures that
+ * in-flight uvc_buffers are returned, so we can safely
+ * call free_request without worrying about last_buf.
+ */
+ uvc_video_free_request(ureq, ep);
+ spin_unlock_irqrestore(&video->req_lock, flags);
+ return;
+ }
+
+ last_buf = ureq->last_buf;
+ ureq->last_buf = NULL;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
switch (req->status) {
case 0:
break;
@@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
uvcg_queue_cancel(queue, 0);
}

- if (ureq->last_buf) {
- uvcg_complete_buffer(&video->queue, ureq->last_buf);
- ureq->last_buf = NULL;
+ if (last_buf) {
+ spin_lock_irqsave(&queue->irqlock, flags);
+ uvcg_complete_buffer(&video->queue, last_buf);
+ spin_unlock_irqrestore(&queue->irqlock, flags);
}

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
- spin_unlock_irqrestore(&video->req_lock, flags);
-
- if (uvc->state == UVC_STATE_STREAMING)
+ /*
+ * Video stream might have been disabled while we were
+ * processing the current usb_request. So make sure
+ * we're still streaming before queueing the usb_request
+ * back to req_free
+ */
+ if (video->is_enabled) {
+ list_add_tail(&req->list, &video->req_free);
queue_work(video->async_wq, &video->pump);
+ } else {
+ uvc_video_free_request(ureq, ep);
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
}

static int
@@ -393,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
- struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
+ while (true) {
+ if (!video->ep->enabled)
+ return;
+
/*
- * Retrieve the first available USB request, protected by the
- * request lock.
+ * Check is_enabled and retrieve the first available USB
+ * request, protected by the request lock.
*/
spin_lock_irqsave(&video->req_lock, flags);
- if (list_empty(&video->req_free)) {
+ if (!video->is_enabled || list_empty(&video->req_free)) {
spin_unlock_irqrestore(&video->req_lock, flags);
return;
}
@@ -488,9 +518,11 @@ static void uvcg_video_pump(struct work_struct *work)
return;

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
+ if (video->is_enabled)
+ list_add_tail(&req->list, &video->req_free);
+ else
+ uvc_video_free_request(req->context, video->ep);
spin_unlock_irqrestore(&video->req_lock, flags);
- return;
}

/*
@@ -499,17 +531,64 @@ static void uvcg_video_pump(struct work_struct *work)
static int
uvcg_video_disable(struct uvc_video *video)
{
- struct uvc_request *ureq;
+ unsigned long flags;
+ struct list_head inflight_bufs;
+ struct usb_request *req, *temp;
+ struct uvc_buffer *buf, *btemp;
+ struct uvc_request *ureq, *utemp;
+
+ INIT_LIST_HEAD(&inflight_bufs);
+ spin_lock_irqsave(&video->req_lock, flags);
+ video->is_enabled = false;
+
+ /*
+ * Remove any in-flight buffers from the uvc_requests
+ * because we want to return them before cancelling the
+ * queue. This ensures that we aren't stuck waiting for
+ * all complete callbacks to come through before disabling
+ * vb2 queue.
+ */
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->last_buf) {
+ list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
+ ureq->last_buf = NULL;
+ }
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);

cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
+ spin_lock_irqsave(&video->req_lock, flags);
+ /*
+ * Remove all uvc_reqeusts from ureqs with list_del_init
+ * This lets uvc_video_free_request correctly identify
+ * if the uvc_request is attached to a list or not when freeing
+ * memory.
+ */
+ list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
+ list_del_init(&ureq->list);
+
+ list_for_each_entry_safe(req, temp, &video->req_free, list) {
+ list_del(&req->list);
+ uvc_video_free_request(req->context, video->ep);
}

- uvc_video_free_requests(video);
+ INIT_LIST_HEAD(&video->ureqs);
+ INIT_LIST_HEAD(&video->req_free);
+ video->req_size = 0;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ /*
+ * Return all the video buffers before disabling the queue.
+ */
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
+ list_del(&buf->queue);
+ uvcg_complete_buffer(&video->queue, buf);
+ }
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
+
uvcg_queue_enable(&video->queue, 0);
return 0;
}
@@ -530,6 +609,14 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
if (!enable)
return uvcg_video_disable(video);

+ /*
+ * Safe to access request related fields without req_lock because
+ * this is the only thread currently active, and no other
+ * request handling thread will become active until this function
+ * returns.
+ */
+ video->is_enabled = true;
+
if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;

@@ -555,6 +642,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ video->is_enabled = false;
INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
--
2.42.0.758.gaed0368e0e-goog

2023-10-19 18:55:34

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v4 3/3] usb: gadget: uvc: Fix use-after-free for inflight usb_requests



On 10/18/23 15:06, Michael Grzeschik wrote:
> On Wed, Oct 18, 2023 at 02:50:08PM -0700, Avichal Rakesh wrote:
>>
>>
>> On 10/18/23 06:10, Michael Grzeschik wrote:
>>> On Wed, Oct 11, 2023 at 05:24:51PM -0700, Avichal Rakesh wrote:
>>>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>>>> and deallocates them all when the video stream stops. This includes
>>>> de-allocating all the usb_requests associated with those uvc_requests.
>>>> This can lead to use-after-free issues if any of those de-allocated
>>>> usb_requests were still owned by the usb controller.
>>>>
>>>> This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
>>>> flag to uvc_video to track when frames and requests should be flowing.
>>>> When disabling the video stream, the flag is tripped and, instead
>>>> of de-allocating all uvc_requests and usb_requests, the gadget
>>>> driver only de-allocates those usb_requests that are currently
>>>> owned by it (as present in req_free). Other usb_requests are left
>>>> untouched until their completion handler is called which takes care
>>>> of freeing the usb_request and its corresponding uvc_request.
>>>>
>>>> Now that uvc_video does not depends on uvc->state, this patch removes
>>>> unnecessary upates to uvc->state that were made to accomodate uvc_video
>>>> logic. This should ensure that uvc gadget driver never accidentally
>>>> de-allocates a usb_request that it doesn't own.
>>>>
>>>> Link: https://lore.kernel.org/[email protected]
>>>> Suggested-by: Michael Grzeschik <[email protected]>
>>>> Signed-off-by: Avichal Rakesh <[email protected]>
>>>> ---
>>>> v1 -> v2: Rebased to ToT, and fixed deadlock reported in
>>>>          https://lore.kernel.org/all/[email protected]/
>>>> v2 -> v3: Fix email threading goof-up
>>>> v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
>>>>          as discussed in
>>>>          https://lore.kernel.org/[email protected]/
>>>
>>> I tested this and I no longer saw any use after free
>>> errors anymore! :)
>>
>> Yay! Glad to hear!
>>
>>>
>>> Here comes some more review:
>>>
>>>> drivers/usb/gadget/function/uvc.h       |   1 +
>>>> drivers/usb/gadget/function/uvc_v4l2.c  |  12 +-
>>>> drivers/usb/gadget/function/uvc_video.c | 156 +++++++++++++++++++-----
>>>> 3 files changed, 128 insertions(+), 41 deletions(-)
>>>>
>>
>>>> +
>>>> +/*
>>>> + * Disable video stream
>>>> + */
>>>> +static int
>>>> +uvcg_video_disable(struct uvc_video *video) {
>>>> +    unsigned long flags;
>>>> +    struct list_head inflight_bufs;
>>>> +    struct usb_request *req, *temp;
>>>> +    struct uvc_buffer *buf, *btemp;
>>>> +    struct uvc_request *ureq, *utemp;
>>>> +
>>>> +    INIT_LIST_HEAD(&inflight_bufs);
>>>> +    spin_lock_irqsave(&video->req_lock, flags);
>>>> +    video->is_enabled = false;
>>>> +
>>>> +    /*
>>>> +     * Remove any in-flight buffers from the uvc_requests
>>>> +     * because we want to return them before cancelling the
>>>> +     * queue. This ensures that we aren't stuck waiting for
>>>> +     * all complete callbacks to come through before disabling
>>>> +     * vb2 queue.
>>>> +     */
>>>> +    list_for_each_entry(ureq, &video->ureqs, list) {
>>>> +        if (ureq->last_buf) {
>>>> +            list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
>>>> +            ureq->last_buf = NULL;
>>>> +        }
>>>> +    }
>>>>     spin_unlock_irqrestore(&video->req_lock, flags);
>>>> -    return;
>>>> +
>>>> +    cancel_work_sync(&video->pump);
>>>> +    uvcg_queue_cancel(&video->queue, 0);
>>>> +
>>>> +    spin_lock_irqsave(&video->req_lock, flags);
>>>> +    /*
>>>> +     * Remove all uvc_reqeusts from from ureqs with list_del_init
>>>> +     * This lets uvc_video_free_request correctly identify
>>>> +     * if the uvc_request is attached to a list or not when freeing
>>>> +     * memory.
>>>> +     */
>>>> +    list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
>>>> +        list_del_init(&ureq->list);
>>>> +
>>>> +    list_for_each_entry_safe(req, temp, &video->req_free, list) {
>>>> +        list_del(&req->list);
>>>> +        uvc_video_free_request(req->context, video->ep);
>>>> +    }
>>>> +
>>>> +    INIT_LIST_HEAD(&video->ureqs);
>>>> +    INIT_LIST_HEAD(&video->req_free);
>>>> +    video->req_size = 0;
>>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>>> +
>>>> +    /*
>>>> +     * Return all the video buffers before disabling the queue.
>>>> +     */
>>>> +    spin_lock_irqsave(&video->queue.irqlock, flags);
>>>> +    list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
>>>> +        list_del(&buf->queue);
>>>> +        uvcg_complete_buffer(&video->queue, buf);
>>>> +    }
>>>> +    spin_unlock_irqrestore(&video->queue.irqlock, flags);
>>>> +
>>>> +    uvcg_queue_enable(&video->queue, 0);
>>>> +    return 0;
>>>> }
>>>>
>>>> /*
>>>> @@ -497,28 +596,22 @@ static void uvcg_video_pump(struct work_struct *work)
>>>> int uvcg_video_enable(struct uvc_video *video, int enable)
>>>> {
>>>>     int ret;
>>>> -    struct uvc_request *ureq;
>>>>
>>>>     if (video->ep == NULL) {
>>>>         uvcg_info(&video->uvc->func,
>>>>               "Video enable failed, device is uninitialized.\n");
>>>>         return -ENODEV;
>>>>     }
>>>> -
>>>> -    if (!enable) {
>>>> -        cancel_work_sync(&video->pump);
>>>> -        uvcg_queue_cancel(&video->queue, 0);
>>>> -
>>>> -        list_for_each_entry(ureq, &video->ureqs, list) {
>>>> -            if (ureq->req)
>>>> -                usb_ep_dequeue(video->ep, ureq->req);
>>>> -        }
>>>> -
>>>> -        uvc_video_free_requests(video);
>>>> -        uvcg_queue_enable(&video->queue, 0);
>>>> -        return 0;
>>>> -    }
>>>> -
>>>> +    if (!enable)
>>>> +        return uvcg_video_disable(video);
>>>
>>> Could you refactor this code as it is to an separate
>>> function and prepand this change as an extra patch
>>> to this one? It would make the changes in the functions
>>> more obvious and better to review.
>>
>> Sure I can send a follow up patch, but I am curious why you think this
>> needs to be a separate function? Refactoring into a function would
>> have the functions structured something like:
>>
>> uvcg_video_disable(video) {
>>    // ...
>>    // disable impl
>>    // ...
>> }
>>
>> uvcg_video_enable(video) {
>>    // ...
>>    // enable impl
>>    // ...
>> }
>>
>> uvcg_video_enable(video, enable) {
>>    // ep test
>>
>>    if (!enable)
>>        return uvcg_video_disable(video);
>>
>>    return uvc_video_enable(video);
>> }
>>
>> instead of the current structure:
>>
>> uvcg_video_disable(video) {
>>    // ...
>>    // disable impl
>>    // ...
>> }
>>
>> uvcg_video_enable(video, enable) {
>>    // ep test
>>
>>    if (!enable)
>>        return uvcg_video_disable(video);
>>
>>    // ...
>>    // enable impl
>>    // ...
>> }
>>
>> I am not sure if one is more readable than the other.
>
> I think you misunderstood. The second structure is all right.
>
> What I did want you to do is as follows.
>
> Lets look at your series:
>
> patch 0/3
> patch 1/3
> patch 2/3
>
> <--- add a patch here that does the refactoring of the separate
>      function uvcg_video_disable without changing the functional
>      content of it:
>
> uvcg_video_disable(video) {
>     // ...
>     // disable impl
>     // ...
> }
>
> uvcg_video_enable(video, enable) {
>     // ep test
>
>     if (!enable)
>         return uvcg_video_disable(video);
>
>     // ...
>     // enable impl
>     // ...
> }
>
> patch 3/3
>
> This way in the patch 3/3 the functional changes you introduce to the
> uvcg_video_diable will get better to review.

I see! I did indeed misunderstand. Sent out v6 with 4 patches!

Thank you!
- Avi.

2023-10-19 19:00:38

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v6 0/4] usb: gadget: uvc: stability fixes on STREAMOFF.



On 10/19/23 11:53, Avichal Rakesh wrote:
> We have been seeing two main stability issues that uvc gadget driver
> runs into when stopping streams:
> 1. Attempting to queue usb_requests to a disabled usb_ep
> 2. use-after-free issue for inflight usb_requests
>
> The four patches below fix the two issues above. Patch 1/4 fixes the
> first issue, and Patch 2/4 and 4/4 fix the second issue. Patch 3/4
> is only there to make the diff in 4/4 cleaner.
>
> Avichal Rakesh (4):
> usb: gadget: uvc: prevent use of disabled endpoint
> usb: gadget: uvc: Allocate uvc_requests one at a time
> usb: gadget: uvc: move video disable logic to its own function
> usb: gadget: uvc: Fix use-after-free for inflight usb_requests
>
> drivers/usb/gadget/function/f_uvc.c | 11 +-
> drivers/usb/gadget/function/f_uvc.h | 2 +-
> drivers/usb/gadget/function/uvc.h | 6 +-
> drivers/usb/gadget/function/uvc_v4l2.c | 12 +-
> drivers/usb/gadget/function/uvc_video.c | 231 +++++++++++++++++-------
> 5 files changed, 189 insertions(+), 73 deletions(-)
>
> --
> 2.42.0.758.gaed0368e0e-goog

Dan and Laurent, please go over the patches whenever you get a
chance. I think they're ready to submit as neither Michael
nor I have seen any use-after-free issues after the patches.

Thank you!
- Avi.

2023-10-19 20:32:53

by kernel test robot

[permalink] [raw]
Subject: Re: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Hi Avichal,

kernel test robot noticed the following build warnings:

[auto build test WARNING on usb/usb-testing]
[also build test WARNING on usb/usb-next usb/usb-linus linus/master v6.6-rc6 next-20231019]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url: https://github.com/intel-lab-lkp/linux/commits/Avichal-Rakesh/usb-gadget-uvc-prevent-use-of-disabled-endpoint/20231020-025512
base: https://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb.git usb-testing
patch link: https://lore.kernel.org/r/20231019185319.2714000-5-arakesh%40google.com
patch subject: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests
config: m68k-allyesconfig (https://download.01.org/0day-ci/archive/20231020/[email protected]/config)
compiler: m68k-linux-gcc (GCC) 13.2.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20231020/[email protected]/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/

All warnings (new ones prefixed by >>):

>> drivers/usb/gadget/function/uvc_video.c:231: warning: This comment starts with '/**', but isn't a kernel-doc comment. Refer Documentation/doc-guide/kernel-doc.rst
* Must be called with req_lock held as it modifies the list ureq is held in


vim +231 drivers/usb/gadget/function/uvc_video.c

225
226 /* --------------------------------------------------------------------------
227 * Request handling
228 */
229
230 /**
> 231 * Must be called with req_lock held as it modifies the list ureq is held in
232 */
233 static void
234 uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
235 {
236 sg_free_table(&ureq->sgt);
237 if (ureq->req && ep) {
238 usb_ep_free_request(ep, ureq->req);
239 ureq->req = NULL;
240 }
241
242 kfree(ureq->req_buffer);
243 ureq->req_buffer = NULL;
244
245 if (!list_empty(&ureq->list))
246 list_del_init(&ureq->list);
247
248 kfree(ureq);
249 }
250

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

2023-10-19 22:30:27

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests



On 10/19/23 13:32, kernel test robot wrote:
> Hi Avichal,
>
> kernel test robot noticed the following build warnings:
>
> [auto build test WARNING on usb/usb-testing]
> [also build test WARNING on usb/usb-next usb/usb-linus linus/master v6.6-rc6 next-20231019]
> [If your patch is applied to the wrong git tree, kindly drop us a note.
> And when submitting patch, we suggest to use '--base' as documented in
> https://git-scm.com/docs/git-format-patch#_base_tree_information]
>
> url: https://github.com/intel-lab-lkp/linux/commits/Avichal-Rakesh/usb-gadget-uvc-prevent-use-of-disabled-endpoint/20231020-025512
> base: https://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb.git usb-testing
> patch link: https://lore.kernel.org/r/20231019185319.2714000-5-arakesh%40google.com
> patch subject: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests
> config: m68k-allyesconfig (https://download.01.org/0day-ci/archive/20231020/[email protected]/config)
> compiler: m68k-linux-gcc (GCC) 13.2.0
> reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20231020/[email protected]/reproduce)
>
> If you fix the issue in a separate patch/commit (i.e. not just a new version of
> the same patch/commit), kindly add following tags
> | Reported-by: kernel test robot <[email protected]>
> | Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/
>
> All warnings (new ones prefixed by >>):
>
>>> drivers/usb/gadget/function/uvc_video.c:231: warning: This comment starts with '/**', but isn't a kernel-doc comment. Refer Documentation/doc-guide/kernel-doc.rst
> * Must be called with req_lock held as it modifies the list ureq is held in
>
>

Greg, apologies for the newb question: do you want me to upload
the fix for this as a reply to [PATCH v6 4/4], or upload a new chain of
v7s with this patch fixed?

I am not familiar with the kernel merging process, so not sure
which one would work better for you to pick up.

Regards,
Avi

2023-10-20 17:37:31

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v7 1/4] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. As the endpoint's state can no longer be used, video_pump is
now guarded by uvc->state as well. To be consistent with the actual
streaming state, uvc->state is now toggled between CONNECTED and STREAMING
from the v4l2 event callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT and reworded commit message.
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT
v4 -> v5: Add Reviewed-by & Tested-by
v5 -> v6: No change
v6 -> v7: No change

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
drivers/usb/gadget/function/uvc_video.c | 3 ++-
5 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index faa398109431..ae08341961eb 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep)
+ usb_ep_disable(uvc->video.ep);
+
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..e7f9f13f14dc 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..7cb8d027ff0c 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0)
+ return ret;
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ if (uvc->state == UVC_STATE_STREAMING)
+ uvc->state = UVC_STATE_CONNECTED;
+
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 91af3b1ef0d4..c334802ac0a4 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
+ struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (video->ep->enabled) {
+ while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
/*
* Retrieve the first available USB request, protected by the
* request lock.
--
2.42.0.758.gaed0368e0e-goog

2023-10-21 10:06:15

by Greg Kroah-Hartman

[permalink] [raw]
Subject: Re: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

On Thu, Oct 19, 2023 at 03:30:00PM -0700, Avichal Rakesh wrote:
>
>
> On 10/19/23 13:32, kernel test robot wrote:
> > Hi Avichal,
> >
> > kernel test robot noticed the following build warnings:
> >
> > [auto build test WARNING on usb/usb-testing]
> > [also build test WARNING on usb/usb-next usb/usb-linus linus/master v6.6-rc6 next-20231019]
> > [If your patch is applied to the wrong git tree, kindly drop us a note.
> > And when submitting patch, we suggest to use '--base' as documented in
> > https://git-scm.com/docs/git-format-patch#_base_tree_information]
> >
> > url: https://github.com/intel-lab-lkp/linux/commits/Avichal-Rakesh/usb-gadget-uvc-prevent-use-of-disabled-endpoint/20231020-025512
> > base: https://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb.git usb-testing
> > patch link: https://lore.kernel.org/r/20231019185319.2714000-5-arakesh%40google.com
> > patch subject: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests
> > config: m68k-allyesconfig (https://download.01.org/0day-ci/archive/20231020/[email protected]/config)
> > compiler: m68k-linux-gcc (GCC) 13.2.0
> > reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20231020/[email protected]/reproduce)
> >
> > If you fix the issue in a separate patch/commit (i.e. not just a new version of
> > the same patch/commit), kindly add following tags
> > | Reported-by: kernel test robot <[email protected]>
> > | Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/
> >
> > All warnings (new ones prefixed by >>):
> >
> >>> drivers/usb/gadget/function/uvc_video.c:231: warning: This comment starts with '/**', but isn't a kernel-doc comment. Refer Documentation/doc-guide/kernel-doc.rst
> > * Must be called with req_lock held as it modifies the list ureq is held in
> >
> >
>
> Greg, apologies for the newb question: do you want me to upload
> the fix for this as a reply to [PATCH v6 4/4], or upload a new chain of
> v7s with this patch fixed?

A whole new v7 series please.

thanks,

greg k-h

2023-10-23 21:26:18

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

On Sat, Oct 21, 2023 at 3:05 AM Greg KH <[email protected]> wrote:
>
> On Thu, Oct 19, 2023 at 03:30:00PM -0700, Avichal Rakesh wrote:
> >
> >
> > On 10/19/23 13:32, kernel test robot wrote:
> > > Hi Avichal,
> > >
> > > kernel test robot noticed the following build warnings:
> > >
> > > [auto build test WARNING on usb/usb-testing]
> > > [also build test WARNING on usb/usb-next usb/usb-linus linus/master v6.6-rc6 next-20231019]
> > > [If your patch is applied to the wrong git tree, kindly drop us a note.
> > > And when submitting patch, we suggest to use '--base' as documented in
> > > https://git-scm.com/docs/git-format-patch#_base_tree_information]
> > >
> > > url: https://github.com/intel-lab-lkp/linux/commits/Avichal-Rakesh/usb-gadget-uvc-prevent-use-of-disabled-endpoint/20231020-025512
> > > base: https://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb.git usb-testing
> > > patch link: https://lore.kernel.org/r/20231019185319.2714000-5-arakesh%40google.com
> > > patch subject: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests
> > > config: m68k-allyesconfig (https://download.01.org/0day-ci/archive/20231020/[email protected]/config)
> > > compiler: m68k-linux-gcc (GCC) 13.2.0
> > > reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20231020/[email protected]/reproduce)
> > >
> > > If you fix the issue in a separate patch/commit (i.e. not just a new version of
> > > the same patch/commit), kindly add following tags
> > > | Reported-by: kernel test robot <[email protected]>
> > > | Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/
> > >
> > > All warnings (new ones prefixed by >>):
> > >
> > >>> drivers/usb/gadget/function/uvc_video.c:231: warning: This comment starts with '/**', but isn't a kernel-doc comment. Refer Documentation/doc-guide/kernel-doc.rst
> > > * Must be called with req_lock held as it modifies the list ureq is held in
> > >
> > >
> >
> > Greg, apologies for the newb question: do you want me to upload
> > the fix for this as a reply to [PATCH v6 4/4], or upload a new chain of
> > v7s with this patch fixed?
>
> A whole new v7 series please.
>

Had a feeling, so sent out v7 series preemptively. Let me know if that
doesn't work.

v7: https://lore.kernel.org/[email protected]/

Thank you!
- Avi

2023-10-24 09:28:19

by Greg Kroah-Hartman

[permalink] [raw]
Subject: Re: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

On Mon, Oct 23, 2023 at 02:25:30PM -0700, Avichal Rakesh wrote:
> On Sat, Oct 21, 2023 at 3:05 AM Greg KH <[email protected]> wrote:
> >
> > On Thu, Oct 19, 2023 at 03:30:00PM -0700, Avichal Rakesh wrote:
> > >
> > >
> > > On 10/19/23 13:32, kernel test robot wrote:
> > > > Hi Avichal,
> > > >
> > > > kernel test robot noticed the following build warnings:
> > > >
> > > > [auto build test WARNING on usb/usb-testing]
> > > > [also build test WARNING on usb/usb-next usb/usb-linus linus/master v6.6-rc6 next-20231019]
> > > > [If your patch is applied to the wrong git tree, kindly drop us a note.
> > > > And when submitting patch, we suggest to use '--base' as documented in
> > > > https://git-scm.com/docs/git-format-patch#_base_tree_information]
> > > >
> > > > url: https://github.com/intel-lab-lkp/linux/commits/Avichal-Rakesh/usb-gadget-uvc-prevent-use-of-disabled-endpoint/20231020-025512
> > > > base: https://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb.git usb-testing
> > > > patch link: https://lore.kernel.org/r/20231019185319.2714000-5-arakesh%40google.com
> > > > patch subject: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests
> > > > config: m68k-allyesconfig (https://download.01.org/0day-ci/archive/20231020/[email protected]/config)
> > > > compiler: m68k-linux-gcc (GCC) 13.2.0
> > > > reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20231020/[email protected]/reproduce)
> > > >
> > > > If you fix the issue in a separate patch/commit (i.e. not just a new version of
> > > > the same patch/commit), kindly add following tags
> > > > | Reported-by: kernel test robot <[email protected]>
> > > > | Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/
> > > >
> > > > All warnings (new ones prefixed by >>):
> > > >
> > > >>> drivers/usb/gadget/function/uvc_video.c:231: warning: This comment starts with '/**', but isn't a kernel-doc comment. Refer Documentation/doc-guide/kernel-doc.rst
> > > > * Must be called with req_lock held as it modifies the list ureq is held in
> > > >
> > > >
> > >
> > > Greg, apologies for the newb question: do you want me to upload
> > > the fix for this as a reply to [PATCH v6 4/4], or upload a new chain of
> > > v7s with this patch fixed?
> >
> > A whole new v7 series please.
> >
>
> Had a feeling, so sent out v7 series preemptively. Let me know if that
> doesn't work.
>
> v7: https://lore.kernel.org/[email protected]/

I have already dropped that from my review queue as your emails crossed
with that, so I thought it was obsolete by now, sorry.

Can you send a v8 please?

thanks,

greg k-h

2023-10-24 18:36:43

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v8 1/4] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. As the endpoint's state can no longer be used, video_pump is
now guarded by uvc->state as well. To be consistent with the actual
streaming state, uvc->state is now toggled between CONNECTED and STREAMING
from the v4l2 event callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT and reworded commit message.
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT
v4 -> v5: Add Reviewed-by & Tested-by
v5 -> v6: No change
v6 -> v7: No change
v7 -> v8: No change. Getting back in review queue

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
drivers/usb/gadget/function/uvc_video.c | 3 ++-
5 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index faa398109431..ae08341961eb 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep)
+ usb_ep_disable(uvc->video.ep);
+
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..e7f9f13f14dc 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..7cb8d027ff0c 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0)
+ return ret;
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ if (uvc->state == UVC_STATE_STREAMING)
+ uvc->state = UVC_STATE_CONNECTED;
+
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 91af3b1ef0d4..c334802ac0a4 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
+ struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (video->ep->enabled) {
+ while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
/*
* Retrieve the first available USB request, protected by the
* request lock.
--
2.42.0.758.gaed0368e0e-goog

2023-10-24 18:36:45

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v8 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This patch is 1 of 2 patches addressing the use-after-free issue.
Instead of bulk allocating all uvc_requests as an array, this patch
allocates uvc_requests one at a time, which should allows for similar
granularity when deallocating the uvc_requests. This patch has no
functional changes other than allocating each uvc_request separately,
and similarly freeing each of them separately.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT
v4 -> v5: Address more review comments. Add Reviewed-by & Tested-by.
v5 -> v6: No change
v6 -> v7: No change
v7 -> v8: No change. Getting back in review queue

drivers/usb/gadget/function/uvc.h | 3 +-
drivers/usb/gadget/function/uvc_video.c | 89 ++++++++++++++-----------
2 files changed, 52 insertions(+), 40 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 989bc6b4e93d..993694da0bbc 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -81,6 +81,7 @@ struct uvc_request {
struct sg_table sgt;
u8 header[UVCG_REQUEST_HEADER_LEN];
struct uvc_buffer *last_buf;
+ struct list_head list;
};

struct uvc_video {
@@ -102,7 +103,7 @@ struct uvc_video {

/* Requests */
unsigned int req_size;
- struct uvc_request *ureq;
+ struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
spinlock_t req_lock;

diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c334802ac0a4..c180866c8e34 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+static void
+uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
+{
+ sg_free_table(&ureq->sgt);
+ if (ureq->req && ep) {
+ usb_ep_free_request(ep, ureq->req);
+ ureq->req = NULL;
+ }
+
+ kfree(ureq->req_buffer);
+ ureq->req_buffer = NULL;
+
+ if (!list_empty(&ureq->list))
+ list_del_init(&ureq->list);
+
+ kfree(ureq);
+}
+
static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
{
int ret;
@@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
static int
uvc_video_free_requests(struct uvc_video *video)
{
- unsigned int i;
-
- if (video->ureq) {
- for (i = 0; i < video->uvc_num_requests; ++i) {
- sg_free_table(&video->ureq[i].sgt);
+ struct uvc_request *ureq, *temp;

- if (video->ureq[i].req) {
- usb_ep_free_request(video->ep, video->ureq[i].req);
- video->ureq[i].req = NULL;
- }
-
- if (video->ureq[i].req_buffer) {
- kfree(video->ureq[i].req_buffer);
- video->ureq[i].req_buffer = NULL;
- }
- }
-
- kfree(video->ureq);
- video->ureq = NULL;
- }
+ list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
+ uvc_video_free_request(ureq, video->ep);

+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
video->req_size = 0;
return 0;
@@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
static int
uvc_video_alloc_requests(struct uvc_video *video)
{
+ struct uvc_request *ureq;
unsigned int req_size;
unsigned int i;
int ret = -ENOMEM;
@@ -332,29 +336,34 @@ uvc_video_alloc_requests(struct uvc_video *video)
* max_t(unsigned int, video->ep->maxburst, 1)
* (video->ep->mult);

- video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
- if (video->ureq == NULL)
- return -ENOMEM;
+ INIT_LIST_HEAD(&video->ureqs);
+ for (i = 0; i < video->uvc_num_requests; i++) {
+ ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
+ if (ureq == NULL)
+ goto error;
+
+ INIT_LIST_HEAD(&ureq->list);
+
+ list_add_tail(&ureq->list, &video->ureqs);

- for (i = 0; i < video->uvc_num_requests; ++i) {
- video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
- if (video->ureq[i].req_buffer == NULL)
+ ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
+ if (ureq->req_buffer == NULL)
goto error;

- video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
- if (video->ureq[i].req == NULL)
+ ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
+ if (ureq->req == NULL)
goto error;

- video->ureq[i].req->buf = video->ureq[i].req_buffer;
- video->ureq[i].req->length = 0;
- video->ureq[i].req->complete = uvc_video_complete;
- video->ureq[i].req->context = &video->ureq[i];
- video->ureq[i].video = video;
- video->ureq[i].last_buf = NULL;
+ ureq->req->buf = ureq->req_buffer;
+ ureq->req->length = 0;
+ ureq->req->complete = uvc_video_complete;
+ ureq->req->context = ureq;
+ ureq->video = video;
+ ureq->last_buf = NULL;

- list_add_tail(&video->ureq[i].req->list, &video->req_free);
+ list_add_tail(&ureq->req->list, &video->req_free);
/* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
- sg_alloc_table(&video->ureq[i].sgt,
+ sg_alloc_table(&ureq->sgt,
DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
PAGE_SIZE) + 2, GFP_KERNEL);
}
@@ -489,8 +498,8 @@ static void uvcg_video_pump(struct work_struct *work)
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
- unsigned int i;
int ret;
+ struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -502,9 +511,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- for (i = 0; i < video->uvc_num_requests; ++i)
- if (video->ureq && video->ureq[i].req)
- usb_ep_dequeue(video->ep, video->ureq[i].req);
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

uvc_video_free_requests(video);
uvcg_queue_enable(&video->queue, 0);
@@ -536,6 +546,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
INIT_WORK(&video->pump, uvcg_video_pump);
--
2.42.0.758.gaed0368e0e-goog

2023-10-24 18:36:45

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v8 3/4] usb: gadget: uvc: move video disable logic to its own function

This patch refactors the video disable logic in uvcg_video_enable
into its own separate function 'uvcg_video_disable'.

Suggested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v6: Introduced this patch to make the next one easier to review
v6 -> v7: Add Suggested-by
v7 -> v8: No change. Getting back in review queue

drivers/usb/gadget/function/uvc_video.c | 37 +++++++++++++++----------
1 file changed, 23 insertions(+), 14 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c180866c8e34..80b8eaea2d39 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -493,13 +493,33 @@ static void uvcg_video_pump(struct work_struct *work)
return;
}

+/*
+ * Disable video stream
+ */
+static int
+uvcg_video_disable(struct uvc_video *video)
+{
+ struct uvc_request *ureq;
+
+ cancel_work_sync(&video->pump);
+ uvcg_queue_cancel(&video->queue, 0);
+
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }
+
+ uvc_video_free_requests(video);
+ uvcg_queue_enable(&video->queue, 0);
+ return 0;
+}
+
/*
* Enable or disable the video stream.
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
int ret;
- struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -507,19 +527,8 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
return -ENODEV;
}

- if (!enable) {
- cancel_work_sync(&video->pump);
- uvcg_queue_cancel(&video->queue, 0);
-
- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
- }
-
- uvc_video_free_requests(video);
- uvcg_queue_enable(&video->queue, 0);
- return 0;
- }
+ if (!enable)
+ return uvcg_video_disable(video);

if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;
--
2.42.0.758.gaed0368e0e-goog

2023-10-24 18:37:04

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v8 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
flag to uvc_video to track when frames and requests should be flowing.
When disabling the video stream, the flag is tripped and, instead
of de-allocating all uvc_requests and usb_requests, the gadget
driver only de-allocates those usb_requests that are currently
owned by it (as present in req_free). Other usb_requests are left
untouched until their completion handler is called which takes care
of freeing the usb_request and its corresponding uvc_request.

Now that uvc_video does not depends on uvc->state, this patch removes
unnecessary upates to uvc->state that were made to accommodate uvc_video
logic. This should ensure that uvc gadget driver never accidentally
de-allocates a usb_request that it doesn't own.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT, and fixed deadlock reported in
https://lore.kernel.org/all/[email protected]/
v2 -> v3: Fix email threading goof-up
v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
as discussed in
https://lore.kernel.org/[email protected]/
v4 -> v5: Address review comments. Add Reviewed-by & Tested-by.
v5 -> v6: Added another patch before this one to make uvcg_video_disable
easier to review.
v6 -> v7: Fix warning reported in
https://lore.kernel.org/[email protected]/
v7 -> v8: No change. Getting back in review queue

drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_v4l2.c | 12 +--
drivers/usb/gadget/function/uvc_video.c | 128 ++++++++++++++++++++----
3 files changed, 111 insertions(+), 30 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 993694da0bbc..be0d012aa244 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -102,6 +102,7 @@ struct uvc_video {
unsigned int uvc_num_requests;

/* Requests */
+ bool is_enabled; /* tracks whether video stream is enabled */
unsigned int req_size;
struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 7cb8d027ff0c..f4d2e24835d4 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;
+ uvc_function_setup_continue(uvc, 0);

return 0;
}
@@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
if (type != video->queue.queue.type)
return -EINVAL;

- uvc->state = UVC_STATE_CONNECTED;
ret = uvcg_video_enable(video, 0);
if (ret < 0)
return ret;

+ uvc->state = UVC_STATE_CONNECTED;
uvc_function_setup_continue(uvc, 1);
return 0;
}
@@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
- /*
- * Drop uvc->state to CONNECTED if it was streaming before.
- * This ensures that the usb_requests are no longer queued
- * to the controller.
- */
- if (uvc->state == UVC_STATE_STREAMING)
- uvc->state = UVC_STATE_CONNECTED;
-
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 80b8eaea2d39..ab3f02054e85 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+/*
+ * Must be called with req_lock held as it modifies the list ureq is held in
+ */
static void
uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
{
@@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
struct uvc_request *ureq = req->context;
struct uvc_video *video = ureq->video;
struct uvc_video_queue *queue = &video->queue;
- struct uvc_device *uvc = video->uvc;
+ struct uvc_buffer *last_buf = NULL;
unsigned long flags;

+ spin_lock_irqsave(&video->req_lock, flags);
+ if (!video->is_enabled) {
+ /*
+ * When is_enabled is false, uvc_video_disable ensures that
+ * in-flight uvc_buffers are returned, so we can safely
+ * call free_request without worrying about last_buf.
+ */
+ uvc_video_free_request(ureq, ep);
+ spin_unlock_irqrestore(&video->req_lock, flags);
+ return;
+ }
+
+ last_buf = ureq->last_buf;
+ ureq->last_buf = NULL;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
switch (req->status) {
case 0:
break;
@@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
uvcg_queue_cancel(queue, 0);
}

- if (ureq->last_buf) {
- uvcg_complete_buffer(&video->queue, ureq->last_buf);
- ureq->last_buf = NULL;
+ if (last_buf) {
+ spin_lock_irqsave(&queue->irqlock, flags);
+ uvcg_complete_buffer(&video->queue, last_buf);
+ spin_unlock_irqrestore(&queue->irqlock, flags);
}

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
- spin_unlock_irqrestore(&video->req_lock, flags);
-
- if (uvc->state == UVC_STATE_STREAMING)
+ /*
+ * Video stream might have been disabled while we were
+ * processing the current usb_request. So make sure
+ * we're still streaming before queueing the usb_request
+ * back to req_free
+ */
+ if (video->is_enabled) {
+ list_add_tail(&req->list, &video->req_free);
queue_work(video->async_wq, &video->pump);
+ } else {
+ uvc_video_free_request(ureq, ep);
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
}

static int
@@ -393,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
- struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
+ while (true) {
+ if (!video->ep->enabled)
+ return;
+
/*
- * Retrieve the first available USB request, protected by the
- * request lock.
+ * Check is_enabled and retrieve the first available USB
+ * request, protected by the request lock.
*/
spin_lock_irqsave(&video->req_lock, flags);
- if (list_empty(&video->req_free)) {
+ if (!video->is_enabled || list_empty(&video->req_free)) {
spin_unlock_irqrestore(&video->req_lock, flags);
return;
}
@@ -488,9 +518,11 @@ static void uvcg_video_pump(struct work_struct *work)
return;

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
+ if (video->is_enabled)
+ list_add_tail(&req->list, &video->req_free);
+ else
+ uvc_video_free_request(req->context, video->ep);
spin_unlock_irqrestore(&video->req_lock, flags);
- return;
}

/*
@@ -499,17 +531,64 @@ static void uvcg_video_pump(struct work_struct *work)
static int
uvcg_video_disable(struct uvc_video *video)
{
- struct uvc_request *ureq;
+ unsigned long flags;
+ struct list_head inflight_bufs;
+ struct usb_request *req, *temp;
+ struct uvc_buffer *buf, *btemp;
+ struct uvc_request *ureq, *utemp;
+
+ INIT_LIST_HEAD(&inflight_bufs);
+ spin_lock_irqsave(&video->req_lock, flags);
+ video->is_enabled = false;
+
+ /*
+ * Remove any in-flight buffers from the uvc_requests
+ * because we want to return them before cancelling the
+ * queue. This ensures that we aren't stuck waiting for
+ * all complete callbacks to come through before disabling
+ * vb2 queue.
+ */
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->last_buf) {
+ list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
+ ureq->last_buf = NULL;
+ }
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);

cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
+ spin_lock_irqsave(&video->req_lock, flags);
+ /*
+ * Remove all uvc_reqeusts from ureqs with list_del_init
+ * This lets uvc_video_free_request correctly identify
+ * if the uvc_request is attached to a list or not when freeing
+ * memory.
+ */
+ list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
+ list_del_init(&ureq->list);
+
+ list_for_each_entry_safe(req, temp, &video->req_free, list) {
+ list_del(&req->list);
+ uvc_video_free_request(req->context, video->ep);
}

- uvc_video_free_requests(video);
+ INIT_LIST_HEAD(&video->ureqs);
+ INIT_LIST_HEAD(&video->req_free);
+ video->req_size = 0;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ /*
+ * Return all the video buffers before disabling the queue.
+ */
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
+ list_del(&buf->queue);
+ uvcg_complete_buffer(&video->queue, buf);
+ }
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
+
uvcg_queue_enable(&video->queue, 0);
return 0;
}
@@ -530,6 +609,14 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
if (!enable)
return uvcg_video_disable(video);

+ /*
+ * Safe to access request related fields without req_lock because
+ * this is the only thread currently active, and no other
+ * request handling thread will become active until this function
+ * returns.
+ */
+ video->is_enabled = true;
+
if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;

@@ -555,6 +642,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ video->is_enabled = false;
INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
--
2.42.0.758.gaed0368e0e-goog

2023-10-24 20:01:15

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

On Tue, Oct 24, 2023 at 2:27 AM Greg KH <[email protected]> wrote:
>
> On Mon, Oct 23, 2023 at 02:25:30PM -0700, Avichal Rakesh wrote:
> > On Sat, Oct 21, 2023 at 3:05 AM Greg KH <[email protected]> wrote:
> > >
> > > On Thu, Oct 19, 2023 at 03:30:00PM -0700, Avichal Rakesh wrote:
> > > >
> > > >
> > > > On 10/19/23 13:32, kernel test robot wrote:
> > > > > Hi Avichal,
> > > > >
> > > > > kernel test robot noticed the following build warnings:
> > > > >
> > > > > [auto build test WARNING on usb/usb-testing]
> > > > > [also build test WARNING on usb/usb-next usb/usb-linus linus/master v6.6-rc6 next-20231019]
> > > > > [If your patch is applied to the wrong git tree, kindly drop us a note.
> > > > > And when submitting patch, we suggest to use '--base' as documented in
> > > > > https://git-scm.com/docs/git-format-patch#_base_tree_information]
> > > > >
> > > > > url: https://github.com/intel-lab-lkp/linux/commits/Avichal-Rakesh/usb-gadget-uvc-prevent-use-of-disabled-endpoint/20231020-025512
> > > > > base: https://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb.git usb-testing
> > > > > patch link: https://lore.kernel.org/r/20231019185319.2714000-5-arakesh%40google.com
> > > > > patch subject: [PATCH v6 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests
> > > > > config: m68k-allyesconfig (https://download.01.org/0day-ci/archive/20231020/[email protected]/config)
> > > > > compiler: m68k-linux-gcc (GCC) 13.2.0
> > > > > reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20231020/[email protected]/reproduce)
> > > > >
> > > > > If you fix the issue in a separate patch/commit (i.e. not just a new version of
> > > > > the same patch/commit), kindly add following tags
> > > > > | Reported-by: kernel test robot <[email protected]>
> > > > > | Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/
> > > > >
> > > > > All warnings (new ones prefixed by >>):
> > > > >
> > > > >>> drivers/usb/gadget/function/uvc_video.c:231: warning: This comment starts with '/**', but isn't a kernel-doc comment. Refer Documentation/doc-guide/kernel-doc.rst
> > > > > * Must be called with req_lock held as it modifies the list ureq is held in
> > > > >
> > > > >
> > > >
> > > > Greg, apologies for the newb question: do you want me to upload
> > > > the fix for this as a reply to [PATCH v6 4/4], or upload a new chain of
> > > > v7s with this patch fixed?
> > >
> > > A whole new v7 series please.
> > >
> >
> > Had a feeling, so sent out v7 series preemptively. Let me know if that
> > doesn't work.
> >
> > v7: https://lore.kernel.org/[email protected]/
>
> I have already dropped that from my review queue as your emails crossed
> with that, so I thought it was obsolete by now, sorry.
>
> Can you send a v8 please?
>
Sent out v8! PTAL when you get the chance.

https://lore.kernel.org/[email protected]/

Thank you!
- Avi.

2023-10-26 20:24:08

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v8 1/4] usb: gadget: uvc: prevent use of disabled endpoint



On 10/24/23 11:36, Avichal Rakesh wrote:
> Currently the set_alt callback immediately disables the endpoint and queues
> the v4l2 streamoff event. However, as the streamoff event is processed
> asynchronously, it is possible that the video_pump thread attempts to queue
> requests to an already disabled endpoint.
>
> This change moves disabling usb endpoint to the end of streamoff event
> callback. As the endpoint's state can no longer be used, video_pump is
> now guarded by uvc->state as well. To be consistent with the actual
> streaming state, uvc->state is now toggled between CONNECTED and STREAMING
> from the v4l2 event callback only.
>
> Link: https://lore.kernel.org/[email protected]/
> Link: https://lore.kernel.org/[email protected]/
> Reviewed-by: Michael Grzeschik <[email protected]>
> Tested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>
> ---
> v1 -> v2: Rebased to ToT and reworded commit message.
> v2 -> v3: Fix email threading goof-up
> v3 -> v4: Address review comments & re-rebase to ToT
> v4 -> v5: Add Reviewed-by & Tested-by
> v5 -> v6: No change
> v6 -> v7: No change
> v7 -> v8: No change. Getting back in review queue
>
> drivers/usb/gadget/function/f_uvc.c | 11 +++++------
> drivers/usb/gadget/function/f_uvc.h | 2 +-
> drivers/usb/gadget/function/uvc.h | 2 +-
> drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
> drivers/usb/gadget/function/uvc_video.c | 3 ++-
> 5 files changed, 26 insertions(+), 12 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
> index faa398109431..ae08341961eb 100644
> --- a/drivers/usb/gadget/function/f_uvc.c
> +++ b/drivers/usb/gadget/function/f_uvc.c
> @@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
> return 0;
> }
>
> -void uvc_function_setup_continue(struct uvc_device *uvc)
> +void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
> {
> struct usb_composite_dev *cdev = uvc->func.config->cdev;
>
> + if (disable_ep && uvc->video.ep)
> + usb_ep_disable(uvc->video.ep);
> +
> usb_composite_setup_continue(cdev);
> }
>
> @@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
> if (uvc->state != UVC_STATE_STREAMING)
> return 0;
>
> - if (uvc->video.ep)
> - usb_ep_disable(uvc->video.ep);
> -
> memset(&v4l2_event, 0, sizeof(v4l2_event));
> v4l2_event.type = UVC_EVENT_STREAMOFF;
> v4l2_event_queue(&uvc->vdev, &v4l2_event);
>
> - uvc->state = UVC_STATE_CONNECTED;
> - return 0;
> + return USB_GADGET_DELAYED_STATUS;
>
> case 1:
> if (uvc->state != UVC_STATE_CONNECTED)
> diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
> index 1db972d4beeb..e7f9f13f14dc 100644
> --- a/drivers/usb/gadget/function/f_uvc.h
> +++ b/drivers/usb/gadget/function/f_uvc.h
> @@ -11,7 +11,7 @@
>
> struct uvc_device;
>
> -void uvc_function_setup_continue(struct uvc_device *uvc);
> +void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);
>
> void uvc_function_connect(struct uvc_device *uvc);
>
> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
> index 6751de8b63ad..989bc6b4e93d 100644
> --- a/drivers/usb/gadget/function/uvc.h
> +++ b/drivers/usb/gadget/function/uvc.h
> @@ -177,7 +177,7 @@ struct uvc_file_handle {
> * Functions
> */
>
> -extern void uvc_function_setup_continue(struct uvc_device *uvc);
> +extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
> extern void uvc_function_connect(struct uvc_device *uvc);
> extern void uvc_function_disconnect(struct uvc_device *uvc);
>
> diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
> index 3f0a9795c0d4..7cb8d027ff0c 100644
> --- a/drivers/usb/gadget/function/uvc_v4l2.c
> +++ b/drivers/usb/gadget/function/uvc_v4l2.c
> @@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
> * Complete the alternate setting selection setup phase now that
> * userspace is ready to provide video frames.
> */
> - uvc_function_setup_continue(uvc);
> + uvc_function_setup_continue(uvc, 0);
> uvc->state = UVC_STATE_STREAMING;
>
> return 0;
> @@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
> struct video_device *vdev = video_devdata(file);
> struct uvc_device *uvc = video_get_drvdata(vdev);
> struct uvc_video *video = &uvc->video;
> + int ret = 0;
>
> if (type != video->queue.queue.type)
> return -EINVAL;
>
> - return uvcg_video_enable(video, 0);
> + uvc->state = UVC_STATE_CONNECTED;
> + ret = uvcg_video_enable(video, 0);
> + if (ret < 0)
> + return ret;
> +
> + uvc_function_setup_continue(uvc, 1);
> + return 0;
> }
>
> static int
> @@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
> static void uvc_v4l2_disable(struct uvc_device *uvc)
> {
> uvc_function_disconnect(uvc);
> + /*
> + * Drop uvc->state to CONNECTED if it was streaming before.
> + * This ensures that the usb_requests are no longer queued
> + * to the controller.
> + */
> + if (uvc->state == UVC_STATE_STREAMING)
> + uvc->state = UVC_STATE_CONNECTED;
> +
> uvcg_video_enable(&uvc->video, 0);
> uvcg_free_buffers(&uvc->video.queue);
> uvc->func_connected = false;
> @@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
> .get_unmapped_area = uvcg_v4l2_get_unmapped_area,
> #endif
> };
> -
> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
> index 91af3b1ef0d4..c334802ac0a4 100644
> --- a/drivers/usb/gadget/function/uvc_video.c
> +++ b/drivers/usb/gadget/function/uvc_video.c
> @@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
> struct uvc_video_queue *queue = &video->queue;
> /* video->max_payload_size is only set when using bulk transfer */
> bool is_bulk = video->max_payload_size;
> + struct uvc_device *uvc = video->uvc;
> struct usb_request *req = NULL;
> struct uvc_buffer *buf;
> unsigned long flags;
> bool buf_done;
> int ret;
>
> - while (video->ep->enabled) {
> + while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
> /*
> * Retrieve the first available USB request, protected by the
> * request lock.
> --
> 2.42.0.758.gaed0368e0e-goog

Hey Greg,

Considering Laurent and Dan haven't responded, and Michael and I have
tested this change, would it be possible to merge this patch set
if the changes look OK to you? I don't think there are any outstanding
items to be done around these fixes.

Thank you!
- Avi.

2023-10-27 10:52:06

by Greg Kroah-Hartman

[permalink] [raw]
Subject: Re: [PATCH v8 1/4] usb: gadget: uvc: prevent use of disabled endpoint

On Thu, Oct 26, 2023 at 01:23:44PM -0700, Avichal Rakesh wrote:
> Considering Laurent and Dan haven't responded, and Michael and I have
> tested this change, would it be possible to merge this patch set
> if the changes look OK to you? I don't think there are any outstanding
> items to be done around these fixes.

I would like their review first please.

thanks,

greg k-h

2023-10-27 10:52:41

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v8 1/4] usb: gadget: uvc: prevent use of disabled endpoint

Good morning

On 27/10/2023 11:51, Greg KH wrote:
> On Thu, Oct 26, 2023 at 01:23:44PM -0700, Avichal Rakesh wrote:
>> Considering Laurent and Dan haven't responded, and Michael and I have
>> tested this change, would it be possible to merge this patch set
>> if the changes look OK to you? I don't think there are any outstanding
>> items to be done around these fixes.
> I would like their review first please.


Apologies for the delay - I am reviewing it now.

>
> thanks,
>
> greg k-h

2023-10-27 12:56:51

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v8 1/4] usb: gadget: uvc: prevent use of disabled endpoint

Hi Avichal

On 24/10/2023 19:36, Avichal Rakesh wrote:
> Currently the set_alt callback immediately disables the endpoint and queues
> the v4l2 streamoff event. However, as the streamoff event is processed
> asynchronously, it is possible that the video_pump thread attempts to queue
> requests to an already disabled endpoint.
>
> This change moves disabling usb endpoint to the end of streamoff event
> callback. As the endpoint's state can no longer be used, video_pump is
> now guarded by uvc->state as well. To be consistent with the actual
> streaming state, uvc->state is now toggled between CONNECTED and STREAMING
> from the v4l2 event callback only.
>
> Link: https://lore.kernel.org/[email protected]/
> Link: https://lore.kernel.org/[email protected]/


Thank you for picking this up! And I'm sorry to have been so remiss looking at your patches - I will
try to be more responsive to them. I spotted a typo below, but otherwise:


Reviewed-by: Daniel Scally <[email protected]>

> Reviewed-by: Michael Grzeschik <[email protected]>
> Tested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>
> ---
> v1 -> v2: Rebased to ToT and reworded commit message.
> v2 -> v3: Fix email threading goof-up
> v3 -> v4: Address review comments & re-rebase to ToT
> v4 -> v5: Add Reviewed-by & Tested-by
> v5 -> v6: No change
> v6 -> v7: No change
> v7 -> v8: No change. Getting back in review queue
>
> drivers/usb/gadget/function/f_uvc.c | 11 +++++------
> drivers/usb/gadget/function/f_uvc.h | 2 +-
> drivers/usb/gadget/function/uvc.h | 2 +-
> drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
> drivers/usb/gadget/function/uvc_video.c | 3 ++-
> 5 files changed, 26 insertions(+), 12 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
> index faa398109431..ae08341961eb 100644
> --- a/drivers/usb/gadget/function/f_uvc.c
> +++ b/drivers/usb/gadget/function/f_uvc.c
> @@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
> return 0;
> }
>
> -void uvc_function_setup_continue(struct uvc_device *uvc)
> +void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
> {
> struct usb_composite_dev *cdev = uvc->func.config->cdev;
>
> + if (disable_ep && uvc->video.ep)
> + usb_ep_disable(uvc->video.ep);
> +
> usb_composite_setup_continue(cdev);
> }
>
> @@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
> if (uvc->state != UVC_STATE_STREAMING)
> return 0;
>
> - if (uvc->video.ep)
> - usb_ep_disable(uvc->video.ep);
> -
> memset(&v4l2_event, 0, sizeof(v4l2_event));
> v4l2_event.type = UVC_EVENT_STREAMOFF;
> v4l2_event_queue(&uvc->vdev, &v4l2_event);
>
> - uvc->state = UVC_STATE_CONNECTED;
> - return 0;
> + return USB_GADGET_DELAYED_STATUS;
>
> case 1:
> if (uvc->state != UVC_STATE_CONNECTED)
> diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
> index 1db972d4beeb..e7f9f13f14dc 100644
> --- a/drivers/usb/gadget/function/f_uvc.h
> +++ b/drivers/usb/gadget/function/f_uvc.h
> @@ -11,7 +11,7 @@
>
> struct uvc_device;
>
> -void uvc_function_setup_continue(struct uvc_device *uvc);
> +void uvc_function_setup_continue(struct uvc_device *uvc, int disale_ep);


s/disale_ep/disable_ep

>
> void uvc_function_connect(struct uvc_device *uvc);
>
> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
> index 6751de8b63ad..989bc6b4e93d 100644
> --- a/drivers/usb/gadget/function/uvc.h
> +++ b/drivers/usb/gadget/function/uvc.h
> @@ -177,7 +177,7 @@ struct uvc_file_handle {
> * Functions
> */
>
> -extern void uvc_function_setup_continue(struct uvc_device *uvc);
> +extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
> extern void uvc_function_connect(struct uvc_device *uvc);
> extern void uvc_function_disconnect(struct uvc_device *uvc);
>
> diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
> index 3f0a9795c0d4..7cb8d027ff0c 100644
> --- a/drivers/usb/gadget/function/uvc_v4l2.c
> +++ b/drivers/usb/gadget/function/uvc_v4l2.c
> @@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
> * Complete the alternate setting selection setup phase now that
> * userspace is ready to provide video frames.
> */
> - uvc_function_setup_continue(uvc);
> + uvc_function_setup_continue(uvc, 0);
> uvc->state = UVC_STATE_STREAMING;
>
> return 0;
> @@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
> struct video_device *vdev = video_devdata(file);
> struct uvc_device *uvc = video_get_drvdata(vdev);
> struct uvc_video *video = &uvc->video;
> + int ret = 0;
>
> if (type != video->queue.queue.type)
> return -EINVAL;
>
> - return uvcg_video_enable(video, 0);
> + uvc->state = UVC_STATE_CONNECTED;
> + ret = uvcg_video_enable(video, 0);
> + if (ret < 0)
> + return ret;
> +
> + uvc_function_setup_continue(uvc, 1);
> + return 0;
> }
>
> static int
> @@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
> static void uvc_v4l2_disable(struct uvc_device *uvc)
> {
> uvc_function_disconnect(uvc);
> + /*
> + * Drop uvc->state to CONNECTED if it was streaming before.
> + * This ensures that the usb_requests are no longer queued
> + * to the controller.
> + */
> + if (uvc->state == UVC_STATE_STREAMING)
> + uvc->state = UVC_STATE_CONNECTED;
> +
> uvcg_video_enable(&uvc->video, 0);
> uvcg_free_buffers(&uvc->video.queue);
> uvc->func_connected = false;
> @@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
> .get_unmapped_area = uvcg_v4l2_get_unmapped_area,
> #endif
> };
> -
> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
> index 91af3b1ef0d4..c334802ac0a4 100644
> --- a/drivers/usb/gadget/function/uvc_video.c
> +++ b/drivers/usb/gadget/function/uvc_video.c
> @@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
> struct uvc_video_queue *queue = &video->queue;
> /* video->max_payload_size is only set when using bulk transfer */
> bool is_bulk = video->max_payload_size;
> + struct uvc_device *uvc = video->uvc;
> struct usb_request *req = NULL;
> struct uvc_buffer *buf;
> unsigned long flags;
> bool buf_done;
> int ret;
>
> - while (video->ep->enabled) {
> + while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
> /*
> * Retrieve the first available USB request, protected by the
> * request lock.
> --
> 2.42.0.758.gaed0368e0e-goog

2023-10-27 12:57:40

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v8 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Hi Avichal - thanks for the patch

On 24/10/2023 19:36, Avichal Rakesh wrote:
> Currently, the uvc gadget driver allocates all uvc_requests as one array
> and deallocates them all when the video stream stops. This includes
> de-allocating all the usb_requests associated with those uvc_requests.
> This can lead to use-after-free issues if any of those de-allocated
> usb_requests were still owned by the usb controller.
>
> This patch is 1 of 2 patches addressing the use-after-free issue.
> Instead of bulk allocating all uvc_requests as an array, this patch
> allocates uvc_requests one at a time, which should allows for similar
> granularity when deallocating the uvc_requests. This patch has no
> functional changes other than allocating each uvc_request separately,
> and similarly freeing each of them separately.
>
> Link: https://lore.kernel.org/[email protected]
> Suggested-by: Michael Grzeschik <[email protected]>
> Reviewed-by: Michael Grzeschik <[email protected]>
> Tested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>
> ---
> v1 -> v2: Rebased to ToT
> v2 -> v3: Fix email threading goof-up
> v3 -> v4: Address review comments & re-rebase to ToT
> v4 -> v5: Address more review comments. Add Reviewed-by & Tested-by.
> v5 -> v6: No change
> v6 -> v7: No change
> v7 -> v8: No change. Getting back in review queue
>
> drivers/usb/gadget/function/uvc.h | 3 +-
> drivers/usb/gadget/function/uvc_video.c | 89 ++++++++++++++-----------
> 2 files changed, 52 insertions(+), 40 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
> index 989bc6b4e93d..993694da0bbc 100644
> --- a/drivers/usb/gadget/function/uvc.h
> +++ b/drivers/usb/gadget/function/uvc.h
> @@ -81,6 +81,7 @@ struct uvc_request {
> struct sg_table sgt;
> u8 header[UVCG_REQUEST_HEADER_LEN];
> struct uvc_buffer *last_buf;
> + struct list_head list;
> };
>
> struct uvc_video {
> @@ -102,7 +103,7 @@ struct uvc_video {
>
> /* Requests */
> unsigned int req_size;
> - struct uvc_request *ureq;
> + struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
> struct list_head req_free;
> spinlock_t req_lock;
>
> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
> index c334802ac0a4..c180866c8e34 100644
> --- a/drivers/usb/gadget/function/uvc_video.c
> +++ b/drivers/usb/gadget/function/uvc_video.c
> @@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
> * Request handling
> */
>
> +static void
> +uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
> +{
> + sg_free_table(&ureq->sgt);
> + if (ureq->req && ep) {
> + usb_ep_free_request(ep, ureq->req);
> + ureq->req = NULL;
> + }
> +
> + kfree(ureq->req_buffer);
> + ureq->req_buffer = NULL;
> +
> + if (!list_empty(&ureq->list))


Is this conditional needed? You can only get here through the list_for_each_entry_safe()

> + list_del_init(&ureq->list);
> +
> + kfree(ureq);
> +}
> +
> static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
> {
> int ret;
> @@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
> static int
> uvc_video_free_requests(struct uvc_video *video)
> {
> - unsigned int i;
> -
> - if (video->ureq) {
> - for (i = 0; i < video->uvc_num_requests; ++i) {
> - sg_free_table(&video->ureq[i].sgt);
> + struct uvc_request *ureq, *temp;
>
> - if (video->ureq[i].req) {
> - usb_ep_free_request(video->ep, video->ureq[i].req);
> - video->ureq[i].req = NULL;
> - }
> -
> - if (video->ureq[i].req_buffer) {
> - kfree(video->ureq[i].req_buffer);
> - video->ureq[i].req_buffer = NULL;
> - }
> - }
> -
> - kfree(video->ureq);
> - video->ureq = NULL;
> - }
> + list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
> + uvc_video_free_request(ureq, video->ep);
>
> + INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> video->req_size = 0;
> return 0;
> @@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
> static int
> uvc_video_alloc_requests(struct uvc_video *video)
> {
> + struct uvc_request *ureq;
> unsigned int req_size;
> unsigned int i;
> int ret = -ENOMEM;
> @@ -332,29 +336,34 @@ uvc_video_alloc_requests(struct uvc_video *video)
> * max_t(unsigned int, video->ep->maxburst, 1)
> * (video->ep->mult);
>
> - video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
> - if (video->ureq == NULL)
> - return -ENOMEM;
> + INIT_LIST_HEAD(&video->ureqs);


Probably unecessary here; it's done in uvc_video_free_requests() and uvcg_video_init() already

> + for (i = 0; i < video->uvc_num_requests; i++) {
> + ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
> + if (ureq == NULL)
> + goto error;
> +
> + INIT_LIST_HEAD(&ureq->list);
> +
> + list_add_tail(&ureq->list, &video->ureqs);
>
> - for (i = 0; i < video->uvc_num_requests; ++i) {
> - video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
> - if (video->ureq[i].req_buffer == NULL)
> + ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
> + if (ureq->req_buffer == NULL)
> goto error;
>
> - video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
> - if (video->ureq[i].req == NULL)
> + ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
> + if (ureq->req == NULL)
> goto error;
>
> - video->ureq[i].req->buf = video->ureq[i].req_buffer;
> - video->ureq[i].req->length = 0;
> - video->ureq[i].req->complete = uvc_video_complete;
> - video->ureq[i].req->context = &video->ureq[i];
> - video->ureq[i].video = video;
> - video->ureq[i].last_buf = NULL;
> + ureq->req->buf = ureq->req_buffer;
> + ureq->req->length = 0;
> + ureq->req->complete = uvc_video_complete;
> + ureq->req->context = ureq;
> + ureq->video = video;
> + ureq->last_buf = NULL;
>
> - list_add_tail(&video->ureq[i].req->list, &video->req_free);
> + list_add_tail(&ureq->req->list, &video->req_free);
> /* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
> - sg_alloc_table(&video->ureq[i].sgt,
> + sg_alloc_table(&ureq->sgt,
> DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
> PAGE_SIZE) + 2, GFP_KERNEL);
> }
> @@ -489,8 +498,8 @@ static void uvcg_video_pump(struct work_struct *work)
> */
> int uvcg_video_enable(struct uvc_video *video, int enable)
> {
> - unsigned int i;
> int ret;
> + struct uvc_request *ureq;
>
> if (video->ep == NULL) {
> uvcg_info(&video->uvc->func,
> @@ -502,9 +511,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> cancel_work_sync(&video->pump);
> uvcg_queue_cancel(&video->queue, 0);
>
> - for (i = 0; i < video->uvc_num_requests; ++i)
> - if (video->ureq && video->ureq[i].req)
> - usb_ep_dequeue(video->ep, video->ureq[i].req);
> + list_for_each_entry(ureq, &video->ureqs, list) {
> + if (ureq->req)
> + usb_ep_dequeue(video->ep, ureq->req);
> + }
>
> uvc_video_free_requests(video);
> uvcg_queue_enable(&video->queue, 0);
> @@ -536,6 +546,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> */
> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
> {
> + INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> spin_lock_init(&video->req_lock);
> INIT_WORK(&video->pump, uvcg_video_pump);
> --
> 2.42.0.758.gaed0368e0e-goog

2023-10-27 12:57:53

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v8 3/4] usb: gadget: uvc: move video disable logic to its own function

Hi Avichal

On 24/10/2023 19:36, Avichal Rakesh wrote:
> This patch refactors the video disable logic in uvcg_video_enable
> into its own separate function 'uvcg_video_disable'.
In that case, can we just replace any calls to uvcg_video_enable(video, 0) with calls to
uvcg_video_disable() directly and drop the enable argument from uvcg_video_enable()?
> Suggested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>
> ---
> v6: Introduced this patch to make the next one easier to review
> v6 -> v7: Add Suggested-by
> v7 -> v8: No change. Getting back in review queue
>
> drivers/usb/gadget/function/uvc_video.c | 37 +++++++++++++++----------
> 1 file changed, 23 insertions(+), 14 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
> index c180866c8e34..80b8eaea2d39 100644
> --- a/drivers/usb/gadget/function/uvc_video.c
> +++ b/drivers/usb/gadget/function/uvc_video.c
> @@ -493,13 +493,33 @@ static void uvcg_video_pump(struct work_struct *work)
> return;
> }
>
> +/*
> + * Disable video stream
> + */
> +static int
> +uvcg_video_disable(struct uvc_video *video)
> +{
> + struct uvc_request *ureq;
> +
> + cancel_work_sync(&video->pump);
> + uvcg_queue_cancel(&video->queue, 0);
> +
> + list_for_each_entry(ureq, &video->ureqs, list) {
> + if (ureq->req)
> + usb_ep_dequeue(video->ep, ureq->req);
> + }
> +
> + uvc_video_free_requests(video);
> + uvcg_queue_enable(&video->queue, 0);
> + return 0;
> +}
> +
> /*
> * Enable or disable the video stream.
> */
> int uvcg_video_enable(struct uvc_video *video, int enable)
> {
> int ret;
> - struct uvc_request *ureq;
>
> if (video->ep == NULL) {
> uvcg_info(&video->uvc->func,
> @@ -507,19 +527,8 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> return -ENODEV;
> }
>
> - if (!enable) {
> - cancel_work_sync(&video->pump);
> - uvcg_queue_cancel(&video->queue, 0);
> -
> - list_for_each_entry(ureq, &video->ureqs, list) {
> - if (ureq->req)
> - usb_ep_dequeue(video->ep, ureq->req);
> - }
> -
> - uvc_video_free_requests(video);
> - uvcg_queue_enable(&video->queue, 0);
> - return 0;
> - }
> + if (!enable)
> + return uvcg_video_disable(video);
>
> if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
> return ret;
> --
> 2.42.0.758.gaed0368e0e-goog

2023-10-27 20:20:24

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v9 1/4] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. As the endpoint's state can no longer be used, video_pump is
now guarded by uvc->state as well. To be consistent with the actual
streaming state, uvc->state is now toggled between CONNECTED and STREAMING
from the v4l2 event callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Reviewed-by: Daniel Scally <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT and reworded commit message.
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT
v4 -> v5: Add Reviewed-by & Tested-by
v5 -> v6: No change
v6 -> v7: No change
v7 -> v8: No change. Getting back in review queue
v8 -> v9: Fix typo. No functional change.

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
drivers/usb/gadget/function/uvc_video.c | 3 ++-
5 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index faa398109431..ae08341961eb 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep)
+ usb_ep_disable(uvc->video.ep);
+
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..083aef0c65c6 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..7cb8d027ff0c 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0)
+ return ret;
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ if (uvc->state == UVC_STATE_STREAMING)
+ uvc->state = UVC_STATE_CONNECTED;
+
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 91af3b1ef0d4..c334802ac0a4 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
+ struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (video->ep->enabled) {
+ while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
/*
* Retrieve the first available USB request, protected by the
* request lock.
--
2.42.0.820.g83a721a137-goog

2023-10-27 20:20:43

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v9 3/4] usb: gadget: uvc: move video disable logic to its own function

This patch refactors the video disable logic in uvcg_video_enable
into its own separate function 'uvcg_video_disable'. This function
is now used anywhere uvcg_video_enable(video, 0) was used.

Reviewed-by: Daniel Scally <[email protected]>
Suggested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v6: Introduced this patch to make the next one easier to review
v6 -> v7: Add Suggested-by
v7 -> v8: No change. Getting back in review queue
v8 -> v9: Call uvcg_video_disable directly instead of uvcg_video_enable(video, 0)

drivers/usb/gadget/function/uvc_v4l2.c | 6 ++--
drivers/usb/gadget/function/uvc_video.c | 40 ++++++++++++++++---------
drivers/usb/gadget/function/uvc_video.h | 3 +-
3 files changed, 31 insertions(+), 18 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 7cb8d027ff0c..904dd283cbf7 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -443,7 +443,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
return -EINVAL;

/* Enable UVC video. */
- ret = uvcg_video_enable(video, 1);
+ ret = uvcg_video_enable(video);
if (ret < 0)
return ret;

@@ -469,7 +469,7 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
return -EINVAL;

uvc->state = UVC_STATE_CONNECTED;
- ret = uvcg_video_enable(video, 0);
+ ret = uvcg_video_disable(video);
if (ret < 0)
return ret;

@@ -515,7 +515,7 @@ static void uvc_v4l2_disable(struct uvc_device *uvc)
if (uvc->state == UVC_STATE_STREAMING)
uvc->state = UVC_STATE_CONNECTED;

- uvcg_video_enable(&uvc->video, 0);
+ uvcg_video_disable(&uvc->video);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
wake_up_interruptible(&uvc->func_connected_queue);
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index f8f9209fee50..1081dd790fd6 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -494,31 +494,43 @@ static void uvcg_video_pump(struct work_struct *work)
}

/*
- * Enable or disable the video stream.
+ * Disable the video stream
*/
-int uvcg_video_enable(struct uvc_video *video, int enable)
+int
+uvcg_video_disable(struct uvc_video *video)
{
- int ret;
struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
- "Video enable failed, device is uninitialized.\n");
+ "Video disable failed, device is uninitialized.\n");
return -ENODEV;
}

- if (!enable) {
- cancel_work_sync(&video->pump);
- uvcg_queue_cancel(&video->queue, 0);
+ cancel_work_sync(&video->pump);
+ uvcg_queue_cancel(&video->queue, 0);

- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
- }
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

- uvc_video_free_requests(video);
- uvcg_queue_enable(&video->queue, 0);
- return 0;
+ uvc_video_free_requests(video);
+ uvcg_queue_enable(&video->queue, 0);
+ return 0;
+}
+
+/*
+ * Enable the video stream.
+ */
+int uvcg_video_enable(struct uvc_video *video)
+{
+ int ret;
+
+ if (video->ep == NULL) {
+ uvcg_info(&video->uvc->func,
+ "Video enable failed, device is uninitialized.\n");
+ return -ENODEV;
}

if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
diff --git a/drivers/usb/gadget/function/uvc_video.h b/drivers/usb/gadget/function/uvc_video.h
index 03adeefa343b..8ef6259741f1 100644
--- a/drivers/usb/gadget/function/uvc_video.h
+++ b/drivers/usb/gadget/function/uvc_video.h
@@ -14,7 +14,8 @@

struct uvc_video;

-int uvcg_video_enable(struct uvc_video *video, int enable);
+int uvcg_video_enable(struct uvc_video *video);
+int uvcg_video_disable(struct uvc_video *video);

int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc);

--
2.42.0.820.g83a721a137-goog

2023-10-27 20:20:45

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v9 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This patch is 1 of 2 patches addressing the use-after-free issue.
Instead of bulk allocating all uvc_requests as an array, this patch
allocates uvc_requests one at a time, which should allows for similar
granularity when deallocating the uvc_requests. This patch has no
functional changes other than allocating each uvc_request separately,
and similarly freeing each of them separately.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Daniel Scally <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT
v2 -> v3: Fix email threading goof-up
v3 -> v4: Address review comments & re-rebase to ToT
v4 -> v5: Address more review comments. Add Reviewed-by & Tested-by.
v5 -> v6: No change
v6 -> v7: No change
v7 -> v8: No change. Getting back in review queue
v8 -> v9: Address review comments.

drivers/usb/gadget/function/uvc.h | 3 +-
drivers/usb/gadget/function/uvc_video.c | 89 ++++++++++++++-----------
2 files changed, 52 insertions(+), 40 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 989bc6b4e93d..993694da0bbc 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -81,6 +81,7 @@ struct uvc_request {
struct sg_table sgt;
u8 header[UVCG_REQUEST_HEADER_LEN];
struct uvc_buffer *last_buf;
+ struct list_head list;
};

struct uvc_video {
@@ -102,7 +103,7 @@ struct uvc_video {

/* Requests */
unsigned int req_size;
- struct uvc_request *ureq;
+ struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
spinlock_t req_lock;

diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c334802ac0a4..f8f9209fee50 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+static void
+uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
+{
+ sg_free_table(&ureq->sgt);
+ if (ureq->req && ep) {
+ usb_ep_free_request(ep, ureq->req);
+ ureq->req = NULL;
+ }
+
+ kfree(ureq->req_buffer);
+ ureq->req_buffer = NULL;
+
+ if (!list_empty(&ureq->list))
+ list_del_init(&ureq->list);
+
+ kfree(ureq);
+}
+
static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
{
int ret;
@@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
static int
uvc_video_free_requests(struct uvc_video *video)
{
- unsigned int i;
-
- if (video->ureq) {
- for (i = 0; i < video->uvc_num_requests; ++i) {
- sg_free_table(&video->ureq[i].sgt);
+ struct uvc_request *ureq, *temp;

- if (video->ureq[i].req) {
- usb_ep_free_request(video->ep, video->ureq[i].req);
- video->ureq[i].req = NULL;
- }
-
- if (video->ureq[i].req_buffer) {
- kfree(video->ureq[i].req_buffer);
- video->ureq[i].req_buffer = NULL;
- }
- }
-
- kfree(video->ureq);
- video->ureq = NULL;
- }
+ list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
+ uvc_video_free_request(ureq, video->ep);

+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
video->req_size = 0;
return 0;
@@ -322,39 +325,45 @@ uvc_video_free_requests(struct uvc_video *video)
static int
uvc_video_alloc_requests(struct uvc_video *video)
{
+ struct uvc_request *ureq;
unsigned int req_size;
unsigned int i;
int ret = -ENOMEM;

BUG_ON(video->req_size);
+ BUG_ON(!list_empty(&video->ureqs));

req_size = video->ep->maxpacket
* max_t(unsigned int, video->ep->maxburst, 1)
* (video->ep->mult);

- video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
- if (video->ureq == NULL)
- return -ENOMEM;
+ for (i = 0; i < video->uvc_num_requests; i++) {
+ ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
+ if (ureq == NULL)
+ goto error;
+
+ INIT_LIST_HEAD(&ureq->list);
+
+ list_add_tail(&ureq->list, &video->ureqs);

- for (i = 0; i < video->uvc_num_requests; ++i) {
- video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
- if (video->ureq[i].req_buffer == NULL)
+ ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
+ if (ureq->req_buffer == NULL)
goto error;

- video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
- if (video->ureq[i].req == NULL)
+ ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
+ if (ureq->req == NULL)
goto error;

- video->ureq[i].req->buf = video->ureq[i].req_buffer;
- video->ureq[i].req->length = 0;
- video->ureq[i].req->complete = uvc_video_complete;
- video->ureq[i].req->context = &video->ureq[i];
- video->ureq[i].video = video;
- video->ureq[i].last_buf = NULL;
+ ureq->req->buf = ureq->req_buffer;
+ ureq->req->length = 0;
+ ureq->req->complete = uvc_video_complete;
+ ureq->req->context = ureq;
+ ureq->video = video;
+ ureq->last_buf = NULL;

- list_add_tail(&video->ureq[i].req->list, &video->req_free);
+ list_add_tail(&ureq->req->list, &video->req_free);
/* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
- sg_alloc_table(&video->ureq[i].sgt,
+ sg_alloc_table(&ureq->sgt,
DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
PAGE_SIZE) + 2, GFP_KERNEL);
}
@@ -489,8 +498,8 @@ static void uvcg_video_pump(struct work_struct *work)
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
- unsigned int i;
int ret;
+ struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -502,9 +511,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- for (i = 0; i < video->uvc_num_requests; ++i)
- if (video->ureq && video->ureq[i].req)
- usb_ep_dequeue(video->ep, video->ureq[i].req);
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

uvc_video_free_requests(video);
uvcg_queue_enable(&video->queue, 0);
@@ -536,6 +546,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
INIT_WORK(&video->pump, uvcg_video_pump);
--
2.42.0.820.g83a721a137-goog

2023-10-27 20:20:55

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v9 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
flag to uvc_video to track when frames and requests should be flowing.
When disabling the video stream, the flag is tripped and, instead
of de-allocating all uvc_requests and usb_requests, the gadget
driver only de-allocates those usb_requests that are currently
owned by it (as present in req_free). Other usb_requests are left
untouched until their completion handler is called which takes care
of freeing the usb_request and its corresponding uvc_request.

Now that uvc_video does not depends on uvc->state, this patch removes
unnecessary upates to uvc->state that were made to accommodate uvc_video
logic. This should ensure that uvc gadget driver never accidentally
de-allocates a usb_request that it doesn't own.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2: Rebased to ToT, and fixed deadlock reported in
https://lore.kernel.org/all/[email protected]/
v2 -> v3: Fix email threading goof-up
v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
as discussed in
https://lore.kernel.org/[email protected]/
v4 -> v5: Address review comments. Add Reviewed-by & Tested-by.
v5 -> v6: Added another patch before this one to make uvcg_video_disable
easier to review.
v6 -> v7: Fix warning reported in
https://lore.kernel.org/[email protected]/
v7 -> v8: No change. Getting back in review queue
v8 -> v9: No change.

drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_v4l2.c | 12 +--
drivers/usb/gadget/function/uvc_video.c | 128 ++++++++++++++++++++----
3 files changed, 111 insertions(+), 30 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 993694da0bbc..be0d012aa244 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -102,6 +102,7 @@ struct uvc_video {
unsigned int uvc_num_requests;

/* Requests */
+ bool is_enabled; /* tracks whether video stream is enabled */
unsigned int req_size;
struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 904dd283cbf7..2f8634e05612 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;
+ uvc_function_setup_continue(uvc, 0);

return 0;
}
@@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
if (type != video->queue.queue.type)
return -EINVAL;

- uvc->state = UVC_STATE_CONNECTED;
ret = uvcg_video_disable(video);
if (ret < 0)
return ret;

+ uvc->state = UVC_STATE_CONNECTED;
uvc_function_setup_continue(uvc, 1);
return 0;
}
@@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
- /*
- * Drop uvc->state to CONNECTED if it was streaming before.
- * This ensures that the usb_requests are no longer queued
- * to the controller.
- */
- if (uvc->state == UVC_STATE_STREAMING)
- uvc->state = UVC_STATE_CONNECTED;
-
uvcg_video_disable(&uvc->video);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 1081dd790fd6..8f330ce696ec 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+/*
+ * Must be called with req_lock held as it modifies the list ureq is held in
+ */
static void
uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
{
@@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
struct uvc_request *ureq = req->context;
struct uvc_video *video = ureq->video;
struct uvc_video_queue *queue = &video->queue;
- struct uvc_device *uvc = video->uvc;
+ struct uvc_buffer *last_buf = NULL;
unsigned long flags;

+ spin_lock_irqsave(&video->req_lock, flags);
+ if (!video->is_enabled) {
+ /*
+ * When is_enabled is false, uvc_video_disable ensures that
+ * in-flight uvc_buffers are returned, so we can safely
+ * call free_request without worrying about last_buf.
+ */
+ uvc_video_free_request(ureq, ep);
+ spin_unlock_irqrestore(&video->req_lock, flags);
+ return;
+ }
+
+ last_buf = ureq->last_buf;
+ ureq->last_buf = NULL;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
switch (req->status) {
case 0:
break;
@@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
uvcg_queue_cancel(queue, 0);
}

- if (ureq->last_buf) {
- uvcg_complete_buffer(&video->queue, ureq->last_buf);
- ureq->last_buf = NULL;
+ if (last_buf) {
+ spin_lock_irqsave(&queue->irqlock, flags);
+ uvcg_complete_buffer(&video->queue, last_buf);
+ spin_unlock_irqrestore(&queue->irqlock, flags);
}

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
- spin_unlock_irqrestore(&video->req_lock, flags);
-
- if (uvc->state == UVC_STATE_STREAMING)
+ /*
+ * Video stream might have been disabled while we were
+ * processing the current usb_request. So make sure
+ * we're still streaming before queueing the usb_request
+ * back to req_free
+ */
+ if (video->is_enabled) {
+ list_add_tail(&req->list, &video->req_free);
queue_work(video->async_wq, &video->pump);
+ } else {
+ uvc_video_free_request(ureq, ep);
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
}

static int
@@ -393,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
- struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
+ while (true) {
+ if (!video->ep->enabled)
+ return;
+
/*
- * Retrieve the first available USB request, protected by the
- * request lock.
+ * Check is_enabled and retrieve the first available USB
+ * request, protected by the request lock.
*/
spin_lock_irqsave(&video->req_lock, flags);
- if (list_empty(&video->req_free)) {
+ if (!video->is_enabled || list_empty(&video->req_free)) {
spin_unlock_irqrestore(&video->req_lock, flags);
return;
}
@@ -488,9 +518,11 @@ static void uvcg_video_pump(struct work_struct *work)
return;

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
+ if (video->is_enabled)
+ list_add_tail(&req->list, &video->req_free);
+ else
+ uvc_video_free_request(req->context, video->ep);
spin_unlock_irqrestore(&video->req_lock, flags);
- return;
}

/*
@@ -499,7 +531,11 @@ static void uvcg_video_pump(struct work_struct *work)
int
uvcg_video_disable(struct uvc_video *video)
{
- struct uvc_request *ureq;
+ unsigned long flags;
+ struct list_head inflight_bufs;
+ struct usb_request *req, *temp;
+ struct uvc_buffer *buf, *btemp;
+ struct uvc_request *ureq, *utemp;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -507,15 +543,58 @@ uvcg_video_disable(struct uvc_video *video)
return -ENODEV;
}

+ INIT_LIST_HEAD(&inflight_bufs);
+ spin_lock_irqsave(&video->req_lock, flags);
+ video->is_enabled = false;
+
+ /*
+ * Remove any in-flight buffers from the uvc_requests
+ * because we want to return them before cancelling the
+ * queue. This ensures that we aren't stuck waiting for
+ * all complete callbacks to come through before disabling
+ * vb2 queue.
+ */
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->last_buf) {
+ list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
+ ureq->last_buf = NULL;
+ }
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
+ spin_lock_irqsave(&video->req_lock, flags);
+ /*
+ * Remove all uvc_reqeusts from ureqs with list_del_init
+ * This lets uvc_video_free_request correctly identify
+ * if the uvc_request is attached to a list or not when freeing
+ * memory.
+ */
+ list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
+ list_del_init(&ureq->list);
+
+ list_for_each_entry_safe(req, temp, &video->req_free, list) {
+ list_del(&req->list);
+ uvc_video_free_request(req->context, video->ep);
}

- uvc_video_free_requests(video);
+ INIT_LIST_HEAD(&video->ureqs);
+ INIT_LIST_HEAD(&video->req_free);
+ video->req_size = 0;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ /*
+ * Return all the video buffers before disabling the queue.
+ */
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
+ list_del(&buf->queue);
+ uvcg_complete_buffer(&video->queue, buf);
+ }
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
+
uvcg_queue_enable(&video->queue, 0);
return 0;
}
@@ -533,6 +612,14 @@ int uvcg_video_enable(struct uvc_video *video)
return -ENODEV;
}

+ /*
+ * Safe to access request related fields without req_lock because
+ * this is the only thread currently active, and no other
+ * request handling thread will become active until this function
+ * returns.
+ */
+ video->is_enabled = true;
+
if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;

@@ -558,6 +645,7 @@ int uvcg_video_enable(struct uvc_video *video)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ video->is_enabled = false;
INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
--
2.42.0.820.g83a721a137-goog

2023-10-27 20:31:49

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v8 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Thank you for the reviews, Dan!

Uploaded v9 with the comments addressed.

On 10/27/23 05:57, Dan Scally wrote:
> Hi Avichal - thanks for the patch
>
> On 24/10/2023 19:36, Avichal Rakesh wrote:
>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>> and deallocates them all when the video stream stops. This includes
>> de-allocating all the usb_requests associated with those uvc_requests.
>> This can lead to use-after-free issues if any of those de-allocated
>> usb_requests were still owned by the usb controller.
>>
>> <snip>
>>
>> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
>> index c334802ac0a4..c180866c8e34 100644
>> --- a/drivers/usb/gadget/function/uvc_video.c
>> +++ b/drivers/usb/gadget/function/uvc_video.c
>> @@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
>>    * Request handling
>>    */
>>
>> +static void
>> +uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
>> +{
>> +    sg_free_table(&ureq->sgt);
>> +    if (ureq->req && ep) {
>> +        usb_ep_free_request(ep, ureq->req);
>> +        ureq->req = NULL;
>> +    }
>> +
>> +    kfree(ureq->req_buffer);
>> +    ureq->req_buffer = NULL;
>> +
>> +    if (!list_empty(&ureq->list))
>
>
> Is this conditional needed? You can only get here through the list_for_each_entry_safe()

Strictly speaking, we don't need this check right now. As you said, we currently
only get to this from within a list_for_each_entry_safe block. However, we end up
needing the check in the very next patch. Considering this is a function
with no real control over who might call it, it seemed reasonable to write
this a little defensively in case of a partial revert of the patchset.

>
>> +        list_del_init(&ureq->list);
>> +
>> +    kfree(ureq);
>> +}
>> +
>>  <snip>
>> @@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
>>   static int
>>   uvc_video_alloc_requests(struct uvc_video *video)
>>   {
>> +    struct uvc_request *ureq;
>>       unsigned int req_size;
>>       unsigned int i;
>>       int ret = -ENOMEM;
>> @@ -332,29 +336,34 @@ uvc_video_alloc_requests(struct uvc_video *video)
>>            * max_t(unsigned int, video->ep->maxburst, 1)
>>            * (video->ep->mult);
>>
>> -    video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
>> -    if (video->ureq == NULL)
>> -        return -ENOMEM;
>> +    INIT_LIST_HEAD(&video->ureqs);
>
>
> Probably unecessary here; it's done in uvc_video_free_requests() and uvcg_video_init() already

Ah, that is fair. Added a BUG_ON instead, like we do for video->req_size
so we still catch cases where the state might be inconsistent.

>
>> +    for (i = 0; i < video->uvc_num_requests; i++) {
>> +        ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
>> +        if (ureq == NULL)
>> +            goto error;
>> +
>> +        INIT_LIST_HEAD(&ureq->list);
>> +
>> +        list_add_tail(&ureq->list, &video->ureqs);
>>
>> <snip>

Regards,
Avi.

2023-10-28 05:31:28

by Greg Kroah-Hartman

[permalink] [raw]
Subject: Re: [PATCH v8 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

On Fri, Oct 27, 2023 at 01:31:26PM -0700, Avichal Rakesh wrote:
> >> @@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
> >> ? static int
> >> ? uvc_video_alloc_requests(struct uvc_video *video)
> >> ? {
> >> +??? struct uvc_request *ureq;
> >> ????? unsigned int req_size;
> >> ????? unsigned int i;
> >> ????? int ret = -ENOMEM;
> >> @@ -332,29 +336,34 @@ uvc_video_alloc_requests(struct uvc_video *video)
> >> ?????????? * max_t(unsigned int, video->ep->maxburst, 1)
> >> ?????????? * (video->ep->mult);
> >>
> >> -??? video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
> >> -??? if (video->ureq == NULL)
> >> -??????? return -ENOMEM;
> >> +??? INIT_LIST_HEAD(&video->ureqs);
> >
> >
> > Probably unecessary here; it's done in uvc_video_free_requests() and uvcg_video_init() already
>
> Ah, that is fair. Added a BUG_ON instead, like we do for video->req_size
> so we still catch cases where the state might be inconsistent.

Please no, that means you just crashed a machine and all data is lost
and the user will get very mad.

Either handle the error properly or it's something that can never happen
and so you don't need to handle it.

thanks,

greg k-h

2023-10-28 10:32:43

by Greg Kroah-Hartman

[permalink] [raw]
Subject: Re: [PATCH v9 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

On Fri, Oct 27, 2023 at 01:19:57PM -0700, Avichal Rakesh wrote:
> BUG_ON(video->req_size);
> + BUG_ON(!list_empty(&video->ureqs));

Again, please do not add new BUG_ON() lines, the existing ones need to
be removed as well, but you can do that in later changes. I can't take
changes that add new ones, sorry.

thanks,

greg k-h

2023-10-28 20:13:53

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v9 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Hi Avichal

On 27/10/2023 21:19, Avichal Rakesh wrote:
> Currently, the uvc gadget driver allocates all uvc_requests as one array
> and deallocates them all when the video stream stops. This includes
> de-allocating all the usb_requests associated with those uvc_requests.
> This can lead to use-after-free issues if any of those de-allocated
> usb_requests were still owned by the usb controller.
>
> This patch is 1 of 2 patches addressing the use-after-free issue.
> Instead of bulk allocating all uvc_requests as an array, this patch
> allocates uvc_requests one at a time, which should allows for similar
> granularity when deallocating the uvc_requests. This patch has no
> functional changes other than allocating each uvc_request separately,
> and similarly freeing each of them separately.
>
> Link: https://lore.kernel.org/[email protected]
> Suggested-by: Michael Grzeschik <[email protected]>
> Reviewed-by: Daniel Scally <[email protected]>


Sorry - I was unclear in my response to the first patch on v8. I meant my R-b to apply to the first
patch only rather than to all of them. For this one I understand now the use of the conditional in
uvc_video_free_request(), so that point is fine. I agree with Greg that the BUG_ON() shouldn't stand
though.

> Reviewed-by: Michael Grzeschik <[email protected]>
> Tested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>
> ---
> v1 -> v2: Rebased to ToT
> v2 -> v3: Fix email threading goof-up
> v3 -> v4: Address review comments & re-rebase to ToT
> v4 -> v5: Address more review comments. Add Reviewed-by & Tested-by.
> v5 -> v6: No change
> v6 -> v7: No change
> v7 -> v8: No change. Getting back in review queue
> v8 -> v9: Address review comments.
>
> drivers/usb/gadget/function/uvc.h | 3 +-
> drivers/usb/gadget/function/uvc_video.c | 89 ++++++++++++++-----------
> 2 files changed, 52 insertions(+), 40 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
> index 989bc6b4e93d..993694da0bbc 100644
> --- a/drivers/usb/gadget/function/uvc.h
> +++ b/drivers/usb/gadget/function/uvc.h
> @@ -81,6 +81,7 @@ struct uvc_request {
> struct sg_table sgt;
> u8 header[UVCG_REQUEST_HEADER_LEN];
> struct uvc_buffer *last_buf;
> + struct list_head list;
> };
>
> struct uvc_video {
> @@ -102,7 +103,7 @@ struct uvc_video {
>
> /* Requests */
> unsigned int req_size;
> - struct uvc_request *ureq;
> + struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
> struct list_head req_free;
> spinlock_t req_lock;
>
> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
> index c334802ac0a4..f8f9209fee50 100644
> --- a/drivers/usb/gadget/function/uvc_video.c
> +++ b/drivers/usb/gadget/function/uvc_video.c
> @@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
> * Request handling
> */
>
> +static void
> +uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
> +{
> + sg_free_table(&ureq->sgt);
> + if (ureq->req && ep) {
> + usb_ep_free_request(ep, ureq->req);
> + ureq->req = NULL;
> + }
> +
> + kfree(ureq->req_buffer);
> + ureq->req_buffer = NULL;
> +
> + if (!list_empty(&ureq->list))
> + list_del_init(&ureq->list);
> +
> + kfree(ureq);
> +}
> +
> static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
> {
> int ret;
> @@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
> static int
> uvc_video_free_requests(struct uvc_video *video)
> {
> - unsigned int i;
> -
> - if (video->ureq) {
> - for (i = 0; i < video->uvc_num_requests; ++i) {
> - sg_free_table(&video->ureq[i].sgt);
> + struct uvc_request *ureq, *temp;
>
> - if (video->ureq[i].req) {
> - usb_ep_free_request(video->ep, video->ureq[i].req);
> - video->ureq[i].req = NULL;
> - }
> -
> - if (video->ureq[i].req_buffer) {
> - kfree(video->ureq[i].req_buffer);
> - video->ureq[i].req_buffer = NULL;
> - }
> - }
> -
> - kfree(video->ureq);
> - video->ureq = NULL;
> - }
> + list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
> + uvc_video_free_request(ureq, video->ep);
>
> + INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> video->req_size = 0;
> return 0;
> @@ -322,39 +325,45 @@ uvc_video_free_requests(struct uvc_video *video)
> static int
> uvc_video_alloc_requests(struct uvc_video *video)
> {
> + struct uvc_request *ureq;
> unsigned int req_size;
> unsigned int i;
> int ret = -ENOMEM;
>
> BUG_ON(video->req_size);
> + BUG_ON(!list_empty(&video->ureqs));
>
> req_size = video->ep->maxpacket
> * max_t(unsigned int, video->ep->maxburst, 1)
> * (video->ep->mult);
>
> - video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
> - if (video->ureq == NULL)
> - return -ENOMEM;
> + for (i = 0; i < video->uvc_num_requests; i++) {
> + ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
> + if (ureq == NULL)
> + goto error;
> +
> + INIT_LIST_HEAD(&ureq->list);
> +
> + list_add_tail(&ureq->list, &video->ureqs);
>
> - for (i = 0; i < video->uvc_num_requests; ++i) {
> - video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
> - if (video->ureq[i].req_buffer == NULL)
> + ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
> + if (ureq->req_buffer == NULL)
> goto error;
>
> - video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
> - if (video->ureq[i].req == NULL)
> + ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
> + if (ureq->req == NULL)
> goto error;
>
> - video->ureq[i].req->buf = video->ureq[i].req_buffer;
> - video->ureq[i].req->length = 0;
> - video->ureq[i].req->complete = uvc_video_complete;
> - video->ureq[i].req->context = &video->ureq[i];
> - video->ureq[i].video = video;
> - video->ureq[i].last_buf = NULL;
> + ureq->req->buf = ureq->req_buffer;
> + ureq->req->length = 0;
> + ureq->req->complete = uvc_video_complete;
> + ureq->req->context = ureq;
> + ureq->video = video;
> + ureq->last_buf = NULL;
>
> - list_add_tail(&video->ureq[i].req->list, &video->req_free);
> + list_add_tail(&ureq->req->list, &video->req_free);
> /* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
> - sg_alloc_table(&video->ureq[i].sgt,
> + sg_alloc_table(&ureq->sgt,
> DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
> PAGE_SIZE) + 2, GFP_KERNEL);
> }
> @@ -489,8 +498,8 @@ static void uvcg_video_pump(struct work_struct *work)
> */
> int uvcg_video_enable(struct uvc_video *video, int enable)
> {
> - unsigned int i;
> int ret;
> + struct uvc_request *ureq;
>
> if (video->ep == NULL) {
> uvcg_info(&video->uvc->func,
> @@ -502,9 +511,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> cancel_work_sync(&video->pump);
> uvcg_queue_cancel(&video->queue, 0);
>
> - for (i = 0; i < video->uvc_num_requests; ++i)
> - if (video->ureq && video->ureq[i].req)
> - usb_ep_dequeue(video->ep, video->ureq[i].req);
> + list_for_each_entry(ureq, &video->ureqs, list) {
> + if (ureq->req)
> + usb_ep_dequeue(video->ep, ureq->req);
> + }
>
> uvc_video_free_requests(video);
> uvcg_queue_enable(&video->queue, 0);
> @@ -536,6 +546,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> */
> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
> {
> + INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> spin_lock_init(&video->req_lock);
> INIT_WORK(&video->pump, uvcg_video_pump);
> --
> 2.42.0.820.g83a721a137-goog

2023-10-28 20:17:02

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v9 3/4] usb: gadget: uvc: move video disable logic to its own function

Hi Avichal

On 27/10/2023 21:19, Avichal Rakesh wrote:
> This patch refactors the video disable logic in uvcg_video_enable
> into its own separate function 'uvcg_video_disable'. This function
> is now used anywhere uvcg_video_enable(video, 0) was used.
>
> Reviewed-by: Daniel Scally <[email protected]>


For this patch you can keep the R-b - it's fine by me now :)

> Suggested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>
> ---
> v6: Introduced this patch to make the next one easier to review
> v6 -> v7: Add Suggested-by
> v7 -> v8: No change. Getting back in review queue
> v8 -> v9: Call uvcg_video_disable directly instead of uvcg_video_enable(video, 0)
>
> drivers/usb/gadget/function/uvc_v4l2.c | 6 ++--
> drivers/usb/gadget/function/uvc_video.c | 40 ++++++++++++++++---------
> drivers/usb/gadget/function/uvc_video.h | 3 +-
> 3 files changed, 31 insertions(+), 18 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
> index 7cb8d027ff0c..904dd283cbf7 100644
> --- a/drivers/usb/gadget/function/uvc_v4l2.c
> +++ b/drivers/usb/gadget/function/uvc_v4l2.c
> @@ -443,7 +443,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
> return -EINVAL;
>
> /* Enable UVC video. */
> - ret = uvcg_video_enable(video, 1);
> + ret = uvcg_video_enable(video);
> if (ret < 0)
> return ret;
>
> @@ -469,7 +469,7 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
> return -EINVAL;
>
> uvc->state = UVC_STATE_CONNECTED;
> - ret = uvcg_video_enable(video, 0);
> + ret = uvcg_video_disable(video);
> if (ret < 0)
> return ret;
>
> @@ -515,7 +515,7 @@ static void uvc_v4l2_disable(struct uvc_device *uvc)
> if (uvc->state == UVC_STATE_STREAMING)
> uvc->state = UVC_STATE_CONNECTED;
>
> - uvcg_video_enable(&uvc->video, 0);
> + uvcg_video_disable(&uvc->video);
> uvcg_free_buffers(&uvc->video.queue);
> uvc->func_connected = false;
> wake_up_interruptible(&uvc->func_connected_queue);
> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
> index f8f9209fee50..1081dd790fd6 100644
> --- a/drivers/usb/gadget/function/uvc_video.c
> +++ b/drivers/usb/gadget/function/uvc_video.c
> @@ -494,31 +494,43 @@ static void uvcg_video_pump(struct work_struct *work)
> }
>
> /*
> - * Enable or disable the video stream.
> + * Disable the video stream
> */
> -int uvcg_video_enable(struct uvc_video *video, int enable)
> +int
> +uvcg_video_disable(struct uvc_video *video)
> {
> - int ret;
> struct uvc_request *ureq;
>
> if (video->ep == NULL) {
> uvcg_info(&video->uvc->func,
> - "Video enable failed, device is uninitialized.\n");
> + "Video disable failed, device is uninitialized.\n");
> return -ENODEV;
> }
>
> - if (!enable) {
> - cancel_work_sync(&video->pump);
> - uvcg_queue_cancel(&video->queue, 0);
> + cancel_work_sync(&video->pump);
> + uvcg_queue_cancel(&video->queue, 0);
>
> - list_for_each_entry(ureq, &video->ureqs, list) {
> - if (ureq->req)
> - usb_ep_dequeue(video->ep, ureq->req);
> - }
> + list_for_each_entry(ureq, &video->ureqs, list) {
> + if (ureq->req)
> + usb_ep_dequeue(video->ep, ureq->req);
> + }
>
> - uvc_video_free_requests(video);
> - uvcg_queue_enable(&video->queue, 0);
> - return 0;
> + uvc_video_free_requests(video);
> + uvcg_queue_enable(&video->queue, 0);
> + return 0;
> +}
> +
> +/*
> + * Enable the video stream.
> + */
> +int uvcg_video_enable(struct uvc_video *video)
> +{
> + int ret;
> +
> + if (video->ep == NULL) {
> + uvcg_info(&video->uvc->func,
> + "Video enable failed, device is uninitialized.\n");
> + return -ENODEV;
> }
>
> if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
> diff --git a/drivers/usb/gadget/function/uvc_video.h b/drivers/usb/gadget/function/uvc_video.h
> index 03adeefa343b..8ef6259741f1 100644
> --- a/drivers/usb/gadget/function/uvc_video.h
> +++ b/drivers/usb/gadget/function/uvc_video.h
> @@ -14,7 +14,8 @@
>
> struct uvc_video;
>
> -int uvcg_video_enable(struct uvc_video *video, int enable);
> +int uvcg_video_enable(struct uvc_video *video);
> +int uvcg_video_disable(struct uvc_video *video);
>
> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc);
>
> --
> 2.42.0.820.g83a721a137-goog

2023-10-28 20:57:23

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v9 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Hi Avichal

On 27/10/2023 21:19, Avichal Rakesh wrote:
> Currently, the uvc gadget driver allocates all uvc_requests as one array
> and deallocates them all when the video stream stops. This includes
> de-allocating all the usb_requests associated with those uvc_requests.
> This can lead to use-after-free issues if any of those de-allocated
> usb_requests were still owned by the usb controller.
>
> This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
> flag to uvc_video to track when frames and requests should be flowing.
> When disabling the video stream, the flag is tripped and, instead
> of de-allocating all uvc_requests and usb_requests, the gadget
> driver only de-allocates those usb_requests that are currently
> owned by it (as present in req_free). Other usb_requests are left
> untouched until their completion handler is called which takes care
> of freeing the usb_request and its corresponding uvc_request.
>
> Now that uvc_video does not depends on uvc->state, this patch removes
> unnecessary upates to uvc->state that were made to accommodate uvc_video
> logic. This should ensure that uvc gadget driver never accidentally
> de-allocates a usb_request that it doesn't own.
>
> Link: https://lore.kernel.org/[email protected]
> Suggested-by: Michael Grzeschik <[email protected]>
> Reviewed-by: Michael Grzeschik <[email protected]>
> Tested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>
> ---
> v1 -> v2: Rebased to ToT, and fixed deadlock reported in
> https://lore.kernel.org/all/[email protected]/
> v2 -> v3: Fix email threading goof-up
> v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
> as discussed in
> https://lore.kernel.org/[email protected]/
> v4 -> v5: Address review comments. Add Reviewed-by & Tested-by.
> v5 -> v6: Added another patch before this one to make uvcg_video_disable
> easier to review.
> v6 -> v7: Fix warning reported in
> https://lore.kernel.org/[email protected]/
> v7 -> v8: No change. Getting back in review queue
> v8 -> v9: No change.
>
> drivers/usb/gadget/function/uvc.h | 1 +
> drivers/usb/gadget/function/uvc_v4l2.c | 12 +--
> drivers/usb/gadget/function/uvc_video.c | 128 ++++++++++++++++++++----
> 3 files changed, 111 insertions(+), 30 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
> index 993694da0bbc..be0d012aa244 100644
> --- a/drivers/usb/gadget/function/uvc.h
> +++ b/drivers/usb/gadget/function/uvc.h
> @@ -102,6 +102,7 @@ struct uvc_video {
> unsigned int uvc_num_requests;
>
> /* Requests */
> + bool is_enabled; /* tracks whether video stream is enabled */
> unsigned int req_size;
> struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
> struct list_head req_free;
> diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
> index 904dd283cbf7..2f8634e05612 100644
> --- a/drivers/usb/gadget/function/uvc_v4l2.c
> +++ b/drivers/usb/gadget/function/uvc_v4l2.c
> @@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
> * Complete the alternate setting selection setup phase now that
> * userspace is ready to provide video frames.
> */
> - uvc_function_setup_continue(uvc, 0);
> uvc->state = UVC_STATE_STREAMING;
> + uvc_function_setup_continue(uvc, 0);
>
> return 0;
> }
> @@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
> if (type != video->queue.queue.type)
> return -EINVAL;
>
> - uvc->state = UVC_STATE_CONNECTED;
> ret = uvcg_video_disable(video);
> if (ret < 0)
> return ret;
>
> + uvc->state = UVC_STATE_CONNECTED;
> uvc_function_setup_continue(uvc, 1);
> return 0;
> }


I'm not sure I understand what these re-orderings are for...can you explain please?

> @@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
> static void uvc_v4l2_disable(struct uvc_device *uvc)
> {
> uvc_function_disconnect(uvc);
> - /*
> - * Drop uvc->state to CONNECTED if it was streaming before.
> - * This ensures that the usb_requests are no longer queued
> - * to the controller.
> - */
> - if (uvc->state == UVC_STATE_STREAMING)
> - uvc->state = UVC_STATE_CONNECTED;
> -
> uvcg_video_disable(&uvc->video);
> uvcg_free_buffers(&uvc->video.queue);
> uvc->func_connected = false;
> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
> index 1081dd790fd6..8f330ce696ec 100644
> --- a/drivers/usb/gadget/function/uvc_video.c
> +++ b/drivers/usb/gadget/function/uvc_video.c
> @@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
> * Request handling
> */
>
> +/*
> + * Must be called with req_lock held as it modifies the list ureq is held in
> + */



This comment probably belongs in patch #2. And in that case, shouldn't uvc_video_free_requests()
hold the lock in that patch?

> static void
> uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
> {
> @@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
> struct uvc_request *ureq = req->context;
> struct uvc_video *video = ureq->video;
> struct uvc_video_queue *queue = &video->queue;
> - struct uvc_device *uvc = video->uvc;
> + struct uvc_buffer *last_buf = NULL;
> unsigned long flags;
>
> + spin_lock_irqsave(&video->req_lock, flags);
> + if (!video->is_enabled) {
> + /*
> + * When is_enabled is false, uvc_video_disable ensures that
s/uvc_video_disable/uvc_video_disable()
> + * in-flight uvc_buffers are returned, so we can safely
> + * call free_request without worrying about last_buf.
> + */
> + uvc_video_free_request(ureq, ep);
Now I understand the conditional in this function in patch 2 :)
> + spin_unlock_irqrestore(&video->req_lock, flags);
> + return;
> + }
> +
> + last_buf = ureq->last_buf;
> + ureq->last_buf = NULL;
> + spin_unlock_irqrestore(&video->req_lock, flags);


I'm not a huge fan of this locking, unlocking and relocking the same spinlock within the same
function. Can we just hold the lock for the duration? if not, can there be an explanatory comment as
to why?
> +
> switch (req->status) {
> case 0:
> break;
> @@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
> uvcg_queue_cancel(queue, 0);
> }
>
> - if (ureq->last_buf) {
> - uvcg_complete_buffer(&video->queue, ureq->last_buf);
> - ureq->last_buf = NULL;
> + if (last_buf) {
> + spin_lock_irqsave(&queue->irqlock, flags);
> + uvcg_complete_buffer(&video->queue, last_buf);
> + spin_unlock_irqrestore(&queue->irqlock, flags);



I think it's right to take the irqlock here but it probably should have always been held, so this
probably ought to go in its own commit with a Fixes:

> }
>
> spin_lock_irqsave(&video->req_lock, flags);
> - list_add_tail(&req->list, &video->req_free);
> - spin_unlock_irqrestore(&video->req_lock, flags);
> -
> - if (uvc->state == UVC_STATE_STREAMING)
> + /*
> + * Video stream might have been disabled while we were
> + * processing the current usb_request. So make sure
> + * we're still streaming before queueing the usb_request
> + * back to req_free
> + */
> + if (video->is_enabled) {
> + list_add_tail(&req->list, &video->req_free);
> queue_work(video->async_wq, &video->pump);
> + } else {
> + uvc_video_free_request(ureq, ep);
> + }
> + spin_unlock_irqrestore(&video->req_lock, flags);
> }
>
> static int
> @@ -393,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
> struct uvc_video_queue *queue = &video->queue;
> /* video->max_payload_size is only set when using bulk transfer */
> bool is_bulk = video->max_payload_size;
> - struct uvc_device *uvc = video->uvc;
> struct usb_request *req = NULL;
> struct uvc_buffer *buf;
> unsigned long flags;
> bool buf_done;
> int ret;
>
> - while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
> + while (true) {
> + if (!video->ep->enabled)
> + return;
> +
> /*
> - * Retrieve the first available USB request, protected by the
> - * request lock.
> + * Check is_enabled and retrieve the first available USB
> + * request, protected by the request lock.
> */
> spin_lock_irqsave(&video->req_lock, flags);
> - if (list_empty(&video->req_free)) {
> + if (!video->is_enabled || list_empty(&video->req_free)) {
> spin_unlock_irqrestore(&video->req_lock, flags);
> return;
> }
> @@ -488,9 +518,11 @@ static void uvcg_video_pump(struct work_struct *work)
> return;
>
> spin_lock_irqsave(&video->req_lock, flags);
> - list_add_tail(&req->list, &video->req_free);
> + if (video->is_enabled)
> + list_add_tail(&req->list, &video->req_free);
> + else
> + uvc_video_free_request(req->context, video->ep);
> spin_unlock_irqrestore(&video->req_lock, flags);
> - return;
> }
>
> /*
> @@ -499,7 +531,11 @@ static void uvcg_video_pump(struct work_struct *work)
> int
> uvcg_video_disable(struct uvc_video *video)
> {
> - struct uvc_request *ureq;
> + unsigned long flags;
> + struct list_head inflight_bufs;
> + struct usb_request *req, *temp;
> + struct uvc_buffer *buf, *btemp;
> + struct uvc_request *ureq, *utemp;
>
> if (video->ep == NULL) {
> uvcg_info(&video->uvc->func,
> @@ -507,15 +543,58 @@ uvcg_video_disable(struct uvc_video *video)
> return -ENODEV;
> }
>
> + INIT_LIST_HEAD(&inflight_bufs);
> + spin_lock_irqsave(&video->req_lock, flags);
> + video->is_enabled = false;
> +
> + /*
> + * Remove any in-flight buffers from the uvc_requests
> + * because we want to return them before cancelling the
> + * queue. This ensures that we aren't stuck waiting for
> + * all complete callbacks to come through before disabling
> + * vb2 queue.
> + */
> + list_for_each_entry(ureq, &video->ureqs, list) {
> + if (ureq->last_buf) {
> + list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
> + ureq->last_buf = NULL;
> + }
> + }
> + spin_unlock_irqrestore(&video->req_lock, flags);
> +
> cancel_work_sync(&video->pump);
> uvcg_queue_cancel(&video->queue, 0);
>
> - list_for_each_entry(ureq, &video->ureqs, list) {
> - if (ureq->req)
> - usb_ep_dequeue(video->ep, ureq->req);
> + spin_lock_irqsave(&video->req_lock, flags);
> + /*
> + * Remove all uvc_reqeusts from ureqs with list_del_init
> + * This lets uvc_video_free_request correctly identify
> + * if the uvc_request is attached to a list or not when freeing
> + * memory.
> + */
> + list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
> + list_del_init(&ureq->list);
> +
> + list_for_each_entry_safe(req, temp, &video->req_free, list) {
> + list_del(&req->list);
> + uvc_video_free_request(req->context, video->ep);
> }
>
> - uvc_video_free_requests(video);
> + INIT_LIST_HEAD(&video->ureqs);
> + INIT_LIST_HEAD(&video->req_free);
> + video->req_size = 0;
> + spin_unlock_irqrestore(&video->req_lock, flags);
> +
> + /*
> + * Return all the video buffers before disabling the queue.
> + */
> + spin_lock_irqsave(&video->queue.irqlock, flags);
> + list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
> + list_del(&buf->queue);
> + uvcg_complete_buffer(&video->queue, buf);
> + }
> + spin_unlock_irqrestore(&video->queue.irqlock, flags);
> +
> uvcg_queue_enable(&video->queue, 0);
> return 0;
> }
> @@ -533,6 +612,14 @@ int uvcg_video_enable(struct uvc_video *video)
> return -ENODEV;
> }
>
> + /*
> + * Safe to access request related fields without req_lock because
> + * this is the only thread currently active, and no other
> + * request handling thread will become active until this function
> + * returns.
> + */
> + video->is_enabled = true;
> +
> if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
> return ret;
>
> @@ -558,6 +645,7 @@ int uvcg_video_enable(struct uvc_video *video)
> */
> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
> {
> + video->is_enabled = false;
> INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> spin_lock_init(&video->req_lock);
> --
> 2.42.0.820.g83a721a137-goog

2023-10-30 20:23:04

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v10 1/4] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. As the endpoint's state can no longer be used, video_pump is
now guarded by uvc->state as well. To be consistent with the actual
streaming state, uvc->state is now toggled between CONNECTED and STREAMING
from the v4l2 event callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Reviewed-by: Daniel Scally <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2 : Rebased to ToT and reworded commit message.
v2 -> v3 : Fix email threading goof-up
v3 -> v4 : Address review comments & re-rebase to ToT
v4 -> v5 : Add Reviewed-by & Tested-by
v5 -> v6 : No change
v6 -> v7 : No change
v7 -> v8 : No change. Getting back in review queue
v8 -> v9 : Fix typo. No functional change.
v9 -> v10: Rebase to ToT (usb-next)

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
drivers/usb/gadget/function/uvc_video.c | 3 ++-
5 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index 786379f1b7b7..77999ed53d33 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep)
+ usb_ep_disable(uvc->video.ep);
+
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..083aef0c65c6 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..7cb8d027ff0c 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0)
+ return ret;
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ if (uvc->state == UVC_STATE_STREAMING)
+ uvc->state = UVC_STATE_CONNECTED;
+
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 91af3b1ef0d4..c334802ac0a4 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
+ struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (video->ep->enabled) {
+ while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
/*
* Retrieve the first available USB request, protected by the
* request lock.
--
2.42.0.820.g83a721a137-goog

2023-10-30 20:23:08

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v10 3/4] usb: gadget: uvc: move video disable logic to its own function

This patch refactors the video disable logic in uvcg_video_enable
into its own separate function 'uvcg_video_disable'. This function
is now used anywhere uvcg_video_enable(video, 0) was used.

Reviewed-by: Daniel Scally <[email protected]>
Suggested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v6 : Introduced this patch to make the next one easier to review
v6 -> v7 : Add Suggested-by
v7 -> v8 : No change. Getting back in review queue
v8 -> v9 : Call uvcg_video_disable directly instead of uvcg_video_enable(video, 0)
v9 -> v10: Rebase to ToT (usb-next)

drivers/usb/gadget/function/uvc_v4l2.c | 6 ++--
drivers/usb/gadget/function/uvc_video.c | 40 ++++++++++++++++---------
drivers/usb/gadget/function/uvc_video.h | 3 +-
3 files changed, 31 insertions(+), 18 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 7cb8d027ff0c..904dd283cbf7 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -443,7 +443,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
return -EINVAL;

/* Enable UVC video. */
- ret = uvcg_video_enable(video, 1);
+ ret = uvcg_video_enable(video);
if (ret < 0)
return ret;

@@ -469,7 +469,7 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
return -EINVAL;

uvc->state = UVC_STATE_CONNECTED;
- ret = uvcg_video_enable(video, 0);
+ ret = uvcg_video_disable(video);
if (ret < 0)
return ret;

@@ -515,7 +515,7 @@ static void uvc_v4l2_disable(struct uvc_device *uvc)
if (uvc->state == UVC_STATE_STREAMING)
uvc->state = UVC_STATE_CONNECTED;

- uvcg_video_enable(&uvc->video, 0);
+ uvcg_video_disable(&uvc->video);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
wake_up_interruptible(&uvc->func_connected_queue);
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 1619f9664748..c3e8c48f46a9 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -493,31 +493,43 @@ static void uvcg_video_pump(struct work_struct *work)
}

/*
- * Enable or disable the video stream.
+ * Disable the video stream
*/
-int uvcg_video_enable(struct uvc_video *video, int enable)
+int
+uvcg_video_disable(struct uvc_video *video)
{
- int ret;
struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
- "Video enable failed, device is uninitialized.\n");
+ "Video disable failed, device is uninitialized.\n");
return -ENODEV;
}

- if (!enable) {
- cancel_work_sync(&video->pump);
- uvcg_queue_cancel(&video->queue, 0);
+ cancel_work_sync(&video->pump);
+ uvcg_queue_cancel(&video->queue, 0);

- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
- }
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

- uvc_video_free_requests(video);
- uvcg_queue_enable(&video->queue, 0);
- return 0;
+ uvc_video_free_requests(video);
+ uvcg_queue_enable(&video->queue, 0);
+ return 0;
+}
+
+/*
+ * Enable the video stream.
+ */
+int uvcg_video_enable(struct uvc_video *video)
+{
+ int ret;
+
+ if (video->ep == NULL) {
+ uvcg_info(&video->uvc->func,
+ "Video enable failed, device is uninitialized.\n");
+ return -ENODEV;
}

if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
diff --git a/drivers/usb/gadget/function/uvc_video.h b/drivers/usb/gadget/function/uvc_video.h
index 03adeefa343b..8ef6259741f1 100644
--- a/drivers/usb/gadget/function/uvc_video.h
+++ b/drivers/usb/gadget/function/uvc_video.h
@@ -14,7 +14,8 @@

struct uvc_video;

-int uvcg_video_enable(struct uvc_video *video, int enable);
+int uvcg_video_enable(struct uvc_video *video);
+int uvcg_video_disable(struct uvc_video *video);

int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc);

--
2.42.0.820.g83a721a137-goog

2023-10-30 20:23:11

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v10 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This patch is 1 of 2 patches addressing the use-after-free issue.
Instead of bulk allocating all uvc_requests as an array, this patch
allocates uvc_requests one at a time, which should allows for similar
granularity when deallocating the uvc_requests. This patch has no
functional changes other than allocating each uvc_request separately,
and similarly freeing each of them separately.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2 : Rebased to ToT
v2 -> v3 : Fix email threading goof-up
v3 -> v4 : Address review comments & re-rebase to ToT
v4 -> v5 : Address more review comments. Add Reviewed-by & Tested-by.
v5 -> v6 : No change
v6 -> v7 : No change
v7 -> v8 : No change. Getting back in review queue
v8 -> v9 : Address review comments.
v9 -> v10: Address review comments; remove BUG_ON(&video->reqs);
Rebase to ToT (usb-next)

drivers/usb/gadget/function/uvc.h | 3 +-
drivers/usb/gadget/function/uvc_video.c | 88 ++++++++++++++-----------
2 files changed, 51 insertions(+), 40 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 989bc6b4e93d..993694da0bbc 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -81,6 +81,7 @@ struct uvc_request {
struct sg_table sgt;
u8 header[UVCG_REQUEST_HEADER_LEN];
struct uvc_buffer *last_buf;
+ struct list_head list;
};

struct uvc_video {
@@ -102,7 +103,7 @@ struct uvc_video {

/* Requests */
unsigned int req_size;
- struct uvc_request *ureq;
+ struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
spinlock_t req_lock;

diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c334802ac0a4..1619f9664748 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+static void
+uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
+{
+ sg_free_table(&ureq->sgt);
+ if (ureq->req && ep) {
+ usb_ep_free_request(ep, ureq->req);
+ ureq->req = NULL;
+ }
+
+ kfree(ureq->req_buffer);
+ ureq->req_buffer = NULL;
+
+ if (!list_empty(&ureq->list))
+ list_del_init(&ureq->list);
+
+ kfree(ureq);
+}
+
static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
{
int ret;
@@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
static int
uvc_video_free_requests(struct uvc_video *video)
{
- unsigned int i;
-
- if (video->ureq) {
- for (i = 0; i < video->uvc_num_requests; ++i) {
- sg_free_table(&video->ureq[i].sgt);
+ struct uvc_request *ureq, *temp;

- if (video->ureq[i].req) {
- usb_ep_free_request(video->ep, video->ureq[i].req);
- video->ureq[i].req = NULL;
- }
-
- if (video->ureq[i].req_buffer) {
- kfree(video->ureq[i].req_buffer);
- video->ureq[i].req_buffer = NULL;
- }
- }
-
- kfree(video->ureq);
- video->ureq = NULL;
- }
+ list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
+ uvc_video_free_request(ureq, video->ep);

+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
video->req_size = 0;
return 0;
@@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
static int
uvc_video_alloc_requests(struct uvc_video *video)
{
+ struct uvc_request *ureq;
unsigned int req_size;
unsigned int i;
int ret = -ENOMEM;
@@ -332,29 +336,33 @@ uvc_video_alloc_requests(struct uvc_video *video)
* max_t(unsigned int, video->ep->maxburst, 1)
* (video->ep->mult);

- video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
- if (video->ureq == NULL)
- return -ENOMEM;
+ for (i = 0; i < video->uvc_num_requests; i++) {
+ ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
+ if (ureq == NULL)
+ goto error;
+
+ INIT_LIST_HEAD(&ureq->list);
+
+ list_add_tail(&ureq->list, &video->ureqs);

- for (i = 0; i < video->uvc_num_requests; ++i) {
- video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
- if (video->ureq[i].req_buffer == NULL)
+ ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
+ if (ureq->req_buffer == NULL)
goto error;

- video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
- if (video->ureq[i].req == NULL)
+ ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
+ if (ureq->req == NULL)
goto error;

- video->ureq[i].req->buf = video->ureq[i].req_buffer;
- video->ureq[i].req->length = 0;
- video->ureq[i].req->complete = uvc_video_complete;
- video->ureq[i].req->context = &video->ureq[i];
- video->ureq[i].video = video;
- video->ureq[i].last_buf = NULL;
+ ureq->req->buf = ureq->req_buffer;
+ ureq->req->length = 0;
+ ureq->req->complete = uvc_video_complete;
+ ureq->req->context = ureq;
+ ureq->video = video;
+ ureq->last_buf = NULL;

- list_add_tail(&video->ureq[i].req->list, &video->req_free);
+ list_add_tail(&ureq->req->list, &video->req_free);
/* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
- sg_alloc_table(&video->ureq[i].sgt,
+ sg_alloc_table(&ureq->sgt,
DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
PAGE_SIZE) + 2, GFP_KERNEL);
}
@@ -489,8 +497,8 @@ static void uvcg_video_pump(struct work_struct *work)
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
- unsigned int i;
int ret;
+ struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -502,9 +510,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- for (i = 0; i < video->uvc_num_requests; ++i)
- if (video->ureq && video->ureq[i].req)
- usb_ep_dequeue(video->ep, video->ureq[i].req);
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

uvc_video_free_requests(video);
uvcg_queue_enable(&video->queue, 0);
@@ -536,6 +545,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
INIT_WORK(&video->pump, uvcg_video_pump);
--
2.42.0.820.g83a721a137-goog

2023-10-30 20:23:22

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v10 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
flag to uvc_video to track when frames and requests should be flowing.
When disabling the video stream, the flag is tripped and, instead
of de-allocating all uvc_requests and usb_requests, the gadget
driver only de-allocates those usb_requests that are currently
owned by it (as present in req_free). Other usb_requests are left
untouched until their completion handler is called which takes care
of freeing the usb_request and its corresponding uvc_request.

Now that uvc_video does not depends on uvc->state, this patch removes
unnecessary upates to uvc->state that were made to accommodate uvc_video
logic. This should ensure that uvc gadget driver never accidentally
de-allocates a usb_request that it doesn't own.

Link: https://lore.kernel.org/[email protected]
Suggested-by: Michael Grzeschik <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2 : Rebased to ToT, and fixed deadlock reported in
https://lore.kernel.org/all/[email protected]/
v2 -> v3 : Fix email threading goof-up
v3 -> v4 : re-rebase to ToT & moved to a uvc_video level lock
as discussed in
https://lore.kernel.org/[email protected]/
v4 -> v5 : Address review comments. Add Reviewed-by & Tested-by.
v5 -> v6 : Added another patch before this one to make uvcg_video_disable
easier to review.
v6 -> v7 : Fix warning reported in
https://lore.kernel.org/[email protected]/
v7 -> v8 : No change. Getting back in review queue
v8 -> v9 : No change.
v9 -> v10: Address review comments. Rebase to ToT (usb-next)

drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_v4l2.c | 10 +-
drivers/usb/gadget/function/uvc_video.c | 129 ++++++++++++++++++++----
3 files changed, 111 insertions(+), 29 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 993694da0bbc..be0d012aa244 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -102,6 +102,7 @@ struct uvc_video {
unsigned int uvc_num_requests;

/* Requests */
+ bool is_enabled; /* tracks whether video stream is enabled */
unsigned int req_size;
struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 904dd283cbf7..c7e5fa4f29e0 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
if (type != video->queue.queue.type)
return -EINVAL;

- uvc->state = UVC_STATE_CONNECTED;
ret = uvcg_video_disable(video);
if (ret < 0)
return ret;

+ uvc->state = UVC_STATE_CONNECTED;
uvc_function_setup_continue(uvc, 1);
return 0;
}
@@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
- /*
- * Drop uvc->state to CONNECTED if it was streaming before.
- * This ensures that the usb_requests are no longer queued
- * to the controller.
- */
- if (uvc->state == UVC_STATE_STREAMING)
- uvc->state = UVC_STATE_CONNECTED;
-
uvcg_video_disable(&uvc->video);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c3e8c48f46a9..53feb790a4c3 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+/*
+ * Must be called with req_lock held as it modifies the list ureq is held in
+ */
static void
uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
{
@@ -271,9 +274,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
struct uvc_request *ureq = req->context;
struct uvc_video *video = ureq->video;
struct uvc_video_queue *queue = &video->queue;
- struct uvc_device *uvc = video->uvc;
+ struct uvc_buffer *last_buf = NULL;
unsigned long flags;

+ spin_lock_irqsave(&video->req_lock, flags);
+ if (!video->is_enabled) {
+ /*
+ * When is_enabled is false, uvcg_video_disable() ensures
+ * that in-flight uvc_buffers are returned, so we can
+ * safely call free_request without worrying about
+ * last_buf.
+ */
+ uvc_video_free_request(ureq, ep);
+ spin_unlock_irqrestore(&video->req_lock, flags);
+ return;
+ }
+
+ last_buf = ureq->last_buf;
+ ureq->last_buf = NULL;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
switch (req->status) {
case 0:
break;
@@ -295,17 +315,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
uvcg_queue_cancel(queue, 0);
}

- if (ureq->last_buf) {
- uvcg_complete_buffer(&video->queue, ureq->last_buf);
- ureq->last_buf = NULL;
+ if (last_buf) {
+ spin_lock_irqsave(&queue->irqlock, flags);
+ uvcg_complete_buffer(&video->queue, last_buf);
+ spin_unlock_irqrestore(&queue->irqlock, flags);
}

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
- spin_unlock_irqrestore(&video->req_lock, flags);
-
- if (uvc->state == UVC_STATE_STREAMING)
+ /*
+ * Video stream might have been disabled while we were
+ * processing the current usb_request. So make sure
+ * we're still streaming before queueing the usb_request
+ * back to req_free
+ */
+ if (video->is_enabled) {
+ list_add_tail(&req->list, &video->req_free);
queue_work(video->async_wq, &video->pump);
+ } else {
+ uvc_video_free_request(ureq, ep);
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
}

static int
@@ -392,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
- struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
+ while (true) {
+ if (!video->ep->enabled)
+ return;
+
/*
- * Retrieve the first available USB request, protected by the
- * request lock.
+ * Check is_enabled and retrieve the first available USB
+ * request, protected by the request lock.
*/
spin_lock_irqsave(&video->req_lock, flags);
- if (list_empty(&video->req_free)) {
+ if (!video->is_enabled || list_empty(&video->req_free)) {
spin_unlock_irqrestore(&video->req_lock, flags);
return;
}
@@ -487,9 +518,11 @@ static void uvcg_video_pump(struct work_struct *work)
return;

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
+ if (video->is_enabled)
+ list_add_tail(&req->list, &video->req_free);
+ else
+ uvc_video_free_request(req->context, video->ep);
spin_unlock_irqrestore(&video->req_lock, flags);
- return;
}

/*
@@ -498,7 +531,11 @@ static void uvcg_video_pump(struct work_struct *work)
int
uvcg_video_disable(struct uvc_video *video)
{
- struct uvc_request *ureq;
+ unsigned long flags;
+ struct list_head inflight_bufs;
+ struct usb_request *req, *temp;
+ struct uvc_buffer *buf, *btemp;
+ struct uvc_request *ureq, *utemp;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -506,15 +543,58 @@ uvcg_video_disable(struct uvc_video *video)
return -ENODEV;
}

+ INIT_LIST_HEAD(&inflight_bufs);
+ spin_lock_irqsave(&video->req_lock, flags);
+ video->is_enabled = false;
+
+ /*
+ * Remove any in-flight buffers from the uvc_requests
+ * because we want to return them before cancelling the
+ * queue. This ensures that we aren't stuck waiting for
+ * all complete callbacks to come through before disabling
+ * vb2 queue.
+ */
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->last_buf) {
+ list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
+ ureq->last_buf = NULL;
+ }
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
+ spin_lock_irqsave(&video->req_lock, flags);
+ /*
+ * Remove all uvc_reqeusts from ureqs with list_del_init
+ * This lets uvc_video_free_request correctly identify
+ * if the uvc_request is attached to a list or not when freeing
+ * memory.
+ */
+ list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
+ list_del_init(&ureq->list);
+
+ list_for_each_entry_safe(req, temp, &video->req_free, list) {
+ list_del(&req->list);
+ uvc_video_free_request(req->context, video->ep);
}

- uvc_video_free_requests(video);
+ INIT_LIST_HEAD(&video->ureqs);
+ INIT_LIST_HEAD(&video->req_free);
+ video->req_size = 0;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ /*
+ * Return all the video buffers before disabling the queue.
+ */
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
+ list_del(&buf->queue);
+ uvcg_complete_buffer(&video->queue, buf);
+ }
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
+
uvcg_queue_enable(&video->queue, 0);
return 0;
}
@@ -532,6 +612,14 @@ int uvcg_video_enable(struct uvc_video *video)
return -ENODEV;
}

+ /*
+ * Safe to access request related fields without req_lock because
+ * this is the only thread currently active, and no other
+ * request handling thread will become active until this function
+ * returns.
+ */
+ video->is_enabled = true;
+
if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;

@@ -557,6 +645,7 @@ int uvcg_video_enable(struct uvc_video *video)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ video->is_enabled = false;
INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
--
2.42.0.820.g83a721a137-goog

2023-10-30 20:27:01

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v9 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time



On 10/28/23 13:13, Dan Scally wrote:
> Hi Avichal
>
> On 27/10/2023 21:19, Avichal Rakesh wrote:
>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>> and deallocates them all when the video stream stops. This includes
>> de-allocating all the usb_requests associated with those uvc_requests.
>> This can lead to use-after-free issues if any of those de-allocated
>> usb_requests were still owned by the usb controller.
>>
>> This patch is 1 of 2 patches addressing the use-after-free issue.
>> Instead of bulk allocating all uvc_requests as an array, this patch
>> allocates uvc_requests one at a time, which should allows for similar
>> granularity when deallocating the uvc_requests. This patch has no
>> functional changes other than allocating each uvc_request separately,
>> and similarly freeing each of them separately.
>>
>> Link: https://lore.kernel.org/[email protected]
>> Suggested-by: Michael Grzeschik <[email protected]>
>> Reviewed-by: Daniel Scally <[email protected]>
>
>
> Sorry - I was unclear in my response to the first patch on v8. I meant my R-b to apply to the first patch only rather than to all of them. For this one I understand now the use of the conditional in uvc_video_free_request(), so that point is fine. I agree with Greg that the BUG_ON() shouldn't stand though.

Ah, didn't realize BUG_ON is discouraged. Removed BUG_ON.
It was supposed to be a defensive bit of code anyway,
so removing the check entirely. If the state is
inconsistent, we'd see other errors, so the BUG_ON
wasn't providing value anyway.

Also removed your Reviewed-by, my apologies.

>
>> Reviewed-by: Michael Grzeschik <[email protected]>
>> Tested-by: Michael Grzeschik <[email protected]>
>> Signed-off-by: Avichal Rakesh <[email protected]>
>> ---
>> v1 -> v2: Rebased to ToT
>> v2 -> v3: Fix email threading goof-up
>> v3 -> v4: Address review comments & re-rebase to ToT
>> v4 -> v5: Address more review comments. Add Reviewed-by & Tested-by.
>> v5 -> v6: No change
>> v6 -> v7: No change
>> v7 -> v8: No change. Getting back in review queue
>> v8 -> v9: Address review comments.
>>
>>   drivers/usb/gadget/function/uvc.h       |  3 +-
>>   drivers/usb/gadget/function/uvc_video.c | 89 ++++++++++++++-----------
>>   2 files changed, 52 insertions(+), 40 deletions(-)
>>
>> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
>> index 989bc6b4e93d..993694da0bbc 100644
>> --- a/drivers/usb/gadget/function/uvc.h
>> +++ b/drivers/usb/gadget/function/uvc.h
>> @@ -81,6 +81,7 @@ struct uvc_request {
>>       struct sg_table sgt;
>>       u8 header[UVCG_REQUEST_HEADER_LEN];
>>       struct uvc_buffer *last_buf;
>> +    struct list_head list;
>>   };
>>
>>   struct uvc_video {
>> @@ -102,7 +103,7 @@ struct uvc_video {
>>
>>       /* Requests */
>>       unsigned int req_size;
>> -    struct uvc_request *ureq;
>> +    struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
>>       struct list_head req_free;
>>       spinlock_t req_lock;
>>
>> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
>> index c334802ac0a4..f8f9209fee50 100644
>> --- a/drivers/usb/gadget/function/uvc_video.c
>> +++ b/drivers/usb/gadget/function/uvc_video.c
>> @@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
>>    * Request handling
>>    */
>>
>> +static void
>> +uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
>> +{
>> +    sg_free_table(&ureq->sgt);
>> +    if (ureq->req && ep) {
>> +        usb_ep_free_request(ep, ureq->req);
>> +        ureq->req = NULL;
>> +    }
>> +
>> +    kfree(ureq->req_buffer);
>> +    ureq->req_buffer = NULL;
>> +
>> +    if (!list_empty(&ureq->list))
>> +        list_del_init(&ureq->list);
>> +
>> +    kfree(ureq);
>> +}
>> +
>>   static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
>>   {
>>       int ret;
>> @@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
>>   static int
>>   uvc_video_free_requests(struct uvc_video *video)
>>   {
>> -    unsigned int i;
>> -
>> -    if (video->ureq) {
>> -        for (i = 0; i < video->uvc_num_requests; ++i) {
>> -            sg_free_table(&video->ureq[i].sgt);
>> +    struct uvc_request *ureq, *temp;
>>
>> -            if (video->ureq[i].req) {
>> -                usb_ep_free_request(video->ep, video->ureq[i].req);
>> -                video->ureq[i].req = NULL;
>> -            }
>> -
>> -            if (video->ureq[i].req_buffer) {
>> -                kfree(video->ureq[i].req_buffer);
>> -                video->ureq[i].req_buffer = NULL;
>> -            }
>> -        }
>> -
>> -        kfree(video->ureq);
>> -        video->ureq = NULL;
>> -    }
>> +    list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
>> +        uvc_video_free_request(ureq, video->ep);
>>
>> +    INIT_LIST_HEAD(&video->ureqs);
>>       INIT_LIST_HEAD(&video->req_free);
>>       video->req_size = 0;
>>       return 0;
>> @@ -322,39 +325,45 @@ uvc_video_free_requests(struct uvc_video *video)
>>   static int
>>   uvc_video_alloc_requests(struct uvc_video *video)
>>   {
>> +    struct uvc_request *ureq;
>>       unsigned int req_size;
>>       unsigned int i;
>>       int ret = -ENOMEM;
>>
>>       BUG_ON(video->req_size);
>> +    BUG_ON(!list_empty(&video->ureqs));
>>
>>       req_size = video->ep->maxpacket
>>            * max_t(unsigned int, video->ep->maxburst, 1)
>>            * (video->ep->mult);
>>
>> -    video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
>> -    if (video->ureq == NULL)
>> -        return -ENOMEM;
>> +    for (i = 0; i < video->uvc_num_requests; i++) {
>> +        ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
>> +        if (ureq == NULL)
>> +            goto error;
>> +
>> +        INIT_LIST_HEAD(&ureq->list);
>> +
>> +        list_add_tail(&ureq->list, &video->ureqs);
>>
>> -    for (i = 0; i < video->uvc_num_requests; ++i) {
>> -        video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
>> -        if (video->ureq[i].req_buffer == NULL)
>> +        ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
>> +        if (ureq->req_buffer == NULL)
>>               goto error;
>>
>> -        video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
>> -        if (video->ureq[i].req == NULL)
>> +        ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
>> +        if (ureq->req == NULL)
>>               goto error;
>>
>> -        video->ureq[i].req->buf = video->ureq[i].req_buffer;
>> -        video->ureq[i].req->length = 0;
>> -        video->ureq[i].req->complete = uvc_video_complete;
>> -        video->ureq[i].req->context = &video->ureq[i];
>> -        video->ureq[i].video = video;
>> -        video->ureq[i].last_buf = NULL;
>> +        ureq->req->buf = ureq->req_buffer;
>> +        ureq->req->length = 0;
>> +        ureq->req->complete = uvc_video_complete;
>> +        ureq->req->context = ureq;
>> +        ureq->video = video;
>> +        ureq->last_buf = NULL;
>>
>> -        list_add_tail(&video->ureq[i].req->list, &video->req_free);
>> +        list_add_tail(&ureq->req->list, &video->req_free);
>>           /* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
>> -        sg_alloc_table(&video->ureq[i].sgt,
>> +        sg_alloc_table(&ureq->sgt,
>>                      DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
>>                           PAGE_SIZE) + 2, GFP_KERNEL);
>>       }
>> @@ -489,8 +498,8 @@ static void uvcg_video_pump(struct work_struct *work)
>>    */
>>   int uvcg_video_enable(struct uvc_video *video, int enable)
>>   {
>> -    unsigned int i;
>>       int ret;
>> +    struct uvc_request *ureq;
>>
>>       if (video->ep == NULL) {
>>           uvcg_info(&video->uvc->func,
>> @@ -502,9 +511,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
>>           cancel_work_sync(&video->pump);
>>           uvcg_queue_cancel(&video->queue, 0);
>>
>> -        for (i = 0; i < video->uvc_num_requests; ++i)
>> -            if (video->ureq && video->ureq[i].req)
>> -                usb_ep_dequeue(video->ep, video->ureq[i].req);
>> +        list_for_each_entry(ureq, &video->ureqs, list) {
>> +            if (ureq->req)
>> +                usb_ep_dequeue(video->ep, ureq->req);
>> +        }
>>
>>           uvc_video_free_requests(video);
>>           uvcg_queue_enable(&video->queue, 0);
>> @@ -536,6 +546,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
>>    */
>>   int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
>>   {
>> +    INIT_LIST_HEAD(&video->ureqs);
>>       INIT_LIST_HEAD(&video->req_free);
>>       spin_lock_init(&video->req_lock);
>>       INIT_WORK(&video->pump, uvcg_video_pump);
>> --
>> 2.42.0.820.g83a721a137-goog

2023-10-30 20:57:09

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v9 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Thank you for taking a look Dan!

On 10/28/23 13:56, Dan Scally wrote:
> Hi Avichal
>
> On 27/10/2023 21:19, Avichal Rakesh wrote:
>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>> and deallocates them all when the video stream stops. This includes
>> de-allocating all the usb_requests associated with those uvc_requests.
>> This can lead to use-after-free issues if any of those de-allocated
>> usb_requests were still owned by the usb controller.
>>
>> This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
>> flag to uvc_video to track when frames and requests should be flowing.
>> When disabling the video stream, the flag is tripped and, instead
>> of de-allocating all uvc_requests and usb_requests, the gadget
>> driver only de-allocates those usb_requests that are currently
>> owned by it (as present in req_free). Other usb_requests are left
>> untouched until their completion handler is called which takes care
>> of freeing the usb_request and its corresponding uvc_request.
>>
>> Now that uvc_video does not depends on uvc->state, this patch removes
>> unnecessary upates to uvc->state that were made to accommodate uvc_video
>> logic. This should ensure that uvc gadget driver never accidentally
>> de-allocates a usb_request that it doesn't own.
>>
>> Link: https://lore.kernel.org/[email protected]
>> Suggested-by: Michael Grzeschik <[email protected]>
>> Reviewed-by: Michael Grzeschik <[email protected]>
>> Tested-by: Michael Grzeschik <[email protected]>
>> Signed-off-by: Avichal Rakesh <[email protected]>
>> ---
>> v1 -> v2: Rebased to ToT, and fixed deadlock reported in
>>            https://lore.kernel.org/all/[email protected]/
>> v2 -> v3: Fix email threading goof-up
>> v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
>>            as discussed in
>>            https://lore.kernel.org/[email protected]/
>> v4 -> v5: Address review comments. Add Reviewed-by & Tested-by.
>> v5 -> v6: Added another patch before this one to make uvcg_video_disable
>>            easier to review.
>> v6 -> v7: Fix warning reported in
>>            https://lore.kernel.org/[email protected]/
>> v7 -> v8: No change. Getting back in review queue
>> v8 -> v9: No change.
>>
>>   drivers/usb/gadget/function/uvc.h       |   1 +
>>   drivers/usb/gadget/function/uvc_v4l2.c  |  12 +--
>>   drivers/usb/gadget/function/uvc_video.c | 128 ++++++++++++++++++++----
>>   3 files changed, 111 insertions(+), 30 deletions(-)
>>
>> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
>> index 993694da0bbc..be0d012aa244 100644
>> --- a/drivers/usb/gadget/function/uvc.h
>> +++ b/drivers/usb/gadget/function/uvc.h
>> @@ -102,6 +102,7 @@ struct uvc_video {
>>       unsigned int uvc_num_requests;
>>
>>       /* Requests */
>> +    bool is_enabled; /* tracks whether video stream is enabled */
>>       unsigned int req_size;
>>       struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
>>       struct list_head req_free;
>> diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
>> index 904dd283cbf7..2f8634e05612 100644
>> --- a/drivers/usb/gadget/function/uvc_v4l2.c
>> +++ b/drivers/usb/gadget/function/uvc_v4l2.c
>> @@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
>>        * Complete the alternate setting selection setup phase now that
>>        * userspace is ready to provide video frames.
>>        */
>> -    uvc_function_setup_continue(uvc, 0);
>>       uvc->state = UVC_STATE_STREAMING;
>> +    uvc_function_setup_continue(uvc, 0);
>>
>>       return 0;
>>   }
>> @@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
>>       if (type != video->queue.queue.type)
>>           return -EINVAL;
>>
>> -    uvc->state = UVC_STATE_CONNECTED;
>>       ret = uvcg_video_disable(video);
>>       if (ret < 0)
>>           return ret;
>>
>> +    uvc->state = UVC_STATE_CONNECTED;
>>       uvc_function_setup_continue(uvc, 1);
>>       return 0;
>>   }
>
>
> I'm not sure I understand what these re-orderings are for...can you explain please?

This specific one was a leftover from testing, removed this hunk.
But the ones below are undoing the change in patch 1, which is
flawed in its use of uvc->state without any memory guarantees.

So from patch 1 to patch 4, we shuffle the code around a bit,
but this makes patch 1 somewhat complete and functional even if
patch 4 were to be reverted.

>
>> @@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
>>   static void uvc_v4l2_disable(struct uvc_device *uvc)
>>   {
>>       uvc_function_disconnect(uvc);
>> -    /*
>> -     * Drop uvc->state to CONNECTED if it was streaming before.
>> -     * This ensures that the usb_requests are no longer queued
>> -     * to the controller.
>> -     */
>> -    if (uvc->state == UVC_STATE_STREAMING)
>> -        uvc->state = UVC_STATE_CONNECTED;
>> -
>>       uvcg_video_disable(&uvc->video);
>>       uvcg_free_buffers(&uvc->video.queue);
>>       uvc->func_connected = false;
>> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
>> index 1081dd790fd6..8f330ce696ec 100644
>> --- a/drivers/usb/gadget/function/uvc_video.c
>> +++ b/drivers/usb/gadget/function/uvc_video.c
>> @@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
>>    * Request handling
>>    */
>>
>> +/*
>> + * Must be called with req_lock held as it modifies the list ureq is held in
>> + */
>
>
>
> This comment probably belongs in patch #2. And in that case, shouldn't uvc_video_free_requests() hold the lock in that patch?

Patch 2 doesn't change any existing locking semantics. The current
code does not enforce any locking on freeing the requests, and neither
does patch 2.

Patch 4 introduces another call site for uvc_video_free_request, so
some synchronization guarantees are needed (and hence the addition
of this comment).

As for uvc_video_free_requests not holding the lock, it is safe because
uvc_video_free_requests is only called if request initialization fails.
So uvc_video_free_requests should be the thread safe, as no other thread
is processing requests when it is called.

I did add a comment in uvcg_video_enable mentioning why it is safe to
not hold req_free even though it accesses request related fields.

Happy to add another comment to uvc_video_free_requests if that makes it
clearer!

>
>>   static void
>>   uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
>>   {
>> @@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
>>       struct uvc_request *ureq = req->context;
>>       struct uvc_video *video = ureq->video;
>>       struct uvc_video_queue *queue = &video->queue;
>> -    struct uvc_device *uvc = video->uvc;
>> +    struct uvc_buffer *last_buf = NULL;
>>       unsigned long flags;
>>
>> +    spin_lock_irqsave(&video->req_lock, flags);
>> +    if (!video->is_enabled) {
>> +        /*
>> +         * When is_enabled is false, uvc_video_disable ensures that
> s/uvc_video_disable/uvc_video_disable()

Done!

>> +         * in-flight uvc_buffers are returned, so we can safely
>> +         * call free_request without worrying about last_buf.
>> +         */
>> +        uvc_video_free_request(ureq, ep);
> Now I understand the conditional in this function in patch 2 :)
>> +        spin_unlock_irqrestore(&video->req_lock, flags);
>> +        return;
>> +    }
>> +
>> +    last_buf = ureq->last_buf;
>> +    ureq->last_buf = NULL;
>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>
>
> I'm not a huge fan of this locking, unlocking and relocking the same spinlock within the same function. Can we just hold the lock for the duration? if not, can there be an explanatory comment as to why?

I agree that this is a little unfortunate, and it'd be nice if we
only had a single driver level lock. However, as it stands, if
we hold req_lock for the entirety of completion handler, we risk
two things:

1. Adding dependencies between queue->irqlock and video->reqlock
2. Starving the video_pump thread.

As of this patch, uvc_video_complete follows the same pattern as
video_pump function:
1. Acquire req_lock
2. Fetch/Query usb_request
3. Drop req_lock

4. Acquire queue->irqlock
5. Buffer ops (encode/free/stop)
6. Drop queue->irqlock

7. Acquire req_lock
8. usb_request cleanup/handling
9. Drop req_lock

(7), (8), and (9) are optional for video_pump, while
(4), (5), and (6) are optional for uvc_video_complete.

We can short-circuit uvc_video_complete with only one lock
on the happy path, but this would have to be the flow for
non-happy paths unless we want to hold the two locks at
the same time (which isn't the worst idea, but comes with
its own set of concerns).


>> +
>>       switch (req->status) {
>>       case 0:
>>           break;
>> @@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
>>           uvcg_queue_cancel(queue, 0);
>>       }
>>
>> -    if (ureq->last_buf) {
>> -        uvcg_complete_buffer(&video->queue, ureq->last_buf);
>> -        ureq->last_buf = NULL;
>> +    if (last_buf) {
>> +        spin_lock_irqsave(&queue->irqlock, flags);
>> +        uvcg_complete_buffer(&video->queue, last_buf);
>> +        spin_unlock_irqrestore(&queue->irqlock, flags);
>
>
>
> I think it's right to take the irqlock here but it probably should have always been held, so this probably ought to go in its own commit with a Fixes:

The lock here wasn't required before, because uvcg_complete_buffer was
only ever called by the completion handler, which is synchronized by
the usb controller. This is the reason we never saw an issue despite
not holding the lock.

This patch introduces another call site in uvcg_video_disable, so to
protect memory consistency, we need to make sure calls to
uvcg_complete_buffer are synchronized on something other than
the usb controller.

>
>>       }
>>
>>       spin_lock_irqsave(&video->req_lock, flags);
>> -    list_add_tail(&req->list, &video->req_free);
>> -    spin_unlock_irqrestore(&video->req_lock, flags);
>> -
>> -    if (uvc->state == UVC_STATE_STREAMING)
>> +    /*
>> +     * Video stream might have been disabled while we were
>> +     * processing the current usb_request. So make sure
>> +     * we're still streaming before queueing the usb_request
>> +     * back to req_free
>> +     */
>> +    if (video->is_enabled) {
>> +        list_add_tail(&req->list, &video->req_free);
>>           queue_work(video->async_wq, &video->pump);
>> +    } else {
>> +        uvc_video_free_request(ureq, ep);
>> +    }
>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>   }
>>
>>   static int
>> @@ -393,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
>>       struct uvc_video_queue *queue = &video->queue;
>>       /* video->max_payload_size is only set when using bulk transfer */
>>       bool is_bulk = video->max_payload_size;
>> -    struct uvc_device *uvc = video->uvc;
>>       struct usb_request *req = NULL;
>>       struct uvc_buffer *buf;
>>       unsigned long flags;
>>       bool buf_done;
>>       int ret;
>>
>> -    while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
>> +    while (true) {
>> +        if (!video->ep->enabled)
>> +            return;
>> +
>>           /*
>> -         * Retrieve the first available USB request, protected by the
>> -         * request lock.
>> +         * Check is_enabled and retrieve the first available USB
>> +         * request, protected by the request lock.
>>            */
>>           spin_lock_irqsave(&video->req_lock, flags);
>> -        if (list_empty(&video->req_free)) {
>> +        if (!video->is_enabled || list_empty(&video->req_free)) {
>>               spin_unlock_irqrestore(&video->req_lock, flags);
>>               return;
>>           }
>> @@ -488,9 +518,11 @@ static void uvcg_video_pump(struct work_struct *work)
>>           return;
>>
>>       spin_lock_irqsave(&video->req_lock, flags);
>> -    list_add_tail(&req->list, &video->req_free);
>> +    if (video->is_enabled)
>> +        list_add_tail(&req->list, &video->req_free);
>> +    else
>> +        uvc_video_free_request(req->context, video->ep);
>>       spin_unlock_irqrestore(&video->req_lock, flags);
>> -    return;
>>   }
>>
>>   /*
>> @@ -499,7 +531,11 @@ static void uvcg_video_pump(struct work_struct *work)
>>   int
>>   uvcg_video_disable(struct uvc_video *video)
>>   {
>> -    struct uvc_request *ureq;
>> +    unsigned long flags;
>> +    struct list_head inflight_bufs;
>> +    struct usb_request *req, *temp;
>> +    struct uvc_buffer *buf, *btemp;
>> +    struct uvc_request *ureq, *utemp;
>>
>>       if (video->ep == NULL) {
>>           uvcg_info(&video->uvc->func,
>> @@ -507,15 +543,58 @@ uvcg_video_disable(struct uvc_video *video)
>>           return -ENODEV;
>>       }
>>
>> +    INIT_LIST_HEAD(&inflight_bufs);
>> +    spin_lock_irqsave(&video->req_lock, flags);
>> +    video->is_enabled = false;
>> +
>> +    /*
>> +     * Remove any in-flight buffers from the uvc_requests
>> +     * because we want to return them before cancelling the
>> +     * queue. This ensures that we aren't stuck waiting for
>> +     * all complete callbacks to come through before disabling
>> +     * vb2 queue.
>> +     */
>> +    list_for_each_entry(ureq, &video->ureqs, list) {
>> +        if (ureq->last_buf) {
>> +            list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
>> +            ureq->last_buf = NULL;
>> +        }
>> +    }
>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>> +
>>       cancel_work_sync(&video->pump);
>>       uvcg_queue_cancel(&video->queue, 0);
>>
>> -    list_for_each_entry(ureq, &video->ureqs, list) {
>> -        if (ureq->req)
>> -            usb_ep_dequeue(video->ep, ureq->req);
>> +    spin_lock_irqsave(&video->req_lock, flags);
>> +    /*
>> +     * Remove all uvc_reqeusts from ureqs with list_del_init
>> +     * This lets uvc_video_free_request correctly identify
>> +     * if the uvc_request is attached to a list or not when freeing
>> +     * memory.
>> +     */
>> +    list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
>> +        list_del_init(&ureq->list);
>> +
>> +    list_for_each_entry_safe(req, temp, &video->req_free, list) {
>> +        list_del(&req->list);
>> +        uvc_video_free_request(req->context, video->ep);
>>       }
>>
>> -    uvc_video_free_requests(video);
>> +    INIT_LIST_HEAD(&video->ureqs);
>> +    INIT_LIST_HEAD(&video->req_free);
>> +    video->req_size = 0;
>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>> +
>> +    /*
>> +     * Return all the video buffers before disabling the queue.
>> +     */
>> +    spin_lock_irqsave(&video->queue.irqlock, flags);
>> +    list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
>> +        list_del(&buf->queue);
>> +        uvcg_complete_buffer(&video->queue, buf);
>> +    }
>> +    spin_unlock_irqrestore(&video->queue.irqlock, flags);
>> +
>>       uvcg_queue_enable(&video->queue, 0);
>>       return 0;
>>   }
>> @@ -533,6 +612,14 @@ int uvcg_video_enable(struct uvc_video *video)
>>           return -ENODEV;
>>       }
>>
>> +    /*
>> +     * Safe to access request related fields without req_lock because
>> +     * this is the only thread currently active, and no other
>> +     * request handling thread will become active until this function
>> +     * returns.
>> +     */
>> +    video->is_enabled = true;
>> +
>>       if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
>>           return ret;
>>
>> @@ -558,6 +645,7 @@ int uvcg_video_enable(struct uvc_video *video)
>>    */
>>   int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
>>   {
>> +    video->is_enabled = false;
>>       INIT_LIST_HEAD(&video->ureqs);
>>       INIT_LIST_HEAD(&video->req_free);
>>       spin_lock_init(&video->req_lock);
>> --
>> 2.42.0.820.g83a721a137-goog

2023-11-01 11:06:44

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v10 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Morning Avichal

On 30/10/2023 20:22, Avichal Rakesh wrote:
> Currently, the uvc gadget driver allocates all uvc_requests as one array
> and deallocates them all when the video stream stops. This includes
> de-allocating all the usb_requests associated with those uvc_requests.
> This can lead to use-after-free issues if any of those de-allocated
> usb_requests were still owned by the usb controller.
>
> This patch is 1 of 2 patches addressing the use-after-free issue.
> Instead of bulk allocating all uvc_requests as an array, this patch
> allocates uvc_requests one at a time, which should allows for similar
> granularity when deallocating the uvc_requests. This patch has no
> functional changes other than allocating each uvc_request separately,
> and similarly freeing each of them separately.
>
> Link: https://lore.kernel.org/[email protected]
> Suggested-by: Michael Grzeschik <[email protected]>
> Reviewed-by: Michael Grzeschik <[email protected]>
> Tested-by: Michael Grzeschik <[email protected]>
> Signed-off-by: Avichal Rakesh <[email protected]>


Thanks for the update; this seems ok now:


Reviewed-by: Daniel Scally <[email protected]>

> ---
> v1 -> v2 : Rebased to ToT
> v2 -> v3 : Fix email threading goof-up
> v3 -> v4 : Address review comments & re-rebase to ToT
> v4 -> v5 : Address more review comments. Add Reviewed-by & Tested-by.
> v5 -> v6 : No change
> v6 -> v7 : No change
> v7 -> v8 : No change. Getting back in review queue
> v8 -> v9 : Address review comments.
> v9 -> v10: Address review comments; remove BUG_ON(&video->reqs);
> Rebase to ToT (usb-next)
>
> drivers/usb/gadget/function/uvc.h | 3 +-
> drivers/usb/gadget/function/uvc_video.c | 88 ++++++++++++++-----------
> 2 files changed, 51 insertions(+), 40 deletions(-)
>
> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
> index 989bc6b4e93d..993694da0bbc 100644
> --- a/drivers/usb/gadget/function/uvc.h
> +++ b/drivers/usb/gadget/function/uvc.h
> @@ -81,6 +81,7 @@ struct uvc_request {
> struct sg_table sgt;
> u8 header[UVCG_REQUEST_HEADER_LEN];
> struct uvc_buffer *last_buf;
> + struct list_head list;
> };
>
> struct uvc_video {
> @@ -102,7 +103,7 @@ struct uvc_video {
>
> /* Requests */
> unsigned int req_size;
> - struct uvc_request *ureq;
> + struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
> struct list_head req_free;
> spinlock_t req_lock;
>
> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
> index c334802ac0a4..1619f9664748 100644
> --- a/drivers/usb/gadget/function/uvc_video.c
> +++ b/drivers/usb/gadget/function/uvc_video.c
> @@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
> * Request handling
> */
>
> +static void
> +uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
> +{
> + sg_free_table(&ureq->sgt);
> + if (ureq->req && ep) {
> + usb_ep_free_request(ep, ureq->req);
> + ureq->req = NULL;
> + }
> +
> + kfree(ureq->req_buffer);
> + ureq->req_buffer = NULL;
> +
> + if (!list_empty(&ureq->list))
> + list_del_init(&ureq->list);
> +
> + kfree(ureq);
> +}
> +
> static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
> {
> int ret;
> @@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
> static int
> uvc_video_free_requests(struct uvc_video *video)
> {
> - unsigned int i;
> -
> - if (video->ureq) {
> - for (i = 0; i < video->uvc_num_requests; ++i) {
> - sg_free_table(&video->ureq[i].sgt);
> + struct uvc_request *ureq, *temp;
>
> - if (video->ureq[i].req) {
> - usb_ep_free_request(video->ep, video->ureq[i].req);
> - video->ureq[i].req = NULL;
> - }
> -
> - if (video->ureq[i].req_buffer) {
> - kfree(video->ureq[i].req_buffer);
> - video->ureq[i].req_buffer = NULL;
> - }
> - }
> -
> - kfree(video->ureq);
> - video->ureq = NULL;
> - }
> + list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
> + uvc_video_free_request(ureq, video->ep);
>
> + INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> video->req_size = 0;
> return 0;
> @@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
> static int
> uvc_video_alloc_requests(struct uvc_video *video)
> {
> + struct uvc_request *ureq;
> unsigned int req_size;
> unsigned int i;
> int ret = -ENOMEM;
> @@ -332,29 +336,33 @@ uvc_video_alloc_requests(struct uvc_video *video)
> * max_t(unsigned int, video->ep->maxburst, 1)
> * (video->ep->mult);
>
> - video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
> - if (video->ureq == NULL)
> - return -ENOMEM;
> + for (i = 0; i < video->uvc_num_requests; i++) {
> + ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
> + if (ureq == NULL)
> + goto error;
> +
> + INIT_LIST_HEAD(&ureq->list);
> +
> + list_add_tail(&ureq->list, &video->ureqs);
>
> - for (i = 0; i < video->uvc_num_requests; ++i) {
> - video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
> - if (video->ureq[i].req_buffer == NULL)
> + ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
> + if (ureq->req_buffer == NULL)
> goto error;
>
> - video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
> - if (video->ureq[i].req == NULL)
> + ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
> + if (ureq->req == NULL)
> goto error;
>
> - video->ureq[i].req->buf = video->ureq[i].req_buffer;
> - video->ureq[i].req->length = 0;
> - video->ureq[i].req->complete = uvc_video_complete;
> - video->ureq[i].req->context = &video->ureq[i];
> - video->ureq[i].video = video;
> - video->ureq[i].last_buf = NULL;
> + ureq->req->buf = ureq->req_buffer;
> + ureq->req->length = 0;
> + ureq->req->complete = uvc_video_complete;
> + ureq->req->context = ureq;
> + ureq->video = video;
> + ureq->last_buf = NULL;
>
> - list_add_tail(&video->ureq[i].req->list, &video->req_free);
> + list_add_tail(&ureq->req->list, &video->req_free);
> /* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
> - sg_alloc_table(&video->ureq[i].sgt,
> + sg_alloc_table(&ureq->sgt,
> DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
> PAGE_SIZE) + 2, GFP_KERNEL);
> }
> @@ -489,8 +497,8 @@ static void uvcg_video_pump(struct work_struct *work)
> */
> int uvcg_video_enable(struct uvc_video *video, int enable)
> {
> - unsigned int i;
> int ret;
> + struct uvc_request *ureq;
>
> if (video->ep == NULL) {
> uvcg_info(&video->uvc->func,
> @@ -502,9 +510,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> cancel_work_sync(&video->pump);
> uvcg_queue_cancel(&video->queue, 0);
>
> - for (i = 0; i < video->uvc_num_requests; ++i)
> - if (video->ureq && video->ureq[i].req)
> - usb_ep_dequeue(video->ep, video->ureq[i].req);
> + list_for_each_entry(ureq, &video->ureqs, list) {
> + if (ureq->req)
> + usb_ep_dequeue(video->ep, ureq->req);
> + }
>
> uvc_video_free_requests(video);
> uvcg_queue_enable(&video->queue, 0);
> @@ -536,6 +545,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
> */
> int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
> {
> + INIT_LIST_HEAD(&video->ureqs);
> INIT_LIST_HEAD(&video->req_free);
> spin_lock_init(&video->req_lock);
> INIT_WORK(&video->pump, uvcg_video_pump);
> --
> 2.42.0.820.g83a721a137-goog

2023-11-01 22:14:14

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v10 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time



On 11/1/23 04:06, Dan Scally wrote:
> Morning Avichal
>
> On 30/10/2023 20:22, Avichal Rakesh wrote:
>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>> and deallocates them all when the video stream stops. This includes
>> de-allocating all the usb_requests associated with those uvc_requests.
>> This can lead to use-after-free issues if any of those de-allocated
>> usb_requests were still owned by the usb controller.
>>
>> This patch is 1 of 2 patches addressing the use-after-free issue.
>> Instead of bulk allocating all uvc_requests as an array, this patch
>> allocates uvc_requests one at a time, which should allows for similar
>> granularity when deallocating the uvc_requests. This patch has no
>> functional changes other than allocating each uvc_request separately,
>> and similarly freeing each of them separately.
>>
>> Link: https://lore.kernel.org/[email protected]
>> Suggested-by: Michael Grzeschik <[email protected]>
>> Reviewed-by: Michael Grzeschik <[email protected]>
>> Tested-by: Michael Grzeschik <[email protected]>
>> Signed-off-by: Avichal Rakesh <[email protected]>
>
>
> Thanks for the update; this seems ok now:
>
>
> Reviewed-by: Daniel Scally <[email protected]>

Awesome, thank you! I'll add the Reviewed-by in the next patchset
(assuming you have more review comments on patch 4/4 v10).

Regards,
Avi.

>
>> ---
>> v1 -> v2 : Rebased to ToT
>> v2 -> v3 : Fix email threading goof-up
>> v3 -> v4 : Address review comments & re-rebase to ToT
>> v4 -> v5 : Address more review comments. Add Reviewed-by & Tested-by.
>> v5 -> v6 : No change
>> v6 -> v7 : No change
>> v7 -> v8 : No change. Getting back in review queue
>> v8 -> v9 : Address review comments.
>> v9 -> v10: Address review comments; remove BUG_ON(&video->reqs);
>>             Rebase to ToT (usb-next)
>>
>>   <snip>

2023-11-02 11:39:07

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v10 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time


On 01/11/2023 22:13, Avichal Rakesh wrote:
>
> On 11/1/23 04:06, Dan Scally wrote:
>> Morning Avichal
>>
>> On 30/10/2023 20:22, Avichal Rakesh wrote:
>>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>>> and deallocates them all when the video stream stops. This includes
>>> de-allocating all the usb_requests associated with those uvc_requests.
>>> This can lead to use-after-free issues if any of those de-allocated
>>> usb_requests were still owned by the usb controller.
>>>
>>> This patch is 1 of 2 patches addressing the use-after-free issue.
>>> Instead of bulk allocating all uvc_requests as an array, this patch
>>> allocates uvc_requests one at a time, which should allows for similar
>>> granularity when deallocating the uvc_requests. This patch has no
>>> functional changes other than allocating each uvc_request separately,
>>> and similarly freeing each of them separately.
>>>
>>> Link: https://lore.kernel.org/[email protected]
>>> Suggested-by: Michael Grzeschik <[email protected]>
>>> Reviewed-by: Michael Grzeschik <[email protected]>
>>> Tested-by: Michael Grzeschik <[email protected]>
>>> Signed-off-by: Avichal Rakesh <[email protected]>
>>
>> Thanks for the update; this seems ok now:
>>
>>
>> Reviewed-by: Daniel Scally <[email protected]>
> Awesome, thank you! I'll add the Reviewed-by in the next patchset
> (assuming you have more review comments on patch 4/4 v10).


Sorry yes - taking me a while to wrap my head around everything but I hope to be done shortly!

>
> Regards,
> Avi.
>
>>> ---
>>> v1 -> v2 : Rebased to ToT
>>> v2 -> v3 : Fix email threading goof-up
>>> v3 -> v4 : Address review comments & re-rebase to ToT
>>> v4 -> v5 : Address more review comments. Add Reviewed-by & Tested-by.
>>> v5 -> v6 : No change
>>> v6 -> v7 : No change
>>> v7 -> v8 : No change. Getting back in review queue
>>> v8 -> v9 : Address review comments.
>>> v9 -> v10: Address review comments; remove BUG_ON(&video->reqs);
>>>             Rebase to ToT (usb-next)
>>>
>>>   <snip>

2023-11-02 13:30:50

by Dan Scally

[permalink] [raw]
Subject: Re: [PATCH v9 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Hi Avichal

On 30/10/2023 20:56, Avichal Rakesh wrote:
> Thank you for taking a look Dan!
>
> On 10/28/23 13:56, Dan Scally wrote:
>> Hi Avichal
>>
>> On 27/10/2023 21:19, Avichal Rakesh wrote:
>>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>>> and deallocates them all when the video stream stops. This includes
>>> de-allocating all the usb_requests associated with those uvc_requests.
>>> This can lead to use-after-free issues if any of those de-allocated
>>> usb_requests were still owned by the usb controller.
>>>
>>> This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
>>> flag to uvc_video to track when frames and requests should be flowing.
>>> When disabling the video stream, the flag is tripped and, instead
>>> of de-allocating all uvc_requests and usb_requests, the gadget
>>> driver only de-allocates those usb_requests that are currently
>>> owned by it (as present in req_free). Other usb_requests are left
>>> untouched until their completion handler is called which takes care
>>> of freeing the usb_request and its corresponding uvc_request.
>>>
>>> Now that uvc_video does not depends on uvc->state, this patch removes
>>> unnecessary upates to uvc->state that were made to accommodate uvc_video
>>> logic. This should ensure that uvc gadget driver never accidentally
>>> de-allocates a usb_request that it doesn't own.
>>>
>>> Link: https://lore.kernel.org/[email protected]
>>> Suggested-by: Michael Grzeschik <[email protected]>
>>> Reviewed-by: Michael Grzeschik <[email protected]>
>>> Tested-by: Michael Grzeschik <[email protected]>
>>> Signed-off-by: Avichal Rakesh <[email protected]>
>>> ---
>>> v1 -> v2: Rebased to ToT, and fixed deadlock reported in
>>>            https://lore.kernel.org/all/[email protected]/
>>> v2 -> v3: Fix email threading goof-up
>>> v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
>>>            as discussed in
>>>            https://lore.kernel.org/[email protected]/
>>> v4 -> v5: Address review comments. Add Reviewed-by & Tested-by.
>>> v5 -> v6: Added another patch before this one to make uvcg_video_disable
>>>            easier to review.
>>> v6 -> v7: Fix warning reported in
>>>            https://lore.kernel.org/[email protected]/
>>> v7 -> v8: No change. Getting back in review queue
>>> v8 -> v9: No change.
>>>
>>>   drivers/usb/gadget/function/uvc.h       |   1 +
>>>   drivers/usb/gadget/function/uvc_v4l2.c  |  12 +--
>>>   drivers/usb/gadget/function/uvc_video.c | 128 ++++++++++++++++++++----
>>>   3 files changed, 111 insertions(+), 30 deletions(-)
>>>
>>> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
>>> index 993694da0bbc..be0d012aa244 100644
>>> --- a/drivers/usb/gadget/function/uvc.h
>>> +++ b/drivers/usb/gadget/function/uvc.h
>>> @@ -102,6 +102,7 @@ struct uvc_video {
>>>       unsigned int uvc_num_requests;
>>>
>>>       /* Requests */
>>> +    bool is_enabled; /* tracks whether video stream is enabled */
>>>       unsigned int req_size;
>>>       struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
>>>       struct list_head req_free;
>>> diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
>>> index 904dd283cbf7..2f8634e05612 100644
>>> --- a/drivers/usb/gadget/function/uvc_v4l2.c
>>> +++ b/drivers/usb/gadget/function/uvc_v4l2.c
>>> @@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
>>>        * Complete the alternate setting selection setup phase now that
>>>        * userspace is ready to provide video frames.
>>>        */
>>> -    uvc_function_setup_continue(uvc, 0);
>>>       uvc->state = UVC_STATE_STREAMING;
>>> +    uvc_function_setup_continue(uvc, 0);
>>>
>>>       return 0;
>>>   }
>>> @@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
>>>       if (type != video->queue.queue.type)
>>>           return -EINVAL;
>>>
>>> -    uvc->state = UVC_STATE_CONNECTED;
>>>       ret = uvcg_video_disable(video);
>>>       if (ret < 0)
>>>           return ret;
>>>
>>> +    uvc->state = UVC_STATE_CONNECTED;
>>>       uvc_function_setup_continue(uvc, 1);
>>>       return 0;
>>>   }
>>
>> I'm not sure I understand what these re-orderings are for...can you explain please?
> This specific one was a leftover from testing, removed this hunk.
> But the ones below are undoing the change in patch 1, which is
> flawed in its use of uvc->state without any memory guarantees.
>
> So from patch 1 to patch 4, we shuffle the code around a bit,
> but this makes patch 1 somewhat complete and functional even if
> patch 4 were to be reverted.


Okedokey - that's fine (and good)

>
>>> @@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
>>>   static void uvc_v4l2_disable(struct uvc_device *uvc)
>>>   {
>>>       uvc_function_disconnect(uvc);
>>> -    /*
>>> -     * Drop uvc->state to CONNECTED if it was streaming before.
>>> -     * This ensures that the usb_requests are no longer queued
>>> -     * to the controller.
>>> -     */
>>> -    if (uvc->state == UVC_STATE_STREAMING)
>>> -        uvc->state = UVC_STATE_CONNECTED;
>>> -
>>>       uvcg_video_disable(&uvc->video);
>>>       uvcg_free_buffers(&uvc->video.queue);
>>>       uvc->func_connected = false;
>>> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
>>> index 1081dd790fd6..8f330ce696ec 100644
>>> --- a/drivers/usb/gadget/function/uvc_video.c
>>> +++ b/drivers/usb/gadget/function/uvc_video.c
>>> @@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
>>>    * Request handling
>>>    */
>>>
>>> +/*
>>> + * Must be called with req_lock held as it modifies the list ureq is held in
>>> + */
>>
>>
>> This comment probably belongs in patch #2. And in that case, shouldn't uvc_video_free_requests() hold the lock in that patch?
> Patch 2 doesn't change any existing locking semantics. The current
> code does not enforce any locking on freeing the requests, and neither
> does patch 2.
>
> Patch 4 introduces another call site for uvc_video_free_request, so
> some synchronization guarantees are needed (and hence the addition
> of this comment).
>
> As for uvc_video_free_requests not holding the lock, it is safe because
> uvc_video_free_requests is only called if request initialization fails.
> So uvc_video_free_requests should be the thread safe, as no other thread
> is processing requests when it is called.
>
> I did add a comment in uvcg_video_enable mentioning why it is safe to
> not hold req_free even though it accesses request related fields.


I understand, but in that case I think the comment is a little confusing - it's not the fact that
the function modifies the list ureq is held in that requires it to be locked but that there're
potentially multiple threads doing so. Can we go for something like "Callers must take care to hold
req_lock when using this function outside of a single thread"? Feel free to re-word that however you
like, as long as it's clear that it's only necessary when multiple threads could be active.

>
> Happy to add another comment to uvc_video_free_requests if that makes it
> clearer!
>
>>>   static void
>>>   uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
>>>   {
>>> @@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
>>>       struct uvc_request *ureq = req->context;
>>>       struct uvc_video *video = ureq->video;
>>>       struct uvc_video_queue *queue = &video->queue;
>>> -    struct uvc_device *uvc = video->uvc;
>>> +    struct uvc_buffer *last_buf = NULL;


This initialisation's unnecessary since it's unconditionally set below.

>>>       unsigned long flags;
>>>
>>> +    spin_lock_irqsave(&video->req_lock, flags);
>>> +    if (!video->is_enabled) {
>>> +        /*
>>> +         * When is_enabled is false, uvc_video_disable ensures that
>> s/uvc_video_disable/uvc_video_disable()
> Done!
>
>>> +         * in-flight uvc_buffers are returned, so we can safely
>>> +         * call free_request without worrying about last_buf.
>>> +         */
>>> +        uvc_video_free_request(ureq, ep);
>> Now I understand the conditional in this function in patch 2 :)
>>> +        spin_unlock_irqrestore(&video->req_lock, flags);
>>> +        return;
>>> +    }
>>> +
>>> +    last_buf = ureq->last_buf;
>>> +    ureq->last_buf = NULL;
>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>
>> I'm not a huge fan of this locking, unlocking and relocking the same spinlock within the same function. Can we just hold the lock for the duration? if not, can there be an explanatory comment as to why?
> I agree that this is a little unfortunate, and it'd be nice if we
> only had a single driver level lock. However, as it stands, if
> we hold req_lock for the entirety of completion handler, we risk
> two things:
>
> 1. Adding dependencies between queue->irqlock and video->reqlock
> 2. Starving the video_pump thread.
>
> As of this patch, uvc_video_complete follows the same pattern as
> video_pump function:
> 1. Acquire req_lock
> 2. Fetch/Query usb_request
> 3. Drop req_lock
>
> 4. Acquire queue->irqlock
> 5. Buffer ops (encode/free/stop)
> 6. Drop queue->irqlock
>
> 7. Acquire req_lock
> 8. usb_request cleanup/handling
> 9. Drop req_lock
>
> (7), (8), and (9) are optional for video_pump, while
> (4), (5), and (6) are optional for uvc_video_complete.
>
> We can short-circuit uvc_video_complete with only one lock
> on the happy path, but this would have to be the flow for
> non-happy paths unless we want to hold the two locks at
> the same time (which isn't the worst idea, but comes with
> its own set of concerns).
>

Yeah. Sorry - I had missed that uvcg_video_pump() follows the same pattern too. Alright, I think
leave this as-is.

>>> +
>>>       switch (req->status) {
>>>       case 0:
>>>           break;
>>> @@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
>>>           uvcg_queue_cancel(queue, 0);
>>>       }
>>>
>>> -    if (ureq->last_buf) {
>>> -        uvcg_complete_buffer(&video->queue, ureq->last_buf);
>>> -        ureq->last_buf = NULL;
>>> +    if (last_buf) {
>>> +        spin_lock_irqsave(&queue->irqlock, flags);
>>> +        uvcg_complete_buffer(&video->queue, last_buf);
>>> +        spin_unlock_irqrestore(&queue->irqlock, flags);
>>
>>
>> I think it's right to take the irqlock here but it probably should have always been held, so this probably ought to go in its own commit with a Fixes:
> The lock here wasn't required before, because uvcg_complete_buffer was
> only ever called by the completion handler, which is synchronized by
> the usb controller. This is the reason we never saw an issue despite
> not holding the lock.


Ah - I was misled by the "called with &queue_irqlock held..." comment on uvc_complete_buffer()...I
assumed that meant "you must call this function with &queue->irqlock held", but that turns out to be
something of a hangover - originally it was placed there by 95faf82bd3ea6 because the callers of (at
the time uvc_queue_next_buffer(), later renamed to) uvcg_complete_buffer() already held the irqlock.
Clearly it needed locking then, but the function at the time manipulated queue->irqqueue and now it
no longer does - that part has been stripped out to encode_bulk/encode_isoc/encode_isoc_sg(). So,
probably that comment ought to have been removed at some point.

> This patch introduces another call site in uvcg_video_disable, so to
> protect memory consistency, we need to make sure calls to
> uvcg_complete_buffer are synchronized on something other than
> the usb controller.

If it's simply to prevent double calling uvcg_complete_buffer() for a buffer, is holding the irqlock
necessary? Both uvc_video_complete() and uvc_video_disable() conditionally call
uvcg_complete_buffer() based on whether ureq->last_buf is set or not, and both functions assess that
whilst holding req_lock, so unless I'm missing something that situation is already guarded against.


>
>>>       }
>>>
>>>       spin_lock_irqsave(&video->req_lock, flags);
>>> -    list_add_tail(&req->list, &video->req_free);
>>> -    spin_unlock_irqrestore(&video->req_lock, flags);
>>> -
>>> -    if (uvc->state == UVC_STATE_STREAMING)
>>> +    /*
>>> +     * Video stream might have been disabled while we were
>>> +     * processing the current usb_request. So make sure
>>> +     * we're still streaming before queueing the usb_request
>>> +     * back to req_free
>>> +     */
>>> +    if (video->is_enabled) {
>>> +        list_add_tail(&req->list, &video->req_free);
>>>           queue_work(video->async_wq, &video->pump);
>>> +    } else {
>>> +        uvc_video_free_request(ureq, ep);
>>> +    }
>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>>   }
>>>
>>>   static int
>>> @@ -393,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
>>>       struct uvc_video_queue *queue = &video->queue;
>>>       /* video->max_payload_size is only set when using bulk transfer */
>>>       bool is_bulk = video->max_payload_size;
>>> -    struct uvc_device *uvc = video->uvc;
>>>       struct usb_request *req = NULL;
>>>       struct uvc_buffer *buf;
>>>       unsigned long flags;
>>>       bool buf_done;
>>>       int ret;
>>>
>>> -    while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
>>> +    while (true) {
>>> +        if (!video->ep->enabled)
>>> +            return;
>>> +
>>>           /*
>>> -         * Retrieve the first available USB request, protected by the
>>> -         * request lock.
>>> +         * Check is_enabled and retrieve the first available USB
>>> +         * request, protected by the request lock.
>>>            */
>>>           spin_lock_irqsave(&video->req_lock, flags);
>>> -        if (list_empty(&video->req_free)) {
>>> +        if (!video->is_enabled || list_empty(&video->req_free)) {
>>>               spin_unlock_irqrestore(&video->req_lock, flags);
>>>               return;
>>>           }
>>> @@ -488,9 +518,11 @@ static void uvcg_video_pump(struct work_struct *work)
>>>           return;
>>>
>>>       spin_lock_irqsave(&video->req_lock, flags);
>>> -    list_add_tail(&req->list, &video->req_free);
>>> +    if (video->is_enabled)
>>> +        list_add_tail(&req->list, &video->req_free);
>>> +    else
>>> +        uvc_video_free_request(req->context, video->ep);
>>>       spin_unlock_irqrestore(&video->req_lock, flags);
>>> -    return;
>>>   }
>>>
>>>   /*
>>> @@ -499,7 +531,11 @@ static void uvcg_video_pump(struct work_struct *work)
>>>   int
>>>   uvcg_video_disable(struct uvc_video *video)
>>>   {
>>> -    struct uvc_request *ureq;
>>> +    unsigned long flags;
>>> +    struct list_head inflight_bufs;
>>> +    struct usb_request *req, *temp;
>>> +    struct uvc_buffer *buf, *btemp;
>>> +    struct uvc_request *ureq, *utemp;
>>>
>>>       if (video->ep == NULL) {
>>>           uvcg_info(&video->uvc->func,
>>> @@ -507,15 +543,58 @@ uvcg_video_disable(struct uvc_video *video)
>>>           return -ENODEV;
>>>       }
>>>
>>> +    INIT_LIST_HEAD(&inflight_bufs);
>>> +    spin_lock_irqsave(&video->req_lock, flags);
>>> +    video->is_enabled = false;
>>> +
>>> +    /*
>>> +     * Remove any in-flight buffers from the uvc_requests
>>> +     * because we want to return them before cancelling the
>>> +     * queue. This ensures that we aren't stuck waiting for
>>> +     * all complete callbacks to come through before disabling
>>> +     * vb2 queue.
>>> +     */
>>> +    list_for_each_entry(ureq, &video->ureqs, list) {
>>> +        if (ureq->last_buf) {
>>> +            list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
>>> +            ureq->last_buf = NULL;
>>> +        }
>>> +    }
>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>> +
>>>       cancel_work_sync(&video->pump);
>>>       uvcg_queue_cancel(&video->queue, 0);
>>>
>>> -    list_for_each_entry(ureq, &video->ureqs, list) {
>>> -        if (ureq->req)
>>> -            usb_ep_dequeue(video->ep, ureq->req);
>>> +    spin_lock_irqsave(&video->req_lock, flags);
>>> +    /*
>>> +     * Remove all uvc_reqeusts from ureqs with list_del_init
s/uvc_reqeusts/uvc_requests
>>> +     * This lets uvc_video_free_request correctly identify
>>> +     * if the uvc_request is attached to a list or not when freeing
>>> +     * memory.
>>> +     */
>>> +    list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
>>> +        list_del_init(&ureq->list);
>>> +
>>> +    list_for_each_entry_safe(req, temp, &video->req_free, list) {
>>> +        list_del(&req->list);
>>> +        uvc_video_free_request(req->context, video->ep);
>>>       }
>>>
>>> -    uvc_video_free_requests(video);
>>> +    INIT_LIST_HEAD(&video->ureqs);
>>> +    INIT_LIST_HEAD(&video->req_free);
>>> +    video->req_size = 0;
>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>> +
>>> +    /*
>>> +     * Return all the video buffers before disabling the queue.
>>> +     */
>>> +    spin_lock_irqsave(&video->queue.irqlock, flags);
>>> +    list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
>>> +        list_del(&buf->queue);
>>> +        uvcg_complete_buffer(&video->queue, buf);
>>> +    }
>>> +    spin_unlock_irqrestore(&video->queue.irqlock, flags);
>>> +
>>>       uvcg_queue_enable(&video->queue, 0);
>>>       return 0;
>>>   }
>>> @@ -533,6 +612,14 @@ int uvcg_video_enable(struct uvc_video *video)
>>>           return -ENODEV;
>>>       }
>>>
>>> +    /*
>>> +     * Safe to access request related fields without req_lock because
>>> +     * this is the only thread currently active, and no other
>>> +     * request handling thread will become active until this function
>>> +     * returns.
>>> +     */
>>> +    video->is_enabled = true;
>>> +
>>>       if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
>>>           return ret;
>>>
>>> @@ -558,6 +645,7 @@ int uvcg_video_enable(struct uvc_video *video)
>>>    */
>>>   int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
>>>   {
>>> +    video->is_enabled = false;
>>>       INIT_LIST_HEAD(&video->ureqs);
>>>       INIT_LIST_HEAD(&video->req_free);
>>>       spin_lock_init(&video->req_lock);
>>> --
>>> 2.42.0.820.g83a721a137-goog

2023-11-02 20:19:49

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v11 1/4] usb: gadget: uvc: prevent use of disabled endpoint

Currently the set_alt callback immediately disables the endpoint and queues
the v4l2 streamoff event. However, as the streamoff event is processed
asynchronously, it is possible that the video_pump thread attempts to queue
requests to an already disabled endpoint.

This change moves disabling usb endpoint to the end of streamoff event
callback. As the endpoint's state can no longer be used, video_pump is
now guarded by uvc->state as well. To be consistent with the actual
streaming state, uvc->state is now toggled between CONNECTED and STREAMING
from the v4l2 event callback only.

Link: https://lore.kernel.org/[email protected]/
Link: https://lore.kernel.org/[email protected]/
Reviewed-by: Daniel Scally <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2 : Rebased to ToT and reworded commit message.
v2 -> v3 : Fix email threading goof-up
v3 -> v4 : Address review comments & re-rebase to ToT
v4 -> v5 : Add Reviewed-by & Tested-by
v5 -> v6 : No change
v6 -> v7 : No change
v7 -> v8 : No change. Getting back in review queue
v8 -> v9 : Fix typo. No functional change.
v9 -> v10 : Rebase to ToT (usb-next)
v10 -> v11 : No change

drivers/usb/gadget/function/f_uvc.c | 11 +++++------
drivers/usb/gadget/function/f_uvc.h | 2 +-
drivers/usb/gadget/function/uvc.h | 2 +-
drivers/usb/gadget/function/uvc_v4l2.c | 20 +++++++++++++++++---
drivers/usb/gadget/function/uvc_video.c | 3 ++-
5 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index 786379f1b7b7..77999ed53d33 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -263,10 +263,13 @@ uvc_function_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
return 0;
}

-void uvc_function_setup_continue(struct uvc_device *uvc)
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep)
{
struct usb_composite_dev *cdev = uvc->func.config->cdev;

+ if (disable_ep && uvc->video.ep)
+ usb_ep_disable(uvc->video.ep);
+
usb_composite_setup_continue(cdev);
}

@@ -337,15 +340,11 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
if (uvc->state != UVC_STATE_STREAMING)
return 0;

- if (uvc->video.ep)
- usb_ep_disable(uvc->video.ep);
-
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMOFF;
v4l2_event_queue(&uvc->vdev, &v4l2_event);

- uvc->state = UVC_STATE_CONNECTED;
- return 0;
+ return USB_GADGET_DELAYED_STATUS;

case 1:
if (uvc->state != UVC_STATE_CONNECTED)
diff --git a/drivers/usb/gadget/function/f_uvc.h b/drivers/usb/gadget/function/f_uvc.h
index 1db972d4beeb..083aef0c65c6 100644
--- a/drivers/usb/gadget/function/f_uvc.h
+++ b/drivers/usb/gadget/function/f_uvc.h
@@ -11,7 +11,7 @@

struct uvc_device;

-void uvc_function_setup_continue(struct uvc_device *uvc);
+void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);

void uvc_function_connect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 6751de8b63ad..989bc6b4e93d 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -177,7 +177,7 @@ struct uvc_file_handle {
* Functions
*/

-extern void uvc_function_setup_continue(struct uvc_device *uvc);
+extern void uvc_function_setup_continue(struct uvc_device *uvc, int disable_ep);
extern void uvc_function_connect(struct uvc_device *uvc);
extern void uvc_function_disconnect(struct uvc_device *uvc);

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 3f0a9795c0d4..7cb8d027ff0c 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -451,7 +451,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
- uvc_function_setup_continue(uvc);
+ uvc_function_setup_continue(uvc, 0);
uvc->state = UVC_STATE_STREAMING;

return 0;
@@ -463,11 +463,18 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
struct video_device *vdev = video_devdata(file);
struct uvc_device *uvc = video_get_drvdata(vdev);
struct uvc_video *video = &uvc->video;
+ int ret = 0;

if (type != video->queue.queue.type)
return -EINVAL;

- return uvcg_video_enable(video, 0);
+ uvc->state = UVC_STATE_CONNECTED;
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0)
+ return ret;
+
+ uvc_function_setup_continue(uvc, 1);
+ return 0;
}

static int
@@ -500,6 +507,14 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
+ /*
+ * Drop uvc->state to CONNECTED if it was streaming before.
+ * This ensures that the usb_requests are no longer queued
+ * to the controller.
+ */
+ if (uvc->state == UVC_STATE_STREAMING)
+ uvc->state = UVC_STATE_CONNECTED;
+
uvcg_video_enable(&uvc->video, 0);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
@@ -647,4 +662,3 @@ const struct v4l2_file_operations uvc_v4l2_fops = {
.get_unmapped_area = uvcg_v4l2_get_unmapped_area,
#endif
};
-
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 91af3b1ef0d4..c334802ac0a4 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -384,13 +384,14 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
+ struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (video->ep->enabled) {
+ while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
/*
* Retrieve the first available USB request, protected by the
* request lock.
--
2.42.0.869.gea05f2083d-goog

2023-11-02 20:19:57

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v11 2/4] usb: gadget: uvc: Allocate uvc_requests one at a time

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This patch is 1 of 2 patches addressing the use-after-free issue.
Instead of bulk allocating all uvc_requests as an array, this patch
allocates uvc_requests one at a time, which should allows for similar
granularity when deallocating the uvc_requests. This patch has no
functional changes other than allocating each uvc_request separately,
and similarly freeing each of them separately.

Link: https://lore.kernel.org/[email protected]
Reviewed-by: Daniel Scally <[email protected]>
Reviewed-by: Michael Grzeschik <[email protected]>
Suggested-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2 : Rebased to ToT
v2 -> v3 : Fix email threading goof-up
v3 -> v4 : Address review comments & re-rebase to ToT
v4 -> v5 : Address more review comments. Add Reviewed-by & Tested-by.
v5 -> v6 : No change
v6 -> v7 : No change
v7 -> v8 : No change. Getting back in review queue
v8 -> v9 : Address review comments.
v9 -> v10 : Address review comments; remove BUG_ON(&video->reqs);
Rebase to ToT (usb-next)
v10 -> v11 : Add Reviewed-by

drivers/usb/gadget/function/uvc.h | 3 +-
drivers/usb/gadget/function/uvc_video.c | 88 ++++++++++++++-----------
2 files changed, 51 insertions(+), 40 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 989bc6b4e93d..993694da0bbc 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -81,6 +81,7 @@ struct uvc_request {
struct sg_table sgt;
u8 header[UVCG_REQUEST_HEADER_LEN];
struct uvc_buffer *last_buf;
+ struct list_head list;
};

struct uvc_video {
@@ -102,7 +103,7 @@ struct uvc_video {

/* Requests */
unsigned int req_size;
- struct uvc_request *ureq;
+ struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
spinlock_t req_lock;

diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c334802ac0a4..1619f9664748 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,24 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+static void
+uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
+{
+ sg_free_table(&ureq->sgt);
+ if (ureq->req && ep) {
+ usb_ep_free_request(ep, ureq->req);
+ ureq->req = NULL;
+ }
+
+ kfree(ureq->req_buffer);
+ ureq->req_buffer = NULL;
+
+ if (!list_empty(&ureq->list))
+ list_del_init(&ureq->list);
+
+ kfree(ureq);
+}
+
static int uvcg_video_ep_queue(struct uvc_video *video, struct usb_request *req)
{
int ret;
@@ -293,27 +311,12 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
static int
uvc_video_free_requests(struct uvc_video *video)
{
- unsigned int i;
-
- if (video->ureq) {
- for (i = 0; i < video->uvc_num_requests; ++i) {
- sg_free_table(&video->ureq[i].sgt);
+ struct uvc_request *ureq, *temp;

- if (video->ureq[i].req) {
- usb_ep_free_request(video->ep, video->ureq[i].req);
- video->ureq[i].req = NULL;
- }
-
- if (video->ureq[i].req_buffer) {
- kfree(video->ureq[i].req_buffer);
- video->ureq[i].req_buffer = NULL;
- }
- }
-
- kfree(video->ureq);
- video->ureq = NULL;
- }
+ list_for_each_entry_safe(ureq, temp, &video->ureqs, list)
+ uvc_video_free_request(ureq, video->ep);

+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
video->req_size = 0;
return 0;
@@ -322,6 +325,7 @@ uvc_video_free_requests(struct uvc_video *video)
static int
uvc_video_alloc_requests(struct uvc_video *video)
{
+ struct uvc_request *ureq;
unsigned int req_size;
unsigned int i;
int ret = -ENOMEM;
@@ -332,29 +336,33 @@ uvc_video_alloc_requests(struct uvc_video *video)
* max_t(unsigned int, video->ep->maxburst, 1)
* (video->ep->mult);

- video->ureq = kcalloc(video->uvc_num_requests, sizeof(struct uvc_request), GFP_KERNEL);
- if (video->ureq == NULL)
- return -ENOMEM;
+ for (i = 0; i < video->uvc_num_requests; i++) {
+ ureq = kzalloc(sizeof(struct uvc_request), GFP_KERNEL);
+ if (ureq == NULL)
+ goto error;
+
+ INIT_LIST_HEAD(&ureq->list);
+
+ list_add_tail(&ureq->list, &video->ureqs);

- for (i = 0; i < video->uvc_num_requests; ++i) {
- video->ureq[i].req_buffer = kmalloc(req_size, GFP_KERNEL);
- if (video->ureq[i].req_buffer == NULL)
+ ureq->req_buffer = kmalloc(req_size, GFP_KERNEL);
+ if (ureq->req_buffer == NULL)
goto error;

- video->ureq[i].req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
- if (video->ureq[i].req == NULL)
+ ureq->req = usb_ep_alloc_request(video->ep, GFP_KERNEL);
+ if (ureq->req == NULL)
goto error;

- video->ureq[i].req->buf = video->ureq[i].req_buffer;
- video->ureq[i].req->length = 0;
- video->ureq[i].req->complete = uvc_video_complete;
- video->ureq[i].req->context = &video->ureq[i];
- video->ureq[i].video = video;
- video->ureq[i].last_buf = NULL;
+ ureq->req->buf = ureq->req_buffer;
+ ureq->req->length = 0;
+ ureq->req->complete = uvc_video_complete;
+ ureq->req->context = ureq;
+ ureq->video = video;
+ ureq->last_buf = NULL;

- list_add_tail(&video->ureq[i].req->list, &video->req_free);
+ list_add_tail(&ureq->req->list, &video->req_free);
/* req_size/PAGE_SIZE + 1 for overruns and + 1 for header */
- sg_alloc_table(&video->ureq[i].sgt,
+ sg_alloc_table(&ureq->sgt,
DIV_ROUND_UP(req_size - UVCG_REQUEST_HEADER_LEN,
PAGE_SIZE) + 2, GFP_KERNEL);
}
@@ -489,8 +497,8 @@ static void uvcg_video_pump(struct work_struct *work)
*/
int uvcg_video_enable(struct uvc_video *video, int enable)
{
- unsigned int i;
int ret;
+ struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -502,9 +510,10 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- for (i = 0; i < video->uvc_num_requests; ++i)
- if (video->ureq && video->ureq[i].req)
- usb_ep_dequeue(video->ep, video->ureq[i].req);
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

uvc_video_free_requests(video);
uvcg_queue_enable(&video->queue, 0);
@@ -536,6 +545,7 @@ int uvcg_video_enable(struct uvc_video *video, int enable)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
INIT_WORK(&video->pump, uvcg_video_pump);
--
2.42.0.869.gea05f2083d-goog

2023-11-02 20:20:03

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v11 3/4] usb: gadget: uvc: move video disable logic to its own function

This patch refactors the video disable logic in uvcg_video_enable
into its own separate function 'uvcg_video_disable'. This function
is now used anywhere uvcg_video_enable(video, 0) was used.

Reviewed-by: Daniel Scally <[email protected]>
Suggested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
XX -> v6 : Introduced this patch to make the next one easier to review
v6 -> v7 : Add Suggested-by
v7 -> v8 : No change. Getting back in review queue
v8 -> v9 : Call uvcg_video_disable directly instead of uvcg_video_enable(video, 0)
v9 -> v10 : Rebase to ToT (usb-next)
v10 -> v11 : No change

drivers/usb/gadget/function/uvc_v4l2.c | 6 ++--
drivers/usb/gadget/function/uvc_video.c | 40 ++++++++++++++++---------
drivers/usb/gadget/function/uvc_video.h | 3 +-
3 files changed, 31 insertions(+), 18 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 7cb8d027ff0c..904dd283cbf7 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -443,7 +443,7 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
return -EINVAL;

/* Enable UVC video. */
- ret = uvcg_video_enable(video, 1);
+ ret = uvcg_video_enable(video);
if (ret < 0)
return ret;

@@ -469,7 +469,7 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
return -EINVAL;

uvc->state = UVC_STATE_CONNECTED;
- ret = uvcg_video_enable(video, 0);
+ ret = uvcg_video_disable(video);
if (ret < 0)
return ret;

@@ -515,7 +515,7 @@ static void uvc_v4l2_disable(struct uvc_device *uvc)
if (uvc->state == UVC_STATE_STREAMING)
uvc->state = UVC_STATE_CONNECTED;

- uvcg_video_enable(&uvc->video, 0);
+ uvcg_video_disable(&uvc->video);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
wake_up_interruptible(&uvc->func_connected_queue);
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 1619f9664748..c3e8c48f46a9 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -493,31 +493,43 @@ static void uvcg_video_pump(struct work_struct *work)
}

/*
- * Enable or disable the video stream.
+ * Disable the video stream
*/
-int uvcg_video_enable(struct uvc_video *video, int enable)
+int
+uvcg_video_disable(struct uvc_video *video)
{
- int ret;
struct uvc_request *ureq;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
- "Video enable failed, device is uninitialized.\n");
+ "Video disable failed, device is uninitialized.\n");
return -ENODEV;
}

- if (!enable) {
- cancel_work_sync(&video->pump);
- uvcg_queue_cancel(&video->queue, 0);
+ cancel_work_sync(&video->pump);
+ uvcg_queue_cancel(&video->queue, 0);

- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
- }
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->req)
+ usb_ep_dequeue(video->ep, ureq->req);
+ }

- uvc_video_free_requests(video);
- uvcg_queue_enable(&video->queue, 0);
- return 0;
+ uvc_video_free_requests(video);
+ uvcg_queue_enable(&video->queue, 0);
+ return 0;
+}
+
+/*
+ * Enable the video stream.
+ */
+int uvcg_video_enable(struct uvc_video *video)
+{
+ int ret;
+
+ if (video->ep == NULL) {
+ uvcg_info(&video->uvc->func,
+ "Video enable failed, device is uninitialized.\n");
+ return -ENODEV;
}

if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
diff --git a/drivers/usb/gadget/function/uvc_video.h b/drivers/usb/gadget/function/uvc_video.h
index 03adeefa343b..8ef6259741f1 100644
--- a/drivers/usb/gadget/function/uvc_video.h
+++ b/drivers/usb/gadget/function/uvc_video.h
@@ -14,7 +14,8 @@

struct uvc_video;

-int uvcg_video_enable(struct uvc_video *video, int enable);
+int uvcg_video_enable(struct uvc_video *video);
+int uvcg_video_disable(struct uvc_video *video);

int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc);

--
2.42.0.869.gea05f2083d-goog

2023-11-02 20:20:06

by Avichal Rakesh

[permalink] [raw]
Subject: [PATCH v11 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Currently, the uvc gadget driver allocates all uvc_requests as one array
and deallocates them all when the video stream stops. This includes
de-allocating all the usb_requests associated with those uvc_requests.
This can lead to use-after-free issues if any of those de-allocated
usb_requests were still owned by the usb controller.

This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
flag to uvc_video to track when frames and requests should be flowing.
When disabling the video stream, the flag is tripped and, instead
of de-allocating all uvc_requests and usb_requests, the gadget
driver only de-allocates those usb_requests that are currently
owned by it (as present in req_free). Other usb_requests are left
untouched until their completion handler is called which takes care
of freeing the usb_request and its corresponding uvc_request.

Now that uvc_video does not depends on uvc->state, this patch removes
unnecessary upates to uvc->state that were made to accommodate uvc_video
logic. This should ensure that uvc gadget driver never accidentally
de-allocates a usb_request that it doesn't own.

Link: https://lore.kernel.org/[email protected]
Reviewed-by: Michael Grzeschik <[email protected]>
Suggested-by: Michael Grzeschik <[email protected]>
Tested-by: Michael Grzeschik <[email protected]>
Signed-off-by: Avichal Rakesh <[email protected]>
---
v1 -> v2 : Rebased to ToT, and fixed deadlock reported in
https://lore.kernel.org/all/[email protected]/
v2 -> v3 : Fix email threading goof-up
v3 -> v4 : re-rebase to ToT & moved to a uvc_video level lock
as discussed in
https://lore.kernel.org/[email protected]/
v4 -> v5 : Address review comments. Add Reviewed-by & Tested-by.
v5 -> v6 : Added another patch before this one to make uvcg_video_disable
easier to review.
v6 -> v7 : Fix warning reported in
https://lore.kernel.org/[email protected]/
v7 -> v8 : No change. Getting back in review queue
v8 -> v9 : No change.
v9 -> v10 : Address review comments. Rebase to ToT (usb-next)
v10 -> v11 : Address review comments

drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_v4l2.c | 10 +-
drivers/usb/gadget/function/uvc_video.c | 130 ++++++++++++++++++++----
3 files changed, 112 insertions(+), 29 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 993694da0bbc..be0d012aa244 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -102,6 +102,7 @@ struct uvc_video {
unsigned int uvc_num_requests;

/* Requests */
+ bool is_enabled; /* tracks whether video stream is enabled */
unsigned int req_size;
struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
struct list_head req_free;
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 904dd283cbf7..c7e5fa4f29e0 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
if (type != video->queue.queue.type)
return -EINVAL;

- uvc->state = UVC_STATE_CONNECTED;
ret = uvcg_video_disable(video);
if (ret < 0)
return ret;

+ uvc->state = UVC_STATE_CONNECTED;
uvc_function_setup_continue(uvc, 1);
return 0;
}
@@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
- /*
- * Drop uvc->state to CONNECTED if it was streaming before.
- * This ensures that the usb_requests are no longer queued
- * to the controller.
- */
- if (uvc->state == UVC_STATE_STREAMING)
- uvc->state = UVC_STATE_CONNECTED;
-
uvcg_video_disable(&uvc->video);
uvcg_free_buffers(&uvc->video.queue);
uvc->func_connected = false;
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index c3e8c48f46a9..164bdeb7f2a9 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -227,6 +227,10 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
* Request handling
*/

+/*
+ * Callers must take care to hold req_lock when this function may be called
+ * from multiple threads. For example, when frames are streaming to the host.
+ */
static void
uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
{
@@ -271,9 +275,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
struct uvc_request *ureq = req->context;
struct uvc_video *video = ureq->video;
struct uvc_video_queue *queue = &video->queue;
- struct uvc_device *uvc = video->uvc;
+ struct uvc_buffer *last_buf;
unsigned long flags;

+ spin_lock_irqsave(&video->req_lock, flags);
+ if (!video->is_enabled) {
+ /*
+ * When is_enabled is false, uvcg_video_disable() ensures
+ * that in-flight uvc_buffers are returned, so we can
+ * safely call free_request without worrying about
+ * last_buf.
+ */
+ uvc_video_free_request(ureq, ep);
+ spin_unlock_irqrestore(&video->req_lock, flags);
+ return;
+ }
+
+ last_buf = ureq->last_buf;
+ ureq->last_buf = NULL;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
switch (req->status) {
case 0:
break;
@@ -295,17 +316,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
uvcg_queue_cancel(queue, 0);
}

- if (ureq->last_buf) {
- uvcg_complete_buffer(&video->queue, ureq->last_buf);
- ureq->last_buf = NULL;
+ if (last_buf) {
+ spin_lock_irqsave(&queue->irqlock, flags);
+ uvcg_complete_buffer(queue, last_buf);
+ spin_unlock_irqrestore(&queue->irqlock, flags);
}

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
- spin_unlock_irqrestore(&video->req_lock, flags);
-
- if (uvc->state == UVC_STATE_STREAMING)
+ /*
+ * Video stream might have been disabled while we were
+ * processing the current usb_request. So make sure
+ * we're still streaming before queueing the usb_request
+ * back to req_free
+ */
+ if (video->is_enabled) {
+ list_add_tail(&req->list, &video->req_free);
queue_work(video->async_wq, &video->pump);
+ } else {
+ uvc_video_free_request(ureq, ep);
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
}

static int
@@ -392,20 +422,22 @@ static void uvcg_video_pump(struct work_struct *work)
struct uvc_video_queue *queue = &video->queue;
/* video->max_payload_size is only set when using bulk transfer */
bool is_bulk = video->max_payload_size;
- struct uvc_device *uvc = video->uvc;
struct usb_request *req = NULL;
struct uvc_buffer *buf;
unsigned long flags;
bool buf_done;
int ret;

- while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
+ while (true) {
+ if (!video->ep->enabled)
+ return;
+
/*
- * Retrieve the first available USB request, protected by the
- * request lock.
+ * Check is_enabled and retrieve the first available USB
+ * request, protected by the request lock.
*/
spin_lock_irqsave(&video->req_lock, flags);
- if (list_empty(&video->req_free)) {
+ if (!video->is_enabled || list_empty(&video->req_free)) {
spin_unlock_irqrestore(&video->req_lock, flags);
return;
}
@@ -487,9 +519,11 @@ static void uvcg_video_pump(struct work_struct *work)
return;

spin_lock_irqsave(&video->req_lock, flags);
- list_add_tail(&req->list, &video->req_free);
+ if (video->is_enabled)
+ list_add_tail(&req->list, &video->req_free);
+ else
+ uvc_video_free_request(req->context, video->ep);
spin_unlock_irqrestore(&video->req_lock, flags);
- return;
}

/*
@@ -498,7 +532,11 @@ static void uvcg_video_pump(struct work_struct *work)
int
uvcg_video_disable(struct uvc_video *video)
{
- struct uvc_request *ureq;
+ unsigned long flags;
+ struct list_head inflight_bufs;
+ struct usb_request *req, *temp;
+ struct uvc_buffer *buf, *btemp;
+ struct uvc_request *ureq, *utemp;

if (video->ep == NULL) {
uvcg_info(&video->uvc->func,
@@ -506,15 +544,58 @@ uvcg_video_disable(struct uvc_video *video)
return -ENODEV;
}

+ INIT_LIST_HEAD(&inflight_bufs);
+ spin_lock_irqsave(&video->req_lock, flags);
+ video->is_enabled = false;
+
+ /*
+ * Remove any in-flight buffers from the uvc_requests
+ * because we want to return them before cancelling the
+ * queue. This ensures that we aren't stuck waiting for
+ * all complete callbacks to come through before disabling
+ * vb2 queue.
+ */
+ list_for_each_entry(ureq, &video->ureqs, list) {
+ if (ureq->last_buf) {
+ list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
+ ureq->last_buf = NULL;
+ }
+ }
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
cancel_work_sync(&video->pump);
uvcg_queue_cancel(&video->queue, 0);

- list_for_each_entry(ureq, &video->ureqs, list) {
- if (ureq->req)
- usb_ep_dequeue(video->ep, ureq->req);
+ spin_lock_irqsave(&video->req_lock, flags);
+ /*
+ * Remove all uvc_requests from ureqs with list_del_init
+ * This lets uvc_video_free_request correctly identify
+ * if the uvc_request is attached to a list or not when freeing
+ * memory.
+ */
+ list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
+ list_del_init(&ureq->list);
+
+ list_for_each_entry_safe(req, temp, &video->req_free, list) {
+ list_del(&req->list);
+ uvc_video_free_request(req->context, video->ep);
}

- uvc_video_free_requests(video);
+ INIT_LIST_HEAD(&video->ureqs);
+ INIT_LIST_HEAD(&video->req_free);
+ video->req_size = 0;
+ spin_unlock_irqrestore(&video->req_lock, flags);
+
+ /*
+ * Return all the video buffers before disabling the queue.
+ */
+ spin_lock_irqsave(&video->queue.irqlock, flags);
+ list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
+ list_del(&buf->queue);
+ uvcg_complete_buffer(&video->queue, buf);
+ }
+ spin_unlock_irqrestore(&video->queue.irqlock, flags);
+
uvcg_queue_enable(&video->queue, 0);
return 0;
}
@@ -532,6 +613,14 @@ int uvcg_video_enable(struct uvc_video *video)
return -ENODEV;
}

+ /*
+ * Safe to access request related fields without req_lock because
+ * this is the only thread currently active, and no other
+ * request handling thread will become active until this function
+ * returns.
+ */
+ video->is_enabled = true;
+
if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
return ret;

@@ -557,6 +646,7 @@ int uvcg_video_enable(struct uvc_video *video)
*/
int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
{
+ video->is_enabled = false;
INIT_LIST_HEAD(&video->ureqs);
INIT_LIST_HEAD(&video->req_free);
spin_lock_init(&video->req_lock);
--
2.42.0.869.gea05f2083d-goog

2023-11-02 20:40:59

by Avichal Rakesh

[permalink] [raw]
Subject: Re: [PATCH v9 4/4] usb: gadget: uvc: Fix use-after-free for inflight usb_requests

Hey Dan, thank you for taking the time to review the patch!

Sent out v11 with some comments addressed:
https://lore.kernel.org/all/[email protected]/

On 11/2/23 06:29, Dan Scally wrote:
> Hi Avichal
>
> On 30/10/2023 20:56, Avichal Rakesh wrote:
>> Thank you for taking a look Dan!
>>
>> On 10/28/23 13:56, Dan Scally wrote:
>>> Hi Avichal
>>>
>>> On 27/10/2023 21:19, Avichal Rakesh wrote:
>>>> Currently, the uvc gadget driver allocates all uvc_requests as one array
>>>> and deallocates them all when the video stream stops. This includes
>>>> de-allocating all the usb_requests associated with those uvc_requests.
>>>> This can lead to use-after-free issues if any of those de-allocated
>>>> usb_requests were still owned by the usb controller.
>>>>
>>>> This is patch 2 of 2 in fixing the use-after-free issue. It adds a new
>>>> flag to uvc_video to track when frames and requests should be flowing.
>>>> When disabling the video stream, the flag is tripped and, instead
>>>> of de-allocating all uvc_requests and usb_requests, the gadget
>>>> driver only de-allocates those usb_requests that are currently
>>>> owned by it (as present in req_free). Other usb_requests are left
>>>> untouched until their completion handler is called which takes care
>>>> of freeing the usb_request and its corresponding uvc_request.
>>>>
>>>> Now that uvc_video does not depends on uvc->state, this patch removes
>>>> unnecessary upates to uvc->state that were made to accommodate uvc_video
>>>> logic. This should ensure that uvc gadget driver never accidentally
>>>> de-allocates a usb_request that it doesn't own.
>>>>
>>>> Link: https://lore.kernel.org/[email protected]
>>>> Suggested-by: Michael Grzeschik <[email protected]>
>>>> Reviewed-by: Michael Grzeschik <[email protected]>
>>>> Tested-by: Michael Grzeschik <[email protected]>
>>>> Signed-off-by: Avichal Rakesh <[email protected]>
>>>> ---
>>>> v1 -> v2: Rebased to ToT, and fixed deadlock reported in
>>>>             https://lore.kernel.org/all/[email protected]/
>>>> v2 -> v3: Fix email threading goof-up
>>>> v3 -> v4: re-rebase to ToT & moved to a uvc_video level lock
>>>>             as discussed in
>>>>             https://lore.kernel.org/[email protected]/
>>>> v4 -> v5: Address review comments. Add Reviewed-by & Tested-by.
>>>> v5 -> v6: Added another patch before this one to make uvcg_video_disable
>>>>             easier to review.
>>>> v6 -> v7: Fix warning reported in
>>>>             https://lore.kernel.org/[email protected]/
>>>> v7 -> v8: No change. Getting back in review queue
>>>> v8 -> v9: No change.
>>>>
>>>>    drivers/usb/gadget/function/uvc.h       |   1 +
>>>>    drivers/usb/gadget/function/uvc_v4l2.c  |  12 +--
>>>>    drivers/usb/gadget/function/uvc_video.c | 128 ++++++++++++++++++++----
>>>>    3 files changed, 111 insertions(+), 30 deletions(-)
>>>>
>>>> diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
>>>> index 993694da0bbc..be0d012aa244 100644
>>>> --- a/drivers/usb/gadget/function/uvc.h
>>>> +++ b/drivers/usb/gadget/function/uvc.h
>>>> @@ -102,6 +102,7 @@ struct uvc_video {
>>>>        unsigned int uvc_num_requests;
>>>>
>>>>        /* Requests */
>>>> +    bool is_enabled; /* tracks whether video stream is enabled */
>>>>        unsigned int req_size;
>>>>        struct list_head ureqs; /* all uvc_requests allocated by uvc_video */
>>>>        struct list_head req_free;
>>>> diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
>>>> index 904dd283cbf7..2f8634e05612 100644
>>>> --- a/drivers/usb/gadget/function/uvc_v4l2.c
>>>> +++ b/drivers/usb/gadget/function/uvc_v4l2.c
>>>> @@ -451,8 +451,8 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
>>>>         * Complete the alternate setting selection setup phase now that
>>>>         * userspace is ready to provide video frames.
>>>>         */
>>>> -    uvc_function_setup_continue(uvc, 0);
>>>>        uvc->state = UVC_STATE_STREAMING;
>>>> +    uvc_function_setup_continue(uvc, 0);
>>>>
>>>>        return 0;
>>>>    }
>>>> @@ -468,11 +468,11 @@ uvc_v4l2_streamoff(struct file *file, void *fh, enum v4l2_buf_type type)
>>>>        if (type != video->queue.queue.type)
>>>>            return -EINVAL;
>>>>
>>>> -    uvc->state = UVC_STATE_CONNECTED;
>>>>        ret = uvcg_video_disable(video);
>>>>        if (ret < 0)
>>>>            return ret;
>>>>
>>>> +    uvc->state = UVC_STATE_CONNECTED;
>>>>        uvc_function_setup_continue(uvc, 1);
>>>>        return 0;
>>>>    }
>>>
>>> I'm not sure I understand what these re-orderings are for...can you explain please?
>> This specific one was a leftover from testing, removed this hunk.
>> But the ones below are undoing the change in patch 1, which is
>> flawed in its use of uvc->state without any memory guarantees.
>>
>> So from patch 1 to patch 4, we shuffle the code around a bit,
>> but this makes patch 1 somewhat complete and functional even if
>> patch 4 were to be reverted.
>
>
> Okedokey - that's fine (and good)
>
>>
>>>> @@ -507,14 +507,6 @@ uvc_v4l2_subscribe_event(struct v4l2_fh *fh,
>>>>    static void uvc_v4l2_disable(struct uvc_device *uvc)
>>>>    {
>>>>        uvc_function_disconnect(uvc);
>>>> -    /*
>>>> -     * Drop uvc->state to CONNECTED if it was streaming before.
>>>> -     * This ensures that the usb_requests are no longer queued
>>>> -     * to the controller.
>>>> -     */
>>>> -    if (uvc->state == UVC_STATE_STREAMING)
>>>> -        uvc->state = UVC_STATE_CONNECTED;
>>>> -
>>>>        uvcg_video_disable(&uvc->video);
>>>>        uvcg_free_buffers(&uvc->video.queue);
>>>>        uvc->func_connected = false;
>>>> diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
>>>> index 1081dd790fd6..8f330ce696ec 100644
>>>> --- a/drivers/usb/gadget/function/uvc_video.c
>>>> +++ b/drivers/usb/gadget/function/uvc_video.c
>>>> @@ -227,6 +227,9 @@ uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
>>>>     * Request handling
>>>>     */
>>>>
>>>> +/*
>>>> + * Must be called with req_lock held as it modifies the list ureq is held in
>>>> + */
>>>
>>>
>>> This comment probably belongs in patch #2. And in that case, shouldn't uvc_video_free_requests() hold the lock in that patch?
>> Patch 2 doesn't change any existing locking semantics. The current
>> code does not enforce any locking on freeing the requests, and neither
>> does patch 2.
>>
>> Patch 4 introduces another call site for uvc_video_free_request, so
>> some synchronization guarantees are needed (and hence the addition
>> of this comment).
>>
>> As for uvc_video_free_requests not holding the lock, it is safe because
>> uvc_video_free_requests is only called if request initialization fails.
>> So uvc_video_free_requests should be the thread safe, as no other thread
>> is processing requests when it is called.
>>
>> I did add a comment in uvcg_video_enable mentioning why it is safe to
>> not hold req_free even though it accesses request related fields.
>
>
> I understand, but in that case I think the comment is a little confusing - it's not the fact that the function modifies the list ureq is held in that requires it to be locked but that there're potentially multiple threads doing so. Can we go for something like "Callers must take care to hold req_lock when using this function outside of a single thread"? Feel free to re-word that however you like, as long as it's clear that it's only necessary when multiple threads could be active.

I see. I think having all the context made it obvious in my mind.
Updated the comment to be more explicit!

>
>>
>> Happy to add another comment to uvc_video_free_requests if that makes it
>> clearer!
>>
>>>>    static void
>>>>    uvc_video_free_request(struct uvc_request *ureq, struct usb_ep *ep)
>>>>    {
>>>> @@ -271,9 +274,25 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
>>>>        struct uvc_request *ureq = req->context;
>>>>        struct uvc_video *video = ureq->video;
>>>>        struct uvc_video_queue *queue = &video->queue;
>>>> -    struct uvc_device *uvc = video->uvc;
>>>> +    struct uvc_buffer *last_buf = NULL;
>
>
> This initialisation's unnecessary since it's unconditionally set below.
>
>>>>        unsigned long flags;
>>>>
>>>> +    spin_lock_irqsave(&video->req_lock, flags);
>>>> +    if (!video->is_enabled) {
>>>> +        /*
>>>> +         * When is_enabled is false, uvc_video_disable ensures that
>>> s/uvc_video_disable/uvc_video_disable()
>> Done!
>>
>>>> +         * in-flight uvc_buffers are returned, so we can safely
>>>> +         * call free_request without worrying about last_buf.
>>>> +         */
>>>> +        uvc_video_free_request(ureq, ep);
>>> Now I understand the conditional in this function in patch 2 :)
>>>> +        spin_unlock_irqrestore(&video->req_lock, flags);
>>>> +        return;
>>>> +    }
>>>> +
>>>> +    last_buf = ureq->last_buf;
>>>> +    ureq->last_buf = NULL;
>>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>>
>>> I'm not a huge fan of this locking, unlocking and relocking the same spinlock within the same function. Can we just hold the lock for the duration? if not, can there be an explanatory comment as to why?
>> I agree that this is a little unfortunate, and it'd be nice if we
>> only had a single driver level lock. However, as it stands, if
>> we hold req_lock for the entirety of completion handler, we risk
>> two things:
>>
>> 1. Adding dependencies between queue->irqlock and video->reqlock
>> 2. Starving the video_pump thread.
>>
>> As of this patch, uvc_video_complete follows the same pattern as
>> video_pump function:
>> 1. Acquire req_lock
>> 2. Fetch/Query usb_request
>> 3. Drop req_lock
>>
>> 4. Acquire queue->irqlock
>> 5. Buffer ops (encode/free/stop)
>> 6. Drop queue->irqlock
>>
>> 7. Acquire req_lock
>> 8. usb_request cleanup/handling
>> 9. Drop req_lock
>>
>> (7), (8), and (9) are optional for video_pump, while
>> (4), (5), and (6) are optional for uvc_video_complete.
>>
>> We can short-circuit uvc_video_complete with only one lock
>> on the happy path, but this would have to be the flow for
>> non-happy paths unless we want to hold the two locks at
>> the same time (which isn't the worst idea, but comes with
>> its own set of concerns).
>>
>
> Yeah. Sorry - I had missed that uvcg_video_pump() follows the same pattern too. Alright, I think leave this as-is.
>
>>>> +
>>>>        switch (req->status) {
>>>>        case 0:
>>>>            break;
>>>> @@ -295,17 +314,26 @@ uvc_video_complete(struct usb_ep *ep, struct usb_request *req)
>>>>            uvcg_queue_cancel(queue, 0);
>>>>        }
>>>>
>>>> -    if (ureq->last_buf) {
>>>> -        uvcg_complete_buffer(&video->queue, ureq->last_buf);
>>>> -        ureq->last_buf = NULL;
>>>> +    if (last_buf) {
>>>> +        spin_lock_irqsave(&queue->irqlock, flags);
>>>> +        uvcg_complete_buffer(&video->queue, last_buf);
>>>> +        spin_unlock_irqrestore(&queue->irqlock, flags);
>>>
>>>
>>> I think it's right to take the irqlock here but it probably should have always been held, so this probably ought to go in its own commit with a Fixes:
>> The lock here wasn't required before, because uvcg_complete_buffer was
>> only ever called by the completion handler, which is synchronized by
>> the usb controller. This is the reason we never saw an issue despite
>> not holding the lock.
>
>
> Ah - I was misled by the "called with &queue_irqlock held..." comment on uvc_complete_buffer()...I assumed that meant "you must call this function with &queue->irqlock held", but that turns out to be something of a hangover - originally it was placed there by 95faf82bd3ea6 because the callers of (at the time uvc_queue_next_buffer(), later renamed to) uvcg_complete_buffer() already held the irqlock. Clearly it needed locking then, but the function at the time manipulated queue->irqqueue and now it no longer does - that part has been stripped out to encode_bulk/encode_isoc/encode_isoc_sg(). So, probably that comment ought to have been removed at some point.
>
>> This patch introduces another call site in uvcg_video_disable, so to
>> protect memory consistency, we need to make sure calls to
>> uvcg_complete_buffer are synchronized on something other than
>> the usb controller.
>
> If it's simply to prevent double calling uvcg_complete_buffer() for a buffer, is holding the irqlock necessary? Both uvc_video_complete() and uvc_video_disable() conditionally call uvcg_complete_buffer() based on whether ureq->last_buf is set or not, and both functions assess that whilst holding req_lock, so unless I'm missing something that situation is already guarded against.

Even without holding queue->irqlock, we guaratee that uvcg_complete_buffer()
is only called once per buffer (by querying last_buf with req_lock held).
However, uvcg_complete_buffer modifies queue->flags which in honesty
will not cause any problems, but is promlematic from theoretical
correctness standpoint.

Either way, this shouldn't be too much overhead, as
this is only called once per frame, and most requests don't
have last_buf set anyway. Let me know if you feel strongly
about not having the locks there, and I can drop it.

>
>
>>
>>>>        }
>>>>
>>>>        spin_lock_irqsave(&video->req_lock, flags);
>>>> -    list_add_tail(&req->list, &video->req_free);
>>>> -    spin_unlock_irqrestore(&video->req_lock, flags);
>>>> -
>>>> -    if (uvc->state == UVC_STATE_STREAMING)
>>>> +    /*
>>>> +     * Video stream might have been disabled while we were
>>>> +     * processing the current usb_request. So make sure
>>>> +     * we're still streaming before queueing the usb_request
>>>> +     * back to req_free
>>>> +     */
>>>> +    if (video->is_enabled) {
>>>> +        list_add_tail(&req->list, &video->req_free);
>>>>            queue_work(video->async_wq, &video->pump);
>>>> +    } else {
>>>> +        uvc_video_free_request(ureq, ep);
>>>> +    }
>>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>>>    }
>>>>
>>>>    static int
>>>> @@ -393,20 +421,22 @@ static void uvcg_video_pump(struct work_struct *work)
>>>>        struct uvc_video_queue *queue = &video->queue;
>>>>        /* video->max_payload_size is only set when using bulk transfer */
>>>>        bool is_bulk = video->max_payload_size;
>>>> -    struct uvc_device *uvc = video->uvc;
>>>>        struct usb_request *req = NULL;
>>>>        struct uvc_buffer *buf;
>>>>        unsigned long flags;
>>>>        bool buf_done;
>>>>        int ret;
>>>>
>>>> -    while (uvc->state == UVC_STATE_STREAMING && video->ep->enabled) {
>>>> +    while (true) {
>>>> +        if (!video->ep->enabled)
>>>> +            return;
>>>> +
>>>>            /*
>>>> -         * Retrieve the first available USB request, protected by the
>>>> -         * request lock.
>>>> +         * Check is_enabled and retrieve the first available USB
>>>> +         * request, protected by the request lock.
>>>>             */
>>>>            spin_lock_irqsave(&video->req_lock, flags);
>>>> -        if (list_empty(&video->req_free)) {
>>>> +        if (!video->is_enabled || list_empty(&video->req_free)) {
>>>>                spin_unlock_irqrestore(&video->req_lock, flags);
>>>>                return;
>>>>            }
>>>> @@ -488,9 +518,11 @@ static void uvcg_video_pump(struct work_struct *work)
>>>>            return;
>>>>
>>>>        spin_lock_irqsave(&video->req_lock, flags);
>>>> -    list_add_tail(&req->list, &video->req_free);
>>>> +    if (video->is_enabled)
>>>> +        list_add_tail(&req->list, &video->req_free);
>>>> +    else
>>>> +        uvc_video_free_request(req->context, video->ep);
>>>>        spin_unlock_irqrestore(&video->req_lock, flags);
>>>> -    return;
>>>>    }
>>>>
>>>>    /*
>>>> @@ -499,7 +531,11 @@ static void uvcg_video_pump(struct work_struct *work)
>>>>    int
>>>>    uvcg_video_disable(struct uvc_video *video)
>>>>    {
>>>> -    struct uvc_request *ureq;
>>>> +    unsigned long flags;
>>>> +    struct list_head inflight_bufs;
>>>> +    struct usb_request *req, *temp;
>>>> +    struct uvc_buffer *buf, *btemp;
>>>> +    struct uvc_request *ureq, *utemp;
>>>>
>>>>        if (video->ep == NULL) {
>>>>            uvcg_info(&video->uvc->func,
>>>> @@ -507,15 +543,58 @@ uvcg_video_disable(struct uvc_video *video)
>>>>            return -ENODEV;
>>>>        }
>>>>
>>>> +    INIT_LIST_HEAD(&inflight_bufs);
>>>> +    spin_lock_irqsave(&video->req_lock, flags);
>>>> +    video->is_enabled = false;
>>>> +
>>>> +    /*
>>>> +     * Remove any in-flight buffers from the uvc_requests
>>>> +     * because we want to return them before cancelling the
>>>> +     * queue. This ensures that we aren't stuck waiting for
>>>> +     * all complete callbacks to come through before disabling
>>>> +     * vb2 queue.
>>>> +     */
>>>> +    list_for_each_entry(ureq, &video->ureqs, list) {
>>>> +        if (ureq->last_buf) {
>>>> +            list_add_tail(&ureq->last_buf->queue, &inflight_bufs);
>>>> +            ureq->last_buf = NULL;
>>>> +        }
>>>> +    }
>>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>>> +
>>>>        cancel_work_sync(&video->pump);
>>>>        uvcg_queue_cancel(&video->queue, 0);
>>>>
>>>> -    list_for_each_entry(ureq, &video->ureqs, list) {
>>>> -        if (ureq->req)
>>>> -            usb_ep_dequeue(video->ep, ureq->req);
>>>> +    spin_lock_irqsave(&video->req_lock, flags);
>>>> +    /*
>>>> +     * Remove all uvc_reqeusts from ureqs with list_del_init
> s/uvc_reqeusts/uvc_requests
>>>> +     * This lets uvc_video_free_request correctly identify
>>>> +     * if the uvc_request is attached to a list or not when freeing
>>>> +     * memory.
>>>> +     */
>>>> +    list_for_each_entry_safe(ureq, utemp, &video->ureqs, list)
>>>> +        list_del_init(&ureq->list);
>>>> +
>>>> +    list_for_each_entry_safe(req, temp, &video->req_free, list) {
>>>> +        list_del(&req->list);
>>>> +        uvc_video_free_request(req->context, video->ep);
>>>>        }
>>>>
>>>> -    uvc_video_free_requests(video);
>>>> +    INIT_LIST_HEAD(&video->ureqs);
>>>> +    INIT_LIST_HEAD(&video->req_free);
>>>> +    video->req_size = 0;
>>>> +    spin_unlock_irqrestore(&video->req_lock, flags);
>>>> +
>>>> +    /*
>>>> +     * Return all the video buffers before disabling the queue.
>>>> +     */
>>>> +    spin_lock_irqsave(&video->queue.irqlock, flags);
>>>> +    list_for_each_entry_safe(buf, btemp, &inflight_bufs, queue) {
>>>> +        list_del(&buf->queue);
>>>> +        uvcg_complete_buffer(&video->queue, buf);
>>>> +    }
>>>> +    spin_unlock_irqrestore(&video->queue.irqlock, flags);
>>>> +
>>>>        uvcg_queue_enable(&video->queue, 0);
>>>>        return 0;
>>>>    }
>>>> @@ -533,6 +612,14 @@ int uvcg_video_enable(struct uvc_video *video)
>>>>            return -ENODEV;
>>>>        }
>>>>
>>>> +    /*
>>>> +     * Safe to access request related fields without req_lock because
>>>> +     * this is the only thread currently active, and no other
>>>> +     * request handling thread will become active until this function
>>>> +     * returns.
>>>> +     */
>>>> +    video->is_enabled = true;
>>>> +
>>>>        if ((ret = uvcg_queue_enable(&video->queue, 1)) < 0)
>>>>            return ret;
>>>>
>>>> @@ -558,6 +645,7 @@ int uvcg_video_enable(struct uvc_video *video)
>>>>     */
>>>>    int uvcg_video_init(struct uvc_video *video, struct uvc_device *uvc)
>>>>    {
>>>> +    video->is_enabled = false;
>>>>        INIT_LIST_HEAD(&video->ureqs);
>>>>        INIT_LIST_HEAD(&video->req_free);
>>>>        spin_lock_init(&video->req_lock);
>>>> -- 
>>>> 2.42.0.820.g83a721a137-goog