From e2c64c1899f349282015172f3fd27b6e9eb23363 Mon Sep 17 00:00:00 2001 From: Pawel Osmolski Date: Mon, 12 Oct 2026 12:13:38 +0100 Subject: [PATCH 1/2] Support usbip-win2 0.9.8.1 native attachment and receive mode --- docs/api/xboxone-exact-removal.md | 27 +++++++++ docs/getting-started/usbip.md | 32 ++++++++--- internal/cmd/usbip_prerequisite_windows.go | 2 +- .../cmd/usbip_prerequisite_windows_test.go | 8 +-- .../api/autoattach_operation_windows_test.go | 2 +- internal/server/api/autoattach_windows.go | 55 ++++++++++++++----- .../server/api/autoattach_windows_test.go | 49 ++++++++++++++--- .../handler/bus_device_activate_xboxone.go | 9 +-- .../bus_device_add_xboxone_auth_test.go | 2 +- .../api/xboxone_retry_cleanup_windows.go | 7 ++- .../api/xboxone_retry_cleanup_windows_test.go | 8 +-- scripts/install.ps1 | 49 +++++++++-------- scripts/test-install-first-run.ps1 | 25 +++++++++ 13 files changed, 204 insertions(+), 71 deletions(-) diff --git a/docs/api/xboxone-exact-removal.md b/docs/api/xboxone-exact-removal.md index 66539cf4..34fb8fa8 100644 --- a/docs/api/xboxone-exact-removal.md +++ b/docs/api/xboxone-exact-removal.md @@ -1,4 +1,31 @@ # Exact production Xbox One removal +## Windows driver ABI for VIIPER 0.1.10 + +The Windows integration now targets the published usbip-win2 **0.9.8.1** +headers at commit `55e1fa7f0c2157017b02dc1f3236e98169a535e4`. +Microsoft's C++ compiler independently confirms these layouts: + +| Native request | Input bytes | Response bytes | +| --- | ---: | ---: | +| `PLUGIN_HARDWARE` / `PLUGIN_HARDWARE_ONCE` | 1124 | 8 | +| Exact-location `STOP_ATTACH_ATTEMPTS` | 1108 | 1108 | + +Attach fields are size at 0, output port at 4, location hash at 8, bus ID at +12, service at 44, host at 76, optional serial at 1104, and `wsk_events` at +1120. Retry cancellation has its count at 1104 and does not contain attach's +serial or receive-mode fields. Golden tests bind these offsets and the separate +request lengths to the upstream header, alongside completion/cancellation tests. + +Windows attaches select `low-latency` WSK reception. Production Xbox One retains +`PLUGIN_HARDWARE_ONCE`, its exact export alias, token-bound removal, and the +existing registration lifetime checks. Serial overrides remain empty because +the published 0.9.8.1 driver has an upstream bug; the post-release fix +`a2a1159763088c85d940078cfa6491f38540953d` is not bundled. Port numbers and +serial display values do not grant removal authority. Native ABI failures do +not fall back to a command-line attach. + +The implementation notes below retain their original 0.9.7.7 source context. +Their older request layouts are not the current Windows ABI. The authenticated `bus/{id}/add-authorized-xboxone` response includes a top-level `removalToken`. It is a fresh 256-bit `crypto/rand` nonce encoded as diff --git a/docs/getting-started/usbip.md b/docs/getting-started/usbip.md index 6f90393e..5f23777d 100644 --- a/docs/getting-started/usbip.md +++ b/docs/getting-started/usbip.md @@ -3,16 +3,32 @@ === "Windows" VIIPER for DS4Windows requires the signed - [usbip-win2 0.9.7.7 x64 release](https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.7.7). + [usbip-win2 0.9.8.1 x64 release](https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.8.1) + with VIIPER 0.1.10 and DS4Windows 5.0.15 or newer. Prefer the DS4Windows **Install / Repair VIIPER** action because it bundles - and verifies this exact package. Do not install 0.9.7.8 or substitute a + and verifies this exact package. Older DS4Windows packages require their + original dependency; update the complete DS4Windows package together. + Do not substitute a different userspace/driver build; VIIPER rejects an incompatible ABI. - !!! warning "USBIP-Win2 signing certificate" - The 0.9.7.7 installer may add the publicly available USBIP test-signing - CA as a trusted root. After installation, you may remove the certificate - named **USBIP** with `certlm.msc` (run as administrator). Keep the - 0.9.7.7 driver and userspace files installed together. + ### Upgrading from 0.9.7.7 + + Close games and controller tools, then use the matching DS4Windows + installer or **Install / Repair**. A driver replacement first removes the + previous package, then requires a Windows restart before installing the + new package. Run setup again after restarting. If the new driver requests + another restart, complete it before using VIIPER. + + The 0.9.8.1 x64 installer and command-line executable have a valid Cloudyne + Systems Authenticode signature. Both bundled driver binaries are signed by + Microsoft Windows Hardware Compatibility Publisher. This upgrade does not + require enabling test-signing mode or adding a test certificate. + + Windows attachments use upstream's `low-latency` WSK receive mode for + frequent controller reports. Linux attachments keep the Linux usbip tools. + The published 0.9.8.1 serial override has a known upstream bug; VIIPER leaves + it empty and retains exact endpoint/export identity checks. See the + [upstream release](https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.8.1). === "Linux" @@ -65,3 +81,5 @@ ```bash lsmod | grep vhci_hcd ``` + +Developer standalone setup requires `-ViiperReleaseTag` naming an upstream build with the 0.9.8.1 ABI. The matching DS4Windows package must also be rebuilt; this change does not retag an existing upstream release. diff --git a/internal/cmd/usbip_prerequisite_windows.go b/internal/cmd/usbip_prerequisite_windows.go index 7b6a1c3c..953fd2d0 100644 --- a/internal/cmd/usbip_prerequisite_windows.go +++ b/internal/cmd/usbip_prerequisite_windows.go @@ -14,7 +14,7 @@ import ( ) const ( - requiredUSBIPVersion = "0.9.7.7" + requiredUSBIPVersion = "0.9.8.1" usbipProbeTimeout = 10 * time.Second usbipPipeDrainTimeout = 250 * time.Millisecond ) diff --git a/internal/cmd/usbip_prerequisite_windows_test.go b/internal/cmd/usbip_prerequisite_windows_test.go index 3bd4e45e..8da6deb8 100644 --- a/internal/cmd/usbip_prerequisite_windows_test.go +++ b/internal/cmd/usbip_prerequisite_windows_test.go @@ -113,7 +113,7 @@ func TestProbeUSBIPRuntimeAcceptsPinnedCompatibleRuntime(t *testing.T) { calls = append(calls, call) switch args[0] { case "--version": - return []byte("0.9.7.7\r\n"), nil + return []byte("0.9.8.1\r\n"), nil case "port": return []byte("Imported USB devices\r\n====================\r\n"), nil default: @@ -143,7 +143,7 @@ func TestProbeUSBIPRuntimeRejectsEveryOtherVersionBeforeDriverProbe(t *testing.T err := probeUSBIPRuntime(`C:\Program Files\USBip\usbip.exe`, run) require.Error(t, err) - assert.Contains(t, err.Error(), "requires usbip-win2 0.9.7.7") + assert.Contains(t, err.Error(), "requires usbip-win2 0.9.8.1") assert.Contains(t, err.Error(), "found 0.9.7.8") assert.False(t, portCalled) } @@ -151,7 +151,7 @@ func TestProbeUSBIPRuntimeRejectsEveryOtherVersionBeforeDriverProbe(t *testing.T func TestProbeUSBIPRuntimeRejectsSuccessfulABIErrorOutput(t *testing.T) { run := func(_ context.Context, _ string, args ...string) ([]byte, error) { if args[0] == "--version" { - return []byte("0.9.7.7"), nil + return []byte("0.9.8.1"), nil } return []byte("error: ABI mismatch, unexpected size of the input structure"), nil } @@ -165,7 +165,7 @@ func TestProbeUSBIPRuntimeRejectsSuccessfulABIErrorOutput(t *testing.T) { func TestProbeUSBIPRuntimeIncludesPortFailureOutput(t *testing.T) { run := func(_ context.Context, _ string, args ...string) ([]byte, error) { if args[0] == "--version" { - return []byte("0.9.7.7"), nil + return []byte("0.9.8.1"), nil } return []byte("driver query failed"), errors.New("exit status 1") } diff --git a/internal/server/api/autoattach_operation_windows_test.go b/internal/server/api/autoattach_operation_windows_test.go index f90fc932..d28c817f 100644 --- a/internal/server/api/autoattach_operation_windows_test.go +++ b/internal/server/api/autoattach_operation_windows_test.go @@ -57,7 +57,7 @@ func TestContextNativeAttachPreservesABIAndOperationChoice(t *testing.T) { require.Equal(t, expected, *req) require.Equal(t, (*byte)(unsafe.Pointer(req)), in) require.Equal(t, in, out) - require.Equal(t, uint32(1100), inSize) + require.Equal(t, uint32(1124), inSize) require.Equal(t, uint32(8), outSize) require.NotNil(t, ov) require.Equal(t, windows.Handle(101), ov.HEvent) diff --git a/internal/server/api/autoattach_windows.go b/internal/server/api/autoattach_windows.go index 81612645..7d89678e 100644 --- a/internal/server/api/autoattach_windows.go +++ b/internal/server/api/autoattach_windows.go @@ -57,13 +57,25 @@ const ( niMaxServ = 32 ) -// PLUGIN_HARDWARE structure from usbip-win2 +// base + imported_device_location from usbip-win2 v.0.9.8.1. +// MSVC retains the location's trailing padding before derived members. +type locationIOCTL struct { + Size uint32 + PortOutput int32 + LocationHash uint32 + BusID [32]byte + Service [niMaxServ]byte + Host [niMaxHost]byte + _ [3]byte +} + +// plugin_hardware adds the optional serial and WSK receive selector. +// Leave Serial empty: the published 0.9.8.1 serial override is broken. type attachIOCTL struct { - Size uint32 - PortOutput int32 - BusID [32]byte - Service [niMaxServ]byte - Host [niMaxHost]byte + locationIOCTL + Serial [16]byte + WskEvents uint8 + _ [3]byte } const ( @@ -72,7 +84,7 @@ const ( fileReadData = 0x0001 fileWriteData = 0x0002 ioctlPluginHardware = (fileDeviceUnknown << 16) | ((fileReadData | fileWriteData) << 14) | (0x800 << 2) | methodBuffered - // usbip-win2 v.0.9.7.7 include/usbip/vhci.h, function::plugin_hardware_once. + // usbip-win2 v.0.9.8.1 include/usbip/vhci.h, function::plugin_hardware_once. // The payload and 8-byte response ABI are identical to PLUGIN_HARDWARE. ioctlPluginHardwareOnce = (fileDeviceUnknown << 16) | ((fileReadData | fileWriteData) << 14) | (0x806 << 2) | methodBuffered ) @@ -80,7 +92,7 @@ const ( func attachLocalhostClientImpl(ctx context.Context, deviceExportMeta *usbip.ExportMeta, usbipServerPort uint16, useNativeIOCTL bool, logger *slog.Logger) (AutoAttachResult, error) { if useNativeIOCTL { // Never hide a native ABI mismatch behind usbip.exe. A mismatch means - // the pinned 0.9.7.7 userspace and loaded driver are not a valid pair and must + // the pinned 0.9.8.1 userspace and loaded driver are not a valid pair and must // be repaired/rebooted before VIIPER creates devices. return attachViaIOCTL(ctx, deviceExportMeta, usbipServerPort, logger) } @@ -136,7 +148,7 @@ func attachViaIOCTL(ctx context.Context, deviceExportMeta *usbip.ExportMeta, usb if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return AutoAttachResult{}, fmt.Errorf("IOControl: native attach canceled after operation completion: %w", err) } - return AutoAttachResult{}, fmt.Errorf("IOControl: usbip-win2 0.9.7.7 native attach failed (repair or reboot USBIP; no command fallback was attempted): %w", err) + return AutoAttachResult{}, fmt.Errorf("IOControl: usbip-win2 0.9.8.1 native attach failed (repair or reboot USBIP; no command fallback was attempted): %w", err) } logger.Debug("IOCTL completed", "bytesReturned", bytesReturned, "portOutput", port) @@ -163,7 +175,11 @@ func newAttachIOCTL(meta *usbip.ExportMeta, port uint16) (attachIOCTL, error) { if err != nil { return attachIOCTL{}, err } - result := attachIOCTL{Size: uint32(unsafe.Sizeof(attachIOCTL{}))} + result := attachIOCTL{ + locationIOCTL: locationIOCTL{Size: uint32(unsafe.Sizeof(attachIOCTL{}))}, + // WSK callbacks are upstream's low-latency mode for frequent HID reports. + WskEvents: 1, + } copy(result.BusID[:], busID) copy(result.Service[:], strconv.FormatUint(uint64(port), 10)) copy(result.Host[:], "localhost") @@ -180,14 +196,23 @@ func nativeAttachControlCode(request *attachIOCTL) uint32 { return uint32(ioctlPluginHardware) } -func attachViaCommand(ctx context.Context, deviceExportMeta *usbip.ExportMeta, usbipServerPort uint16, logger *slog.Logger) (AutoAttachResult, error) { +func windowsAttachArguments(deviceExportMeta *usbip.ExportMeta, usbipServerPort uint16) ([]string, error) { arguments, err := localhostAttachArguments(deviceExportMeta, usbipServerPort) if err != nil { - return AutoAttachResult{}, err + return nil, err } + arguments = append(arguments, "--receive-mode=low-latency") if alias, aliasErr := usbip.ExportBusID(*deviceExportMeta); aliasErr == nil && usbip.ValidProductionXboxOneBusID(alias) { arguments = append(arguments, "--once") } + return arguments, nil +} + +func attachViaCommand(ctx context.Context, deviceExportMeta *usbip.ExportMeta, usbipServerPort uint16, logger *slog.Logger) (AutoAttachResult, error) { + arguments, err := windowsAttachArguments(deviceExportMeta, usbipServerPort) + if err != nil { + return AutoAttachResult{}, err + } logger.Info("Auto-attaching localhost client", "busID", deviceExportMeta.BusID, "deviceID", deviceExportMeta.DevID) cmd := exec.CommandContext(ctx, resolveUsbipExecutable(), arguments...) @@ -208,7 +233,7 @@ func resolveUsbipExecutable() string { // The usbip-win2 installer does not consistently add its directory to // PATH for already-running services. Prefer the canonical installation so // a stale copy elsewhere cannot pair a different userspace ABI with the - // pinned 0.9.7.7 driver. + // pinned 0.9.8.1 driver. seen := make(map[string]struct{}) for _, root := range []string{ os.Getenv("ProgramW6432"), @@ -320,8 +345,8 @@ func CheckAutoAttachPrerequisites(useNativeIOCTL bool, logger *slog.Logger) bool if err != nil { logger.Warn("Native IOCTL auto-attach prerequisites not met", "error", err) logger.Warn("Native IOCTL auto-attach is unavailable until discovery succeeds") - logger.Info("Install the exact signed usbip-win2 0.9.7.7 x64 package:") - logger.Info(" https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.7.7") + logger.Info("Install the exact signed usbip-win2 0.9.8.1 x64 package:") + logger.Info(" https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.8.1") return false } logger.Debug("usbip-win2 driver found") diff --git a/internal/server/api/autoattach_windows_test.go b/internal/server/api/autoattach_windows_test.go index 4e00777b..38819004 100644 --- a/internal/server/api/autoattach_windows_test.go +++ b/internal/server/api/autoattach_windows_test.go @@ -22,7 +22,7 @@ func submitAttachIOCTLTest(t *testing.T, request *attachIOCTL, _, calls, closed := nativeOperationFixture(t) calls.control = control port, returned, err := submitAttachIOCTLContext(context.Background(), 202, request, calls) - if request != nil && request.Size == 1100 && control != nil { + if request != nil && request.Size == 1124 && control != nil { require.Equal(t, int32(1), closed.Load()) } else { require.Zero(t, closed.Load()) @@ -31,8 +31,18 @@ func submitAttachIOCTLTest(t *testing.T, request *attachIOCTL, } func TestUsbipAttachABISizes(t *testing.T) { - require.Equal(t, uintptr(1100), unsafe.Sizeof(attachIOCTL{}), - "usbip-win2 0.9.7.7 plugin_hardware ABI changed") + require.Equal(t, uintptr(1124), unsafe.Sizeof(attachIOCTL{}), + "usbip-win2 0.9.8.1 plugin_hardware ABI changed") + // Values measured by MSVC against the exact upstream release header, + // commit 55e1fa7f0c2157017b02dc1f3236e98169a535e4. + var request attachIOCTL + require.Equal(t, uintptr(4), unsafe.Offsetof(request.PortOutput)) + require.Equal(t, uintptr(8), unsafe.Offsetof(request.LocationHash)) + require.Equal(t, uintptr(12), unsafe.Offsetof(request.BusID)) + require.Equal(t, uintptr(44), unsafe.Offsetof(request.Service)) + require.Equal(t, uintptr(76), unsafe.Offsetof(request.Host)) + require.Equal(t, uintptr(1104), unsafe.Offsetof(request.Serial)) + require.Equal(t, uintptr(1120), unsafe.Offsetof(request.WskEvents)) } func TestNativeAttachPayloadPreservesExactAliasWithoutOpeningDriver(t *testing.T) { @@ -42,11 +52,14 @@ func TestNativeAttachPayloadPreservesExactAliasWithoutOpeningDriver(t *testing.T copy(meta.USBBusID[:], alias) request, err := newAttachIOCTL(&meta, 3241) require.NoError(t, err) - require.Equal(t, uint32(1100), request.Size) + require.Equal(t, uint32(1124), request.Size) require.Equal(t, meta.USBBusID, request.BusID) require.Equal(t, "3241", string(bytes.TrimRight(request.Service[:], "\x00"))) require.Equal(t, "localhost", string(bytes.TrimRight(request.Host[:], "\x00"))) require.Zero(t, request.PortOutput) + require.Zero(t, request.LocationHash) + require.Equal(t, [16]byte{}, request.Serial) + require.Equal(t, uint8(1), request.WskEvents) meta.USBBusID = [32]byte{} _, err = newAttachIOCTL(&meta, 3241) require.Error(t, err) @@ -61,6 +74,24 @@ func TestNativeAutoAttachResultCarriesExactPort(t *testing.T) { require.Empty(t, got.USBIPOwnerSerial) } +func TestWindowsCommandAttachUsesLowLatencyAndExactXboxRetryPolicy(t *testing.T) { + alias, err := usbip.NewProductionXboxOneBusID() + require.NoError(t, err) + for _, busID := range []string{"17-4", alias} { + meta := usbip.ExportMeta{BusID: 17, DevID: 4} + copy(meta.USBBusID[:], busID) + args, err := windowsAttachArguments(&meta, 3241) + require.NoError(t, err) + expected := []string{"--tcp-port", "3241", "attach", "-r", "localhost", "-b", busID, "--receive-mode=low-latency"} + if busID == alias { + expected = append(expected, "--once") + } + require.Equal(t, expected, args) + } + _, err = windowsAttachArguments(nil, 3241) + require.Error(t, err) +} + func TestNativeXboxOneAttachFailureUsesOneAttemptWithoutFallback(t *testing.T) { alias, err := usbip.NewProductionXboxOneBusID() require.NoError(t, err) @@ -77,7 +108,7 @@ func TestNativeXboxOneAttachFailureUsesOneAttemptWithoutFallback(t *testing.T) { controlCode = code require.Equal(t, (*byte)(unsafe.Pointer(&request)), in) require.Equal(t, in, out) - require.Equal(t, uint32(1100), inSize) + require.Equal(t, uint32(1124), inSize) require.Equal(t, uint32(8), outSize) require.NotNil(t, overlapped) return windows.ERROR_TIMEOUT @@ -86,7 +117,7 @@ func TestNativeXboxOneAttachFailureUsesOneAttemptWithoutFallback(t *testing.T) { require.Zero(t, port) require.Zero(t, returned) require.Equal(t, 1, calls) - // v.0.9.7.7 include/usbip/vhci.h: PLUGIN_HARDWARE_ONCE, + // v.0.9.8.1 include/usbip/vhci.h: PLUGIN_HARDWARE_ONCE, // CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_BUFFERED, READ|WRITE). require.Equal(t, uint32(0x0022e018), controlCode, "failed transactional Xbox activation must not start background kernel attaches") @@ -145,17 +176,17 @@ func TestNativeAttachInvalidRequestDoesNotReachDriver(t *testing.T) { called = true return nil } - for _, request := range []*attachIOCTL{nil, {}, {Size: 8}, {Size: 1104}} { + for _, request := range []*attachIOCTL{nil, {}, {locationIOCTL: locationIOCTL{Size: 8}}, {locationIOCTL: locationIOCTL{Size: 1100}}} { _, _, err := submitAttachIOCTLTest(t, request, fake) require.Error(t, err) } - _, _, err := submitAttachIOCTLTest(t, &attachIOCTL{Size: 1100}, nil) + _, _, err := submitAttachIOCTLTest(t, &attachIOCTL{locationIOCTL: locationIOCTL{Size: 1124}}, nil) require.Error(t, err) require.False(t, called) } func TestNativeAttachRejectsTruncatedAndOversizedSuccessWithoutRetry(t *testing.T) { - for _, size := range []uint32{0, 4, 7, 9, 1100} { + for _, size := range []uint32{0, 4, 7, 9, 1124} { meta := usbip.ExportMeta{BusID: 17, DevID: 4} copy(meta.USBBusID[:], "17-4") request, err := newAttachIOCTL(&meta, 3241) diff --git a/internal/server/api/handler/bus_device_activate_xboxone.go b/internal/server/api/handler/bus_device_activate_xboxone.go index 71190feb..07906d78 100644 --- a/internal/server/api/handler/bus_device_activate_xboxone.go +++ b/internal/server/api/handler/bus_device_activate_xboxone.go @@ -127,13 +127,14 @@ func BusDeviceActivateAuthorizedXboxOne( } } -// usbip-win2 0.9.7.7's native attach IOCTL returns exactly the base size and -// assigned hub port; it has no attach-time serial override or owner-token -// field. The authenticated capability selects the exact registration; its +// usbip-win2 0.9.8.1's native attach IOCTL returns exactly the base size and +// assigned hub port, not an owner-token field. The published release's serial +// override is broken, so we leave it empty. The authenticated capability +// selects the exact registration; its // separate public export alias survives automatic reconnect without selecting // a same-numeric-address successor. A positive port is an attach result, never // authority to detach that number later. Requiring a nonempty serial here -// would reject every real 0.9.7.7 native attach after it already succeeded. +// would reject every real 0.9.8.1 native attach after it already succeeded. func validXboxOneActivationAttachResult(result api.AutoAttachResult) bool { return result.USBIPPort > 0 } diff --git a/internal/server/api/handler/bus_device_add_xboxone_auth_test.go b/internal/server/api/handler/bus_device_add_xboxone_auth_test.go index 62268e2b..5efdf111 100644 --- a/internal/server/api/handler/bus_device_add_xboxone_auth_test.go +++ b/internal/server/api/handler/bus_device_add_xboxone_auth_test.go @@ -55,7 +55,7 @@ func TestXboxOneActivationAcceptsPinned0977NativePortWithoutOwnerSerial( result api.AutoAttachResult want bool }{ - {name: "native 0.9.7.7", result: api.AutoAttachResult{USBIPPort: 7}, want: true}, + {name: "native 0.9.8.1", result: api.AutoAttachResult{USBIPPort: 7}, want: true}, {name: "future owner token", result: api.AutoAttachResult{ USBIPPort: 8, USBIPOwnerSerial: "DS4W123456789AB"}, want: true}, {name: "missing port", result: api.AutoAttachResult{}, want: false}, diff --git a/internal/server/api/xboxone_retry_cleanup_windows.go b/internal/server/api/xboxone_retry_cleanup_windows.go index 2b0ee744..568ecef5 100644 --- a/internal/server/api/xboxone_retry_cleanup_windows.go +++ b/internal/server/api/xboxone_retry_cleanup_windows.go @@ -11,10 +11,10 @@ import ( "golang.org/x/sys/windows" ) -// usbip-win2 0.9.7.7 vhci::ioctl::stop_attach_attempts, not PLUGOUT. +// usbip-win2 0.9.8.1 vhci::ioctl::stop_attach_attempts, not PLUGOUT. // All three location strings must be present: zero strings mean stop ALL. type stopAttachIOCTL struct { - attachIOCTL + locationIOCTL Count int32 } @@ -29,7 +29,8 @@ func newStopAttachIOCTL(meta *usbip.ExportMeta, port uint16) (stopAttachIOCTL, e if err != nil { return stopAttachIOCTL{}, err } - request := stopAttachIOCTL{attachIOCTL: location} + // Stop uses only base + location; serial and receive mode are attach-only. + request := stopAttachIOCTL{locationIOCTL: location.locationIOCTL} request.Size = uint32(unsafe.Sizeof(request)) return request, nil } diff --git a/internal/server/api/xboxone_retry_cleanup_windows_test.go b/internal/server/api/xboxone_retry_cleanup_windows_test.go index 9f8c8e80..4782c76e 100644 --- a/internal/server/api/xboxone_retry_cleanup_windows_test.go +++ b/internal/server/api/xboxone_retry_cleanup_windows_test.go @@ -13,15 +13,15 @@ import ( "golang.org/x/sys/windows" ) -func TestStopRetry0977ABIHasExactLocationAndNeverDetachesOrStopsAll(t *testing.T) { +func TestStopRetry0981ABIHasExactLocationAndNeverDetachesOrStopsAll(t *testing.T) { alias, err := usbip.NewProductionXboxOneBusID() require.NoError(t, err) meta := usbip.ExportMeta{BusID: 17, DevID: 4} copy(meta.USBBusID[:], alias) request, err := newStopAttachIOCTL(&meta, 3241) require.NoError(t, err) - require.Equal(t, uintptr(1104), unsafe.Sizeof(request)) - require.Equal(t, uintptr(1100), unsafe.Offsetof(request.Count)) + require.Equal(t, uintptr(1108), unsafe.Sizeof(request)) + require.Equal(t, uintptr(1104), unsafe.Offsetof(request.Count)) require.Equal(t, uint32(0x0022e014), uint32(ioctlStopAttachAttempts)) require.Equal(t, alias, string(bytes.TrimRight(request.BusID[:], "\x00"))) require.Equal(t, "localhost", string(bytes.TrimRight(request.Host[:], "\x00"))) @@ -31,7 +31,7 @@ func TestStopRetry0977ABIHasExactLocationAndNeverDetachesOrStopsAll(t *testing.T calls.control = func(_ windows.Handle, code uint32, in *byte, inSize uint32, out *byte, outSize uint32, returned *uint32, _ *windows.Overlapped) error { require.Equal(t, uint32(ioctlStopAttachAttempts), code) require.Equal(t, in, out) - require.Equal(t, uint32(1104), inSize) + require.Equal(t, uint32(1108), inSize) require.Equal(t, inSize, outSize) request.Count = 3 *returned = outSize diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 77740f7d..21af4c15 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,6 +1,8 @@ param( [switch]$Yes, - [switch]$DeveloperStandalone + [switch]$DeveloperStandalone, + [ValidatePattern("^v[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$")] + [string]$ViiperReleaseTag ) $ErrorActionPreference = "Stop" @@ -21,8 +23,11 @@ if (-not $DeveloperStandalone -or ) } -$viiperVersion = "v0.1.2" -$usbipTargetVersion = [Version]"0.9.7.7" +if ([string]::IsNullOrWhiteSpace($ViiperReleaseTag)) { + throw "Specify -ViiperReleaseTag for an upstream VIIPER build supporting USB/IP 0.9.8.1. No release is selected automatically." +} +$viiperVersion = $ViiperReleaseTag +$usbipTargetVersion = [Version]"0.9.8.1" $installDir = Join-Path $env:LOCALAPPDATA "VIIPER" $usbipReplacementStatePath = Join-Path $installDir ` "usbip-replacement-pending.json" @@ -37,7 +42,7 @@ $canonicalUsbipPath = Join-Path $programFilesRoot "USBip\usbip.exe" Write-Host "VIIPER setup plan:" -ForegroundColor Cyan Write-Host " 1. Download and install VIIPER first." -ForegroundColor Cyan -Write-Host " 2. Verify or install signed usbip-win2 0.9.7.7." -ForegroundColor Cyan +Write-Host " 2. Verify or install signed usbip-win2 0.9.8.1." -ForegroundColor Cyan Write-Host " 3. Verify the USBIP ABI before starting VIIPER." -ForegroundColor Cyan Write-Host "USB hubs may restart, and Windows may require a reboot." -ForegroundColor Yellow if (-not $Yes) { @@ -67,7 +72,7 @@ $arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { } if ((Get-CimInstance Win32_ComputerSystem).SystemType -match "ARM") { - throw "The current hbashton VIIPER package supports Windows x64 only." + throw "The current VIIPER package supports Windows x64 only." } $preferredAssetNames = @("viiper-windows-$arch.zip", "viiper.exe") @@ -215,7 +220,7 @@ try { if (-not $state.BootSessionId -or $state.BootSessionId -eq (Get-WindowsBootSessionId)) { throw "USBIP replacement phase 1 is complete. Restart Windows " + - "yourself, then run setup again to install 0.9.7.7." + "yourself, then run setup again to install 0.9.8.1." } Write-Host "Validating the required reboot after USBIP removal..." ` @@ -664,7 +669,7 @@ exit 32 return [pscustomobject]@{ Healthy = $true Reason = "ready" - Detail = "0.9.7.7 ABI probe passed" + Detail = "0.9.8.1 ABI probe passed" } } @@ -722,17 +727,17 @@ exit 32 $block, '(?im)^\s*->\s+serial\b(?.*)$') if (-not $serialLine.Success -or [string]::IsNullOrWhiteSpace( - $serialLine.Groups['value'].Value.Trim(" '"))) { - # usbip-win2 0.9.7.7 has no attach-time owner serial. The exact + $serialLine.Groups['value'].Value.Trim(" :'"))) { + # The published 0.9.8.1 serial override is broken. The exact # loopback VIIPER endpoint and bus/device tuple are its identity. return $true } $serial = [regex]::Match( $serialLine.Groups['value'].Value, - "^\s*'(?[^']+)'\s*$") + "^\s*(?::\s*(?[^\r\n]+)|'(?[^']+)')\s*$") return $serial.Success -and - $serial.Groups['serial'].Value -cmatch '^DS4W[0-9A-Fa-f]{11}$' + $serial.Groups['serial'].Value.Trim() -cmatch '^DS4W[0-9A-Fa-f]{11}$' } function Disconnect-UsbipImports([string]$usbipPath) { @@ -1120,16 +1125,16 @@ exit 32 if (-not $needsUsbipInstall) { if ($usbipRuntimeBefore.Healthy) { Write-Host ( - "USBIP 0.9.7.7 userspace/driver ABI is ready at the canonical " + + "USBIP 0.9.8.1 userspace/driver ABI is ready at the canonical " + "Program Files path." ) -ForegroundColor Green } else { Write-Host ( - "USBIP 0.9.7.7 is installed, but its live ABI probe failed: " + + "USBIP 0.9.8.1 is installed, but its live ABI probe failed: " + "$($usbipRuntimeBefore.Detail). Setup will not overlay a " + "running kernel driver. Restart Windows, then run setup " + - "again. If the probe still fails, uninstall 0.9.7.7, " + + "again. If the probe still fails, uninstall 0.9.8.1, " + "restart, and rerun setup." ) -ForegroundColor Yellow } @@ -1144,13 +1149,13 @@ exit 32 elseif ($usbipInstalledVersion) { Write-Host ( "USBIP reports version $usbipInstalledVersion, but its canonical " + - "0.9.7.7 runtime is not usable ($($usbipRuntimeBefore.Detail)). " + - "Repairing the exact 0.9.7.7 package..." + "0.9.8.1 runtime is not usable ($($usbipRuntimeBefore.Detail)). " + + "Repairing the exact 0.9.8.1 package..." ) -ForegroundColor Yellow } else { Write-Host ( - "USBIP 0.9.7.7 is missing or unusable " + + "USBIP 0.9.8.1 is missing or unusable " + "($($usbipRuntimeBefore.Detail)). Installing the exact package..." ) -ForegroundColor Yellow } @@ -1187,9 +1192,9 @@ exit 32 ) -ForegroundColor Yellow } else { - $usbipInstallerUrl = "https://github.com/vadimgrn/usbip-win2/releases/download/v.0.9.7.7/USBip-0.9.7.7-x64.exe" + $usbipInstallerUrl = "https://github.com/vadimgrn/usbip-win2/releases/download/v.0.9.8.1/USBip-0.9.8.1-x64.exe" $usbipInstaller = Join-Path $tempDir "USBip-setup.exe" - Write-Host " Downloading exact usbip-win2 0.9.7.7..." ` + Write-Host " Downloading exact usbip-win2 0.9.8.1..." ` -ForegroundColor Cyan Invoke-WebRequest -Uri $usbipInstallerUrl ` -OutFile $usbipInstaller -ErrorAction Stop @@ -1197,7 +1202,7 @@ exit 32 -LiteralPath $usbipInstaller -Algorithm SHA256). Hash.ToLowerInvariant() if ($usbipInstallerHash -ne - "51620fa5f9f8be5932bc9d786deee557ce06d5407a99cab490dcfac71f185fea") { + "38cad6d4432b52d5bb9409d9ad03b72fdffc4ada4cd3a48fbeca1a2752a8518a") { throw "USBIP installer integrity check failed." } @@ -1246,7 +1251,7 @@ exit 32 } if (-not $usbipRuntime.Healthy -and -not $needsReboot) { $needsReboot = $true - Write-Host "USBIP 0.9.7.7 is installed but not active yet: $($usbipRuntime.Detail)" -ForegroundColor Yellow + Write-Host "USBIP 0.9.8.1 is installed but not active yet: $($usbipRuntime.Detail)" -ForegroundColor Yellow } if ($usbipRuntime.Healthy -and -not $needsReboot) { @@ -1284,7 +1289,7 @@ exit 32 if ($needsReboot) { Write-Host "" - Write-Host "USBIP 0.9.7.7 requires a restart before VIIPER can run safely." -ForegroundColor Yellow + Write-Host "USBIP 0.9.8.1 requires a restart before VIIPER can run safely." -ForegroundColor Yellow Write-Host "VIIPER startup remains disabled. Restart Windows yourself, then run this setup again." -ForegroundColor Yellow } # `viiper install` above starts the single verified server instance. diff --git a/scripts/test-install-first-run.ps1 b/scripts/test-install-first-run.ps1 index 17c730cd..302a600d 100644 --- a/scripts/test-install-first-run.ps1 +++ b/scripts/test-install-first-run.ps1 @@ -63,3 +63,28 @@ finally { } Write-Host "VIIPER installer first-run registry contract passed." + +$ownership = $ast.Find({ + param($node) + $node -is [Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq 'Test-ViiperOwnedUsbipPort' +}, $true) +if ($null -eq $ownership) { throw 'USB-IP ownership helper is missing.' } +Invoke-Expression $ownership.Extent.Text +foreach ($case in @( + @{ Line = '-> serial: '; Owned = $true }, + @{ Line = '-> serial: DS4W0123456789A'; Owned = $true }, + @{ Line = '-> serial: foreign'; Owned = $false }, + @{ Line = "-> serial 'DS4W0123456789A'"; Owned = $true }, + @{ Line = "-> serial 'foreign'"; Owned = $false }, + @{ Line = '-> serial foreign'; Owned = $false })) { + foreach ($location in @('localhost:3241/17-4', 'remote.example:3241/17-4', + 'localhost:3240/17-4', 'localhost:3241/x1-protected')) { + $block = "Port 07: device in use at high-speed`n -> usbip://$location`n $($case.Line)`n -> mode: low-latency" + $expected = $case.Owned -and $location -eq 'localhost:3241/17-4' + if ((Test-ViiperOwnedUsbipPort $block) -ne $expected) { + throw "USB-IP cleanup ownership mismatch: $location / $($case.Line)" + } + } +} +Write-Host "VIIPER cleanup ownership passes with old and 0.9.8.1 port output." From c5d8a589cfabd3547e83c72d476119a24030da80 Mon Sep 17 00:00:00 2001 From: Pawel Osmolski Date: Mon, 12 Oct 2026 12:17:12 +0100 Subject: [PATCH 2/2] Document coordinated driver upgrades and explicit developer release selection --- docs/getting-started/installation.md | 32 ++++++++++++---------------- docs/getting-started/quickstart.md | 32 ++++++++++++++-------------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 459f5b3d..3db907fa 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -18,23 +18,16 @@ You must have a USBIP-Client implementation available on your system to use VIIP === "Windows" - VIIPER for DS4Windows requires the signed - [usbip-win2 0.9.7.7 x64 release](https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.7.7). - Use the DS4Windows **Install / Repair VIIPER** action whenever possible; its - package contains this exact verified installer. - - !!! danger "Do not substitute another USBIP build" - This release is ABI-pinned to **0.9.7.7**. Version 0.9.7.8 reproduced - kernel pool corruption during controller attachment and is explicitly - rejected. The installer verifies the version and live ABI before - allowing VIIPER to start. - - !!! warning "USBIP-Win2 signing certificate" - The upstream 0.9.7.7 installer may add the publicly available USBIP - test-signing CA to **Trusted Root Certification Authorities**. After - installation, you may remove the certificate named **USBIP** with - `certlm.msc` (run as administrator). Do not replace 0.9.7.7 with a - newer or OSSign build: VIIPER validates this exact userspace/driver ABI. + This ABI update requires the signed + [usbip-win2 0.9.8.1 x64 release](https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.8.1). + Use the complete matching DS4Windows package once it is published; do not + install the new driver underneath a backend built for the older ABI. + + !!! warning "Coordinate the app, backend and driver" + Existing published DS4Windows packages retain their original dependency + pin until a matching backend is released. Driver replacement preserves + the uninstall/restart/install boundary. Setup verifies identities and + the live ABI before starting output. === "Linux" @@ -141,7 +134,10 @@ You must have a USBIP-Client implementation available on your system to use VIIP === "Windows" ```powershell - irm https://raw.githubusercontent.com/hbashton/VIIPER/main/scripts/install.ps1 | iex + Invoke-WebRequest https://raw.githubusercontent.com/hbashton/VIIPER/main/scripts/install.ps1 -OutFile install.ps1 + $approvedTag = Read-Host "Upstream VIIPER release tag supporting USB/IP 0.9.8.1" + $env:VIIPER_DEVELOPER_STANDALONE = "1" + .\install.ps1 -DeveloperStandalone -ViiperReleaseTag $approvedTag ``` Installs to: `%LOCALAPPDATA%\VIIPER\viiper.exe` diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 47937a37..7583f5b1 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -5,7 +5,7 @@ Ensure you have: 1. **USBIP installed** on your system (see [Installation](installation.md#requirements)) -2. **VIIPER binary** downloaded from [hbashton/VIIPER Releases](https://github.com/hbashton/VIIPER/releases) or [built from source](installation.md#building-from-source) +2. **VIIPER binary** downloaded from [hbashton/VIIPER Releases](https://github.com/hbashton/VIIPER/releases) or [built from source](installation.md#building-from-source) ## Starting the Server @@ -17,10 +17,10 @@ viiper server This starts two services: -- **USBIP Server** on `127.0.0.1:3241` (standard USBIP protocol) +- **USBIP Server** on `127.0.0.1:3241` (standard USBIP protocol) - **VIIPER API Server** on port `3242` (management and device interactions) -!!! warning "Authentication for Remote Connections" +!!! warning "Authentication for Remote Connections" On first start, VIIPER generates a random password and saves it to `/viiper.key.txt`. Windows: `%APPDATA%\VIIPER\viiper.key.txt` @@ -32,13 +32,13 @@ This starts two services: All authenticated connections use **ChaCha20-Poly1305 encryption** to protect against man-in-the-middle attacks. - You can change the password at any time by editing `viiper.key.txt`. - -!!! warning "USB/IP is local-only by default" - The password and encryption above protect the API, not the separate USB/IP - protocol. USB/IP listens only on `127.0.0.1:3241` by default. If you - explicitly bind it to a non-loopback address, place it behind a trusted - tunnel, host firewall, or equivalent authenticated network boundary. + You can change the password at any time by editing `viiper.key.txt`. + +!!! warning "USB/IP is local-only by default" + The password and encryption above protect the API, not the separate USB/IP + protocol. USB/IP listens only on `127.0.0.1:3241` by default. If you + explicitly bind it to a non-loopback address, place it behind a trusted + tunnel, host firewall, or equivalent authenticated network boundary. !!! tip "Auto-attach Feature" By default, VIIPER automatically attaches newly created devices to the local machine. You can disable this with `--api.auto-attach-local-client=false`. @@ -210,14 +210,14 @@ Now that you have a working setup: ```bash # Use custom ports -viiper server --usb.addr=127.0.0.1:9000 --api.addr=:9001 +viiper server --usb.addr=127.0.0.1:9000 --api.addr=:9001 ``` **Permission denied (Linux):** ```bash # Use ports above 1024 or run with sudo -viiper server --usb.addr=127.0.0.1:3241 --api.addr=:3242 +viiper server --usb.addr=127.0.0.1:3241 --api.addr=:3242 ``` ### Auto-Attach Not Working @@ -247,10 +247,10 @@ See [Linux Kernel Module Setup](installation.md#linux-kernel-module-setup-for-au **Windows - USBIP tool not found:** -Install the exact signed -[usbip-win2 0.9.7.7 x64 release](https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.7.7). -Do not mix it with another usbip-win2 userspace or driver version; VIIPER -checks the live ABI before starting. +Install the exact signed +[usbip-win2 0.9.8.1 x64 release](https://github.com/vadimgrn/usbip-win2/releases/tag/v.0.9.8.1). +Do not mix it with another usbip-win2 userspace or driver version; VIIPER +checks the live ABI before starting. ### Device Not Attaching