Skip to content

Returns incorrect response after cancelled control transfer #157

@kevinmehall

Description

@kevinmehall

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions