Guides

USB devices

A running machine can use real USB hardware plugged into your computer. This page explains how the WebUSB passthrough works, what kinds of devices it can and cannot carry, the per-OS driver requirements, and the practical details you'll hit first.

How it works

Browsers expose a device-level USB API — WebUSB — that lets a page talk to a USB device the user has explicitly granted. brintOS bridges that API to the guest kernel:

  1. You click Attach USB device on the launch surface and pick a device in the browser's chooser. The grant is yours to give — a page can never enumerate or open hardware you didn't select.
  2. The granted device is plugged into a port of the machine's virtual USB host controller. This is not an emulated xHCI chip — it is a minimal controller that forwards the kernel's USB request blocks (URBs) one-for-one onto WebUSB transfers.
  3. The guest's stock Linux USB stack takes over: the kernel sees a port-connect, resets the port, reads descriptors, and binds the same in-tree driver the device would get on a native Linux box. An FTDI serial adapter really is driven by ftdi_sio and appears as /dev/ttyUSB0; lsusb and /sys/bus/usb show the device exactly as Linux sees it.

Control, bulk, and interrupt transfers are forwarded verbatim. Transfer data moves directly between your browser tab and the device — it never touches brintOS servers. A handful of standard control requests are answered by the bridge itself rather than the device, because WebUSB deliberately hides bus mechanics from pages: the guest's SET_ADDRESS is absorbed (the OS owns real bus addressing), descriptors are served from the metadata the browser exposes, and SET_CONFIGURATION becomes the WebUSB configuration-select plus an interface claim. Class- and vendor-specific requests — the ones your device's driver actually cares about — pass through untouched.

Attaching and detaching

  • Attach — the button works while the machine is running (hot-plug) or before boot (the device is plugged in as the machine comes up). Grants persist in the browser's per-site permission store, so a device you've granted once reattaches on the next visit without re-prompting.
  • Disconnect — unplugs the device from the guest (the kernel sees a normal USB disconnect) and revokes the browser grant.
  • Each machine has 4 virtual ports. The USB telemetry card shows what's occupied, and whether a granted device actually made it into the guest — a device the browser granted but could not open shows an amber “granted, but not attached” state with the reason.
  • Repo owners can pre-approve device types under Settings → USB devices; the chooser then filters to those vendor/product IDs.

What can attach — and what can't

WebUSB is deliberately restricted, and those restrictions carry straight into the passthrough. The rules below come from the browser, not from brintOS — no amount of guest-side driver support can lift them.

Blocked: protected device classes

Browsers refuse to hand pages any interface belonging to a protected class, because a page controlling them could log keystrokes, read disks, or spy through a camera:

  • Keyboards, mice, and other HID devices
  • Mass storage (flash drives, card readers)
  • Audio and video devices (headsets, webcams)
  • Smart cards, and wireless controllers (Bluetooth adapters)

A device with mixed interfaces can still attach — the browser just withholds the protected interfaces. What's left is the sweet spot of the passthrough: vendor-class devices. USB-serial adapters, microcontroller dev boards, DFU/flash programmers, JTAG/SWD probes, software-defined radios, lab instruments — devices whose interfaces are class 0xFF (vendor-specific) are exactly what WebUSB was designed for.

Blocked: isochronous endpoints

WebUSB has no isochronous transfer surface, so streaming endpoints (USB audio, UVC video) cannot be serviced even when the class itself isn't protected. The guest controller refuses isochronous URBs loudly rather than stalling silently.

No hubs, no tiers

Grants are per-device, so the guest never sees your physical hub topology: every granted device appears directly on a root-hub port. USB hubs themselves cannot be attached.

Your operating system's driver matters

The browser can only open a device the host OS lets it claim. This is the #1 source of “granted, but not attached” failures, and it differs sharply by platform:

Host OSWhat to expect
LinuxWorks out of the box: Chrome detaches a bound kernel driver (e.g. the host's own ftdi_sio) automatically when it claims the interface. You may need a udev rule granting your user access to the device node.
WindowsWebUSB can only open devices bound to the WinUSB driver. A device with a vendor driver installed — an FTDI adapter with the VCP COM-port driver is the canonical case — fails with Access denied. Replace the driver with WinUSB using Zadig (note this removes the COM port until you revert it in Device Manager).
macOSGenerally works for vendor-class devices (no codeless kernel driver claims them). A device captured by a third-party kext will refuse until that driver is removed.

WebUSB itself is Chromium-only: Chrome, Edge, Opera, and other Chromium-based browsers support it; Firefox and Safari do not. The launch surface detects this and disables the attach button with a notice.

Worked example: a USB-serial adapter

An FTDI TTL-232R (vendor-class, so it passes every rule above) attaches and binds the stock ftdi_sio driver:

usb 1-1: New USB device found, idVendor=0403, idProduct=6001
usb 1-1: Product: TTL232R-3V3
ftdi_sio 1-1:1.0: FTDI USB Serial Device converter detected
usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0

With a TX↔RX loop-back jumper on the adapter, this sequence proves data integrity end to end:

stty -F /dev/ttyUSB0 115200 raw -echo -crtscts
cat /dev/ttyUSB0 > /root/rx &
sleep 3                                # let the reader spawn and open the port
printf 'loopback-test-hello\n' > /dev/ttyUSB0
sleep 1
kill %1
cat /root/rx                           # -> loopback-test-hello

Two platform details make the ordering above load-bearing:

  • Start the reader first, and give it time. Every exec in the guest loads its binary from the cloud drive, so a freshly-spawned cat can take a few seconds to open the port on a cold cache — while a loop-back round trip completes in microseconds. Bytes that return before any reader holds the port open are discarded (the serial driver purges device buffers on first open, as on native Linux). For the same reason, wrapping cold commands in tight timeout 2-style budgets is unreliable.
  • Disable echo before writing. With a loop-back jumper and echo left on, every received byte is retransmitted out the port — an infinite feedback loop.

Other details worth knowing

  • Exclusive to one tab. Attaching claims the device's interfaces; no other tab, page, or host application can use it until you disconnect.
  • Virtual bus identity. The bus/device numbers, port paths, and root hub the guest reports are the virtual controller's, not your physical topology. Vendor, product, class, endpoint, and string descriptors are the real device's.
  • Timing rides the browser. Each transfer is an asynchronous browser round trip. Serial links, flashing, and register-poke protocols work well; latency-critical protocols that assume microsecond turnaround on the host CPU may not.
  • Failures are loud. A transfer the bridge can't serve completes into the guest as a real USB error (-EPIPE for a stall, -ENODEV when the device vanishes or its grant is revoked) — never a silent hang. Unplugging mid-run is a normal USB disconnect; the guest driver tears down like it would on hardware.
  • Permission management. Chromium remembers grants per site; review or revoke them under the browser's site-settings USB panel (the USB card links to it), or with the device's Disconnect button.

Related