# ===================================================================== # Maintenance mode: the operator unfreezing a base image to change it. # # THIS IS WHY THE AGENT CAN NEVER UNFREEZE ANYTHING # # The sandbox runs as owuivm. Base images are mode 0444 owned by # owuivmadm, in a directory owuivm does not own and is not in the group # of. So the uid that runs every session physically cannot clear a write # bit, and there is no verb in vmd that asks anyone else to. # # This unit is the only path that can, and it runs as owuivmadm. Starting # it is gated by polkit (50-owuivm.rules) on an ACTIVE LOCAL SESSION of # the operator - not on group membership, which a background process # could inherit. An agent asking for an unfreeze produces a sentence in a # report; a person at the machine is what produces a writable disk. # # FREEZING IS A chmod, WHICH MEANS IT IS VISIBLE # # `ls -l` says whether an image is frozen. This unit does not change it # in either direction: starting a machine and unfreezing its disk are # separate decisions, made by separate buttons, because the most useful # thing the operator can do is boot the image exactly as the agent will # get it - which must not be able to change it. # # A frozen disk therefore boots a throwaway overlay and ExecStopPost # throws it away; an unfrozen one boots itself and keeps what happens. # An image left unfrozen is caught where it matters rather than here: # vmd refuses to open a session on a writable base. # ===================================================================== [Unit] Description=owuitools VM maintenance (%i base image) After=owuivm-nft-guard.service Requires=owuivm-nft-guard.service # Refuses to run while a session is live: one VM at a time, and two # processes with the same disk open is how a qcow2 gets corrupted. Conflicts=owuivm.service # Deliberately NO ConditionPathExists on the disk: starting this unit # with no image is how one gets installed. ExecStartPre creates a blank # disk and points the machine at the installation media. ConditionPathExists=/dev/kvm [Service] Type=simple User=owuivmadm Group=owuivmadm SupplementaryGroups=kvm # 0007 so the console socket qemu creates is group-writable, and the # directory's setgid bit makes that group the OPERATOR's - the person who has # to be able to connect to it. UMask=0007 Environment=OWUIVM_MODE=maint Environment=OWUIVM_SESSION=00000000000000000000000000000000 Environment=OWUIVM_MEM_MB=8192 Environment=OWUIVM_CPUS=8 Environment=OWUIVM_DISK=/var/lib/owuivm/base/%i.qcow2 Environment=OWUIVM_VARS=/run/owuivm/00000000000000000000000000000000/OVMF_VARS.fd ExecStartPre=/usr/local/libexec/owuivm-maint-prepare %i open ExecStart=/usr/local/libexec/owuivm-launch # The power button first, then the tidying. Without this, stopping the # unit yanked power on the disk the operator was editing. ExecStop=/usr/local/libexec/owuivm-powerdown ${OWUIVM_SESSION} ExecStopPost=/usr/local/libexec/owuivm-maint-prepare %i close # The maintenance VM gets exactly the sandbox's network policy. It is the # operator driving it rather than an agent, but it is the same disk that # every session will later run from: if something is installed here that # only works because this VM could reach the LAN, that breaks later, in a # session, where it is much harder to see. IPAddressDeny=127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10 192.0.0.0/24 224.0.0.0/4 240.0.0.0/4 RestrictAddressFamilies=AF_UNIX AF_INET # No exception needed: the console is a unix socket, so this unit opens # no listening TCP port at all - the same as a session. SocketBindDeny=any DevicePolicy=closed DeviceAllow=/dev/kvm rw NoNewPrivileges=yes # THE SANDBOX GETS ITS OWN RESOLVER, AND THIS IS NOT COSMETIC. # # SLIRP hands every guest 10.0.2.3 as its nameserver over DHCP. That is a # forwarder inside the qemu process, and it relays to whatever the HOST's # /etc/resolv.conf says - here 127.0.0.53, systemd-resolved, which # forwards to the LAN's own DNS and knows every internal name in the # house. This unit denies 127.0.0.0/8, correctly, so that relay is dead. # # The result is a guest with working TCP to the internet and no name # resolution whatsoever, which presents as "no internet" and sends # whoever is looking at it to entirely the wrong place. It did exactly # that to a hand-installed image. # # Bind-mounting a resolv.conf of our own fixes it for every guest without # the guest being configured at all, and without the host's resolver ever # becoming reachable from the sandbox. BindReadOnlyPaths=/etc/owuivm/resolv.conf:/etc/resolv.conf ProtectSystem=strict ProtectHome=yes PrivateTmp=yes # The overlay dataset holds the throwaway disk of a frozen boot. It is the same # dataset a session's overlay lives on, and its 40 GB quota is what # stops a guest that writes forever from filling the root filesystem. ReadWritePaths=/run/owuivm /run/owuivm-maint /var/lib/owuivm/base @POOL_ROOT@/vmrun ProtectKernelTunables=yes ProtectKernelLogs=yes ProtectControlGroups=yes RestrictNamespaces=yes RestrictRealtime=yes LockPersonality=yes SystemCallArchitectures=native SystemCallFilter=@system-service # NOT optional, and its absence is not a smaller version of having it. # Without this, a filtered syscall raises SIGSYS and kills the process; # with it, the call returns EPERM and the caller decides what to do. # qemu calls seccomp(2) at startup to probe for its own sandbox - which # @system-service does not permit - so a maintenance boot died instantly # with status=31/SYS until this line was added, while a session boot, # which has always had it, simply got EPERM and carried on. SystemCallErrorNumber=EPERM # MEASURED: 10G made the machine crawl, and it was not obvious why. # # The guest's RAM is a memfd with share=on (virtiofs requires it), so the # kernel accounts all 8 GiB of it as *shmem* in this cgroup - and with no # swap on this host, shmem cannot be reclaimed at all. That left about # 1.5 GiB for every page of ISO and disk I/O the VM does, so the cgroup # hit its hard limit 6,782 times in one boot (memory.events max), evicting # and re-reading the same installer media over and over. # # The first fix was to raise this to 14G so the page cache had room. # That worked - reclaim went to zero - and it was the wrong fix: it gave # one VM twelve gigabytes of a 58 GB host that is also running Ollama, # and the host started running short instead. Raising a limit to make # room for something that should not have been in there. # # The right fix is one line in owuivm-launch: cache=none on every drive, # so this VM's I/O never enters the page cache at all. What is left in # the cgroup is the guest's RAM and qemu itself, which is what this limit # was always meant to bound. 11G is 8 GiB of guest, about 1 GiB of qemu, # and slack. MemoryMax=@VM_MEMORY_MAX@ # 800%, NOT 700%, AND THE DIFFERENCE IS NOT THEORETICAL. # # The machine has 8 vCPUs, so 800% is exactly what it can consume and # 700% means one of its eight can never run. Measured during a Windows # install: qemu sat at 697% of the 700% ceiling, with 1,792 throttle # events and 103 seconds of CPU actually withheld - while the operator # watched a progress bar and wondered whether nested virtualisation was # to blame. It was not; it was this line. # # The session unit keeps 700% on purpose: it runs while the main model # may be answering someone, and it is pinned to eight threads that # Ollama can also use. A maintenance boot is one person doing one thing, # and it should have the whole machine it was given. CPUQuota=@MAINT_CPU_QUOTA@ TasksMax=512 TimeoutStopSec=120 KillMode=mixed