SchOS/docs/architecture/udev.md
Nexoniarz d650ff433b udev: PCI bridges as parents, USB interfaces, the controller's own address, and the attributes lsusb reads
PCI devices now nest under the bridge they sit behind (from each bridge's
secondary..subordinate window), so a controller behind a root port is where
Linux would put it. USB interfaces (1-5:1.0) are directories with class,
number, a driver link and a uevent with INTERFACE= and a MODALIAS, announced
after the drivers have been offered the device and removed before it. The xHCI
driver tells the USB core its PCI address when it comes up, replacing the guess
that bus N was the Nth controller. Devices and root hubs carry version,
bMaxPacketSize0, bNumInterfaces, bmAttributes and the rest; lsusb -t from
Alpine's usbutils now draws the tree.

Checked on q35 with an xHCI behind a pcie-root-port and a USB keyboard on each
controller. Plain lsusb still fails: libusb wants usbfs, and refuses a kernel
that calls itself 0.1.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 17:54:13 +01:00

7.7 KiB

udev

Alpine's eudev runs on SchOS as an ordinary process, started by schinit (etc/schinit/udevd.job, with udev-trigger.job replaying the devices that were there first). It is what lets Xorg find its keyboard and mouse instead of being told, and the road to a Wayland compositor starts here: libinput and libseat both ask udev what exists.

The kernel's half is three pieces, all small, none of them specific to udev.

The device table: kernel/dev/kdev.c

The devices the kernel implements itself — input event nodes, the memory devices, terminals, the framebuffer, the disks — are listed once, with their class, name, /dev name and parent. Everything below is generated from it.

The device numbers are the ones stat() reports for the node, not SchOS's internal ones: /dev/tty1 is 4:1 to a Linux program (linux_tty_rdev in kernel/sys/sys_fs.c), and udev compares what it reads in /sys with what stat() says about the node. dev_stat_numbers() is the one mapping both use.

/sys: kernel/fs/sysfs.c

/sys/devices/pci0000:00/0000:00:1f.2/         vendor device class ... uevent  subsystem -> ../../../bus/pci  driver
/sys/devices/pci0000:00/0000:00:14.0/usb1/1-3 idVendor idProduct ... uevent  subsystem -> ../../../../../bus/usb
/sys/bus/pci/devices/0000:00:1f.2             -> ../../../devices/pci0000:00/0000:00:1f.2
/sys/bus/usb/devices/1-3                      -> ../../../devices/pci0000:00/0000:00:14.0/usb1/1-3
/sys/devices/virtual/input/input0/            name capabilities/{ev,key,rel,abs} properties
/sys/devices/virtual/input/input0/event0/     dev  uevent  subsystem -> ../../../../../class/input
/sys/class/input/event0                       -> ../../devices/virtual/input/input0/event0
/sys/dev/char/13:64 ...                       -> ../../devices/virtual/...

Symlinks are real (T_SYMLINK inodes whose target is generated). uevent is readable (DEVPATH=, SUBSYSTEM=, MAJOR=, MINOR=, DEVNAME=) and writable: echo add > uevent — which is what udevadm trigger does — makes the kernel announce the device again. PCI and USB devices announce the same way: MODALIAS, PCI_ID, PCI_CLASS, PCI_SLOT_NAME for a PCI device, PRODUCT, TYPE, BUSNUM, DEVNUM for a USB one.

PCI devices are nested under the PCI-to-PCI bridge they sit behind: a device's parent is the bridge whose secondary..subordinate bus range holds its bus (the innermost if several do), read from the bridge's config space, so an xHCI behind a root port is pci0000:00/0000:00:04.0/0000:01:00.0. USB devices sit under the xHCI controller that carries their bus, below that bus's root hub usbN; the controller tells the USB core its own PCI address when it comes up, so the link is recorded, not guessed. A hub's children are its siblings, not its children.

USB interfaces (1-5:1.0) are directories below their device, with bInterfaceClass, bInterfaceNumber, a driver link when one is bound, and a uevent carrying DEVTYPE=usb_interface, INTERFACE= and a MODALIAS=usb:... that names the interface's class. They are announced after the drivers have been offered the device, so the event says which interface was taken, and removed before the device. Devices also carry what lsusb -t and libusb read: version, bMaxPacketSize0, bNumInterfaces, bmAttributes, bMaxPower.

Hot-plug is real: the USB core announces add when a device is attached and remove as it detaches, and usbN is announced when its controller comes up. With udevadm monitor, plugging in a mouse prints the kernel event and then the same event after udev's rules have run. PCI has no hot-plug here.

The input capability files are what input_id reads to decide ID_INPUT_KEYBOARD or ID_INPUT_MOUSE. SchOS has two input devices whatever the machine — every keyboard on event0, every mouse on event1 (kernel/dev/input.c) — so each has a fixed, plausible capability set.

/dev: kernel/fs/devfs.c

devtmpfs is no longer read-only: udev creates /dev/char/MAJ:MIN, /dev/fd, /dev/input/by-* and the like. A small in-memory overlay (192 entries) holds directories and symlinks, listed after the kernel's own nodes in whichever directory they are in. Device nodes themselves are still the kernel's: mknod and regular files in /dev are refused.

The sockets: kernel/net/unix.c

  • AF_UNIX SOCK_DGRAM and SOCK_SEQPACKET keep message boundaries. udevd's control socket is SEQPACKET, and its manager and workers talk over a DGRAM socketpair. A message carries its sender's credentials with it (several workers share one end), a zero-length datagram is a datagram, and a datagram socket never reads end-of-stream.
  • AF_NETLINK / NETLINK_KOBJECT_UEVENT sockets bind to multicast groups: group 1 is the kernel's events, group 2 is what udevd re-announces once its rules have run. A datagram can also be addressed to a port id — udevd hands an event to an idle worker that way, and the worker believes it only from the manager's port. The kernel's messages arrive as credentials with uid 0, pid 0. A queue that is full drops the message, as Linux does, and udevadm trigger is how a reader recovers.

libinput and libseat

libinput (Alpine) reads the evdev nodes udev tagged and xf86-input-libinput gives Xorg the same library; libinput list-devices as an ordinary user shows SchOS keyboard and SchOS pointer. Xorg's 40-libinput.conf now claims both, and the log says Using input driver 'libinput'.

seatd (job etc/schinit/seatd.job, socket /run/seatd.sock, group video) and libseat let a program that is not root take devices from the system. The client asks seatd for seat0; seatd takes the VT (VT_SETMODE, KDSETMODE, KDSKBMODE, all there already), opens the device as root and passes the descriptor back over the socket with SCM_RIGHTS. Checked as schos against /dev/input/event0 and event1 with a small program linked to libseat, reading the device's name through the descriptor it was handed.

Getting there found two kernel bugs, both in descriptor passing:

  • A socket returned by accept() was not an AF_UNIX socket as far as sendmsg was concerned. The end connect() queues for the listener was allocated with its domain left at zero, and sendmsg refuses SCM_RIGHTS on anything else. So a server could receive descriptors and never send one: seatd, and equally a Wayland compositor handing a client a buffer.
  • A control buffer longer than its messages was an error. seatd passes a zeroed array with one descriptor at the front; Linux treats a following header too short to be one as the end. sendmsg now does too.

What is not there

  • /dev/bus/usb nodes (usbfs). lsusb -t works because it reads sysfs; plain lsusb goes through libusb, which also refuses to start while uname -r says 0.1.0 (it wants Linux 2.6.32 or newer) and wants usbfs to open devices.
  • A hub's port count (maxchild reads 0), and string descriptors (manufacturer, product, serial).
  • A USB keyboard works because every keyboard is event0; udev knows the interface arrived but the input device does not hang below it.
  • hwdb (/etc/udev/hwdb.bin): udevadm hwdb --update has not been run in the image, so 60-input-id logs "returned non-zero" for the hwdb import. The properties that matter come from input_id.
  • Block device partitions, by-id / by-uuid links, and a /sys/block.
  • A compositor. libinput and libseat are in place, DRM is not; see ../plan/roadmap.md.

Testing

Boot the ISO and run udevd --debug &, then udevadm trigger; udevadm settle. /run/udev/data/c7:0 should contain ID_INPUT_KEYBOARD=1, and udevadm info /dev/input/event0 should print the device. Under X, /var/log/Xorg.0.log says config/udev: Adding input device.