DEV Community

Danil Galeev
Danil Galeev

Posted on Fully Autonomous

Updating Xbox controller firmware from Linux: the USB hotplug trap

Xbox controller connected to a Linux host and Windows VM

I wanted to update two Xbox controllers without booting my laptop into Windows. Both eventually went from 5.9.2709.0 to 5.23.6.0, with Xbox Accessories confirming Updated! and then No update available for each one.

The update reset the USB connection, and getting the controller back into the VM took most of the work.

The scripts and setup guide are in zedxter/xbox-firmware-linux.

This uses Microsoft's official updater inside a Windows VM hosted on Linux. It does not implement a native Linux firmware flasher or distribute firmware files.

Tested setup and limits

The successful session used openSUSE Tumbleweed with SELinux enforcing, KVM, rootful Docker, Windows 10 22H2 and two Microsoft controllers with USB ID 045e:0b12.

I turned the helpers from this session into configurable scripts. The portable version has tests for event filtering, generated configuration and cleanup protections, but the complete refactored workflow has not yet been retested on hardware. Windows 11 is the default for a new setup; that guest path also remains unvalidated. Firmware 5.23.6.0 was the version Accessories installed on these two controllers; other models may receive a different version.

Why the controller disappeared after USB reset

A firmware update can reset a controller's USB connection. The device address can change, and a bootloader may use a different product ID.

QEMU supports selecting a physical USB port, so I used a configuration shaped like this:

-device usb-host,hostbus=3,hostport=1,id=xbox-controller
Enter fullscreen mode Exit fullscreen mode

The physical port 3-1 is an example. The scripts detect the port you select and generate the arguments.

After a reset, Linux could see the controller, and a fresh libusb context inside the container could see it too. The running QEMU process still could not reacquire it.

The device node was accessible and had the correct SELinux label. QEMU's cached USB object was misleading: its existence did not prove a working connection.

The evidence pointed toward missing hotplug events. libusb's Linux backend consumes processed udev notifications. A USB bind mount provides device access, but does not automatically provide those host events inside a separate network namespace.

systemd's device monitor can disable its udev subscription when /run/udev/control is absent and /dev is not devtmpfs.

I added a monitor marker and a small relay. It forwards original root-origin udev packets only for Microsoft USB device add/remove events on the selected physical port. It excludes USB interfaces and unrelated ports. The repository's architecture notes explain the checks and access boundaries.

Step 1: prepare the host

You need a Linux x86-64 host with KVM, local rootful Docker Engine, Compose v2, Python 3.10+, systemd/udev, and a USB data cable. Allow room for a 4 GB Windows guest and preferably at least 80 GB of free storage. On SELinux hosts, the helpers also use chcon and restorecon.

Disconnect other wired Xbox controllers: preparation temporarily unloads their shared xpad driver. Review the scripts before running them as root.

git clone https://github.com/zedxter/xbox-firmware-linux.git
cd xbox-firmware-linux
python3 scripts/configure.py --list

# Replace 3-1 with the controller's actual physical port.
python3 scripts/configure.py --port 3-1
Enter fullscreen mode Exit fullscreen mode

Configuration creates a local Compose file, a random Windows password in an ignored .env, and storage for the VM. No passwords or VM disks are included in the repository. Windows licensing remains your responsibility.

Step 2: start Windows and the relay

sudo docker compose -f compose.local.json create
sudo python3 scripts/host_session.py prepare
sudo docker compose -f compose.local.json start
Enter fullscreen mode Exit fullscreen mode

If preparation fails, keep the container stopped and run sudo python3 scripts/host_session.py cleanup before fixing the error and retrying.

In another terminal, from the same directory:

sudo systemd-inhibit --what=sleep:idle --mode=block \
  --who=XboxFirmwareUpdate --why='Xbox controller firmware update' \
  python3 -u scripts/relay.py
Enter fullscreen mode Exit fullscreen mode

Keep that terminal open. Look for READY and then FORWARDED add. Keep the laptop on power with its lid open.

Open http://127.0.0.1:8006 and let Windows install. The setup builds on dockur/windows; the repository pins the image used in the original session. The console is bound to localhost.

The relay filters its events. The VM still has privileged USB access, so run it as a trusted maintenance tool.

Step 3: prepare Accessories, then test reconnect

Install available Windows updates, finish guest restarts, and install Xbox Accessories from the Microsoft Store.

We encountered three different blockers:

  • Windows OS update required: updating Windows resolved it.
  • Gaming Runtime Services sign-in failed: installing the official Xbox app and signing in resolved our case.
  • Device Manager reported a working controller, but Accessories did not show it: uninstalling the device without deleting the driver package, followed by a Windows restart, restored discovery.

I only needed these fixes when the corresponding error appeared.

Before starting any firmware update, unplug and reconnect the controller once to the same physical port. Accessories must find it again without a Docker restart, and the relay should report an add event.

sudo python3 scripts/status.py
Enter fullscreen mode Exit fullscreen mode

If that pre-flash test fails, stop and fix discovery first. A guest restart preserves the relay; restarting the Docker container requires starting a new relay.

Step 4: update one controller at a time

Start the available update in Xbox Accessories. Do not disconnect USB, stop the relay, suspend Linux or restart Windows/Docker while updating.

Wait for the explicit success screen. Then reopen the controller details and verify the full firmware version and update status.

The first controller needed a Windows restart after the success screen before Accessories would read its new version. The second reported its new version immediately. Both were confirmed at 5.23.6.0.

Only after verifying the first controller should you swap in the second, using the same cable and port. Repeat the reconnect test and update.

Step 5: finish the session

After every update is finished, shut Windows down through its Start menu. Wait for the container to exit:

sudo docker inspect -f '{{.State.Status}}' xbox-firmware-linux
Enter fullscreen mode Exit fullscreen mode

If Windows is fully shut down but Docker remains running, stop it with sudo docker compose -f compose.local.json stop. Never use this to interrupt firmware updating: Docker can force-stop after its grace period.

The relay exits with its container process. Restore the host settings:

sudo python3 scripts/host_session.py cleanup
Enter fullscreen mode Exit fullscreen mode

The cleanup command refuses a running VM. It removes matching temporary rules, restores the USB label and restores the previous xpad loaded state. Reconnect the cable for Linux. The Windows disk is retained for later use and should remain private.

Firmware did not solve every Bluetooth problem

After updating, the first controller could pair but still failed to reconnect after being turned off and on. A clean reboot and fresh pairing did not resolve it.

On our MediaTek adapter with BlueZ 5.87, testing this option made reconnection work:

[LE]
CentralAddressResolution=0
Enter fullscreen mode Exit fullscreen mode

This is a separate, adapter-wide workaround, not a universal Xbox setting. It does not delete pairing keys or disable encryption. Only the first controller's Bluetooth reconnect was independently verified. The repository includes instructions and rollback guidance; the scripts do not apply it automatically.

For general pairing cleanup after firmware changes, consult xpadneo's troubleshooting guide.

Share reproducible results

If you try the toolkit, report your controller model, host and guest versions, whether the pre-flash reconnect test passed, and the firmware version Accessories confirmed. Remove serial numbers, Bluetooth addresses and keys from logs.

I would test USB rediscovery before clicking Update again. If the controller does not return to Accessories after a simple cable reconnect, a firmware reset is unlikely to go better.

Top comments (0)