2019-01-09 07:12:43

by Paul Elder

[permalink] [raw]
Subject: [PATCH v3 0/4] usb: gadget: uvc: fix racing between uvc_function_set_alt and streamon/off

Down the call stack from the ioctl handler for VIDIOC_STREAMON,
uvc_video_alloc_requests contains a BUG_ON, which in the high level,
triggers when VIDIOC_STREAMON ioctl is issued without VIDIOC_STREAMOFF
being issued previously.

This can happen in a few ways, such as if the userspace uvc gadget
application simply doesn't issue VIDIOC_STREAMOFF. Another way is if
uvc_function_set_alt with alt 0 is called after it is called with 1 but
before VIDIOC_STREAMON is called; in this case, UVC_EVENT_STREAMOFF will
not be queued to userspace, and therefore userspace will never call
VIDIOC_STREAMOFF.

To fix this, add two more uvc states: starting and stopping. The
starting state is entered when uvc_function_set_alt 1 is called, and is
exited in uvc_v4l2_streamon, when the state is changed to streaming. The
stopping state is entered when uvc_function_set_alt 0 is called, and is
exited in uvc_v4l2_streamoff, when the state is changed to connected.

The status phase of the SET_INTERFACE request doesn't need to be delayed
by the uvc gadget driver, so that is removed.

Finally, there is another way to trigger the aforementioned BUG: start
streaming and (physically) disconnect usb. To fix this, call
uvcg_video_enable 0 in uvc_function_disable.


Changes in v3:

- add state guard to uvc_function_set_alt 1
- add documentation for newly added uvc states
- reorder uvc states to more or less follow the flow diagram
- add more state guards to ioctl handlers for streamon and streamoff
- added interrupt-safe uvcg_video_cancel and used instead of the
non-interrupt-save uvcg_video_enable 0 in uvc_function_disable

Changes in v2:
1. Remove delay usb status phase

Paul Elder (4):
usb: gadget: uvc: synchronize streamon/off with uvc_function_set_alt
usb: gadget: uvc: don't delay the status phase of non-zero
SET_INTERFACE requests
usb: gadget: uvc: disable stream when disconnected
usb: gadget: uvc: remove unused/duplicate function prototypes from
uvc.h

drivers/usb/gadget/function/f_uvc.c | 23 ++++++++----
drivers/usb/gadget/function/uvc.h | 47 +++++++++++++++++++------
drivers/usb/gadget/function/uvc_v4l2.c | 28 +++++++++++----
drivers/usb/gadget/function/uvc_video.c | 13 +++++++
drivers/usb/gadget/function/uvc_video.h | 2 ++
5 files changed, 91 insertions(+), 22 deletions(-)

--
2.20.1



2019-01-09 07:13:07

by Paul Elder

[permalink] [raw]
Subject: [PATCH v3 2/4] usb: gadget: uvc: don't delay the status phase of non-zero SET_INTERFACE requests

Reception of a SET_INTERFACE request with a non-zero alternate setting
signals the start of the video stream. The gadget has to enable the
video streaming endpoint and to signal stream start to userspace, in
order to start receiving video frames to transmit over USB. As userspace
can be slow to react, the UVC function driver delays the status phase of
the SET_INTERFACE control transfer until userspace is ready.

The status phase is delayed by returning USB_GADGET_DELAYED_STATUS from
the function's .set_alt() handler. This creates a race condition as the
userspace application could process the stream start event before the
composite layer processes the USB_GADGET_DELAYED_STATUS return value.
The race has been observed in practice, and can't be solved without a
change to the USB_GADGET_DELAYED_STATUS API.

Fortunately the UVC function driver doesn't strictly require delaying
the status phase for non-zero SET_INTERFACE, as the only requirement
from a USB point of view is that the streaming endpoint must be enabled
before the status phase completes, and that is already guaranteed by the
current code. We can thus complete the status phase synchronously,
removing the race condition at the same time.

Without delaying the status phase the host will likely start issuing
isochronous transfers before we queue the first USB requests. The UDC
will reply with NAKs which should be handled properly by the host. If
this ends up causing issues another option will be to modify the status
phase delay API to fix the race condition.

Signed-off-by: Paul Elder <[email protected]>
---
Changes in v3: Nothing

Changes in v2:
1. Remove delay usb status phase

drivers/usb/gadget/function/f_uvc.c | 3 ++-
drivers/usb/gadget/function/uvc_v4l2.c | 6 ------
2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index 2ec3b73b2b75..b407b10a95ed 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -356,7 +356,8 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMON;
v4l2_event_queue(&uvc->vdev, &v4l2_event);
- return USB_GADGET_DELAYED_STATUS;
+
+ return 0;

default:
return -EINVAL;
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index 97e624214287..7816ea9886e1 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -210,12 +210,6 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)

uvc->state = UVC_STATE_STREAMING;

- /*
- * Complete the alternate setting selection setup phase now that
- * userspace is ready to provide video frames.
- */
- uvc_function_setup_continue(uvc);
-
return 0;
}

--
2.20.1


2019-01-09 07:13:54

by Paul Elder

[permalink] [raw]
Subject: [PATCH v3 3/4] usb: gadget: uvc: disable stream when disconnected

If the streamon ioctl is issued while the stream is already on, then the
kernel BUGs. This happens at the BUG_ON in uvc_video_alloc_requests
within the call stack from the ioctl handler for VIDIOC_STREAMON.

This can also be triggered by starting the stream and then physically
disconnecting usb. To fix this, do the streamoff procedures on usb
disconnect. Since uvcg_video_enable is not interrupt-safe, add an
interrupt-safe version uvcg_video_cancel, and use that.

Signed-off-by: Paul Elder <[email protected]>
v2 Reviewed-by: Kieran Bingham <[email protected]>
---
Changes in v3:

- added interrupt-safe uvcg_video_cancel and used instead of the
non-interrupt-save uvcg_video_enable 0

Changes in v2: Nothing

drivers/usb/gadget/function/f_uvc.c | 3 +++
drivers/usb/gadget/function/uvc_video.c | 13 +++++++++++++
drivers/usb/gadget/function/uvc_video.h | 2 ++
3 files changed, 18 insertions(+)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index b407b10a95ed..4134117b5f81 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -369,9 +369,12 @@ uvc_function_disable(struct usb_function *f)
{
struct uvc_device *uvc = to_uvc(f);
struct v4l2_event v4l2_event;
+ struct uvc_video *video = &uvc->video;

uvcg_info(f, "%s()\n", __func__);

+ uvcg_video_cancel(video, 1);
+
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_DISCONNECT;
v4l2_event_queue(&uvc->vdev, &v4l2_event);
diff --git a/drivers/usb/gadget/function/uvc_video.c b/drivers/usb/gadget/function/uvc_video.c
index 5c042f380708..5f3ed9e0b5ad 100644
--- a/drivers/usb/gadget/function/uvc_video.c
+++ b/drivers/usb/gadget/function/uvc_video.c
@@ -348,6 +348,19 @@ int uvcg_video_pump(struct uvc_video *video)
return 0;
}

+int uvcg_video_cancel(struct uvc_video *video, int disconnect)
+{
+ unsigned int i;
+
+ for (i = 0; i < UVC_NUM_REQUESTS; ++i)
+ if (video->req[i])
+ usb_ep_dequeue(video->ep, video->req[i]);
+
+ uvc_video_free_requests(video);
+ uvcg_queue_cancel(&video->queue, disconnect);
+ return 0;
+}
+
/*
* Enable or disable the video stream.
*/
diff --git a/drivers/usb/gadget/function/uvc_video.h b/drivers/usb/gadget/function/uvc_video.h
index 278dc52c7604..1f310fa58ce5 100644
--- a/drivers/usb/gadget/function/uvc_video.h
+++ b/drivers/usb/gadget/function/uvc_video.h
@@ -16,6 +16,8 @@ struct uvc_video;

int uvcg_video_pump(struct uvc_video *video);

+int uvcg_video_cancel(struct uvc_video *video, int disconnect);
+
int uvcg_video_enable(struct uvc_video *video, int enable);

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


2019-01-09 07:14:03

by Paul Elder

[permalink] [raw]
Subject: [PATCH v3 4/4] usb: gadget: uvc: remove unused/duplicate function prototypes from uvc.h

Defined in uvc.h, uvc_endpoint_stream doesn't exist, and
uvc_function_connect, uvc_function_disconnect, and
uvc_function_setup_continue have duplicates in f_uvc.h.
Remove these four function prototypes from uvc.h

Signed-off-by: Paul Elder <[email protected]>
---
drivers/usb/gadget/function/uvc.h | 10 ----------
1 file changed, 10 deletions(-)

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index f183e349499c..671020c8a836 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -185,14 +185,4 @@ struct uvc_file_handle {
#define to_uvc_file_handle(handle) \
container_of(handle, struct uvc_file_handle, vfh)

-/* ------------------------------------------------------------------------
- * Functions
- */
-
-extern void uvc_function_setup_continue(struct uvc_device *uvc);
-extern void uvc_endpoint_stream(struct uvc_device *dev);
-
-extern void uvc_function_connect(struct uvc_device *uvc);
-extern void uvc_function_disconnect(struct uvc_device *uvc);
-
#endif /* _UVC_GADGET_H_ */
--
2.20.1


2019-01-09 07:14:08

by Paul Elder

[permalink] [raw]
Subject: [PATCH v3 1/4] usb: gadget: uvc: synchronize streamon/off with uvc_function_set_alt

If the streamon ioctl is issued while the stream is already on, then the
kernel BUGs. This happens at the BUG_ON in uvc_video_alloc_requests
within the call stack from the ioctl handler for VIDIOC_STREAMON.

This could happen when uvc_function_set_alt 0 races and wins against
uvc_v4l2_streamon, or when userspace neglects to issue the
VIDIOC_STREAMOFF ioctl.

To fix this, add two more uvc states: starting and stopping. Use these
to prevent the racing, and to detect when VIDIOC_STREAMON is issued
without previously issuing VIDIOC_STREAMOFF.

Signed-off-by: Paul Elder <[email protected]>
---
Changes in v3:

- add state guard to uvc_function_set_alt 1
- add documentation for newly added uvc states
- reorder uvc states to more or less follow the flow diagram
- add more state guards to ioctl handlers for streamon and streamoff

Changes in v2: Nothing

drivers/usb/gadget/function/f_uvc.c | 17 ++++++++----
drivers/usb/gadget/function/uvc.h | 37 ++++++++++++++++++++++++++
drivers/usb/gadget/function/uvc_v4l2.c | 26 ++++++++++++++++--
3 files changed, 73 insertions(+), 7 deletions(-)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index 8c99392df593..2ec3b73b2b75 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -317,26 +317,31 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)

switch (alt) {
case 0:
- if (uvc->state != UVC_STATE_STREAMING)
+ if (uvc->state != UVC_STATE_STREAMING &&
+ uvc->state != UVC_STATE_STARTING)
return 0;

if (uvc->video.ep)
usb_ep_disable(uvc->video.ep);

+ uvc->state = UVC_STATE_STOPPING;
+
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;

case 1:
- if (uvc->state != UVC_STATE_CONNECTED)
- return 0;
-
if (!uvc->video.ep)
return -EINVAL;

+ if (uvc->state == UVC_STATE_STOPPING)
+ return -EINVAL;
+
+ if (uvc->state != UVC_STATE_CONNECTED)
+ return 0;
+
uvcg_info(f, "reset UVC\n");
usb_ep_disable(uvc->video.ep);

@@ -346,6 +351,8 @@ uvc_function_set_alt(struct usb_function *f, unsigned interface, unsigned alt)
return ret;
usb_ep_enable(uvc->video.ep);

+ uvc->state = UVC_STATE_STARTING;
+
memset(&v4l2_event, 0, sizeof(v4l2_event));
v4l2_event.type = UVC_EVENT_STREAMON;
v4l2_event_queue(&uvc->vdev, &v4l2_event);
diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 099d650082e5..f183e349499c 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -101,10 +101,47 @@ struct uvc_video {
unsigned int fid;
};

+/**
+ * enum uvc_state - the states of a struct uvc_device
+ * @UVC_STATE_DISCONNECTED: not connected state
+ * - transition to connected state on .set_alt
+ * @UVC_STATE_CONNECTED: connected state
+ * - transition to disconnected state on .disable
+ * and alloc
+ * - transition to starting state on .set_alt 1
+ * @UVC_STATE_STARTING: starting state
+ * - transition to streaming state on streamon ioctl
+ * - transition to stopping state on set_alt 0
+ * @UVC_STATE_STREAMING: streaming state
+ * - transition to stopping state on .set_alt 0
+ * @UVC_STATE_STOPPING: stopping state
+ * - transition to connected on streamoff ioctl
+ *
+ * Diagram of state transitions:
+ *
+ * disable
+ * +---------------------------+
+ * v |
+ * +--------------+ set_alt +-----------+
+ * | DISCONNECTED | ---------> | CONNECTED |
+ * +--------------+ +-----------+
+ * | ^
+ * set_alt 1 | | streamoff
+ * +----------------------+ --------------------+
+ * V |
+ * +----------+ streamon +-----------+ set_alt 0 +----------+
+ * | STARTING | ----------> | STREAMING | -----------> | STOPPING |
+ * +----------+ +-----------+ +----------+
+ * | ^
+ * | set_alt 0 |
+ * +------------------------------------------------+
+ */
enum uvc_state {
UVC_STATE_DISCONNECTED,
UVC_STATE_CONNECTED,
+ UVC_STATE_STARTING,
UVC_STATE_STREAMING,
+ UVC_STATE_STOPPING,
};

struct uvc_device {
diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index a1183eccee22..97e624214287 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -197,17 +197,24 @@ uvc_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
if (type != video->queue.queue.type)
return -EINVAL;

+ if (uvc->state == UVC_STATE_STREAMING)
+ return 0;
+
+ if (uvc->state != UVC_STATE_STARTING)
+ return -EINVAL;
+
/* Enable UVC video. */
ret = uvcg_video_enable(video, 1);
if (ret < 0)
return ret;

+ uvc->state = UVC_STATE_STREAMING;
+
/*
* Complete the alternate setting selection setup phase now that
* userspace is ready to provide video frames.
*/
uvc_function_setup_continue(uvc);
- uvc->state = UVC_STATE_STREAMING;

return 0;
}
@@ -218,11 +225,26 @@ 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;

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

- return uvcg_video_enable(video, 0);
+ /*
+ * Check for connected state also because we want to reset buffers
+ * if this is called when the stream is already off.
+ */
+ if (uvc->state != UVC_STATE_STOPPING &&
+ uvc->state != UVC_STATE_CONNECTED)
+ return 0;
+
+ ret = uvcg_video_enable(video, 0);
+ if (ret < 0)
+ return ret;
+
+ uvc->state = UVC_STATE_CONNECTED;
+
+ return 0;
}

static int
--
2.20.1