usbip: defer darwin RET_UNLINK until completion; route VHCI through primary; close lease-consume race

Three independent correctness bugs that all violated "decide before
publishing": each surface (wire reply, sysfs path, broadcast state)
committed to a state the code later had to contradict.

darwin: switch IOUSBHostPipe abort to IOUSBHostAbortOptionSynchronous and
add the EP-0 path via abortDeviceRequestsWithOption (previously a silent
no-op for control transfers). After abort returns, await a per-pending
drained channel before writing RET_UNLINK so the late RetSubmit
suppression in finishSubmit has actually taken effect — the drained
channel is allocated lazily by markSubmitUnlinked, so the hot submit
path is unchanged.

linux: collapse the per-controller VHCI abstraction. The kernel exposes
attach/detach/status/status.N only on vhci_hcd.0 with globally unique
port numbers; the previous code wrote vhci_hcd.N/attach for N>0 and
silently failed past the primary controller's port range. Glob status*
on the primary, key reservations on bare port int, and carry the
status-suffix as a diagnostic-only secondary index for Description().

export_ledger: ConsumeLeaseAndReserve no longer publishes busy=true and
then conditionally rolls it back. Read seq under fast first, then make
every decision (including the lease.Generation check) inside one slow
critical section before any busy mutation. This removes the window
where a concurrent BroadcastIfChanged could observe transient busy and
poison l.state with no corrective broadcast on rollback.
This commit is contained in:
世界
2026-05-15 17:32:30 +08:00
parent cc83b9185a
commit 8799b10b9a
6 changed files with 174 additions and 168 deletions
+60 -71
View File
@@ -21,7 +21,6 @@ import (
const (
sysBusUSBDevices = "/sys/bus/usb/devices"
sysUsbipHostDriver = "/sys/bus/usb/drivers/usbip-host"
sysVHCIPlatform = "/sys/devices/platform"
sysVHCIControllerV0 = "/sys/devices/platform/vhci_hcd.0"
usbipStatusAvailable = 1
@@ -29,18 +28,6 @@ const (
usbipStatusError = 3
)
// vhciController is the canonical sysfs path of a vhci_hcd platform device,
// e.g. /sys/devices/platform/vhci_hcd.0. The kernel module instantiates
// controllers at load time and never adds more at runtime.
type vhciController string
func (c vhciController) name() string { return filepath.Base(string(c)) }
type vhciPortKey struct {
controller vhciController
port int
}
type sysfsDevice struct {
BusID string
Path string
@@ -79,11 +66,15 @@ func (d *sysfsDevice) toProtocol() DeviceInfoTruncated {
return info
}
// vhciStatusRecord is one row of /sys/devices/platform/vhci_hcd.0/status
// or status.N. The kernel emits globally unique port numbers across every
// status* file; secondary identifies which file the row came from
// (0 for status, N for status.N) and exists only for diagnostic logging.
type vhciStatusRecord struct {
controller vhciController
hub string
port int
state int
secondary int
hub string
port int
state int
}
func listUSBDevices() ([]sysfsDevice, error) {
@@ -218,76 +209,74 @@ func waitForUsbipStatusCleared(ctx context.Context, busid string) {
}
}
func discoverVHCIControllers() ([]vhciController, error) {
matches, err := filepath.Glob(filepath.Join(sysVHCIPlatform, "vhci_hcd.*"))
// readPrimaryVHCIStatus reads every status* file under
// /sys/devices/platform/vhci_hcd.0 and concatenates the rows in lexical
// order. status reports controller 0; status.N reports controller N.
// Port numbers are already globally unique — no remapping is needed.
func readPrimaryVHCIStatus() ([]vhciStatusRecord, error) {
matches, err := filepath.Glob(filepath.Join(sysVHCIControllerV0, "status*"))
if err != nil {
return nil, err
}
sort.Strings(matches)
out := make([]vhciController, 0, len(matches))
records := make([]vhciStatusRecord, 0)
for _, path := range matches {
out = append(out, vhciController(path))
secondary, parseErr := vhciSecondaryFromStatusFile(filepath.Base(path))
if parseErr != nil {
continue
}
raw, readErr := os.ReadFile(path)
if readErr != nil {
return nil, readErr
}
records = append(records, parseVHCIStatus(secondary, string(raw))...)
}
return out, nil
return records, nil
}
func vhciPickFreePort(controllers []vhciController, speed uint32, skip map[vhciPortKey]struct{}) (vhciController, int, error) {
func readAllVHCIStatus() []vhciStatusRecord {
records, err := readPrimaryVHCIStatus()
if err != nil {
return nil
}
return records
}
func vhciPickFreePort(speed uint32, skip map[int]struct{}) (int, error) {
targetHub := "hs"
switch speed {
case SpeedSuper, SpeedSuperPlus:
targetHub = "ss"
}
var firstErr error
for _, ctrl := range controllers {
records, err := readVHCIStatus(ctrl)
if err != nil {
if firstErr == nil {
firstErr = err
}
records, err := readPrimaryVHCIStatus()
if err != nil {
return -1, err
}
for _, record := range records {
if record.hub != targetHub || record.state != 4 {
continue
}
for _, record := range records {
if record.hub != targetHub || record.state != 4 {
continue
}
key := vhciPortKey{controller: ctrl, port: record.port}
if _, skipped := skip[key]; skipped {
continue
}
return ctrl, record.port, nil
}
}
if firstErr != nil {
return "", -1, firstErr
}
return "", -1, E.New("no free ", targetHub, " vhci port")
}
func readVHCIStatus(ctrl vhciController) ([]vhciStatusRecord, error) {
raw, err := os.ReadFile(filepath.Join(string(ctrl), "status"))
if err != nil {
return nil, err
}
return parseVHCIStatus(ctrl, string(raw)), nil
}
func readAllVHCIStatus() []vhciStatusRecord {
controllers, err := discoverVHCIControllers()
if err != nil {
return nil
}
var out []vhciStatusRecord
for _, ctrl := range controllers {
records, err := readVHCIStatus(ctrl)
if err != nil {
_, skipped := skip[record.port]
if skipped {
continue
}
out = append(out, records...)
return record.port, nil
}
return out
return -1, E.New("no free ", targetHub, " vhci port")
}
func parseVHCIStatus(ctrl vhciController, raw string) []vhciStatusRecord {
func vhciSecondaryFromStatusFile(name string) (int, error) {
if name == "status" {
return 0, nil
}
suffix := strings.TrimPrefix(name, "status.")
if suffix == name {
return 0, E.New("not a status file: ", name)
}
return strconv.Atoi(suffix)
}
func parseVHCIStatus(secondary int, raw string) []vhciStatusRecord {
scanner := bufio.NewScanner(strings.NewReader(raw))
records := make([]vhciStatusRecord, 0)
first := true
@@ -313,10 +302,10 @@ func parseVHCIStatus(ctrl vhciController, raw string) []vhciStatusRecord {
continue
}
records = append(records, vhciStatusRecord{
controller: ctrl,
hub: fields[0],
port: port,
state: state,
secondary: secondary,
hub: fields[0],
port: port,
state: state,
})
}
return records