Hacktakes · Edition 10
Hacktakes · Edition 10 · July 22, 2026

How does software turn off USB ports? (stracing uhubctl)

Software turns off a physical USB port by opening its Linux device file and using an ioctl system call to send hex bytes that clear its power feature.

By Poppy Lin

Sparked by Uhubctl – Control USB power per-port on smart USB hubs · discussion

I used to just flip the switch, but I found a Python script that does the same thing.
I used to just flip the switch, but I found a Python script that does the same thing.

Someone on Hacker News recently posted a discussion about uhubctl, which is a tool called uhubctl that lets you physically cut power to USB ports. I was reading through the comments and asked myself a really basic question: wait, how does software actually do that?

Usually, when I want to turn off a USB device, I just reach over and physically yank the cord out of my computer. The idea that a purely software-based command could flip a physical electrical switch inside a hub felt a little mind-bending to me. Is the operating system sending a special electrical pulse? Does it require a custom kernel module?

I initially tried to answer this by looking up the official USB 2.0 Specification to figure out how hubs manage electricity. The document is hundreds of pages long and filled with impenetrable jargon. I found a tiny snippet mentioning a ClearPortFeature request, but the documentation was completely overwhelming. I spent twenty minutes staring at state diagrams for power management and it left me more confused than when I started.

Luckily, one of the commenters on the Hacker News thread posted an strace log showing what uhubctl actually does when it turns off a port. I love reading strace output because it cuts through the theory and shows you the exact system calls a program makes to the Linux kernel. Let's look at the literal system calls from that trace at the exact moment the power gets cut:

openat(AT_FDCWD, "/dev/bus/usb/001/005", O_RDWR|O_CLOEXEC) = 3
ioctl(3, USBDEVFS_CONTROL, {bRequestType=0x23, bRequest=0x01, wValue=0x0008, wIndex=0x0001, wLength=0x0000}) = 0
close(3) = 0
+++ exited with 0 +++

Let's deconstruct what is actually happening in those three lines, because it is wonderfully simple!

First, the program calls openat to open a device file located at /dev/bus/usb/001/005. In Linux, the philosophy is that "everything is a file." That means physical USB devices plugged into your motherboard are represented as regular files in this specific directory! By opening it with the O_RDWR flag, the program is just asking the kernel for read and write access to the hardware.

Next comes the ioctl system call, which stands for input/output control. This is how user programs send special, weird commands to hardware devices that don't fit into standard read or write operations. The program passes the USBDEVFS_CONTROL flag alongside a data structure filled with hexadecimal bytes. This struct tells the kernel to send a raw control transfer directly over the physical USB cable to the hub.

Finally, the program calls close on the file descriptor, cleaning up the connection. So it turns out the tool just opens a file path to the hardware, fires a single control message with some specific hex numbers, and then shuts the file! It’s amazing how straightforward the kernel interface is.

But what do those mysterious hex bytes actually mean? The ioctl struct contains bRequestType=0x23, bRequest=0x01, and wValue=0x08.

To decode this, we can look at the linux kernel headers (ch11.h). Kernel headers are incredibly useful for this kind of thing because they map confusing hex numbers back to readable variables that a human can actually parse.

Hex Byte        Kernel Header Variable          Plain English Meaning
---------------------------------------------------------------------
0x01      ->    USB_REQ_CLEAR_FEATURE     ->    Clear a specific port feature
0x08      ->    USB_PORT_FEAT_POWER       ->    The feature to clear is Power
0x01      ->    (wIndex argument)         ->    Target Port Number 1

Okay, seeing the bytes mapped out like that made everything click! The 0x01 in the request field is just the standardized code for ClearPortFeature, and the 0x08 tells the hardware that the specific feature we want to clear is the electrical power itself. The 0x01 in the index field just says "do this to port 1".

And if this whole process is just sending a standardized control transfer to a file descriptor, we can do it ourselves without any C code or complex binaries. I was looking at the PyUSB Tutorial and realized you could theoretically write a terrible, hacky 15-line Python script using pyusb that sends the exact same control transfer to turn off a USB device. PyUSB handles the underlying ioctl stuff for us:

import usb.core

# Find the specific USB hub device (use lsusb to find yours!)
dev = usb.core.find(idVendor=0x1a2b, idProduct=0x3c4d)

if dev is None:
    raise ValueError("Device not found")

# Send the exact control transfer from our trace
# bRequestType=0x23, bRequest=0x01 (ClearPortFeature)
# wValue=0x08 (USB_PORT_FEAT_POWER), wIndex=0x01 (Port 1)
dev.ctrl_transfer(
    bmRequestType=0x23,
    bRequest=0x01,
    wValue=0x08,
    wIndex=0x01
)

print("Power disable command sent! :)")

I haven't actually tested this script myself because I don't own a USB hub that supports per-port power switching (and I don't want to accidentally fry my motherboard!). That brings up a huge caveat here. Apparently a lot of cheap USB hubs don't even have the physical hardware circuitry to support per-port power switching. If you send this request to a cheap hub, the software command will just be silently ignored by the controller, and the device will stay powered on. You actually need a hub that explicitly supports per-port power switching for the hardware to care about our ClearPortFeature request.

There is still a lot I don't understand about USB. I spent an hour staring at the USB 3.0 descriptor specifications and my eyes completely glazed over, I TOTALLY didn't get it. I’m also not a hardware engineer and I don't really know how the internal microcontroller inside the hub translates this digital control message into physically opening an electrical circuit. Does it trigger a tiny relay? A transistor? (I really have no idea what happens to the physical electricity). And there are probably tons of edge cases around power states that I'm completely ignoring by just looking at raw ioctl commands.

But I am seriously amazed that operating systems give us the tools to try to control physical electricity with just 15 lines of Python!

← Back to Edition 10