Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@ explicitly or use the package registry. Each call enumerates each provider once.

Import `usb/discovery` to register all-device USB enumeration, or call its
`Register` with an explicit registry. Its detached `Attachment` retains the
exact `usb.DeviceInfo` snapshot for later opening. USB identity breaks final
ordering ties by numeric bus/address and VID/PID; missing or duplicate serials
cannot keep physical-device ordering stable when replugging changes addresses.
exact `usb.DeviceInfo` snapshot for later opening. For equal serials, USB
ordering compares the unpadded `bus:address` location lexicographically, then
product and the fixed-width identity key; `10:2` therefore precedes `2:1`.
Missing or duplicate serials cannot keep physical-device ordering stable when
replugging changes addresses.

Each concrete driver exposes `OpenProbe` to acquire an exact USB attachment
under a generic `probe.Probe`. Interface claim and protocol configuration happen
Expand Down
7 changes: 7 additions & 0 deletions docs/linux-usb.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ without opening another attachment or falling back to a control transfer. An
`ENODEV` during either cached descriptor read matches `usb.ErrDeviceGone` and
preserves the native cause; other read errors retain their identity.

Standard device, configuration, interface and endpoint descriptors retain their
defined fields while ignoring legal trailing bytes. Parsing advances by the
returned descriptor length. Zero-bandwidth isochronous alternates may report a
zero maximum packet size; bulk endpoints still require a nonzero payload size.
After a successful alternate selection, endpoint lookup reloads its descriptors
using the known selection without another `GET_INTERFACE` request.

The control API accepts buffers up to the USB limit of 65,535 bytes; Linux usbfs
can reject a request larger than the host page size. The host timeout defaults
to five seconds; a context deadline rounds up to the next millisecond and clamps
Expand Down
29 changes: 15 additions & 14 deletions usb/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const (
descriptorConfiguration = 0x02
descriptorInterface = 0x04
descriptorEndpoint = 0x05
classAudio = 0x01
)

// TransferType identifies a USB endpoint transfer type.
Expand Down Expand Up @@ -58,7 +57,8 @@ type AlternateSetting struct {

// Endpoint describes one endpoint in an interface alternate setting.
// MaxPacketSize is the descriptor's packet payload size, excluding periodic
// transaction multipliers and burst capacity.
// transaction multipliers and burst capacity. Zero payload is retained for
// zero-bandwidth periodic endpoints.
type Endpoint struct {
Address uint8
TransferType TransferType
Expand Down Expand Up @@ -175,7 +175,7 @@ func configurationCount(ctx context.Context, device controlTransferer) (int, err
if err := readStandard(ctx, device, requestGetDescriptor, descriptorDevice<<8, deviceDescriptor); err != nil {
return 0, fmt.Errorf("usb: read device descriptor: %w", err)
}
if int(deviceDescriptor[0]) != len(deviceDescriptor) || deviceDescriptor[1] != descriptorDevice || deviceDescriptor[17] == 0 {
if int(deviceDescriptor[0]) < len(deviceDescriptor) || deviceDescriptor[1] != descriptorDevice || deviceDescriptor[17] == 0 {
return 0, errors.New("usb: malformed device descriptor")
}
return int(deviceDescriptor[17]), nil
Expand All @@ -185,6 +185,7 @@ type configurationHeader struct {
index int
value uint8
total int
size uint8
}

func configurationHeaderAt(ctx context.Context, device controlTransferer, index int) (configurationHeader, error) {
Expand All @@ -193,10 +194,10 @@ func configurationHeaderAt(ctx context.Context, device controlTransferer, index
if err := readStandard(ctx, device, requestGetDescriptor, descriptorValue, header); err != nil {
return configurationHeader{}, fmt.Errorf("usb: read configuration %d header: %w", index, err)
}
if int(header[0]) != len(header) || header[1] != descriptorConfiguration {
if int(header[0]) < len(header) || header[1] != descriptorConfiguration {
return configurationHeader{}, fmt.Errorf("usb: malformed configuration %d header", index)
}
return configurationHeader{index: index, value: header[5], total: int(binary.LittleEndian.Uint16(header[2:4]))}, nil
return configurationHeader{index: index, value: header[5], total: int(binary.LittleEndian.Uint16(header[2:4])), size: header[0]}, nil
}

func configurationAt(ctx context.Context, device controlTransferer, header configurationHeader, active uint8) (Configuration, error) {
Expand All @@ -207,6 +208,9 @@ func configurationAt(ctx context.Context, device controlTransferer, header confi
if err != nil {
return Configuration{}, fmt.Errorf("usb: read configuration %d: %w", header.index, err)
}
if raw[0] != header.size {
return Configuration{}, fmt.Errorf("usb: configuration %d descriptor length changed from %d to %d", header.index, header.size, raw[0])
}
if raw[5] != active {
return Configuration{}, fmt.Errorf("usb: configuration %d value changed from %d to %d", header.index, active, raw[5])
}
Expand Down Expand Up @@ -286,7 +290,7 @@ func validConfigurationHeader(raw []byte) bool {
if len(raw) < 9 {
return false
}
return raw[0] == 9 && raw[1] == descriptorConfiguration && int(binary.LittleEndian.Uint16(raw[2:4])) == len(raw) && raw[7]&0x80 != 0 && raw[7]&0x1f == 0
return raw[0] >= 9 && int(raw[0]) <= len(raw) && raw[1] == descriptorConfiguration && int(binary.LittleEndian.Uint16(raw[2:4])) == len(raw) && raw[7]&0x80 != 0 && raw[7]&0x1f == 0
}

type configurationParser struct {
Expand Down Expand Up @@ -317,7 +321,7 @@ func configurationDescriptor(raw []byte, position int) ([]byte, int, error) {
}

func (p *configurationParser) addInterface(descriptor []byte, position int) error {
if len(descriptor) != 9 {
if len(descriptor) < 9 {
return fmt.Errorf("usb: invalid interface descriptor length %d at offset %d", len(descriptor), position)
}
if err := p.finishAlternate(); err != nil {
Expand Down Expand Up @@ -357,7 +361,7 @@ func (p *configurationParser) addEndpoint(descriptor []byte, position int) error
if p.alternate == nil {
return fmt.Errorf("usb: invalid endpoint descriptor at offset %d", position)
}
endpoint, err := endpointFromDescriptor(descriptor, position, p.alternate.Class)
endpoint, err := endpointFromDescriptor(descriptor, position)
if err != nil {
return err
}
Expand All @@ -369,13 +373,10 @@ func (p *configurationParser) addEndpoint(descriptor []byte, position int) error
return nil
}

func endpointFromDescriptor(descriptor []byte, position int, class uint8) (Endpoint, error) {
if len(descriptor) != 7 && len(descriptor) != 9 {
func endpointFromDescriptor(descriptor []byte, position int) (Endpoint, error) {
if len(descriptor) < 7 {
return Endpoint{}, fmt.Errorf("usb: invalid endpoint descriptor length %d at offset %d", len(descriptor), position)
}
if len(descriptor) == 9 && class != classAudio {
return Endpoint{}, fmt.Errorf("usb: nine-byte endpoint descriptor outside audio interface at offset %d", position)
}
address := descriptor[2]
if address&0x0f == 0 || address&0x70 != 0 {
return Endpoint{}, fmt.Errorf("usb: invalid endpoint address %#02x at offset %d", address, position)
Expand Down Expand Up @@ -424,7 +425,7 @@ func endpointMaxPacketSize(encoded uint16, transferType TransferType) (uint16, b
}
size := encoded & 0x07ff
minimum := endpointPacketSizeMinimum(transactions)
return size, size <= 1024 && (size >= minimum || transactions == 0 && size == 0 && transferType == TransferInterrupt)
return size, size <= 1024 && (size >= minimum || transactions == 0 && size == 0 && (transferType == TransferInterrupt || transferType == TransferIsochronous))
}

func validEndpointTransactions(transactions uint16, transferType TransferType) bool {
Expand Down
2 changes: 1 addition & 1 deletion usb/configuration_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func attachmentConfigurationLength(reader io.ReaderAt, index int, offset int64)
return 0, fmt.Errorf("usb: read attachment configuration %d header: %w", index, linuxUSBError(err))
}
length := int(binary.LittleEndian.Uint16(header[2:4]))
if header[0] != 9 || header[1] != descriptorConfiguration || length < 9 {
if header[0] < 9 || int(header[0]) > length || header[1] != descriptorConfiguration || length < 9 {
return 0, fmt.Errorf("usb: malformed attachment configuration %d header", index)
}
return length, nil
Expand Down
12 changes: 4 additions & 8 deletions usb/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func TestActiveConfigurationRejectsChangedConfigurationLength(t *testing.T) {
{request: 6, value: 0x0200, data: full},
{request: 8, data: []byte{7}},
}}
if _, err := activeConfiguration(context.Background(), device); err == nil || !strings.Contains(err.Error(), "malformed configuration descriptor") {
if _, err := activeConfiguration(context.Background(), device); err == nil || !strings.Contains(err.Error(), "descriptor length changed") {
t.Fatalf("ActiveConfiguration() error = %v", err)
}
}
Expand Down Expand Up @@ -269,15 +269,12 @@ func TestConfigurationValidatesAlternateSettings(t *testing.T) {
}

func TestConfigurationRejectsInvalidInterfaceHierarchy(t *testing.T) {
long := append(interfaceDescriptor(0, 0, 0), 0)
long[0] = 10
tests := []struct {
name string
raw []byte
want string
}{
{name: "zero interfaces", raw: rawConfiguration(1, 0), want: "configuration descriptor has no interfaces"},
{name: "long interface descriptor", raw: rawConfiguration(1, 1, long), want: "invalid interface descriptor length 10"},
{name: "noncontiguous interface numbers", raw: rawConfiguration(1, 2, interfaceDescriptor(0, 0, 0), interfaceDescriptor(2, 0, 0)), want: "interface 2 is outside configuration interface count 2"},
}
for _, test := range tests {
Expand All @@ -302,12 +299,12 @@ func TestConfigurationValidatesConfigurationAttributes(t *testing.T) {
func TestConfigurationValidatesEndpointDescriptorLengths(t *testing.T) {
long := append(endpointDescriptor(0x81), 0)
long[0] = 8
if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), long)); err == nil || !strings.Contains(err.Error(), "invalid endpoint descriptor length 8") {
if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), long)); err != nil {
t.Fatalf("parseConfiguration() error = %v", err)
}
vendor := append(endpointDescriptor(0x81), 0, 0)
vendor[0] = 9
if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), vendor)); err == nil || !strings.Contains(err.Error(), "outside audio interface") {
if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), vendor)); err != nil {
t.Fatalf("parseConfiguration() error = %v", err)
}
audioInterface := interfaceDescriptor(0, 0, 1)
Expand Down Expand Up @@ -426,7 +423,7 @@ func TestActiveConfigurationRejectsUnavailableMetadata(t *testing.T) {
for _, test := range []struct {
name string
length byte
}{{name: "short device descriptor header", length: 17}, {name: "long device descriptor header", length: 19}} {
}{{name: "short device descriptor header", length: 17}, {name: "zero device descriptor header", length: 0}} {
t.Run(test.name, func(t *testing.T) {
deviceDescriptor := make([]byte, 18)
deviceDescriptor[0], deviceDescriptor[1], deviceDescriptor[17] = test.length, 1, 1
Expand All @@ -452,7 +449,6 @@ func TestActiveConfigurationRejectsShortAndMalformedDescriptors(t *testing.T) {
})
tests := [][]byte{
{10, 2, 9, 0, 0, 1, 0, 0x80, 50},
{10, 2, 19, 0, 1, 1, 0, 0x80, 50, 0, 9, 4, 0, 0, 0, 0xff, 0xff, 0xff, 0},
{9, 2, 11, 0, 0, 1, 0, 0x80, 50, 0, 4},
{9, 2, 16, 0, 0, 1, 0, 0x80, 50, 7, 5, 0x81, 2, 64, 0, 0},
{9, 2, 18, 0, 1, 1, 0, 0x80, 50, 9, 4, 0, 0, 1, 0xff, 0xff, 0xff, 0},
Expand Down
7 changes: 4 additions & 3 deletions usb/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ func NewAttachment(identity usb.DeviceInfo) Attachment { return Attachment{ident
// Identity returns the exact snapshot needed by usb.Enumerator.Open.
func (a Attachment) Identity() usb.DeviceInfo { return a.identity }

// Info returns common metadata. The fixed-width key sorts bus, address, VID,
// and PID numerically. Missing or duplicate serials cannot guarantee physical
// ordering across replugging because the host may assign another address.
// Info returns common metadata. For equal serials, registry ordering compares
// the unpadded bus:address location lexicographically, then product and the
// fixed-width identity key. Missing or duplicate serials cannot guarantee
// physical ordering across replugging because the host may assign another address.
func (a Attachment) Info() discover.AttachmentInfo {
i := a.identity
return discover.AttachmentInfo{
Expand Down
28 changes: 28 additions & 0 deletions usb/discovery/discovery_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package discovery

import (
"context"
"errors"
"reflect"
"testing"

"github.com/jon/ostiole/discover"
Expand Down Expand Up @@ -29,3 +31,29 @@ func TestRegistrationAndDependencyInstallation(t *testing.T) {
t.Fatal(err)
}
}

func TestEqualSerialUSBLocationsSortLexicographically(t *testing.T) {
attachments := []discover.Attachment{
NewAttachment(usb.DeviceInfo{Bus: 2, Address: 2, Serial: "same"}),
NewAttachment(usb.DeviceInfo{Bus: 10, Address: 2, Serial: "same"}),
NewAttachment(usb.DeviceInfo{Bus: 2, Address: 10, Serial: "same"}),
}
provider := discover.NewTransportProvider("fixture", func(context.Context) ([]discover.Attachment, error) {
return attachments, nil
})
var registry discover.Registry
if err := registry.RegisterTransport(provider); err != nil {
t.Fatal(err)
}
inventory, err := registry.Transports(t.Context())
if err != nil {
t.Fatal(err)
}
var locations []string
for transport := range inventory {
locations = append(locations, transport.Info().Location)
}
if !reflect.DeepEqual(locations, []string{"10:2", "2:10", "2:2"}) {
t.Fatalf("USB location ordering: %v", locations)
}
}
Loading
Loading