Skip to content

Commit f9a85d8

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 7a86e2c commit f9a85d8

File tree

2 files changed

+37
-6
lines changed

2 files changed

+37
-6
lines changed
+1-1
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

+36-5
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):
@@ -176,7 +186,9 @@ def maybe_set(value, idx):
176186
itf.desc_cfg(desc, itf_num, ep_num, strs)
177187

178188
for _ in range(itf.num_itfs()):
179-
self._itfs[itf_num] = itf # Mapping from interface numbers to interfaces
189+
self._itfs[itf_num] = (
190+
itf # Mapping from interface numbers to interfaces
191+
)
180192
itf_num += 1
181193

182194
ep_num += itf.num_eps()
@@ -298,7 +310,7 @@ def _submit_xfer(self, ep_addr, data, done_cb=None):
298310
# that function for documentation about the possible parameter values.
299311
if ep_addr not in self._eps:
300312
raise ValueError("ep_addr")
301-
if self._ep_cbs[ep_addr]:
313+
if self._xfer_pending(ep_addr):
302314
raise RuntimeError("xfer_pending")
303315

304316
# USBDevice callback may be called immediately, before Python execution
@@ -308,12 +320,26 @@ def _submit_xfer(self, ep_addr, data, done_cb=None):
308320
self._ep_cbs[ep_addr] = done_cb or True
309321
return self._usbd.submit_xfer(ep_addr, data)
310322

323+
def _xfer_pending(self, ep_addr):
324+
# Singleton function to return True if transfer is pending on this endpoint.
325+
#
326+
# Generally, drivers should call Interface.xfer_pending() instead. See that
327+
# function for more documentation.
328+
return bool(self._ep_cbs[ep_addr]) or (
329+
self._cb_ep == ep_addr and self._cb_thread != get_ident()
330+
)
331+
311332
def _xfer_cb(self, ep_addr, result, xferred_bytes):
312333
# Singleton callback from TinyUSB custom class driver when a transfer completes.
313334
cb = self._ep_cbs.get(ep_addr, None)
335+
self._cb_thread = get_ident()
336+
self._cb_ep = ep_addr # Track while callback is running
314337
self._ep_cbs[ep_addr] = None
315-
if callable(cb):
316-
cb(ep_addr, result, xferred_bytes)
338+
try:
339+
if callable(cb):
340+
cb(ep_addr, result, xferred_bytes)
341+
finally:
342+
self._cb_ep = None
317343

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

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

0 commit comments

Comments
 (0)