-
Notifications
You must be signed in to change notification settings - Fork 141
Description
I was trying to write tests for nusb's timeout handling using Cynthion + Facedancer, but I ended up testing Cynthion's timeout handling instead.
I found that when a control transfer is cancelled by the host before Facedancer can respond, it will still prime the control endpoint to respond with the written data, and the subsequent control IN transfer returns that data instead of waiting for its own response from the facedancer app.
From section 5.5.5 of the USB 2.0 spec:
If a Setup transaction is received by an endpoint before a previously initiated control transfer is completed,
the device must abort the current transfer/operation and handle the new control Setup transaction. A Setup
transaction should not normally be sent before the completion of a previous control transfer. However, if a
transfer is aborted, for example, due to errors on the bus, the host can send the next Setup transaction
prematurely from the endpoint’s perspective.
Most USB hardware will clear the endpoint ready bits when receiving a SETUP packet to avoid this problem, so this part seems Cynthion-specific. However, because of the long lag due to the extra USB round-trip, Cynthion and other Facedancer backends are additionally susceptible to a race condition between the Facedancer app getting notified of a new SETUP packet and writing a response to the previous one. May need to add some kind of ID to specify which SETUP packet it's responding to so the response can be ignored by the firmware if stale.
Facedancer
#!/usr/bin/env python3
# pylint: disable=unused-wildcard-import, wildcard-import
import time
from facedancer import *
from facedancer import main
@use_inner_classes_automatically
class TestDevice(USBDevice):
product_string : str = "test device"
vendor_id : int = 0x1234
product_id : int = 0x1234
class TestConfiguration(USBConfiguration):
class TestInterface(USBInterface):
pass
@vendor_request_handler(number=1, direction=USBDirection.IN)
@to_device
def req_count(self, request):
time.sleep(0.1)
request.reply(b'aaa')
@vendor_request_handler(number=2, direction=USBDirection.IN)
@to_device
def req_echo(self, request):
request.reply(b'bbb')
device = TestDevice()
main(device)
Host
import usb1
import time
with usb1.USBContext() as context:
handle = context.openByVendorIDAndProductID(0x1234, 0x1234)
try:
handle.controlRead(
request_type=usb1.ENDPOINT_IN | usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE,
request=0x01,
value=0x0000,
index=0x0000,
length=64,
timeout=50
)
except usb1.USBErrorTimeout:
print("request timed out (as expected)")
time.sleep(0.1)
res = handle.controlRead(
request_type=usb1.ENDPOINT_IN | usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE,
request=0x02,
value=0x0000,
index=0x0000,
length=64,
timeout=5000
)
print("response: ", res)
print("expected: b'bbb' for request 0x02")
Cynthion version: 0.1.8
Facedancer version: 3.1.0