diff --git a/README.md b/README.md index e83a9f3..ade84af 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ People and coding agents changing Ostiole itself should follow the shared ## Requirements -Ostiole requires Go 1.25.12 or newer. On macOS 12 or newer, install Xcode or the +Ostiole requires Go 1.26.10 or newer. On macOS 12 or newer, install Xcode or the Xcode command-line tools. The native USB implementation uses cgo to call the system IOKit and CoreFoundation frameworks; it does not require libusb or a third-party USB package. diff --git a/docs/linux-usb.md b/docs/linux-usb.md index 9a193d2..4e20588 100644 --- a/docs/linux-usb.md +++ b/docs/linux-usb.md @@ -5,6 +5,14 @@ commands should run as an unprivileged user. Do not work around device permissions with `sudo go run` or `sudo go test`: those commands compile and execute repository code as root. +Endpoint-zero control transfers accept buffers up to 65,535 bytes. The host +timeout defaults to five seconds; a context deadline rounds up to the next +millisecond and clamps to the Linux unsigned 32-bit millisecond limit. Control +requests use the native pointer width, and their buffers remain pinned until the +ioctl returns. Interface and bulk requests use the ioctl direction encoding for +the host architecture, including the PowerPC and MIPS variants. This ABI +handling does not establish physical validation on every Linux architecture. + ## Grant device access Give the interactive user permission to open only the intended USB products. For diff --git a/go.mod b/go.mod index 80a613d..45cd5af 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/jon/ostiole -go 1.25.12 +go 1.26.10 require ( github.com/yuin/goldmark v1.8.6 - golang.org/x/net v0.58.0 + golang.org/x/net v0.62.0 ) diff --git a/go.sum b/go.sum index 08bf7ad..9f7ff36 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= diff --git a/usb/control.go b/usb/control.go index c856401..9505a9e 100644 --- a/usb/control.go +++ b/usb/control.go @@ -5,11 +5,13 @@ package usb import ( "context" "errors" + "math" + "runtime" "time" "unsafe" ) -const usbfsControl = 0xc0185500 +const usbfsControl = usbfsIOCRead | usbfsIOCWrite | uintptr(0x5500) | unsafe.Sizeof(usbControlTransfer{})<<16 const defaultTransferTimeout = 5 * time.Second @@ -24,7 +26,13 @@ type usbControlTransfer struct { } // ControlTransfer performs one deadline-bounded endpoint-zero transfer. +// Buffers may contain at most 65,535 bytes. Without a context deadline, the +// transfer timeout is five seconds. Positive deadlines round up to milliseconds +// and clamp to the host's 32-bit millisecond limit. func (d *Device) ControlTransfer(ctx context.Context, requestType, request uint8, value, index uint16, data []byte) (int, error) { + if len(data) > math.MaxUint16 { + return 0, errors.New("usb: control buffer exceeds USB limit") + } timeout, err := transferTimeout(ctx) if err != nil { return 0, err @@ -37,10 +45,14 @@ func (d *Device) ControlTransfer(ctx context.Context, requestType, request uint8 Length: uint16(len(data)), Timeout: timeout, } + var pinned runtime.Pinner if len(data) > 0 { + pinned.Pin(&data[0]) + defer pinned.Unpin() control.Data = uintptr(unsafe.Pointer(&data[0])) } count, err := d.runIOCTL(usbfsControl, &control) + runtime.KeepAlive(data) return int(count), err } @@ -58,6 +70,12 @@ func transferTimeout(ctx context.Context) (uint32, error) { return 0, context.DeadlineExceeded } } - milliseconds := (timeout + time.Millisecond - 1) / time.Millisecond + milliseconds := timeout / time.Millisecond + if timeout%time.Millisecond != 0 { + milliseconds++ + } + if milliseconds > math.MaxUint32 { + milliseconds = math.MaxUint32 + } return uint32(milliseconds), nil } diff --git a/usb/control_test.go b/usb/control_test.go index e60e28b..aff356c 100644 --- a/usb/control_test.go +++ b/usb/control_test.go @@ -6,7 +6,11 @@ import ( "context" "errors" "os" + "runtime" + "syscall" "testing" + "time" + "unsafe" ) func TestDevicePerformsBoundedControlTransfer(t *testing.T) { @@ -14,6 +18,7 @@ func TestDevicePerformsBoundedControlTransfer(t *testing.T) { if err != nil { t.Fatal(err) } + t.Cleanup(func() { _ = file.Close() }) device := &Device{file: file} var got *usbControlTransfer device.ioctl = func(_ uintptr, request uintptr, argument any) (uintptr, error) { @@ -40,6 +45,112 @@ func TestDevicePerformsBoundedControlTransfer(t *testing.T) { } } +func TestControlTransferNativeABI(t *testing.T) { + wantSize, wantOffset, wantRequest := uintptr(24), uintptr(16), uintptr(0xc0185500) + if unsafe.Sizeof(uintptr(0)) == 4 { + wantSize, wantOffset, wantRequest = 16, 12, 0xc0105500 + } + if unsafe.Sizeof(usbControlTransfer{}) != wantSize || unsafe.Offsetof(usbControlTransfer{}.Data) != wantOffset { + t.Fatal("control structure does not match the Linux USB ABI") + } + if uintptr(usbfsControl) != wantRequest { + t.Fatalf("control request = %#x, want %#x", uintptr(usbfsControl), wantRequest) + } +} + +func TestControlTransferBufferBounds(t *testing.T) { + file, err := os.CreateTemp(t.TempDir(), "usb-device") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = file.Close() }) + device := &Device{file: file} + data := make([]byte, 65535) + called := 0 + device.ioctl = func(_ uintptr, _ uintptr, argument any) (uintptr, error) { + called++ + control := argument.(*usbControlTransfer) + runtime.GC() + if control.Length != 65535 || control.Data != uintptr(unsafe.Pointer(&data[0])) { + t.Fatal("control transfer did not retain the complete buffer") + } + data[len(data)-1] = 0xa5 + return uintptr(len(data)), nil + } + count, err := device.ControlTransfer(context.Background(), 0x80, 1, 0, 0, data) + if err != nil || count != len(data) || data[len(data)-1] != 0xa5 { + t.Fatalf("ControlTransfer() = %d, %v", count, err) + } + if _, err := device.ControlTransfer(context.Background(), 0x80, 1, 0, 0, make([]byte, 65536)); err == nil { + t.Fatal("oversized control buffer was accepted") + } + if called != 1 { + t.Fatalf("ioctl calls = %d, want 1", called) + } +} + +func TestControlTransferDistantDeadline(t *testing.T) { + for _, duration := range []time.Duration{50 * 24 * time.Hour, time.Duration(1<<63 - 1)} { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(duration)) + got, err := transferTimeout(ctx) + cancel() + if err != nil || got != 0xffffffff { + t.Fatalf("timeout for %v = %d, %v; want 4294967295", duration, got, err) + } + } +} + +func TestControlTransferPinsStackBuffer(t *testing.T) { + file, err := os.CreateTemp(t.TempDir(), "control-input") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = file.Close() }) + if _, err := file.WriteAt([]byte{0xa5}, 0); err != nil { + t.Fatal(err) + } + device := &Device{file: file} + device.ioctl = func(fd, _ uintptr, argument any) (uintptr, error) { + control := argument.(*usbControlTransfer) + return controlReadAfterStackGrowth(64, fd, control.Data) + } + done := make(chan error, 1) + go func() { done <- controlTransferStackBuffer(device) }() + if err := <-done; err != nil { + t.Fatal(err) + } +} + +func controlTransferStackBuffer(device *Device) error { + var data [16]byte + count, err := device.ControlTransfer(context.Background(), 0x80, 1, 0, 0, data[:]) + if err != nil { + return err + } + if count != 1 || data[0] != 0xa5 { + return errors.New("control input did not reach the caller's buffer after stack growth") + } + return nil +} + +func controlReadAfterStackGrowth(depth int, fd, address uintptr) (uintptr, error) { + var padding [4096]byte + padding[0] = byte(depth) + var count uintptr + var err error + if depth > 0 { + count, err = controlReadAfterStackGrowth(depth-1, fd, address) + } else { + result, _, errno := syscall.Syscall(syscall.SYS_READ, fd, address, 1) + count = result + if errno != 0 { + err = errno + } + } + runtime.KeepAlive(padding) + return count, err +} + func TestControlTransferHonorsPreCanceledContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/usb/interface.go b/usb/interface.go index 436aad9..0b70028 100644 --- a/usb/interface.go +++ b/usb/interface.go @@ -10,9 +10,9 @@ import ( ) const ( - usbfsClaimInterface = 0x8004550f - usbfsReleaseInterface = 0x80045510 - usbfsSetInterface = 0x80085504 + usbfsClaimInterface = usbfsIOCRead | uintptr(0x4550f) + usbfsReleaseInterface = usbfsIOCRead | uintptr(0x45510) + usbfsSetInterface = usbfsIOCRead | uintptr(0x85504) ) type ioctlFunc func(fd, request uintptr, argument any) (uintptr, error) diff --git a/usb/ioctl_extended_linux.go b/usb/ioctl_extended_linux.go new file mode 100644 index 0000000..6672f26 --- /dev/null +++ b/usb/ioctl_extended_linux.go @@ -0,0 +1,9 @@ +//go:build linux && (ppc64 || ppc64le || mips || mipsle || mips64 || mips64le) + +package usb + +const ( + usbfsIOCNone = uintptr(0x20000000) + usbfsIOCRead = uintptr(0x40000000) + usbfsIOCWrite = uintptr(0x80000000) +) diff --git a/usb/ioctl_extended_linux_test.go b/usb/ioctl_extended_linux_test.go new file mode 100644 index 0000000..4d6429e --- /dev/null +++ b/usb/ioctl_extended_linux_test.go @@ -0,0 +1,21 @@ +//go:build linux && (ppc64 || ppc64le || mips || mipsle || mips64 || mips64le) + +package usb + +import "unsafe" + +const ( + expectedControl = uintptr(0xc0105500) + (unsafe.Sizeof(uintptr(0))/8)*0x80000 + expectedSubmitURB = uintptr(0x402c550a) + (unsafe.Sizeof(uintptr(0))/8)*0xc0000 + expectedReapURB = uintptr(0x8000550d) + unsafe.Sizeof(uintptr(0))*0x10000 +) + +var ( + _ [0 - int(usbfsControl^expectedControl)]byte + _ [0 - int(usbfsClaimInterface^uintptr(0x4004550f))]byte + _ [0 - int(usbfsReleaseInterface^uintptr(0x40045510))]byte + _ [0 - int(usbfsSetInterface^uintptr(0x40085504))]byte + _ [0 - int(usbfsDiscardURB^uintptr(0x2000550b))]byte + _ [0 - int(usbfsSubmitURB^expectedSubmitURB)]byte + _ [0 - int(usbfsReapURBNoDelay^expectedReapURB)]byte +) diff --git a/usb/ioctl_generic_linux.go b/usb/ioctl_generic_linux.go new file mode 100644 index 0000000..6be9316 --- /dev/null +++ b/usb/ioctl_generic_linux.go @@ -0,0 +1,9 @@ +//go:build linux && !(ppc64 || ppc64le || mips || mipsle || mips64 || mips64le) + +package usb + +const ( + usbfsIOCNone = uintptr(0) + usbfsIOCRead = uintptr(0x80000000) + usbfsIOCWrite = uintptr(0x40000000) +) diff --git a/usb/ioctl_generic_linux_test.go b/usb/ioctl_generic_linux_test.go new file mode 100644 index 0000000..9c3a8dc --- /dev/null +++ b/usb/ioctl_generic_linux_test.go @@ -0,0 +1,21 @@ +//go:build linux && !(ppc64 || ppc64le || mips || mipsle || mips64 || mips64le) + +package usb + +import "unsafe" + +const ( + expectedControl = uintptr(0xc0105500) + (unsafe.Sizeof(uintptr(0))/8)*0x80000 + expectedSubmitURB = uintptr(0x802c550a) + (unsafe.Sizeof(uintptr(0))/8)*0xc0000 + expectedReapURB = uintptr(0x4000550d) + unsafe.Sizeof(uintptr(0))*0x10000 +) + +var ( + _ [0 - int(usbfsControl^expectedControl)]byte + _ [0 - int(usbfsClaimInterface^uintptr(0x8004550f))]byte + _ [0 - int(usbfsReleaseInterface^uintptr(0x80045510))]byte + _ [0 - int(usbfsSetInterface^uintptr(0x80085504))]byte + _ [0 - int(usbfsDiscardURB^uintptr(0x550b))]byte + _ [0 - int(usbfsSubmitURB^expectedSubmitURB)]byte + _ [0 - int(usbfsReapURBNoDelay^expectedReapURB)]byte +) diff --git a/usb/transfer_linux.go b/usb/transfer_linux.go index 66e1909..864f5eb 100644 --- a/usb/transfer_linux.go +++ b/usb/transfer_linux.go @@ -16,12 +16,12 @@ import ( const ( usbfsURBBulk = 3 - usbfsDiscardURB = uintptr(0x550b) + usbfsDiscardURB = usbfsIOCNone | uintptr(0x550b) ) const ( - usbfsSubmitURB = uintptr(0x8000550a) | unsafe.Sizeof(usbURB{})<<16 - usbfsReapURBNoDelay = uintptr(0x4000550d) | unsafe.Sizeof(uintptr(0))<<16 + usbfsSubmitURB = usbfsIOCRead | uintptr(0x550a) | unsafe.Sizeof(usbURB{})<<16 + usbfsReapURBNoDelay = usbfsIOCWrite | uintptr(0x550d) | unsafe.Sizeof(uintptr(0))<<16 ) type usbURB struct {