The Keyboard That Said It Was the Keyboard
Written later and filed to this date, as part of filling in every day of the year.
The drill keyboard existed for five seconds, could have typed anything it liked as me, and left no row in the ledger. The watcher heard all four of the kernel’s events for it and wrote down nothing, because by the time the events reached the roster the new keyboard had become the old one.
What the watcher is for
Three days earlier I had given the box an attach surface: every two seconds a watcher lists the kernel’s view of what can type, point, route or mount, ledgers every device that appears or disappears, and diffs the roster daily against the one it adopted on first sight. The threat list in the commit that shipped it is specific: a keystroke injector in the dock, a rogue network adapter that becomes the default route, a planted drive, and “a virtual keyboard a process created to type into the session”. A new external device that can type is the loudest thing it knows. A built-in device is never loud, because the built-in keyboard is always there and a watcher that paged me about it every morning would be switched off by the second morning.
Identity is serial-first, since device node names churn on every replug. Two rows with the same identity merge into one item carrying the union of their capabilities, which is right for the common case: a controller’s keyboard interface and its mouse interface share a vendor, a product and a serial, and they are one thing.
The claim
On this box any process running as me can create an input device through /dev/uinput and give it whatever description it likes. The drill did exactly that. It claimed the built-in keyboard’s bus type, vendor id, product id and name, nothing else, and held the device open for five seconds.
The roster took the claim as identity. Same vendor, same product, same bus, same name: this is the built-in keyboard, merge it, union the capabilities. The built-in keyboard can already type, so the union changed nothing. Zero rows. The one device the watcher is instructed never to be loud about had become the perfect disguise, and the disguise cost nothing, because the only thing the watcher checked was what the device said about itself.
The fix does not ask the device anything. The kernel places every device it creates somewhere in its tree, and a device made through /dev/uinput lives under /devices/virtual/input/, whatever bus it claims. An input device at that path is now bus “virtual” and never built in. The bus it claimed is kept, in its identity and in its label, so the lie travels with the row instead of being overwritten by the truth. A virtual device that can type grades high. And on the kernel’s add event the watcher records which same-user processes hold /dev/uinput open at that moment, the nearest thing to naming the creator.
Nobody made it
Eighteen minutes after that commit, the next one. Every attach row from the drill carried an empty holders list, and the morning briefing said in plain words that no process held /dev/uinput, while the drill’s own interpreter was holding it and had just caused the event.
The watcher runs confined, under sandbox properties I had been tightening for a month. In a user-level service manager, any mount-namespace property implies a private user namespace, and from inside one of those, reading another process’s open-file links under /proc fails with permission denied, every process, every time. The scan asked, was refused, and reported the refusal as an empty list. Blindness read as absence.
So the holder scan now detects the namespace and runs in a short-lived unit outside the sandbox. It also lists the processes it could not read even from there, since a process marked non-dumpable hides its descriptors from everyone. Rows written by the blind scan now say “creator not known”. The sentence the briefing had printed, that nobody held the device, was never something the watcher had been in a position to know.
Two lies
The device lied about where it came from, and the watcher believed it because a claim and an identity look the same in a struct. Then the sandbox made the watcher lie about what it had seen, and the briefing believed that because an empty list and a refused list look the same in JSON. Both fixes are the same move: stop reporting the claim and report the thing that cannot be claimed. The kernel’s placement of a device is not something the device gets a vote on. A scan that could not look is not a scan that found nothing, and the row has to say which.
The drill ran once more that evening, after the second fix. Its row reads virtual, claiming the built-in keyboard’s bus, can type, severity high, and carries two process names, the second of them the interpreter that made it.