usbip: fix lease identity, linux replacement, and darwin stalls

This commit is contained in:
世界 2026-05-15 18:13:36 +08:00
parent 8799b10b9a
commit 249ba662c4
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
11 changed files with 534 additions and 88 deletions

View file

@ -127,6 +127,7 @@ type serverImportLease struct {
BusID string
ClientNonce uint64
Generation uint64
Identity ExportLeaseIdentity
Expires time.Time
}

View file

@ -157,6 +157,7 @@ func (l *exportLedger) TryReserveForImport(ctx context.Context, busid string) (E
if !found {
return nil, false, "unknown busid"
}
identity := export.LeaseIdentity()
if busy || leased {
return nil, false, deviceStateBusy
}
@ -167,7 +168,7 @@ func (l *exportLedger) TryReserveForImport(ctx context.Context, busid string) (E
l.slow.Lock()
defer l.slow.Unlock()
current, stillExported := l.exports[busid]
if !stillExported || current != export {
if !stillExported || current.LeaseIdentity() != identity {
return nil, false, "unknown busid"
}
if l.busy[busid] {
@ -190,9 +191,9 @@ func (l *exportLedger) ReleaseImport(ctx context.Context, busid string, removeEx
l.BroadcastIfChanged(ctx)
}
// IssueLease captures the seq generation at entry so a subsequent
// ConsumeLeaseAndReserve can reject stale leases issued before a
// topology change.
// IssueLease captures the current broadcast sequence as opaque metadata
// for control clients. Lease correctness itself is pinned to the
// export's internal identity, not to that sequence number.
func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request controlLeaseRequest) controlLeaseResponse {
response := controlLeaseResponse{
BusID: request.BusID,
@ -232,6 +233,7 @@ func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request con
}
l.slow.Unlock()
identity := export.LeaseIdentity()
leaseOK, leaseReason := export.LeaseCheck(ctx)
if !leaseOK {
response.ErrorCode = leaseErrorUnavailable
@ -244,7 +246,7 @@ func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request con
now = l.now()
l.cleanupExpiredLocked(now)
current, stillExported := l.exports[request.BusID]
if !stillExported || current != export {
if !stillExported || current.LeaseIdentity() != identity {
response.ErrorCode = leaseErrorUnavailable
response.ErrorMessage = "unknown busid"
return response
@ -266,6 +268,7 @@ func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request con
BusID: request.BusID,
ClientNonce: request.ClientNonce,
Generation: generation,
Identity: current.LeaseIdentity(),
Expires: now.Add(l.ttl),
}
l.leases[request.BusID] = lease
@ -275,60 +278,101 @@ func (l *exportLedger) IssueLease(ctx context.Context, subID uint64, request con
return response
}
// ConsumeLeaseAndReserve atomically validates a lease, removes its entry,
// and marks the busid busy under a single slow-lock critical section so
// no concurrent import can observe the consume-before-reserve gap, and
// no concurrent broadcast can observe a transient busy=true that we
// later roll back. The generation snapshot is taken before the slow
// critical section so the comparison either matches the lease exactly
// or is strictly stale (the safe rejection direction): a broadcast that
// fires between the snapshot and slow.Lock can only advance seq.
// ConsumeLeaseAndReserve validates the requested lease against the
// current export identity, reruns LeaseCheck outside the slow lock, then
// atomically consumes the lease and marks the busid busy. Correctness is
// tied to the export identity rather than to the control sequence.
//
// Consume-on-read semantics from the old ConsumeLease are preserved:
// the lease entry is removed on every outcome except a nonce/ID
// mismatch (which preserves the lease for the legitimate holder).
// LeaseCheck is not re-run — the lease attests that it passed at issue
// time, and the generation equality test confirms the export has not
// been reconciled since. The caller must pair every success with a
// later ReleaseImport.
func (l *exportLedger) ConsumeLeaseAndReserve(request ImportExtRequest) (Export, bool, string) {
l.fast.Lock()
currentGeneration := l.seq
l.fast.Unlock()
// The caller must pair every success with a later ReleaseImport.
func (l *exportLedger) ConsumeLeaseAndReserve(ctx context.Context, request ImportExtRequest) (Export, bool, string) {
var (
export Export
identity ExportLeaseIdentity
)
l.slow.Lock()
defer l.slow.Unlock()
now := l.now()
l.cleanupExpiredLocked(now)
lease, found := l.leases[request.BusID]
if !found {
l.slow.Unlock()
return nil, false, "lease not found"
}
if lease.ID != request.LeaseID || lease.ClientNonce != request.ClientNonce {
l.slow.Unlock()
return nil, false, "lease mismatch"
}
if !now.Before(lease.Expires) {
delete(l.leases, request.BusID)
l.slow.Unlock()
return nil, false, "lease expired"
}
export, stillExported := l.exports[request.BusID]
if !stillExported {
delete(l.leases, request.BusID)
l.slow.Unlock()
return nil, false, "unknown busid"
}
identity = export.LeaseIdentity()
if identity != lease.Identity {
delete(l.leases, request.BusID)
l.slow.Unlock()
return nil, false, "lease stale"
}
if l.busy[request.BusID] {
delete(l.leases, request.BusID)
l.slow.Unlock()
return nil, false, deviceStateBusy
}
l.slow.Unlock()
leaseOK, leaseReason := export.LeaseCheck(ctx)
if !leaseOK {
l.slow.Lock()
currentLease, exists := l.leases[request.BusID]
if exists && currentLease.ID == request.LeaseID && currentLease.ClientNonce == request.ClientNonce {
delete(l.leases, request.BusID)
}
l.slow.Unlock()
return nil, false, leaseReason
}
l.slow.Lock()
defer l.slow.Unlock()
now = l.now()
l.cleanupExpiredLocked(now)
lease, found = l.leases[request.BusID]
if !found {
return nil, false, "lease not found"
}
if lease.ID != request.LeaseID || lease.ClientNonce != request.ClientNonce {
return nil, false, "lease mismatch"
}
if lease.Generation != currentGeneration {
delete(l.leases, request.BusID)
return nil, false, "lease stale"
}
if !now.Before(lease.Expires) {
delete(l.leases, request.BusID)
return nil, false, "lease expired"
}
export, stillExported := l.exports[request.BusID]
current, stillExported := l.exports[request.BusID]
if !stillExported {
delete(l.leases, request.BusID)
return nil, false, "unknown busid"
}
if lease.Identity != identity || current.LeaseIdentity() != identity {
delete(l.leases, request.BusID)
return nil, false, "lease stale"
}
if l.busy[request.BusID] {
delete(l.leases, request.BusID)
return nil, false, deviceStateBusy
}
delete(l.leases, request.BusID)
l.busy[request.BusID] = true
return export, true, ""
return current, true, ""
}
func (l *exportLedger) cleanupExpiredLocked(now time.Time) {

View file

@ -53,11 +53,105 @@ func TestSubscribeRetriesSnapshotWhenSequenceAdvances(t *testing.T) {
}
}
func TestConsumeLeaseAndReserveRejectsIdentityReplacement(t *testing.T) {
ctx := context.Background()
ledger := newExportLedger(nil, time.Second, func() time.Time { return time.Unix(0, 0) })
original := &testExport{busid: "1-1", vendorID: 0x1111, productID: 0x0001, identity: "linux:original"}
replacement := &testExport{busid: "1-1", vendorID: 0x1111, productID: 0x0001, identity: "linux:replacement"}
ledger.ApplyHostSnapshot(map[string]Export{original.busid: original}, nil)
lease := ledger.IssueLease(ctx, 1, controlLeaseRequest{BusID: original.busid, ClientNonce: 7})
ledger.ApplyHostSnapshot(map[string]Export{replacement.busid: replacement}, nil)
_, ok, reason := ledger.ConsumeLeaseAndReserve(ctx, ImportExtRequest{
BusID: original.busid,
LeaseID: lease.LeaseID,
ClientNonce: lease.ClientNonce,
})
if ok {
t.Fatal("expected replaced export lease to be rejected")
}
if reason != "lease stale" {
t.Fatalf("expected lease stale, got %q", reason)
}
}
func TestConsumeLeaseAndReserveRejectsUnavailableExport(t *testing.T) {
ctx := context.Background()
ledger := newExportLedger(nil, time.Second, func() time.Time { return time.Unix(0, 0) })
available := true
exp := &testExport{
busid: "1-1",
vendorID: 0x1111,
productID: 0x0001,
leaseCheck: func(context.Context) (bool, string) {
if available {
return true, ""
}
return false, "capture released"
},
}
ledger.ApplyHostSnapshot(map[string]Export{exp.busid: exp}, nil)
lease := ledger.IssueLease(ctx, 1, controlLeaseRequest{BusID: exp.busid, ClientNonce: 9})
available = false
_, ok, reason := ledger.ConsumeLeaseAndReserve(ctx, ImportExtRequest{
BusID: exp.busid,
LeaseID: lease.LeaseID,
ClientNonce: lease.ClientNonce,
})
if ok {
t.Fatal("expected unavailable export lease to be rejected")
}
if reason != "capture released" {
t.Fatalf("expected capture released, got %q", reason)
}
_, ok, reason = ledger.ConsumeLeaseAndReserve(ctx, ImportExtRequest{
BusID: exp.busid,
LeaseID: lease.LeaseID,
ClientNonce: lease.ClientNonce,
})
if ok {
t.Fatal("expected consumed lease to stay unavailable on retry")
}
if reason != "lease not found" {
t.Fatalf("expected consumed lease to disappear, got %q", reason)
}
}
func TestConsumeLeaseAndReserveMarksBusyOnSuccess(t *testing.T) {
ctx := context.Background()
ledger := newExportLedger(nil, time.Second, func() time.Time { return time.Unix(0, 0) })
exp := &testExport{busid: "1-1", vendorID: 0x1111, productID: 0x0001}
ledger.ApplyHostSnapshot(map[string]Export{exp.busid: exp}, nil)
lease := ledger.IssueLease(ctx, 1, controlLeaseRequest{BusID: exp.busid, ClientNonce: 11})
reserved, ok, reason := ledger.ConsumeLeaseAndReserve(ctx, ImportExtRequest{
BusID: exp.busid,
LeaseID: lease.LeaseID,
ClientNonce: lease.ClientNonce,
})
if !ok {
t.Fatalf("expected lease reservation success, got %q", reason)
}
if reserved != exp {
t.Fatal("expected to reserve the original export instance")
}
if !ledger.IsBusy(exp.busid) {
t.Fatal("expected successful lease reservation to mark busid busy")
}
}
type testExport struct {
busid string
vendorID uint16
productID uint16
identity ExportLeaseIdentity
leaseCheck func(context.Context) (bool, string)
onSnapshot func()
}
@ -65,6 +159,13 @@ func (e *testExport) BusID() string {
return e.busid
}
func (e *testExport) LeaseIdentity() ExportLeaseIdentity {
if e.identity != "" {
return e.identity
}
return ExportLeaseIdentity(e.busid)
}
func (e *testExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
onSnapshot := e.onSnapshot
e.onSnapshot = nil
@ -80,6 +181,9 @@ func (e *testExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
}
func (e *testExport) LeaseCheck(ctx context.Context) (bool, string) {
if e.leaseCheck != nil {
return e.leaseCheck(ctx)
}
return true, ""
}

View file

@ -28,9 +28,12 @@ type ImportHost interface {
Attach(ctx context.Context, info DeviceInfoTruncated, conn net.Conn) (AttachedSession, error)
}
type ExportLeaseIdentity string
type Export interface {
BusID() string
Snapshot(ctx context.Context, busy bool) ExportSnapshot
LeaseIdentity() ExportLeaseIdentity
LeaseCheck(ctx context.Context) (ok bool, reason string)
DeviceInfo(ctx context.Context) (DeviceInfoTruncated, error)
NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error)

View file

@ -282,6 +282,10 @@ func (e *darwinExport) BusID() string {
return e.busid
}
func (e *darwinExport) LeaseIdentity() ExportLeaseIdentity {
return ExportLeaseIdentity(fmt.Sprintf("darwin:%016x", e.registryID))
}
func (e *darwinExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
stableID := fmt.Sprintf("darwin-registry:%016x", e.registryID)
if e.stale {

View file

@ -10,6 +10,7 @@ import (
"net"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
@ -50,6 +51,90 @@ func linuxUSBIPStatusReason(status int) string {
}
}
type linuxExportIdentity struct {
BusNum uint32
DevNum uint32
Speed uint32
VendorID uint16
ProductID uint16
BCDDevice uint16
DeviceClass uint8
DeviceSubClass uint8
DeviceProtocol uint8
ConfigValue uint8
NumConfigs uint8
NumInterfaces uint8
Serial string
Interfaces []DeviceInterface
}
func newLinuxExportIdentity(descriptor sysfsDevice) linuxExportIdentity {
return linuxExportIdentity{
BusNum: descriptor.BusNum,
DevNum: descriptor.DevNum,
Speed: descriptor.Speed,
VendorID: descriptor.VendorID,
ProductID: descriptor.ProductID,
BCDDevice: descriptor.BCDDevice,
DeviceClass: descriptor.DeviceClass,
DeviceSubClass: descriptor.DeviceSubClass,
DeviceProtocol: descriptor.DeviceProtocol,
ConfigValue: descriptor.ConfigValue,
NumConfigs: descriptor.NumConfigs,
NumInterfaces: descriptor.NumInterfaces,
Serial: descriptor.Serial,
Interfaces: slices.Clone(descriptor.Interfaces),
}
}
func (i linuxExportIdentity) Equal(other linuxExportIdentity) bool {
if i.BusNum != other.BusNum ||
i.DevNum != other.DevNum ||
i.Speed != other.Speed ||
i.VendorID != other.VendorID ||
i.ProductID != other.ProductID ||
i.BCDDevice != other.BCDDevice ||
i.DeviceClass != other.DeviceClass ||
i.DeviceSubClass != other.DeviceSubClass ||
i.DeviceProtocol != other.DeviceProtocol ||
i.ConfigValue != other.ConfigValue ||
i.NumConfigs != other.NumConfigs ||
i.NumInterfaces != other.NumInterfaces ||
i.Serial != other.Serial ||
len(i.Interfaces) != len(other.Interfaces) {
return false
}
for index := range i.Interfaces {
if i.Interfaces[index] != other.Interfaces[index] {
return false
}
}
return true
}
func (i linuxExportIdentity) LeaseIdentity() ExportLeaseIdentity {
var builder strings.Builder
fmt.Fprintf(&builder, "linux:%d:%d:%d:%04x:%04x:%04x:%02x:%02x:%02x:%02x:%02x:%02x:%s",
i.BusNum,
i.DevNum,
i.Speed,
i.VendorID,
i.ProductID,
i.BCDDevice,
i.DeviceClass,
i.DeviceSubClass,
i.DeviceProtocol,
i.ConfigValue,
i.NumConfigs,
i.NumInterfaces,
i.Serial,
)
for _, iface := range i.Interfaces {
fmt.Fprintf(&builder, "|%02x.%02x.%02x", iface.BInterfaceClass, iface.BInterfaceSubClass, iface.BInterfaceProtocol)
}
return ExportLeaseIdentity(builder.String())
}
type linuxExportHost struct {
logger log.ContextLogger
matches []option.USBIPDeviceMatch
@ -58,6 +143,13 @@ type linuxExportHost struct {
exports map[string]*linuxExport
}
type linuxReconcilePlan struct {
toRelease []*linuxExport
toStale []string
toBind map[string]sysfsDevice
released []string
}
func newLinuxExportHost(logger log.ContextLogger, matches []option.USBIPDeviceMatch) *linuxExportHost {
return &linuxExportHost{
logger: logger,
@ -76,9 +168,7 @@ func (h *linuxExportHost) Close() error {
h.exports = make(map[string]*linuxExport)
h.access.Unlock()
for _, exp := range exports {
_, statErr := os.Stat(filepath.Join(sysBusUSBDevices, exp.busid))
restore := statErr == nil
releaseErr := h.releaseExport(exp, restore)
releaseErr := h.releaseExport(exp, exp.shouldRestoreCurrentDevice())
if releaseErr != nil {
h.logger.Warn("rollback ", exp.busid, ": ", releaseErr)
}
@ -155,16 +245,48 @@ const (
ueventListenerBackoffMax = 30 * time.Second
)
func classifyLinuxReconcile(current map[string]*linuxExport, desired map[string]sysfsDevice, isBusy func(busid string) bool) linuxReconcilePlan {
remainingDesired := maps.Clone(desired)
plan := linuxReconcilePlan{
toBind: make(map[string]sysfsDevice),
}
for busid, exp := range current {
device, wanted := remainingDesired[busid]
busy := isBusy(busid)
identityMatches := wanted && exp.identity.Equal(newLinuxExportIdentity(device))
switch {
case exp.stale:
if busy {
delete(remainingDesired, busid)
continue
}
plan.toRelease = append(plan.toRelease, exp)
plan.released = append(plan.released, busid)
case identityMatches:
delete(remainingDesired, busid)
case busy:
plan.toStale = append(plan.toStale, busid)
delete(remainingDesired, busid)
default:
plan.toRelease = append(plan.toRelease, exp)
plan.released = append(plan.released, busid)
}
}
for busid, device := range remainingDesired {
if isBusy(busid) {
continue
}
plan.toBind[busid] = device
}
return plan
}
func (h *linuxExportHost) Reconcile(ctx context.Context, isBusy func(busid string) bool) (map[string]Export, []string, error) {
devices, err := listUSBDevices()
if err != nil {
return h.snapshotSelf(), nil, E.Cause(err, "enumerate usb devices")
}
desired := make(map[string]sysfsDevice)
present := make(map[string]struct{}, len(devices))
for i := range devices {
present[devices[i].BusID] = struct{}{}
}
for _, m := range h.matches {
for i := range devices {
deviceKey := DeviceKey{
@ -201,36 +323,42 @@ func (h *linuxExportHost) Reconcile(ctx context.Context, isBusy func(busid strin
maps.Copy(current, h.exports)
h.access.Unlock()
for busid, device := range desired {
if _, ok := current[busid]; ok {
continue
plan := classifyLinuxReconcile(current, desired, isBusy)
toAdd := make([]*linuxExport, 0, len(plan.toBind))
for _, exp := range plan.toRelease {
err := h.releaseExport(exp, false)
if err != nil {
h.logger.Warn("release ", exp.busid, ": ", err)
}
}
for busid, device := range plan.toBind {
exp, bindErr := h.bindOne(&device)
if bindErr != nil {
return h.snapshotSelf(), nil, E.Cause(bindErr, "bind ", busid)
}
h.access.Lock()
h.exports[busid] = exp
h.access.Unlock()
toAdd = append(toAdd, exp)
}
var released []string
for busid, exp := range current {
if _, ok := desired[busid]; ok {
continue
h.access.Lock()
for _, busid := range plan.toStale {
exp, ok := h.exports[busid]
if ok {
exp.stale = true
}
_, restore := present[busid]
err := h.releaseExport(exp, restore)
if err != nil {
h.logger.Warn("release ", busid, ": ", err)
}
h.access.Lock()
delete(h.exports, busid)
h.access.Unlock()
released = append(released, busid)
}
for _, exp := range plan.toRelease {
currentExport, ok := h.exports[exp.busid]
if ok && currentExport == exp {
delete(h.exports, exp.busid)
}
}
for _, exp := range toAdd {
h.exports[exp.busid] = exp
}
h.access.Unlock()
return h.snapshotSelf(), released, nil
return h.snapshotSelf(), plan.released, nil
}
func (h *linuxExportHost) FinishImport(ctx context.Context, busid string) (bool, error) {
@ -239,7 +367,23 @@ func (h *linuxExportHost) FinishImport(ctx context.Context, busid string) (bool,
h.logger.Debug("release ", busid, " from usbip-host: ", err)
}
waitForUsbipStatusCleared(ctx, busid)
return false, nil
h.access.Lock()
exp, ok := h.exports[busid]
h.access.Unlock()
if !ok || !exp.stale {
return false, nil
}
releaseErr := h.releaseExport(exp, false)
h.access.Lock()
current, stillPresent := h.exports[busid]
if stillPresent && current == exp {
delete(h.exports, busid)
}
h.access.Unlock()
if releaseErr != nil {
h.logger.Warn("release stale ", busid, ": ", releaseErr)
}
return true, E.Errors(err, releaseErr)
}
func (h *linuxExportHost) snapshotSelf() map[string]Export {
@ -247,6 +391,9 @@ func (h *linuxExportHost) snapshotSelf() map[string]Export {
defer h.access.Unlock()
out := make(map[string]Export, len(h.exports))
for busid, exp := range h.exports {
if exp.stale {
continue
}
out[busid] = exp
}
return out
@ -343,7 +490,7 @@ func (h *linuxExportHost) releaseExport(exp *linuxExport, restore bool) error {
return err
}
if !restore {
h.logger.Info("removed export state for disappeared device ", exp.busid)
h.logger.Info("removed export state for ", exp.busid)
return nil
}
if exp.originalDriver == "" {
@ -362,6 +509,7 @@ func (h *linuxExportHost) newExport(descriptor sysfsDevice, managed bool, origin
return &linuxExport{
busid: descriptor.BusID,
descriptor: descriptor,
identity: newLinuxExportIdentity(descriptor),
managed: managed,
originalDriver: originalDriver,
logger: h.logger,
@ -373,20 +521,39 @@ func (h *linuxExportHost) newExport(descriptor sysfsDevice, managed bool, origin
type linuxExport struct {
busid string
descriptor sysfsDevice
identity linuxExportIdentity
managed bool
originalDriver string
logger log.ContextLogger
stale bool
}
func (e *linuxExport) BusID() string {
return e.busid
}
func (e *linuxExport) LeaseIdentity() ExportLeaseIdentity {
return e.identity.LeaseIdentity()
}
func (e *linuxExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
stableID := "linux-busid:" + e.descriptor.BusID
if e.descriptor.Serial != "" {
stableID = fmt.Sprintf("usb:%04x:%04x:%s", e.descriptor.VendorID, e.descriptor.ProductID, e.descriptor.Serial)
}
if e.stale {
return ExportSnapshot{
Entry: DeviceEntry{
Info: e.descriptor.toProtocol(),
Interfaces: e.descriptor.Interfaces,
Serial: e.descriptor.Serial,
},
Backend: backendIDLinuxSysfs,
StableID: stableID,
State: deviceStateUnavailable,
StatusReason: "device replaced",
}
}
status, statusErr := readUsbipStatus(e.busid)
var state, reason string
switch {
@ -422,6 +589,9 @@ func (e *linuxExport) Snapshot(ctx context.Context, busy bool) ExportSnapshot {
}
func (e *linuxExport) LeaseCheck(ctx context.Context) (bool, string) {
if e.stale {
return false, "device replaced"
}
status, err := readUsbipStatus(e.busid)
if err != nil {
return false, err.Error()
@ -437,6 +607,9 @@ func (e *linuxExport) DeviceInfo(ctx context.Context) (DeviceInfoTruncated, erro
}
func (e *linuxExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) {
if e.stale {
return nil, E.New("linux export ", e.busid, " is stale")
}
handoff, err := newKernelHandoffSession(ctx, conn, e.logger, "server", e.busid)
if err != nil {
return nil, E.Cause(err, "prepare handoff")
@ -458,6 +631,17 @@ func (e *linuxExport) NewServerDataSession(ctx context.Context, conn net.Conn) (
return handoff, nil
}
func (e *linuxExport) shouldRestoreCurrentDevice() bool {
if e.originalDriver == "" || e.stale {
return false
}
descriptor, err := readSysfsDevice(e.busid, filepath.Join(sysBusUSBDevices, e.busid))
if err != nil {
return false
}
return e.identity.Equal(newLinuxExportIdentity(descriptor))
}
type linuxImportHost struct {
logger log.ContextLogger

View file

@ -282,7 +282,7 @@ func (s *ServerService) handleImportExt(conn net.Conn) bool {
s.logger.Debug("read import-ext body: ", err)
return false
}
export, ok, reason := s.ledger.ConsumeLeaseAndReserve(request)
export, ok, reason := s.ledger.ConsumeLeaseAndReserve(s.ctx, request)
if !ok {
s.logger.Info("import-ext rejected (", request.BusID, ": ", reason, ")")
_ = WriteOpRepImport(conn, OpRepImportExt, OpStatusError, nil)

View file

@ -529,6 +529,8 @@ func darwinIOReturnToUSBIPStatus(status int32) int32 {
return 0
}
switch status {
case int32(C.kIOUSBPipeStalled), int32(C.kUSBHostReturnPipeStalled):
return -int32(unix.EPIPE)
case int32(C.kIOReturnAborted), int32(C.kIOReturnNotResponding):
return -int32(unix.ECONNRESET)
case int32(C.kIOReturnNoDevice), int32(C.kIOReturnOffline):
@ -551,6 +553,8 @@ func darwinUSBIPStatusToCIStatus(status int32) int {
return int(C.IOUSBHostCIMessageStatusSuccess)
}
switch -status {
case int32(unix.EPIPE):
return int(C.IOUSBHostCIMessageStatusStallError)
case int32(unix.ETIMEDOUT):
return int(C.IOUSBHostCIMessageStatusTimeout)
case int32(unix.ENOMEM):

View file

@ -3,6 +3,8 @@
#include <stdint.h>
#include <IOUSBHost/IOUSBHost.h>
#include <IOKit/usb/IOUSBHostFamilyDefinitions.h>
#include <IOKit/usb/USB.h>
#define BOX_USBHOST_MAX_INTERFACES 255

View file

@ -331,6 +331,101 @@ static IOUSBHostInterface *box_interface_for_number(BoxUSBHostDevice *box, uint8
return nil;
}
static void box_refresh_interfaces(BoxUSBHostDevice *box) {
[box withStateLock:^{
box.pipes = [NSMutableDictionary dictionary];
box_load_interfaces(box);
}];
}
static uint8_t box_request_direction(IOUSBDeviceRequest request) {
return request.bmRequestType & kIOUSBDeviceRequestDirectionMask;
}
static uint8_t box_request_type(IOUSBDeviceRequest request) {
return request.bmRequestType & kIOUSBDeviceRequestTypeMask;
}
static uint8_t box_request_recipient(IOUSBDeviceRequest request) {
return request.bmRequestType & kIOUSBDeviceRequestRecipientMask;
}
static BOOL box_request_is_set_configuration(IOUSBDeviceRequest request) {
return box_request_direction(request) == kIOUSBDeviceRequestDirectionOut &&
box_request_type(request) == kIOUSBDeviceRequestTypeStandard &&
box_request_recipient(request) == kIOUSBDeviceRequestRecipientDevice &&
request.bRequest == kIOUSBDeviceRequestSetConfiguration &&
request.wIndex == 0 &&
request.wLength == 0;
}
static BOOL box_request_is_set_interface(IOUSBDeviceRequest request) {
return box_request_direction(request) == kIOUSBDeviceRequestDirectionOut &&
box_request_type(request) == kIOUSBDeviceRequestTypeStandard &&
box_request_recipient(request) == kIOUSBDeviceRequestRecipientInterface &&
request.bRequest == kIOUSBDeviceRequestSetInterface &&
request.wLength == 0;
}
static BOOL box_request_is_clear_endpoint_halt(IOUSBDeviceRequest request) {
return box_request_direction(request) == kIOUSBDeviceRequestDirectionOut &&
box_request_type(request) == kIOUSBDeviceRequestTypeStandard &&
box_request_recipient(request) == kIOUSBDeviceRequestRecipientEndpoint &&
request.bRequest == kIOUSBDeviceRequestClearFeature &&
request.wValue == IOUSBEndpointFeatureSelectorStall &&
request.wLength == 0;
}
static BOOL box_handle_set_configuration(BoxUSBHostDevice *box, IOUSBDeviceRequest request, NSError **error) {
BOOL ok = [box.device configureWithValue:request.wValue matchInterfaces:YES error:error];
if (ok) {
box_refresh_interfaces(box);
}
return ok;
}
static BOOL box_handle_set_interface(BoxUSBHostDevice *box, IOUSBDeviceRequest request, NSError **error) {
__block IOUSBHostInterface *interface = nil;
[box withStateLock:^{
interface = box_interface_for_number(box, request.wIndex & 0xff);
}];
if (interface == nil) {
*error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnNotFound userInfo:nil];
return NO;
}
BOOL ok = [interface selectAlternateSetting:request.wValue error:error];
if (ok) {
box_refresh_interfaces(box);
}
return ok;
}
static BOOL box_handle_clear_endpoint_halt(BoxUSBHostDevice *box, IOUSBDeviceRequest request, NSError **error) {
uint8_t endpoint = (uint8_t)(request.wIndex & 0xff);
if ((endpoint & kIOUSBEndpointDescriptorNumber) == 0) {
return [box.device sendDeviceRequest:request data:nil bytesTransferred:NULL completionTimeout:IOUSBHostDefaultControlCompletionTimeout error:error];
}
IOUSBHostPipe *pipe = box_pipe_for_endpoint(box, endpoint);
if (pipe == nil) {
*error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnNotFound userInfo:nil];
return NO;
}
return [pipe clearStallWithError:error];
}
static BOOL box_dispatch_control_request(BoxUSBHostDevice *box, IOUSBDeviceRequest request, NSMutableData *payload, NSUInteger *actual, NSError **error) {
if (box_request_is_set_configuration(request)) {
return box_handle_set_configuration(box, request, error);
}
if (box_request_is_set_interface(request)) {
return box_handle_set_interface(box, request, error);
}
if (box_request_is_clear_endpoint_halt(request)) {
return box_handle_clear_endpoint_halt(box, request, error);
}
return [box.device sendDeviceRequest:request data:payload bytesTransferred:actual completionTimeout:IOUSBHostDefaultControlCompletionTimeout error:error];
}
bool box_usbhost_copy_devices(box_usbhost_device_list_t *out, char **error_out) {
if (out == NULL) {
box_set_error_string(error_out, @"IOUSBHost copy devices: missing output");
@ -521,34 +616,7 @@ bool box_usbhost_device_control(box_usbhost_device_t *device, const uint8_t setu
}
NSError *error = nil;
NSUInteger actual = 0;
BOOL ok = NO;
if (request.bmRequestType == 0 && request.bRequest == kIOUSBDeviceRequestSetConfiguration && request.wIndex == 0 && request.wLength == 0) {
ok = [box.device configureWithValue:request.wValue matchInterfaces:YES error:&error];
if (ok) {
[box withStateLock:^{
box.pipes = [NSMutableDictionary dictionary];
box_load_interfaces(box);
}];
}
} else if (request.bmRequestType == kIOUSBDeviceRequestRecipientInterface && request.bRequest == kIOUSBDeviceRequestSetInterface && request.wLength == 0) {
__block IOUSBHostInterface *interface = nil;
[box withStateLock:^{
interface = box_interface_for_number(box, request.wIndex & 0xff);
}];
if (interface == nil) {
error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnNotFound userInfo:nil];
} else {
ok = [interface selectAlternateSetting:request.wValue error:&error];
if (ok) {
[box withStateLock:^{
box.pipes = [NSMutableDictionary dictionary];
box_load_interfaces(box);
}];
}
}
} else {
ok = [box.device sendDeviceRequest:request data:payload bytesTransferred:&actual completionTimeout:IOUSBHostDefaultControlCompletionTimeout error:&error];
}
BOOL ok = box_dispatch_control_request(box, request, payload, &actual, &error);
if (actual_out != NULL) {
*actual_out = actual;
}

View file

@ -0,0 +1,32 @@
//go:build darwin && cgo
package usbip
import (
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
const (
// From IOKit/usb/USB.h: kIOUSBPipeStalled
darwinIOReturnPipeStalled int32 = -536854449
// From IOKit/usb/IOUSBHostFamilyDefinitions.h: kUSBHostReturnPipeStalled
darwinUSBHostReturnPipeStalled int32 = -536850432
)
func TestDarwinIOReturnToUSBIPStatusMapsStall(t *testing.T) {
testCases := []int32{
darwinIOReturnPipeStalled,
darwinUSBHostReturnPipeStalled,
}
for _, status := range testCases {
require.Equal(t, -int32(unix.EPIPE), darwinIOReturnToUSBIPStatus(status))
}
}
func TestDarwinUSBIPStatusToCIStatusMapsStall(t *testing.T) {
require.Equal(t, ciStatusStallError, darwinUSBIPStatusToCIStatus(-int32(unix.EPIPE)))
}