Skip to content

Commit 1d3c722

Browse files
committed
usb: Fix race if transfers are submitted by a thread.
The USB pending transfer flag was cleared before calling the completion callback, to allow the callback code to call submit_xfer() again. Unfortunately this isn't safe in a multi-threaded environment, as another thread may see the endpoint is available before the callback is done executing and submit a new transfer. Rather than adding extra locking, specifically treat the transfer as still pending if checked from another thread while the callback is executing. Closes micropython#874 Signed-off-by: Angus Gratton <[email protected]>
1 parent 27e4d73 commit 1d3c722

File tree

2 files changed

+33
-5
lines changed

2 files changed

+33
-5
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
metadata(version="0.1.0")
1+
metadata(version="0.1.1")
22
package("usb")

micropython/usb/usb-device/usb/device/core.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
import machine
99
import struct
1010

11+
try:
12+
from _thread import get_ident
13+
except ImportError:
14+
15+
def get_ident():
16+
return 0 # Placeholder, for no threading support
17+
18+
1119
_EP_IN_FLAG = const(1 << 7)
1220

1321
# USB descriptor types
@@ -76,6 +84,8 @@ def __init__(self):
7684
self._itfs = {} # Mapping from interface number to interface object, set by init()
7785
self._eps = {} # Mapping from endpoint address to interface object, set by _open_cb()
7886
self._ep_cbs = {} # Mapping from endpoint address to Optional[xfer callback]
87+
self._cb_thread = None # Thread currently running endpoint callback
88+
self._cb_ep = None # Endpoint number currently running callback
7989
self._usbd = machine.USBDevice() # low-level API
8090

8191
def init(self, *itfs, **kwargs):
@@ -298,7 +308,7 @@ def _submit_xfer(self, ep_addr, data, done_cb=None):
298308
# that function for documentation about the possible parameter values.
299309
if ep_addr not in self._eps:
300310
raise ValueError("ep_addr")
301-
if self._ep_cbs[ep_addr]:
311+
if self._xfer_pending(ep_addr):
302312
raise RuntimeError("xfer_pending")
303313

304314
# USBDevice callback may be called immediately, before Python execution
@@ -308,12 +318,25 @@ def _submit_xfer(self, ep_addr, data, done_cb=None):
308318
self._ep_cbs[ep_addr] = done_cb or True
309319
return self._usbd.submit_xfer(ep_addr, data)
310320

321+
def _xfer_pending(self, ep_addr):
322+
# Singleton function to return True if transfer is pending on this endpoint.
323+
#
324+
# Generally, drivers should call Interface.xfer_pending() instead. See that
325+
# function for more documentation.
326+
return self._ep_cbs[ep_addr] or (self._cb_ep == ep_addr and self._cb_thread != get_ident())
327+
311328
def _xfer_cb(self, ep_addr, result, xferred_bytes):
312329
# Singleton callback from TinyUSB custom class driver when a transfer completes.
313330
cb = self._ep_cbs.get(ep_addr, None)
331+
self._cb_thread = get_ident()
332+
self._cb_ep = ep_addr # Track while callback is running
314333
self._ep_cbs[ep_addr] = None
315-
if callable(cb):
316-
cb(ep_addr, result, xferred_bytes)
334+
try:
335+
# For a pending xfer, 'cb' should either a callback function or True (if no callback)
336+
if callable(cb):
337+
cb(ep_addr, result, xferred_bytes)
338+
finally:
339+
self._cb_ep = None
317340

318341
def _control_xfer_cb(self, stage, request):
319342
# Singleton callback from TinyUSB custom class driver when a control
@@ -528,7 +551,12 @@ def xfer_pending(self, ep_addr):
528551
# Return True if a transfer is already pending on ep_addr.
529552
#
530553
# Only one transfer can be submitted at a time.
531-
return _dev and bool(_dev._ep_cbs[ep_addr])
554+
#
555+
# The transfer is marked pending while a completion callback is running
556+
# for that endpoint, unless this function is called from the callback
557+
# itself. This makes it simple to submit a new transfer from the
558+
# completion callback.
559+
return _dev and _dev._xfer_pending(ep_addr)
532560

533561
def submit_xfer(self, ep_addr, data, done_cb=None):
534562
# Submit a USB transfer (of any type except control)

0 commit comments

Comments
 (0)