3 Commits

Author SHA1 Message Date
umbra
0ab1f0f1de The GPU boundary did not survive a reboot, and neither did the fix for it
This is the verification I said could not be done until the machine
rebooted. The reboot proved the boot path wrong, and uncovering why took
three rounds.

**The boundary was gone.** /dev/nvidiactl came back 0666 root:root, so
the controller could open the card again - the exact thing the udev rule
was applied to prevent.

**Why: the unit ran before anything existed.** umbra-nvidia-devices ran
at 05:38:24, ordered After=systemd-udev-settle, globbed /dev, found no
nodes, and exited 0. udev-settle means "udev has finished its queue", not
"the proprietary driver has finished initialising". The nodes appeared
moments later at 0666 and nothing revisited them. The helper ended in an
unconditional `exit 0` - I had written that deliberately, so an early boot
would not fail a unit - and it is precisely what made "I fixed the
permissions" indistinguishable from "there were nothing to fix".

**Why the first repair also failed: nvidia-persistenced resets the mode.**
It chmods /dev/nvidiactl back to 0666 on every initialisation, via
nvidia-modprobe. That is out of udev's reach entirely, because udev does
not manage these nodes. So the fix is no longer an ordering problem but an
eventual one: the unit now runs After=nvidia-persistenced.service, and a
two-minute timer re-asserts the permissions because the trigger that
breaks them can fire at any time.

**Why the timer did nothing either.** RemainAfterExit=yes on the oneshot
keeps it "active (exited)", so systemd treated every activation as already
satisfied: the timer fired on schedule, the service reported Result=success
and ExecMainStatus=0, and the permission was still wrong two minutes later.
That is the original bug repeated one level out, inside the mechanism built
to repair it. Removed, and a test now pins it - by directive name, with
comments stripped, since the unit documents the omission by naming it and a
substring search failed on the explanation.

**I also broke nvidia-persistenced.** It drops to uid 122 with setuid and
without initgroups(), so it never receives the video supplementary group and
failed with "Insufficient Permissions" once the nodes were 0660 root:video.
A systemd drop-in runs it as root - upstream's own default - which works
without widening the device permissions. It was running fine before.

The helper now waits for the nodes, verifies the modes afterwards, and
exits non-zero under --strict rather than reporting a no-op as success. It
takes UMBRA_NVIDIA_DEVDIR so its three paths can be tested without root and
without a GPU; the first attempt at that test pointed CANARY at a
non-existent path and still exited 0, because the fix loop globbed /dev
directly and the override only reached the wait.

Also: nvidia-device-access.sh `show` reported "cannot measure the GPU" when
a cached sudo credential had simply expired - a finding about the machine
when the truth was that the tool could not ask. It now distinguishes no from
could-not-check, because I was misled by it during this check.

**Still outstanding, and I could not finish it.** The repository is
correct; the installed state is stale, because clearing the unit's active
(exited) state needs root and that authorization was declined. As of now
/dev/nvidiactl is 0666 and the controller can open it. To finish:
  systemctl stop umbra-nvidia-devices.service
  systemctl start umbra-nvidia-devices.service
  stat -c '%a %U:%G %n' /dev/nvidiactl
and expect 660 root:video.

Otherwise the system came through the reboot intact: the deployment was
down only because a leftover `python3 -m http.server` from an earlier
session was holding port 8442, and the backend mounts the same files
itself. Deployment healthy, database unchanged at users=2 orgs=3
projects=0 audit=43 with no compute or datastore tables, ollama serving,
agent account intact, zero stray agents. 471 pass with a database, 430
without.
2026-10-01 06:23:21 -07:00
umbra
c1741456e2 Apply the GPU boundary, and fix three checks that verified themselves
The udev rule I wrote last commit could not have worked. The nvidia
driver creates /dev/nvidia* in the kernel without registering them in
sysfs, so `udevadm info --name=/dev/nvidiactl` answers "Unknown device"
and udev never evaluates a rule for them - which is also why they are
0666, since nothing sets a MODE. The rule hooks the driver bind event
instead and runs a script, by which point the nodes exist.

The first version of that hook triggered 0000:01:00.0, which on this
machine is an AMD root complex, vendor 0x1022 rather than 0x10de. The
trigger fired on the wrong device, the RUN key never ran, and the
permissions looked right because the apply script had just fixed them
itself. The address is discovered now. Verified by resetting to 0666,
firing only the bind event, and watching it come back 0660 - the script
never invoked.

Applied and verified: ollama can open the card, umbra-poc can, umbra
cannot. Traced, nvidia-smi opens /dev/nvidia0, /dev/nvidiactl and
/dev/nvidia-uvm, so umbra-poc is in video or the agent starts,
heartbeats, and reports a machine with no GPU - a failure that presents
as a scheduling problem and is a permissions one. It is the agent's only
supplementary group and grants a device, not a filesystem, so setup's
"no supplementary groups" check now allows exactly video and rejects
everything else, verified by adding users and watching it fail.

Three tests, each verified against a mutation. The rule test resets the
permissions and fires only the event, so it fails if the rule is inert.
The end-state test fails rather than skips when the rule is missing -
skipping made deleting the rule quieter, three passed becoming one
passed and two skips, which reads as an improvement in the wrong
direction.

Also: the check "the controller cannot open the card" was testing root,
which bypasses permissions entirely, and reported a working rule as a
failure. The account is named now.

Then four runs in a row leaked agents, and the cause was upstream of the
leak. The victim reused the first agent's credential, and a node is
identified by its credential, not by its config's node_name - so it
heartbeated as poc-agent-node, was never offered the long job, and sat
there for the whole step while a correct liveness check reported that the
worker it had killed had survived. It has a third node and its own
credential now, so the requeued job is not immediately reclaimed by an
agent already stopped for the step.

The last leak was a worker asleep in a 600-second job, unable to see a
stop file because the check is at the top of its loop rather than inside
the work. Four runs left one each. The job is 10 seconds now: the victim
exits the instant it claims it and never runs it, so the length was
never the point. PoC node names and idempotency keys carry a per-run
suffix, so a second run against the same database does not die on a
UniqueViolation - and there is no route to delete a node.

Three consecutive runs, zero failures, zero leaks. 451 pass with a
database, 424 without. Deployment untouched: users=2 orgs=3
projects=0 audit=43, no compute tables.
2026-09-30 07:24:27 -07:00
umbra
220bc00823 An upgrade path for the agent, and the GPU device boundary
Two questions worth answering properly rather than by assertion.

How an agent is updated. `setup` has always doubled as an upgrade -
it installs the file unconditionally, which is most of what an
upgrade needs and the one thing it should not do: a broken file
replaces a working agent on every node with no way back. `update`
names the version being replaced, keeps the previous file, and
verifies that the new agent *starts as umbra-poc* rather than that
it parses. A file that imports cleanly under umbra's virtualenv may
not be readable by the account that runs it. Verified against a
deliberately broken agent: installed, detected, previous restored.

The agent does not update itself. An agent that can fetch and
execute code has a remote-code-execution channel, and the point of
the account is that it holds no credentials and reaches nothing.
Updates are an out-of-band deploy per node. Nothing enforces a
minimum agent version either - the console can report "behind", but
refusing a heartbeat would strand a node mid-job for tidiness.

The GPU. /dev/nvidia* is 0666 root:root because the driver creates
it that way and its own 71-nvidia.rules sets no MODE or GROUP.
Measured, not assumed: nvidia-smi -L succeeds for umbra, ollama and
umbra-poc alike - so the controller can open the card today, and the
only thing stopping it from loading a model on top of the node's
work is the scheduler choosing not to.

72-umbra-nvidia.rules sets 0660 root:video, sorting after the
driver's rule so it wins. video already contains ollama, which is
right: ollama is the account that opens the GPU. umbra-poc must be
added to it, and traced nvidia-smi opens /dev/nvidia0,
/dev/nvidiactl and /dev/nvidia-uvm - so without the membership the
agent starts, heartbeats, and reports a machine with no GPU. That
fails silently, as a scheduling problem rather than a permissions
one. It is the agent's only supplementary group and grants a device,
not a filesystem, so it is added by the apply script rather than by
setup.

What it does not buy: separation between two agents. Both are
umbra-poc, both in video, both could open the card. On one box with
one GPU, placement is the control and the kernel is not. What it
buys is that every other account stops being able to touch the card,
the controller above all. apply verifies the rule took, that ollama
and the agent can still measure the GPU, and that ollama still
serves models - and reverts itself if any of those fail.

Also fixes a check this session broke: a sed meant for new code
rewrote verify's agent-start line to run a command called `x`, so it
had been failing on every run and I read past it. `bash -n` cannot
see that class of damage; verify did, which is the argument for it.
2026-09-30 06:32:11 -07:00