fix usbip control resync and darwin bounds

This commit is contained in:
世界 2026-04-24 08:27:18 +08:00
parent 959605661f
commit 0809b45eb4
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
8 changed files with 214 additions and 17 deletions

View File

@ -50,7 +50,7 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y usbip
sudo apt-get install -y linux-tools-common
- name: Test (unix)
if: matrix.os != 'windows-latest'
run: go test -v -exec sudo -tags "$BUILD_TAGS" -ldflags "$LDFLAGS_SHARED" ./...

View File

@ -206,3 +206,27 @@ func (c *ClientService) clearControlDeviceState() {
c.remoteDevicesV2 = nil
c.remoteMu.Unlock()
}
func (c *ClientService) syncRemoteStateAndResetControlState(ctx context.Context) error {
entries, err := c.fetchDevList(ctx)
if err != nil {
return err
}
c.resetControlDeviceStateFromEntries(entries)
c.applyRemoteEntries(entries)
return nil
}
func (c *ClientService) resetControlDeviceStateFromEntries(entries []DeviceEntry) {
devices := make(map[string]DeviceInfoV2, len(entries))
for _, entry := range entries {
device := deviceInfoV2FromEntry(entry, "", "", deviceStateAvailable, 0, "available")
if device.BusID == "" {
continue
}
devices[device.BusID] = device
}
c.remoteMu.Lock()
c.remoteDevicesV2 = devices
c.remoteMu.Unlock()
}

View File

@ -239,7 +239,12 @@ func (c *ClientService) runControlSession() error {
return E.Cause(errImmediateReconnect, "control sequence jumped from ", lastSeq, " to ", frame.Sequence)
}
lastSeq = frame.Sequence
if err := c.syncRemoteState(); err != nil {
if extended {
err = c.syncRemoteStateAndResetControlState(c.ctx)
} else {
err = c.syncRemoteState()
}
if err != nil {
return E.Cause(errImmediateReconnect, "devlist sync after change ", frame.Sequence, ": ", err)
}
case controlFrameDeviceSnapshot:
@ -257,10 +262,9 @@ func (c *ClientService) runControlSession() error {
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
}
if frame.Sequence != lastSeq+1 {
if err := c.syncRemoteState(); err != nil {
if err := c.syncRemoteStateAndResetControlState(c.ctx); err != nil {
return E.Cause(errImmediateReconnect, "devlist sync after sequence jump ", frame.Sequence, ": ", err)
}
c.clearControlDeviceState()
lastSeq = frame.Sequence
continue
}
@ -1054,8 +1058,8 @@ func (c *darwinVirtualController) handleControlDataTransfer(key darwinEndpointKe
if err != nil {
return -int32(unix.EIO), 0
}
if direction == USBIPDirIn && len(response.Buffer) > 0 {
copyToUnsafe(message.bufferPointer(), response.Buffer)
if direction == USBIPDirIn {
return c.completeSubmitInTransfer(message.bufferPointer(), response, length)
}
return response.Status, int(response.ActualLength)
}
@ -1110,8 +1114,8 @@ func (c *darwinVirtualController) handleNormalTransfer(key darwinEndpointKey, me
if err != nil {
return -int32(unix.EIO), 0
}
if direction == USBIPDirIn && len(response.Buffer) > 0 {
copyToUnsafe(message.bufferPointer(), response.Buffer)
if direction == USBIPDirIn {
return c.completeSubmitInTransfer(message.bufferPointer(), response, length)
}
return response.Status, int(response.ActualLength)
}
@ -1144,12 +1148,34 @@ func (c *darwinVirtualController) handleIsoTransfer(key darwinEndpointKey, messa
if err != nil {
return -int32(unix.EIO), 0
}
if direction == USBIPDirIn && len(response.Buffer) > 0 {
copyToUnsafe(message.bufferPointer(), response.Buffer)
if direction == USBIPDirIn {
return c.completeSubmitInTransfer(message.bufferPointer(), response, length)
}
return response.Status, int(response.ActualLength)
}
func (c *darwinVirtualController) completeSubmitInTransfer(ptr unsafe.Pointer, response SubmitResponse, requestLength int) (int32, int) {
if response.ActualLength < 0 {
c.logger.Debug("RET_SUBMIT actual_length is negative: ", response.ActualLength)
c.requestClose()
return -int32(unix.EPROTO), 0
}
actualLength := int(response.ActualLength)
if actualLength > requestLength || len(response.Buffer) > requestLength {
c.logger.Debug("RET_SUBMIT actual_length ", actualLength, " exceeds request length ", requestLength)
c.requestClose()
return -int32(unix.EOVERFLOW), 0
}
copyLength := actualLength
if copyLength > len(response.Buffer) {
copyLength = len(response.Buffer)
}
if copyLength > 0 {
copyToUnsafe(ptr, response.Buffer[:copyLength])
}
return response.Status, actualLength
}
func (c *darwinVirtualController) sendSubmit(command SubmitCommand) (SubmitResponse, error) {
seq := c.seq.Add(1)
command.Header.SeqNum = seq

View File

@ -262,7 +262,12 @@ func (c *ClientService) runControlSession() error {
return E.Cause(errImmediateReconnect, "control sequence jumped from ", lastSeq, " to ", frame.Sequence)
}
lastSeq = frame.Sequence
if err := c.syncRemoteState(); err != nil {
if extended {
err = c.syncRemoteStateAndResetControlState(c.ctx)
} else {
err = c.syncRemoteState()
}
if err != nil {
return E.Cause(errImmediateReconnect, "devlist sync after change ", frame.Sequence, ": ", err)
}
case controlFrameDeviceSnapshot:
@ -280,10 +285,9 @@ func (c *ClientService) runControlSession() error {
return E.Cause(errImmediateReconnect, "unexpected control frame ", frame.Type)
}
if frame.Sequence != lastSeq+1 {
if err := c.syncRemoteState(); err != nil {
if err := c.syncRemoteStateAndResetControlState(c.ctx); err != nil {
return E.Cause(errImmediateReconnect, "devlist sync after sequence jump ", frame.Sequence, ": ", err)
}
c.clearControlDeviceState()
lastSeq = frame.Sequence
continue
}

View File

@ -21,6 +21,7 @@ import (
M "github.com/sagernet/sing/common/metadata"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
const (
@ -176,6 +177,28 @@ func TestDarwinVirtualControllerReadsCompliantSubmitResponsePayload(t *testing.T
}
}
func TestDarwinSubmitInTransferRejectsOversizedPayload(t *testing.T) {
t.Parallel()
controller := newDarwinVirtualController(context.Background(), newTestLogger(), nil, DeviceInfoTruncated{})
buffer := []byte{0xaa, 0xbb}
status, length := controller.completeSubmitInTransfer(unsafe.Pointer(&buffer[0]), SubmitResponse{
Status: 0,
ActualLength: 3,
Buffer: []byte{1, 2, 3},
}, len(buffer))
require.Equal(t, -int32(unix.EOVERFLOW), status)
require.Zero(t, length)
require.Equal(t, []byte{0xaa, 0xbb}, buffer)
select {
case <-controller.ctx.Done():
default:
t.Fatal("controller context stayed active after oversized payload")
}
}
func TestWaitDarwinControllerClosesOnContextCancel(t *testing.T) {
t.Parallel()

View File

@ -1327,6 +1327,75 @@ func TestServerReconcileBroadcastsStatusOnlyDeviceDelta(t *testing.T) {
require.True(t, netErr.Timeout())
}
func TestServerControlSnapshotPreservesPendingDelta(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
device := newTestDevice("1-1", 0x1d6b, 0x0002, "serial-1", SpeedHigh)
store := newTestDeviceStore(device)
store.setStatus("1-1", usbipStatusUsed)
serverOps := newTestUSBIPOps(t)
serverOps.listUSBDevices = store.listUSBDevices
serverOps.readUsbipStatus = store.readUsbipStatus
serverOps.readSysfsDevice = store.readSysfsDevice
server := &ServerService{
ctx: ctx,
cancel: cancel,
logger: newTestLogger(),
matches: []option.USBIPDeviceMatch{{BusID: "1-1"}},
exports: map[string]serverExport{"1-1": {busid: "1-1"}},
controlSubs: make(map[uint64]*serverControlConn),
ops: serverOps,
}
server.refreshControlState()
serverAddr, closeServer := startDispatchServer(t, server)
defer closeServer()
firstConn, err := net.Dial("tcp", serverAddr.String())
require.NoError(t, err)
defer firstConn.Close()
setConnDeadline(t, firstConn)
require.NoError(t, WriteControlPreface(firstConn))
require.NoError(t, WriteControlHello(firstConn))
_, err = ReadControlFrame(firstConn)
require.NoError(t, err)
firstSnapshot, err := readControlMessage(firstConn)
require.NoError(t, err)
require.Equal(t, controlFrameDeviceSnapshot, firstSnapshot.Frame.Type)
store.setStatus("1-1", usbipStatusAvailable)
secondConn, err := net.Dial("tcp", serverAddr.String())
require.NoError(t, err)
defer secondConn.Close()
setConnDeadline(t, secondConn)
require.NoError(t, WriteControlPreface(secondConn))
require.NoError(t, WriteControlHello(secondConn))
_, err = ReadControlFrame(secondConn)
require.NoError(t, err)
secondSnapshotMessage, err := readControlMessage(secondConn)
require.NoError(t, err)
require.Equal(t, controlFrameDeviceSnapshot, secondSnapshotMessage.Frame.Type)
var secondSnapshot controlDeviceSnapshot
require.NoError(t, unmarshalControlPayload(secondSnapshotMessage.Payload, &secondSnapshot))
require.Len(t, secondSnapshot.Devices, 1)
require.Equal(t, deviceStateAvailable, secondSnapshot.Devices[0].State)
require.NoError(t, server.reconcileAndBroadcast(true))
changed, err := readControlMessage(firstConn)
require.NoError(t, err)
require.Equal(t, controlFrameDeviceDelta, changed.Frame.Type)
var delta controlDeviceDelta
require.NoError(t, unmarshalControlPayload(changed.Payload, &delta))
require.Len(t, delta.Updated, 1)
require.Equal(t, "1-1", delta.Updated[0].BusID)
require.Equal(t, deviceStateAvailable, delta.Updated[0].State)
}
func TestServerControlLeaseEnablesImportExt(t *testing.T) {
t.Parallel()
@ -1960,6 +2029,61 @@ func TestClientFetchDevListReturnsOnContextCancelWhileServerStalls(t *testing.T)
require.NoError(t, <-serverErr)
}
func TestClientSyncRemoteStateAndResetControlStateRebuildsV2Map(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
device := newTestDevice("1-1", 0x1d6b, 0x0002, "serial-1", SpeedHigh)
entry := DeviceEntry{Info: device.toProtocol(), Interfaces: device.Interfaces}
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
serverErr := make(chan error, 1)
go func() {
conn, acceptErr := listener.Accept()
if acceptErr != nil {
serverErr <- acceptErr
return
}
defer conn.Close()
header, readErr := ReadOpHeader(conn)
if readErr != nil {
serverErr <- readErr
return
}
if header.Code != OpReqDevList {
serverErr <- fmt.Errorf("unexpected request code 0x%s", hex16(header.Code))
return
}
serverErr <- WriteOpRepDevList(conn, []DeviceEntry{entry})
}()
client := &ClientService{
ctx: ctx,
cancel: cancel,
logger: newTestLogger(),
dialer: testDialer{},
serverAddr: M.SocksaddrFromNet(listener.Addr()),
matches: []option.USBIPDeviceMatch{{BusID: "unused"}},
ops: newTestUSBIPOps(t),
remoteDevicesV2: map[string]DeviceInfoV2{"stale": {BusID: "stale", State: deviceStateAvailable}},
}
require.NoError(t, client.syncRemoteStateAndResetControlState(ctx))
require.NoError(t, <-serverErr)
client.remoteMu.Lock()
devices := client.remoteDevicesV2
client.remoteMu.Unlock()
require.Len(t, devices, 1)
require.Contains(t, devices, "1-1")
require.Equal(t, deviceStateAvailable, devices["1-1"].State)
require.Equal(t, uint16(0x1d6b), devices["1-1"].VendorID)
}
func TestClientAttemptAttachRejectsUnexpectedReplyVersion(t *testing.T) {
t.Parallel()

View File

@ -544,9 +544,6 @@ func (s *ServerService) enqueueControlPayload(sub *serverControlConn, frame cont
func (s *ServerService) enqueueControlSnapshot(sub *serverControlConn, sequence uint64) {
devices := s.buildDeviceStateV2()
s.controlMu.Lock()
s.controlState = deviceInfoV2Map(devices)
s.controlMu.Unlock()
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameDeviceSnapshot,
Version: controlProtocolVersion,

View File

@ -718,7 +718,6 @@ func (s *ServerService) enqueueControlPayload(sub *serverControlConn, frame cont
func (s *ServerService) enqueueControlSnapshot(sub *serverControlConn, sequence uint64) {
devices := s.buildDeviceStateV2()
s.setControlState(deviceInfoV2Map(devices))
s.enqueueControlPayload(sub, controlFrame{
Type: controlFrameDeviceSnapshot,
Version: controlProtocolVersion,