board/common: fix authorization cascade for internal USB hubs

Two related problems prevented devices behind an intermediate hub (e.g.
the VIA Labs hub on the RPi 400's VL805 xHCI) from being authorized
when confd unlocks a USB bus:

1. Cascade ordering: usb_authorize() ran nftw() with FTW_DEPTH, which
   visits children before the root-hub entry.  The hub was authorized
   while authorized_default was still 2, so when the kernel probed the
   hub's children it immediately denied them.  Fix: write
   authorized_default=1 on the bus before entering nftw.

2. Slow/async probe: hub port enumeration is asynchronous — devices
   behind a hub may appear in sysfs after nftw has already finished.
   Fix: add a udev rule that catches usb_device add events on buses
   where authorized_default=1 is already set on any ancestor root hub
   and authorizes them immediately.

The ATTRS{} matcher in udev walks the full sysfs parent chain, so
authorized_default=1 on the root hub is visible even for devices several
hubs deep.  Buses still locked at authorized_default=0 are left alone.

Also expand the docstring in generic_usb_ports() to document the
design: each root hub gets its own uniquely-named entry (USB, or USB1/
USB2/... when multiple) so buses can be individually controlled, and
boards needing finer control should use DT usb-ports annotations.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-03-10 12:11:08 +01:00
parent 726a3132e3
commit 799fd93cac
3 changed files with 49 additions and 17 deletions
@@ -0,0 +1,19 @@
# Authorize USB devices on buses that Infix has unlocked (authorized_default=1).
#
# With usbcore.authorized_default=2 only DT-marked hard-wired ports are
# auto-authorized by the kernel. When confd unlocks a bus it sets
# authorized_default=1 on the root hub and uses nftw to authorize already-
# present devices. However, devices behind an intermediate hub may appear
# asynchronously — after nftw has already finished — because hub port probing
# is slow. This rule catches those out-of-band additions: if a device arrives
# unauthorized (authorized=0) on a bus where confd has set authorized_default=1
# on any ancestor root hub, authorize it immediately.
#
# Note: ATTRS{} matches the attribute on the device itself or any parent,
# so authorized_default=1 on the root hub (usbN) is found even for devices
# connected through one or more intermediate hubs. Locked buses keep
# authorized_default=0, so this rule correctly leaves them alone.
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
ATTR{authorized}=="0", \
ATTRS{authorized_default}=="1", \
RUN+="/bin/sh -c 'echo 1 > /sys%p/authorized'"
@@ -433,7 +433,13 @@ def probe_qemusystem(out):
def generic_usb_ports(out):
"""Generic USB port discovery - works for all systems"""
"""Generic USB port discovery - works for all systems.
Each root hub gets its own uniquely-named entry so it can be independently
enabled or disabled. Boards that need explicit per-port control (e.g. to
exclude internal buses) should annotate their DT with usb-ports /
usb-port-names in the chosen/infix node instead.
"""
ports = []
usb_base = "/sys/bus/usb/devices"
@@ -441,26 +447,22 @@ def generic_usb_ports(out):
return
for entry in sorted(os.listdir(usb_base)):
# Only look at root hubs (usbN)
if not entry.startswith("usb"):
continue
device_path = os.path.join(usb_base, entry)
# Check if this is a root hub (has authorized files)
if os.path.exists(os.path.join(device_path, "authorized")):
# Extract number from usbN
num = entry.replace("usb", "")
ports.append({
"num": num,
"path": device_path
})
if not os.path.exists(os.path.join(device_path, "authorized")):
continue
num = entry.replace("usb", "")
ports.append({"num": num, "path": device_path})
# Name ports: "USB" for single port, "USB1", "USB2", etc. for multiple
if ports:
if len(ports) == 1:
out["usb-ports"] = [{"name": "USB", "path": ports[0]["path"]}]
else:
out["usb-ports"] = [{"name": f"USB{p['num']}", "path": p["path"]} for p in ports]
if not ports:
return
# Single bus → "USB"; multiple → "USB1", "USB2", ... (by bus number)
if len(ports) == 1:
out["usb-ports"] = [{"name": "USB", "path": ports[0]["path"]}]
else:
out["usb-ports"] = [{"name": f"USB{p['num']}", "path": p["path"]} for p in ports]
def probe_dmisystem(out):
+11
View File
@@ -78,6 +78,17 @@ static bool usb_authorize(struct json_t *root, const char *name, int enabled)
}
}
} else {
/*
* Set authorized_default=1 on the bus BEFORE nftw
* authorizes individual devices. With FTW_DEPTH,
* nftw visits children before the root-hub entry, so
* without this a hub would be authorized first while
* authorized_default is still 2. Setting it here
* ensures devices appearing behind an intermediate
* hub are auto-authorized by the kernel as they probe.
*/
if (fexist(apath))
writedf(1, "w", "%s", apath);
if (realpath(path, apath))
nftw(apath, dir_cb, 20, FTW_DEPTH | FTW_PHYS);
}