8799b10b9a
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.
600 lines
15 KiB
Go
600 lines
15 KiB
Go
//go:build linux
|
|
|
|
package usbip
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/option"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func newPlatformExportHost(logger log.ContextLogger, matches []option.USBIPDeviceMatch) (ExportHost, error) {
|
|
return newLinuxExportHost(logger, matches), nil
|
|
}
|
|
|
|
func newPlatformImportHost(logger log.ContextLogger) (ImportHost, error) {
|
|
return &linuxImportHost{
|
|
logger: logger,
|
|
ports: make(map[int]struct{}),
|
|
}, nil
|
|
}
|
|
|
|
func isMissingUSBDeviceError(err error) bool {
|
|
return errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENODEV)
|
|
}
|
|
|
|
func linuxUSBIPStatusReason(status int) string {
|
|
switch status {
|
|
case usbipStatusAvailable:
|
|
return "available"
|
|
case usbipStatusUsed:
|
|
return "used"
|
|
case usbipStatusError:
|
|
return "error"
|
|
default:
|
|
return fmt.Sprintf("status=0x%08x", uint32(status))
|
|
}
|
|
}
|
|
|
|
type linuxExportHost struct {
|
|
logger log.ContextLogger
|
|
matches []option.USBIPDeviceMatch
|
|
|
|
access sync.Mutex
|
|
exports map[string]*linuxExport
|
|
}
|
|
|
|
func newLinuxExportHost(logger log.ContextLogger, matches []option.USBIPDeviceMatch) *linuxExportHost {
|
|
return &linuxExportHost{
|
|
logger: logger,
|
|
matches: matches,
|
|
exports: make(map[string]*linuxExport),
|
|
}
|
|
}
|
|
|
|
func (h *linuxExportHost) Start(ctx context.Context) error {
|
|
return ensureKernelPath(sysUsbipHostDriver, "usbip-host", "usbip-host driver")
|
|
}
|
|
|
|
func (h *linuxExportHost) Close() error {
|
|
h.access.Lock()
|
|
exports := h.exports
|
|
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)
|
|
if releaseErr != nil {
|
|
h.logger.Warn("rollback ", exp.busid, ": ", releaseErr)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *linuxExportHost) Events(ctx context.Context) (<-chan struct{}, error) {
|
|
ch := make(chan struct{}, 1)
|
|
go h.ueventLoop(ctx, ch)
|
|
return ch, nil
|
|
}
|
|
|
|
func (h *linuxExportHost) ueventLoop(ctx context.Context, ch chan<- struct{}) {
|
|
defer close(ch)
|
|
signal := func() {
|
|
select {
|
|
case ch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
backoff := ueventListenerBackoffInitial
|
|
for {
|
|
listener, err := newUEventListener()
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
h.logger.Warn("open uevent listener: ", err)
|
|
if !sleepCtx(ctx, backoff) {
|
|
return
|
|
}
|
|
backoff *= 2
|
|
if backoff > ueventListenerBackoffMax {
|
|
backoff = ueventListenerBackoffMax
|
|
}
|
|
continue
|
|
}
|
|
backoff = ueventListenerBackoffInitial
|
|
listenerDone := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
_ = listener.Close()
|
|
case <-listenerDone:
|
|
}
|
|
}()
|
|
signal()
|
|
for {
|
|
err = listener.WaitUSBEvent()
|
|
if err != nil {
|
|
close(listenerDone)
|
|
_ = listener.Close()
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
h.logger.Warn("read uevent: ", err)
|
|
if !sleepCtx(ctx, backoff) {
|
|
return
|
|
}
|
|
backoff *= 2
|
|
if backoff > ueventListenerBackoffMax {
|
|
backoff = ueventListenerBackoffMax
|
|
}
|
|
break
|
|
}
|
|
signal()
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
ueventListenerBackoffInitial = time.Second
|
|
ueventListenerBackoffMax = 30 * time.Second
|
|
)
|
|
|
|
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{
|
|
BusID: devices[i].BusID,
|
|
VendorID: devices[i].VendorID,
|
|
ProductID: devices[i].ProductID,
|
|
Serial: devices[i].Serial,
|
|
}
|
|
if !matches(m, deviceKey) {
|
|
continue
|
|
}
|
|
path := devices[i].Path
|
|
isVHCIImport := strings.Contains(path, "vhci_hcd")
|
|
if !isVHCIImport {
|
|
realPath, err := filepath.EvalSymlinks(path)
|
|
if err == nil {
|
|
isVHCIImport = strings.Contains(realPath, "vhci_hcd")
|
|
}
|
|
}
|
|
if isVHCIImport {
|
|
h.logger.Debug("skip vhci-imported device ", devices[i].BusID, " matched by ", describeMatch(m))
|
|
continue
|
|
}
|
|
if devices[i].DeviceClass == 0x09 {
|
|
h.logger.Warn("skip hub device ", devices[i].BusID, " matched by ", describeMatch(m))
|
|
continue
|
|
}
|
|
desired[devices[i].BusID] = devices[i]
|
|
}
|
|
}
|
|
|
|
h.access.Lock()
|
|
current := make(map[string]*linuxExport, len(h.exports))
|
|
maps.Copy(current, h.exports)
|
|
h.access.Unlock()
|
|
|
|
for busid, device := range desired {
|
|
if _, ok := current[busid]; ok {
|
|
continue
|
|
}
|
|
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()
|
|
}
|
|
|
|
var released []string
|
|
for busid, exp := range current {
|
|
if _, ok := desired[busid]; ok {
|
|
continue
|
|
}
|
|
_, 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)
|
|
}
|
|
|
|
return h.snapshotSelf(), released, nil
|
|
}
|
|
|
|
func (h *linuxExportHost) FinishImport(ctx context.Context, busid string) (bool, error) {
|
|
err := writeSysfs(filepath.Join(sysBusUSBDevices, busid, "usbip_sockfd"), "-1")
|
|
if err != nil && !os.IsNotExist(err) && !isMissingUSBDeviceError(err) {
|
|
h.logger.Debug("release ", busid, " from usbip-host: ", err)
|
|
}
|
|
waitForUsbipStatusCleared(ctx, busid)
|
|
return false, nil
|
|
}
|
|
|
|
func (h *linuxExportHost) snapshotSelf() map[string]Export {
|
|
h.access.Lock()
|
|
defer h.access.Unlock()
|
|
out := make(map[string]Export, len(h.exports))
|
|
for busid, exp := range h.exports {
|
|
out[busid] = exp
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (h *linuxExportHost) bindOne(d *sysfsDevice) (*linuxExport, error) {
|
|
var (
|
|
exp *linuxExport
|
|
err error
|
|
)
|
|
for attempt := range 2 {
|
|
exp, err = h.bindOneOnce(d)
|
|
if err == nil {
|
|
return exp, nil
|
|
}
|
|
if attempt > 0 || !errors.Is(err, unix.ENODEV) {
|
|
break
|
|
}
|
|
h.logger.Warn("reset usbip-host after bind failure on ", d.BusID, ": ", err)
|
|
h.access.Lock()
|
|
active := len(h.exports) > 0
|
|
h.access.Unlock()
|
|
if active {
|
|
return nil, E.Cause(E.New("active usbip-host exports are present"), "reset usbip-host after bind failure")
|
|
}
|
|
resetErr := reloadHostDriver()
|
|
if resetErr != nil {
|
|
return nil, E.Cause(resetErr, "reset usbip-host after bind failure")
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
func (h *linuxExportHost) bindOneOnce(d *sysfsDevice) (*linuxExport, error) {
|
|
driver, err := currentDriver(d.BusID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if driver == "usbip-host" {
|
|
h.logger.Info("device ", d.BusID, " already bound to usbip-host; co-opting")
|
|
return h.newExport(*d, false, ""), nil
|
|
}
|
|
if driver != "" {
|
|
err = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "unbind"), d.BusID)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "unbind from ", driver)
|
|
}
|
|
}
|
|
matchBusIDPath := filepath.Join(sysUsbipHostDriver, "match_busid")
|
|
err = writeSysfs(matchBusIDPath, "add "+d.BusID)
|
|
if err != nil {
|
|
if driver != "" {
|
|
_ = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "bind"), d.BusID)
|
|
}
|
|
return nil, E.Cause(err, "match_busid add")
|
|
}
|
|
err = writeSysfs(filepath.Join(sysUsbipHostDriver, "bind"), d.BusID)
|
|
if err != nil {
|
|
_ = writeSysfs(matchBusIDPath, "del "+d.BusID)
|
|
if driver != "" {
|
|
_ = writeSysfs(filepath.Join("/sys/bus/usb/drivers", driver, "bind"), d.BusID)
|
|
}
|
|
return nil, E.Cause(err, "bind to usbip-host")
|
|
}
|
|
previousDriver := driver
|
|
if previousDriver == "" {
|
|
previousDriver = "(no driver)"
|
|
}
|
|
h.logger.Info("exported ", d.BusID, " (previously on ", previousDriver, ")")
|
|
return h.newExport(*d, true, driver), nil
|
|
}
|
|
|
|
func (h *linuxExportHost) releaseExport(exp *linuxExport, restore bool) error {
|
|
if !exp.managed {
|
|
h.logger.Info("stopped tracking ", exp.busid, " on usbip-host")
|
|
return nil
|
|
}
|
|
status, statusErr := readUsbipStatus(exp.busid)
|
|
if statusErr != nil && !os.IsNotExist(statusErr) && !isMissingUSBDeviceError(statusErr) {
|
|
return statusErr
|
|
}
|
|
if statusErr == nil && status == usbipStatusUsed {
|
|
err := writeSysfs(filepath.Join(sysBusUSBDevices, exp.busid, "usbip_sockfd"), "-1")
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
err := writeSysfs(filepath.Join(sysUsbipHostDriver, "unbind"), exp.busid)
|
|
if err != nil && !os.IsNotExist(err) && !(isMissingUSBDeviceError(err) && !restore) {
|
|
return err
|
|
}
|
|
err = writeSysfs(filepath.Join(sysUsbipHostDriver, "match_busid"), "del "+exp.busid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !restore {
|
|
h.logger.Info("removed export state for disappeared device ", exp.busid)
|
|
return nil
|
|
}
|
|
if exp.originalDriver == "" {
|
|
h.logger.Info("released ", exp.busid, " from usbip-host")
|
|
return nil
|
|
}
|
|
err = writeSysfs(filepath.Join("/sys/bus/usb/drivers", exp.originalDriver, "bind"), exp.busid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.logger.Info("restored ", exp.busid, " to ", exp.originalDriver)
|
|
return nil
|
|
}
|
|
|
|
func (h *linuxExportHost) newExport(descriptor sysfsDevice, managed bool, originalDriver string) *linuxExport {
|
|
return &linuxExport{
|
|
busid: descriptor.BusID,
|
|
descriptor: descriptor,
|
|
managed: managed,
|
|
originalDriver: originalDriver,
|
|
logger: h.logger,
|
|
}
|
|
}
|
|
|
|
// linuxExport caches the bind-time descriptor because it is immutable
|
|
// post-enumeration; Snapshot only re-reads usbip_status.
|
|
type linuxExport struct {
|
|
busid string
|
|
descriptor sysfsDevice
|
|
managed bool
|
|
originalDriver string
|
|
logger log.ContextLogger
|
|
}
|
|
|
|
func (e *linuxExport) BusID() string {
|
|
return e.busid
|
|
}
|
|
|
|
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)
|
|
}
|
|
status, statusErr := readUsbipStatus(e.busid)
|
|
var state, reason string
|
|
switch {
|
|
case statusErr != nil:
|
|
state = deviceStateUnavailable
|
|
reason = statusErr.Error()
|
|
case busy:
|
|
status = usbipStatusUsed
|
|
state = deviceStateBusy
|
|
reason = linuxUSBIPStatusReason(status)
|
|
case status == usbipStatusAvailable:
|
|
state = deviceStateAvailable
|
|
reason = linuxUSBIPStatusReason(status)
|
|
case status == usbipStatusUsed:
|
|
state = deviceStateBusy
|
|
reason = linuxUSBIPStatusReason(status)
|
|
default:
|
|
state = deviceStateUnavailable
|
|
reason = linuxUSBIPStatusReason(status)
|
|
}
|
|
return ExportSnapshot{
|
|
Entry: DeviceEntry{
|
|
Info: e.descriptor.toProtocol(),
|
|
Interfaces: e.descriptor.Interfaces,
|
|
Serial: e.descriptor.Serial,
|
|
},
|
|
Backend: backendIDLinuxSysfs,
|
|
StableID: stableID,
|
|
State: state,
|
|
StatusReason: reason,
|
|
RawStatus: status,
|
|
}
|
|
}
|
|
|
|
func (e *linuxExport) LeaseCheck(ctx context.Context) (bool, string) {
|
|
status, err := readUsbipStatus(e.busid)
|
|
if err != nil {
|
|
return false, err.Error()
|
|
}
|
|
if status != usbipStatusAvailable {
|
|
return false, linuxUSBIPStatusReason(status)
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
func (e *linuxExport) DeviceInfo(ctx context.Context) (DeviceInfoTruncated, error) {
|
|
return e.descriptor.toProtocol(), nil
|
|
}
|
|
|
|
func (e *linuxExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) {
|
|
handoff, err := newKernelHandoffSession(ctx, conn, e.logger, "server", e.busid)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "prepare handoff")
|
|
}
|
|
mode := "direct"
|
|
if handoff.relayConn != nil {
|
|
mode = "relay"
|
|
}
|
|
e.logger.Debug("usbip server handoff ", e.busid, ": ", mode)
|
|
err = writeSysfs(filepath.Join(sysBusUSBDevices, e.busid, "usbip_sockfd"), strconv.Itoa(int(handoff.file.Fd())))
|
|
if err != nil {
|
|
_ = handoff.Close()
|
|
return nil, E.Cause(err, "hand off ", e.busid, " to kernel")
|
|
}
|
|
closeErr := handoff.closeKernelFD()
|
|
if closeErr != nil {
|
|
e.logger.Debug("close kernel fd ", e.busid, ": ", closeErr)
|
|
}
|
|
return handoff, nil
|
|
}
|
|
|
|
type linuxImportHost struct {
|
|
logger log.ContextLogger
|
|
|
|
portsAccess sync.Mutex
|
|
ports map[int]struct{}
|
|
}
|
|
|
|
func (h *linuxImportHost) Start(ctx context.Context) error {
|
|
return ensureKernelPath(sysVHCIControllerV0, "vhci-hcd", "vhci_hcd.0")
|
|
}
|
|
|
|
func (h *linuxImportHost) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (h *linuxImportHost) Attach(ctx context.Context, info DeviceInfoTruncated, conn net.Conn) (AttachedSession, error) {
|
|
handoff, err := newKernelHandoffSession(ctx, conn, h.logger, "client", info.BusIDString())
|
|
if err != nil {
|
|
return nil, E.Cause(err, "prepare handoff")
|
|
}
|
|
mode := "direct"
|
|
if handoff.relayConn != nil {
|
|
mode = "relay"
|
|
}
|
|
h.logger.Debug("usbip client handoff ", info.BusIDString(), ": ", mode)
|
|
port, secondary, attachErr := h.attachOnce(ctx, info, handoff)
|
|
if attachErr != nil {
|
|
_ = handoff.Close()
|
|
return nil, attachErr
|
|
}
|
|
_ = handoff.Start()
|
|
return &linuxClientSession{
|
|
handoff: handoff,
|
|
host: h,
|
|
port: port,
|
|
secondary: secondary,
|
|
}, nil
|
|
}
|
|
|
|
func (h *linuxImportHost) attachOnce(ctx context.Context, info DeviceInfoTruncated, handoff *kernelHandoffSession) (int, int, error) {
|
|
triedPorts := make(map[int]struct{})
|
|
for {
|
|
port, err := vhciPickFreePort(info.Speed, triedPorts)
|
|
if err != nil {
|
|
return -1, 0, err
|
|
}
|
|
if !h.reservePort(port) {
|
|
triedPorts[port] = struct{}{}
|
|
continue
|
|
}
|
|
attachLine := fmt.Sprintf("%d %d %d %d", port, int(handoff.file.Fd()), info.DevID(), info.Speed)
|
|
err = writeSysfs(filepath.Join(sysVHCIControllerV0, "attach"), attachLine)
|
|
if err != nil {
|
|
h.releasePort(port)
|
|
if errors.Is(err, unix.EBUSY) {
|
|
triedPorts[port] = struct{}{}
|
|
continue
|
|
}
|
|
return -1, 0, E.Cause(err, "vhci attach")
|
|
}
|
|
err = handoff.closeKernelFD()
|
|
if err != nil {
|
|
h.logger.Debug("close kernel fd ", info.BusIDString(), ": ", err)
|
|
}
|
|
return port, lookupSecondaryForPort(port), nil
|
|
}
|
|
}
|
|
|
|
func lookupSecondaryForPort(port int) int {
|
|
records, err := readPrimaryVHCIStatus()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
for _, record := range records {
|
|
if record.port == port {
|
|
return record.secondary
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (h *linuxImportHost) reservePort(port int) bool {
|
|
h.portsAccess.Lock()
|
|
defer h.portsAccess.Unlock()
|
|
_, exists := h.ports[port]
|
|
if exists {
|
|
h.logger.Debug("vhci port ", port, " already reserved locally")
|
|
return false
|
|
}
|
|
h.logger.Debug("reserve vhci port ", port)
|
|
h.ports[port] = struct{}{}
|
|
return true
|
|
}
|
|
|
|
func (h *linuxImportHost) releasePort(port int) {
|
|
h.portsAccess.Lock()
|
|
defer h.portsAccess.Unlock()
|
|
h.logger.Debug("release vhci port ", port)
|
|
delete(h.ports, port)
|
|
}
|
|
|
|
type linuxClientSession struct {
|
|
handoff *kernelHandoffSession
|
|
host *linuxImportHost
|
|
port int
|
|
secondary int
|
|
|
|
closeOnce sync.Once
|
|
closeErr error
|
|
}
|
|
|
|
func (s *linuxClientSession) Done() <-chan struct{} {
|
|
return s.handoff.Done()
|
|
}
|
|
|
|
func (s *linuxClientSession) Err() error {
|
|
return s.handoff.Err()
|
|
}
|
|
|
|
func (s *linuxClientSession) Start() error {
|
|
return s.handoff.Start()
|
|
}
|
|
|
|
func (s *linuxClientSession) Close() error {
|
|
s.closeOnce.Do(func() {
|
|
detachErr := writeSysfs(filepath.Join(sysVHCIControllerV0, "detach"), strconv.Itoa(s.port))
|
|
closeErr := s.handoff.Close()
|
|
s.host.releasePort(s.port)
|
|
s.closeErr = E.Errors(detachErr, closeErr)
|
|
})
|
|
return s.closeErr
|
|
}
|
|
|
|
func (s *linuxClientSession) Description() string {
|
|
if s.secondary == 0 {
|
|
return fmt.Sprintf("vhci_hcd.0 port %d", s.port)
|
|
}
|
|
return fmt.Sprintf("vhci_hcd.0 (controller %d) port %d", s.secondary, s.port)
|
|
}
|