KDOS has no logind and no polkit agent on the path between the desktop and the kernel.
In their place are five small root daemons, each answering a few fixed questions on a Unix socket
in /run: suspend the machine, mount a USB stick, say which application is spending the battery,
kill a runaway program before the desktop freezes, and mount application packs. This chapter
describes each of them, together with three programs that sit close to privilege without being
root daemons: the per-box Wayland socket kdos-boxsock, the portal backend and the lock screen,
and the two login-record services, utmpd and wtmpd.
It is written for two readers:
Why the authorisation is shaped this way, and what it defends against, is argued in The security model. How init starts services is in Boot and init. Every socket path and verb on the system is listed in Filesystem and IPC.
Each root daemon is one binary in /usr/sbin, started at boot by a script in /etc/init.d and
kept running by ksvc, the service supervisor in kdos-tools.
| Daemon | Socket | Command-line client | Other clients | Init script | Skipped at boot when |
|---|---|---|---|---|---|
kdos-powerd |
/run/kdos-powerd.sock |
kdos-power |
The panel’s and the Start menu’s power items, the power and sleep keys, the compositor’s lid handler kdos-lid, the kdos-firewall, kdos-time and kdos-users surfaces (windows and pop-ups drawn by kdos-shell), kdos theme, kdos doctor (checks that the socket exists) |
55_powerd.sh |
The binary is missing |
kdos-energyd |
/run/kdos-energyd.sock |
kdos-energy |
kdos-res (the Energy and Boxes pages), kdos doctor (checks that the socket exists) |
56_energyd.sh |
The binary is missing, or no /sys/class/powercap/*/energy_uj is readable |
kdos-oomd |
/run/kdos-oomd.sock |
— | kdos doctor (checks that the socket exists) |
57_oomd.sh |
The binary is missing, or /proc/pressure/memory is not writable (the kernel has PSI off) |
kdos-mountd |
/run/kdos-mountd.sock |
kdos-mount |
The kdos-devices, kdos-disks and kdos-connect surfaces, and the session helper kdos-mediad |
58_mountd.sh |
The binary is missing |
kdos-packd |
/run/kdos-packd.sock |
— | kdos-appbox (and kdos app, which runs it), kdos doctor |
59_packd.sh |
The binary is missing, or erofs is absent from /proc/filesystems even after modprobe erofs |
kdos-powerd, kdos-energyd and kdos-mountd are each one binary with two names. Run under the
daemon’s name it serves the socket; run under the client’s name (kdos-power, kdos-energy,
kdos-mount) it sends one request and prints the answer.
The service command (a second name for ksvc) takes the verb first and then the service
name, which is the init script’s name without its two-digit prefix and .sh suffix:
service status powerd
service restart mountd
service stop packd
service start energyd
service list # every service, whether it starts at boot, and its state
service disable oomd # do not start it at boot
rcS and rcK are the boot and shutdown scripts in /etc/init.d that run every numbered service
script in order (see Boot and init). disable creates
/etc/service.disabled/<name>, which both skip; enable removes it. A name that matches no
script exactly is matched as a substring, so service status mount also reaches 58_mountd.sh.
The daemon’s full name (kdos-powerd) matches nothing: it contains the service name rather than
the other way round.
The substring match finds the script for status, start, stop and restart only. enable and
disable check that some script matches, then write or remove the marker under the name exactly
as typed, while rcS and rcK look for the marker under the script’s own service name. They
therefore need the exact service name: service disable mount reports success, creates
/etc/service.disabled/mount, and disables nothing.
| Daemon | root | wheel (administrators) |
seat (the person at the machine) |
Anyone else |
|---|---|---|---|---|
kdos-powerd |
Every verb | Every verb | ping, suspend, poweroff, reboot |
Refused |
kdos-energyd |
Every verb | Every verb | Refused | Refused |
kdos-oomd |
Every verb | Every verb | Every verb | Refused |
kdos-mountd |
Every verb | Every verb | Every verb | Refused |
kdos-packd |
Every verb | Every verb | Refused | Refused |
seat is the group seatd hands the display to. The installer keeps the desktop account in seat
on every install and takes a non-administrator out of wheel; see
kinstall. Such an account keeps the lid, the
power keys and removable media, and loses sudo and every configuration verb.
Every root daemon here is built the same way. Knowing the pattern once tells you how to run, diagnose and review all five.
A daemon runs in the foreground and never forks into the background. The init script calls
supervise <name> <binary> from /etc/init.d/service_helper; the supervisor leads its own process
group, writes /run/<full name>.pid, and restarts the daemon five seconds after it exits.
service stop <name> sends SIGTERM to the whole group, waits a second, then sends SIGKILL to
whatever is left.
The daemon’s output goes to syslog and to a small file in /run. The supervisor points the
daemon’s standard output and error, and its own Starting and Exited lines, at a forwarder
process. Each line becomes a daemon-facility syslog message tagged with the daemon’s full name, and
is also appended to /run/kdos-svc.<full name>.log: for example /run/kdos-svc.kdos-powerd.log,
tagged kdos-powerd. Files and tags use the full name; only the service command takes the short
one. The file holds at most 64 KiB; when it would grow past that it is renamed to
/run/kdos-svc.<full name>.log.old and a fresh one starts. The file is what you read before
syslogd is up or while it restarts.
The daemon never keeps the descriptors it inherited. Under rcS those point at the boot step’s
capture file on the /run tmpfs, so a daemon that crash-looped would fill memory until the next
reboot, and a daemon that logs only to standard error would never reach /var/log/messages. The
forwarder ignores the SIGTERM that service stop sends the group and exits only when the daemon’s
end of the pipe closes, so what a daemon prints while shutting down is kept and it never writes
into a pipe with no reader.
The init script checks the machine first and skips with a reason. A daemon that cannot do its job
on this machine (no energy counter, no PSI, no EROFS) is skipped and prints
[SKIP] <name>: <reason> at boot, because a daemon that refuses to start under a respawn loop is
restarted every five seconds for as long as the machine is up. service_helper also offers
supervise --final-exit <code>, which stops the respawn on an exit status restarting cannot
change. None of these five daemons uses it: each one’s usual refusal is caught by its init script,
and a refusal the script cannot see (kdos-energyd finding no usable energy domain after the
script’s glob matched) is restarted every five seconds.
Each daemon owns one socket in /run, named after the daemon, and removes a stale one before
binding so that a socket file left by a crash cannot hold the name.
The socket is mode 0666, and anyone may connect; the real check is the caller’s identity. The
daemon asks the kernel for the connecting process’s user id (SO_PEERCRED, which the caller cannot
forge) and answers err not permitted to anyone it does not admit. The file mode is left open on
purpose: a mode that looked like the authorisation is a mode somebody eventually loosens.
There is one implementation of that check, kb_uid_allowed() in libkbase. It admits uid 0
outright; otherwise it looks the uid up in the password database and reads /etc/group, and admits
the account when the group’s id is its primary group or its name is in the group’s member list.
Membership is therefore read at the moment of the request, not from the caller’s process
credentials, and a group change applies to the next request without a new login. No daemon keeps
its own copy of the rule: a copy per daemon is a rule that gets tightened on one socket and stays
loose on the others. A daemon that needs a different rule states the difference beside its own
call.
The client never names a path. Every verb takes an identifier from a list the daemon itself published: a row number, a pack id, a service name.
The client connects, writes one short line, reads the answer and the connection closes. There is
no session state. The one exception is kdos-mountd’s subscribe, which keeps its connection open
and is written to when something changes; even then the daemon remembers only a file descriptor.
Each daemon serves one request at a time. kdos-energyd and kdos-oomd give a connection two
seconds to send its request, because the same loop drives their sampler and their pressure
trigger, and a client that connected and said nothing would otherwise stop them. kdos-mountd sets
no such limit although its loop also forwards hotplug events, so a connection that never speaks
holds back every changed notification until it closes. kdos-powerd and kdos-packd set none
either, so a connection that never speaks delays every later request until it closes.
kdos-energyd, kdos-oomd, kdos-mountd and kdos-packd each have a fixture mode: the daemon
can be pointed at a recorded copy of the system state and made to print what it would do without
doing it. This is how the selection logic, which decides what gets mounted, formatted or killed, is
tested without a machine to lose. kdos-powerd has none; its --explain and --set-* flags
run the same rules against a scratch /etc instead (see
Diagnosis and testing).
Each daemon reads another socket path from an environment variable (KDOS_POWERD_SOCKET,
KDOS_ENERGYD_SOCKET, KDOS_OOMD_SOCKET, KDOS_MOUNTD_SOCKET, KDOS_PACKD_SOCKET) so the test
suite can run it unprivileged. Moving the socket grants nothing: the check is the caller’s
identity, never the path. kdos-powerd, kdos-oomd, kdos-mountd and kdos-packd refuse to
start on their real socket unless they are root, because an unprivileged daemon would answer ok
to work it cannot do. kdos-energyd has no such check; it refuses to start when it cannot read the
energy counter, which without root it cannot.
A socket address holds at most 107 bytes of path. kdos-packd refuses to start on a longer path,
because truncating would bind a socket at a path nobody asked for, and the next start would fail
with “address already in use” for a file that appears not to exist. kdos-powerd, kdos-energyd,
kdos-oomd and kdos-mountd cut an over-long path to fit and bind at the shortened one.
Suspend, power-off and reboot for a desktop that does not run as root, plus the few /etc writes
that belong to an administrator: the time zone, the autologin account, the firewall’s open services
and the accent colour of the boot menu and text console.
A setuid helper would put an argument-parsing root process within every user’s reach for three
verbs. polkit is installed, but its answer is to ask for a password, which a closing lid cannot
do. There is no logind. The daemon is the smallest thing that can sit between the desktop and
/sys/power/state.
| Verb | Who | Does |
|---|---|---|
ping |
root, seat, wheel |
Answers ok. Also tells a client whether it would be allowed to suspend |
suspend |
root, seat, wheel |
Suspend to RAM |
poweroff |
root, seat, wheel |
Power off |
reboot |
root, seat, wheel |
Reboot |
timezone <Area/City> |
root, wheel |
Point /etc/localtime and TZ at a zone, and set the Wi-Fi country from it |
autologin <user>\|off |
root, wheel |
Choose which account tty1 logs in without asking, or none |
firewall list |
root, wheel |
One row per named service: name, on or off, and a short description of what the service is for (the For column below, in the daemon’s words; no port appears), tab-separated, then ok |
firewall <service> on\|off |
root, wheel |
Open or close one named service |
accent <scheme> |
root, wheel |
Recolour the boot menu, the text console and the boot splash |
The daemon checks the list the other way round: a seat member may use exactly the four words
ping, suspend, poweroff and reboot, and every other verb is an administrator’s. A verb added
later is therefore an administrator’s unless someone deliberately adds it to that list. Every
refusal is written to the daemon’s log with the caller’s uid, so a dead power key can be attributed.
kdos-power [--no-lock] suspend|poweroff|reboot|ping
kdos-power timezone <Area/City>
kdos-power autologin <user>|off
kdos-power firewall list|<service> on|off
kdos-power accent <scheme>
The request is one line of at most 62 bytes plus its newline (63 in all); the daemon reads nothing past that.
| Exit status | Means |
|---|---|
| 0 | The daemon answered ok, or closed the connection without answering any verb other than ping (for suspend, poweroff and reboot that is the machine going away) |
| 1 | The daemon answered err …, or is not running (kdos-power: no kdos-powerd — followed by the hint service kdos-powerd start, which ksvc rejects; see below) |
| 2 | Bad usage, or an argument too long for the request line |
When the daemon is not running, start it with service start powerd. The command the client
suggests puts the daemon’s full name where the verb goes, so ksvc answers
Error: Unknown command 'kdos-powerd' and starts nothing (see
Starting and stopping one by hand).
For every verb except firewall, an error is printed to standard error as kdos-power: <reply>.
firewall prints the daemon’s whole reply on standard output, the err line included, because
kdos-firewall reads it through a capture that discards standard error.
kdos-power suspend locks the session before the machine sleeps, so a resume never hands back an
unlocked desktop. The order is:
ping. This runs the same identity check as suspend, so a caller who is not allowed to
suspend, or a machine with no kdos-powerd, is refused before the screen is locked. Locking
and then not suspending would be a password prompt in exchange for nothing.kdos-lock is running, start one in its own session and wait up to two
seconds for it to print locked, which it does once the compositor confirms the session is
locked. An existing lock client is left alone, because the compositor refuses a second one.suspend. If the lock did not confirm in time, the client prints
kdos-power: no lock confirmation within 2s — suspending anyway and suspends regardless: a
broken lock screen must not turn the suspend key into a no-op.--no-lock, or a non-empty KDOS_NO_LOCK_ON_SUSPEND in the environment for a caller that cannot
change its own arguments, skips steps 1 and 2.
The daemon answers ok before the machine goes down, so the client is not left waiting for a reply
from a suspended kernel.
The daemon is the only thing on the machine that knows a suspend is happening (there is no logind to announce one), so it tells the two programs that must act around it:
sync.Sleep(true), through dbus-send --system --print-reply, waited for with a
five-second reply timeout so the devices are down before the kernel freezes them.tlp suspend.mem to /sys/power/state (suspend to RAM). Hibernation is not offered, because the
initramfs sets up no resume device.tlp resume, then NetworkManager’s
Sleep(false).NetworkManager sleeps first and wakes last, so the radios TLP restores are back before it rescans.
Without step 2, NetworkManager wakes trusting a Wi-Fi association and DHCP lease that the time
asleep has ended. Without tlp resume, the radio states TLP saved, and the settings the firmware
resets across suspend, are not restored. Each hook is skipped when /usr/bin/dbus-send or
/usr/sbin/tlp is not installed, and none of them can stop the suspend.
poweroff and reboot ask init first, by running /sbin/telinit 0 or /sbin/telinit 6 through
the argument-vector builder. Init ends the consoles, and then /etc/init.d/rcK, its first runlevel
0 and 6 entry, stops every service in reverse order, kdos-powerd among them, before anything is
unmounted. Only if the daemon is still alive sixty seconds later, which means init never acted on
the request, does it call reboot(2) directly. The wait is that long because init gives the
consoles up to three seconds and rcK then stops every supervised service numbered above
55_powerd.sh first, at a cost of about a second each; a shorter wait would cut the power
mid-shutdown. No signal can stand in for telinit: sysvinit ignores SIGTERM, and SIGUSR2
closes /run/initctl, its request channel, for the rest of the boot.
accent <scheme> takes one of the eight scheme names compiled into libkcolor: phosphor,
amber, ice, bone, norton, borland, perfect or paper. The name is matched by
kcol_find(); anything else is refused with err not an accent. There is no path to aim and
nothing to traverse, which is why this verb is safe to reach from a session.
It writes only root-owned files:
/etc/kdos/accent, which rcS reads to recolour the boot splash that is already running;limine.conf on the EFI system partition, the ESP) and the text-console palette
(/etc/vtrgb), by running kdos-bootctl theme <scheme>, which owns both files.
kdos-bootctl is found through the daemon’s inherited PATH, as iw is for the
timezone verb; every other program the daemon runs is named by absolute path.It recolours nothing on the desktop. That is kdos theme’s job, which runs as the user and touches
only the user’s own files; kdos theme calls this verb for the parts that need root.
A machine with no writable ESP is not a failure. The live medium is read-only, and a machine may
have no /boot/efi at all. When kdos-bootctl fails or is absent, the verb answers
ok <scheme> (boot menu unchanged) and succeeds, because refusing would make kdos theme look
broken on the ISO, where everything else recolours correctly.
The time zone is set here rather than by a separate daemon because it is the same question: the
files are root’s, the person changing them is the one administering the machine, and wheel is
already the answer to who that is.
A zone name is checked twice:
+, -, _ and / only; no dot at all; no leading,
trailing or doubled slash; at most 64 characters. A zone is Area/City or Area/Sub/City, so a
slash must be allowed, which makes ../../etc/shadow look legal; this rule is what stops it.
Failure answers err not a zone name./usr/share/zoneinfo. Failure answers err no such zone.Then both halves are written, each to a temporary name and renamed into place:
/etc/localtime becomes a symlink to the zone file. Programs that read the zoneinfo tree follow
it./etc/profile.d/20-timezone.sh exports TZ=':/etc/localtime'. musl reads TZ, and it takes
precedence where it is set, which is on every KDOS login. The colon form points musl at the same
file, so the two can never name different rules. Writing only the symlink would leave date
reporting the old zone in every shell that had already read the profile.The Wi-Fi country follows the zone. The kernel’s wireless layer starts in the “world”
regulatory domain, which keeps every 5 GHz DFS channel (one shared with radar) closed and caps
transmit power until something names a country, and some drivers never take one from an access
point. The verb looks the zone up in zone.tab, which maps each zone to exactly one country code,
then:
options cfg80211 ieee80211_regdom=<CC> to /etc/modprobe.d/kdos-regdom.conf, which
takes effect from the next boot;iw reg set <CC> for the running boot, when an iw is found through the daemon’s
inherited PATH; kdos-bootctl for accent is the other program looked up that way.A zone with no country, such as UTC, removes the file. Neither step failing fails the verb: a
machine with no radio still has a time zone. The installer writes the same file from the zone
chosen during installation.
autologin <user> sets the autologin key in /etc/kdos/login.conf, which decides whether tty1
logs straight into the desktop. autologin off turns it off.
kb_users() in libkbase decides that; a name it
would not list is refused with err no such account. Pointing autologin at a service account
with nologin would give a machine that boots to a login nobody can complete.off replaces the key line with the fixed text #autologin = kdos, whatever account was set,
rather than emptying the value. The login program only hands agetty --autologin a non-empty
name, so an empty value would read as a setting and behave as none.autologin, or with
one # then autologin, and contains =. Prose that merely mentions the key is left alone.err login.conf is too large) rather than
truncated: half a configuration file is a machine whose login settings are whatever survived.The file is written to login.conf.new, flushed, and renamed over the original.
firewall names services, never ports, and the table of what each name means belongs to the
daemon. A client that could name a port could open any port; a client that can only name ssh
opens exactly what the daemon’s table says ssh is. kdos-firewall asks for the list rather than
carrying its own copy.
| Service | Opens | For |
|---|---|---|
ssh |
TCP 22 | Incoming SSH (70_sshd.sh). Turning it on also removes /etc/service.disabled/sshd and starts sshd; turning it off puts the marker back and stops sshd |
http |
TCP 80 | A web server on this machine |
https |
TCP 443 | A TLS web server on this machine |
ipp |
TCP 631 | Sharing a printer with CUPS |
smb |
TCP 445 | Sharing files over SMB |
kiwix |
TCP 8080 | kiwix-serve |
mdns |
UDP 5353 | mDNS beyond the default rule |
mqtt |
TCP 1883, 8883 | An MQTT broker for LAN devices (73_mosquitto) |
xmpp |
TCP 5222 | XMPP clients (74_prosody) |
nfs |
TCP 2049 | Sharing files over NFSv4 (72_nfsd) |
babel |
UDP 6696 | Babel mesh routing (31_babeld) |
nut |
TCP 3493 | UPS status for other machines (56_nut) |
snmp |
UDP 161 | SNMP queries of this machine (71_snmpd) |
mumble |
TCP 64738, UDP 64738 | A Mumble voice server (75_mumble-server) |
caldav |
TCP 5232 | Shared calendars and contacts (77_radicale) |
mail |
TCP 25, 143, 465, 587, 993 | A LAN mail server (78_maddy) |
irc |
TCP 6667, 6697 | An IRC server (79_ngircd) |
dlna |
TCP 8200, UDP 1900 | A DLNA media server (84_minidlna) |
tryton |
TCP 8000 | Tryton clients of GNU Health (85_gnuhealth) |
caddy |
TCP 8443 | Caddy’s shipped site (HTTPS) |
mosh |
UDP 60000–61000 | Incoming mosh sessions (needs ssh on as well) |
syncthing |
TCP 22000, UDP 22000, UDP 21027 | Syncthing sync and local discovery |
kdeconnect |
TCP 1714–1764, UDP 1714–1764 | KDE Connect with a phone on the LAN |
vnc |
TCP 5900 | This desktop over VNC (wayvnc, which listens on 127.0.0.1 until its config or wayvnc 0.0.0.0 says otherwise) |
xonotic |
UDP 26000 | Hosting a Xonotic game for the LAN |
How a change is applied:
/etc/nftables.d/50-kdos-services.nft.input chain of
the inet filter table. It never merges, because merging means parsing nftables syntax, and a
parser that got it wrong would leave a port open that the surface showed as closed. Anything you
write by hand belongs in another file under /etc/nftables.d, which the daemon never reads or
touches. The new file is renamed into place before anything checks it.nft --check -f /etc/nftables.conf. If the ruleset would not load, it answers
err the ruleset would not load; nothing changed and the kernel keeps the previous rules, but
the rewritten file stays on disk: firewall list then reports the requested state, and the next
boot loads that file. Turning the service back to its previous state rewrites the file as it
was.nft -f /etc/nftables.conf. That file deletes and rebuilds only its own inet filter
table, so the NAT that netavark (the container network backend) sets up for rootful containers
and NetworkManager’s for a hotspot survive a toggle. If the load itself fails, it answers
err the ruleset did not apply.A successful toggle answers ok <service> on|off. An unknown name answers
err no service called that; a state other than on or off answers err a service is on or off.
kdos-powerd --explain <user>
kdos-powerd --set-timezone <Area/City>
kdos-powerd --set-autologin <user|off>
kdos-powerd --firewall list
kdos-powerd --firewall <service> <on|off>
--explain answers “would this user be allowed, and why”. It is the first thing to run when the
power key or the panel’s power item does nothing, because a refusal otherwise shows up only in the
daemon’s log. It asks libkbase the same question the socket asks, needs no privilege, and prints
one line, for example:
alice: uid 1001, primary gid 1001, in seat only — permitted to suspend, power off and reboot; refused the configuration verbs
It exits 0 when the user is allowed anything and 1 when refused.
The other flags run a verb’s own rules without the socket, which is the only way to test them: the
socket’s check needs two different users to exercise. They grant nothing, because the binary is
writing to an /etc the person running it could already write to. Three environment variables
point them at a scratch tree: KDOS_POWERD_ETC replaces /etc (and then skips nft and iw),
KDOS_POWERD_ZONEDIR replaces /usr/share/zoneinfo, and KDOS_POWERD_SOCKET moves the socket.
Per-application energy attribution: which application is using the processor’s power.
Measuring the processor’s energy is routine: the kernel exposes the counter under
/sys/class/powercap. Attribution is the hard part on an ordinary Linux desktop: an application is
dozens of processes in scattered groups, and nothing owns enough of the system to name them. On
KDOS every boxed application (one running in its own box, a rootless
container) runs under a known name, listed in /usr/share/kdos/alien-apps, so the attribution
starts from an identity rather than a guess.
| Verb | Answers |
|---|---|
ping |
ok |
report |
The text report |
report-json |
The same report as one JSON object |
kdos-energy [--json|ping]
kdos-energy prints the report (or JSON with --json) and exits 0. It exits 1 when the daemon is
not running or answered err, and 2 on bad usage. An err answer goes to standard error. The
Energy page in kdos-res shows the same report, and its Boxes page shows each box’s share. When
the daemon is not running the client suggests service kdos-energyd start, which ksvc rejects
as an unknown command; the command is service start energyd (see
Starting and stopping one by hand).
This is the report the test fixture in testing/fixtures/energy produces (thirty seconds of
recorded samples) when replayed with the fixture’s own application table, as the test suite runs
it:
KDOS_ALIEN_APPS=testing/fixtures/energy/alien-apps kdos-energyd --fixture testing/fixtures/energy
Without that table the host’s /usr/share/kdos/alien-apps names the processes instead, and the
rows differ.
KDOS energy — 30 s of samples, RAPL package-0
firefox-esr (appbox kdos-apps) 75.5% ███████████████ gpu 75.0%
kdos-comp 15.4% ███ gpu 25.0%
gimp (appbox kdos-apps) 0.4% gpu 0.0%
short-lived and exited processes 8.7%
shares are of ATTRIBUTABLE energy — 57% of the package total; the rest is the idle floor
idle floor 15.00 W, the lowest average power seen in 3 samples (still falling — early shares read low)
an uncore domain is present, so the integrated GPU's energy is already counted
the gpu column is engine TIME from drm fdinfo, never energy
relative only. RAPL cannot see the panel, the radio or the disk, so there are no watt-hours here.
Answers go to root and wheel only: on a multi-user machine this list is what everyone else is
running.
Figures are shares, never watt-hours. The counter (Intel RAPL, read from
/sys/class/powercap) measures the processor package. It cannot see the screen, which is the
largest single draw on a laptop, nor the radio, the storage or a discrete graphics card. “This
application was 41% of attributable CPU energy today” is a measurement; “this application used 12%
of your battery” would be a guess presented as a unit.
The daemon samples every ten seconds. The interval is fixed and not configurable, for the reason given below, and it is short enough to stay far inside the counter’s wrap period. A machine that wakes from a long suspend resumes sampling from the wake rather than catching up on the missed windows. Six rules each change the answer:
psys domain goes the other way: it contains the packages, so where it exists it
replaces them.max_energy_range_uj and corrects a single wrap per window.uncore domain exists); on a discrete card it is outside the counter entirely, and
the report says that instead. A driver that publishes no per-client statistics gets no column
rather than a column of zeroes.Why a daemon rather than a one-shot command. The counter runs freely, so a one-shot command could only report what happened while it was watching. The counter is also readable only by root since Linux 5.10, because the PLATYPUS side-channel attack showed that fine-grained unprivileged reads can recover cryptographic keys. What leaves this daemon is a per-application percentage over minutes; the raw counter and the sampling interval are never republished, and no client can ask for a shorter interval. There is no write path into the power interface at all.
The init script skips the daemon when no /sys/class/powercap/*/energy_uj is readable, which covers
most virtual machines and every non-x86 processor. The daemon itself also refuses to start there,
naming the powercap path in its log, because a daemon sampling an unreadable counter would report a
machine that uses no energy, which reads as “nothing is draining the battery”. kdos doctor tells
the two cases apart: a machine with no energy domain at all, and a daemon that is not running.
kdos-energyd --fixture <dir> [--json] replays recorded snapshots (<dir>/0, <dir>/1, … each
holding a proc tree, a powercap tree and an optional dt in seconds) through the same sampler
and report, and prints the result. KDOS_ENERGY_PROC and KDOS_ENERGY_POWERCAP point the reader
at other /proc and powercap trees, and KDOS_ALIEN_APPS at another application-name table.
Kills the process that is starving the machine of memory, before the desktop freezes.
The kernel has its own out-of-memory killer, but it fires when an allocation fails. On a machine
with swap that is often minutes after the desktop stopped responding, the whole session spent
thrashing while the kernel still had pages to hand out. The kernel’s pressure interface (PSI, in
/proc/pressure/memory) reports instead that the machine is stalling on memory, which is what a
frozen desktop feels like. Boxed applications make this likely: a browser and a 3D slicer on one
modest machine.
full 150000 1000000 into the
pressure file (150 ms of full stall within a one-second window, meaning every runnable thread was
stuck on memory for 15% of the last second) and sleeps until the kernel signals it. A sampling
loop would itself compete for processor time with the stall it is trying to notice.oom_score_adj is -500 or lower (the opt-out other user-space killers honour),
and these by process name: kdos-comp, kdos-shell, kdos-desk, kdos-notifyd, Xwayland,
dbus-daemon, seatd, ksvc, kdos-powerd, kdos-energyd, kdos-oomd, wireplumber, and
every process whose name starts with pipewire. Killing the compositor to save the desktop is
not a trade, and killing the audio session manager leaves a machine whose sound stopped for no
visible reason.memory key is passed to
the container, but a rootless container on a machine with no cgroup delegation accepts the limit
and ignores it; this rule is what makes the key mean something. The daemon adds up the resident
memory of each box’s processes, and when a box is over its budget the largest process in that box
is the victim. Budgets come from each user’s own box profiles,
/home/<user>/.config/kdos/boxes/<box>.conf; KDOS_BOX_PROFILES, when set, replaces that search
with one directory (the test suite uses it).libkproc that kdos-res and kdos-energyd use.process_mrelease to free the victim’s memory at once rather than whenever it is reaped. On a
kernel without that call the release is skipped and the kill stands./proc
once a second on a machine that is already stalling.The daemon shields itself from the emergency it watches: at start-up it sets its own
oom_score_adj to -1000 and locks its memory with mlockall. Each kill is logged with the victim
and the ten-second pressure averages that caused it, in the form
kdos-oomd: killed <name> [(appbox <box>)] (pid <pid>, <size> MB) — memory pressure avg10 full=<f> some=<s>.
Nothing in the protocol names a process, so there is nothing to aim. The socket takes no argument
and answers root, seat and wheel:
| Verb | Answers |
|---|---|
ping |
ok |
status |
ok trigger 'full 150000 1000000', kills <n> and, after the first kill, , last: <name> [(appbox <box>)] (pid <pid>, <size> MB) |
kdos doctor reports whether its socket is present.
kdos-oomd --fixture <dir> reads a recorded /proc tree, prints the process it would kill as
would kill <name> … (pid <pid>, <size> kB) with the recorded pressure, and signals nobody. It
exits 0 when there is a candidate and 1 (printing no candidate) when there is none.
testing/oomd-fire.sh exercises the real path in a booted virtual machine: it allocates and
touches memory until the trigger fires, then asks the socket what happened rather than watching
the allocating process die (the kernel’s own killer would also have got it, later). A passing run
shows status going from kills 0 to kills 1, last: <name> (pid <pid>, <size> MB) while the
script that started the allocation keeps running; the recorded run on a 4 GB guest answered
kills 1, last: python3 (pid 1615, 3644 MB). That run has one large process to
choose; which process the daemon picks on a busy desktop is what --fixture tests.
Removable media, encrypted volumes, SMART health and network shares for a desktop that is not root.
udisks2 serves the natively ported toolkit applications (see
Administration); for the desktop’s own surfaces this daemon is
the whole of what stands between them and mount. Both read /proc/mounts, so a device udisks2
mounted under /run/media/<user> is listed here as mounted, with its mountpoint.
The client never names a path or a mountpoint. It asks for a row out of a list the daemon
published, and the daemon decides the device, the mountpoint and the options. A design that takes
“a path and a mountpoint” ends with someone mounting a stick over /etc from a shell.
A row below is a number from the daemon’s own list; a share row is a number from
shares. <count> is the byte length of a second frame, described under
The request format.
| Request | Does | Answer |
|---|---|---|
list |
The eligible devices | One row each: index, kernel name, label, filesystem, size in GiB (28.7G), mountpoint, tab-separated, with - for an empty label or mountpoint; then ok |
mount <row> |
Mount the device | ok <mountpoint>; a device already mounted answers with its mountpoint |
unmount <row> |
Unmount it and remove the mountpoint the daemon made | ok <mountpoint> |
eject <row> |
Power the medium down. An optical disc ejects its own drive; any other device ejects its parent disk, because a stop command sent to one partition means nothing to the hardware | ok <kname> |
unlock <row> <count> |
Open a LUKS volume. The passphrase is the second frame | ok kdos-<kname> |
close <row> |
Close the mapping unlock made |
ok kdos-<kname> |
format <row> <fs> <count> |
Write a filesystem. Off unless format = yes. The second frame is the device’s kernel name, typed by the person |
ok <kname> |
smart <row> |
The drive’s health | ok <model>\t<serial>\t<health> |
cifs <server> <share> <user> <domain> <count> |
Mount an SMB share. The password is the second frame | ok <mountpoint> |
krb5 <server> <share> <user\|-> <domain\|-> |
Mount an SMB share with the Kerberos ticket kinit left in the caller’s credential cache. No second frame |
ok <mountpoint> |
shares |
The mounted network shares | One row each: index, //server/share, mountpoint, tab-separated; then ok |
browse |
Machines that answered an mDNS and a NetBIOS broadcast at the time of the request | One row each: name<TAB>address; then ok |
disconnect <share row> |
Unmount a network share | ok //server/share |
subscribe |
Hold the connection open and write changed on every block-device hotplug event |
ok, then changed lines; never closes |
ping |
— | ok |
Every verb answers root, seat and wheel: mounting a stick is the desktop user’s job whether or
not they administer the machine. Every failure answers err <reason>, for example
err unmount it first or err no such device.
kdos-mount list
kdos-mount mount <index>
kdos-mount unmount <index>
kdos-mount smart <index>
kdos-mount write <index> <image> <disk>
kdos-mount shares
kdos-mount browse
kdos-mount krb5 <server> <share> <user|-> <domain|->
kdos-mount ping
kdos-mount subscribe
For example:
$ kdos-mount list
0 sdb1 KDOS vfat 28.7G -
1 sdb2 backup ext4 120.0G /media/kdos/backup
ok
$ kdos-mount mount 0
ok /media/kdos/KDOS
The client prints the daemon’s reply unchanged, flushing after every read so that subscribe can
be piped into another program.
| Exit status | Means |
|---|---|
| 0 | The daemon answered, including when the answer was err …. A script must check standard output for a leading err to tell a failed mount |
| 2 | No kdos-mountd to ask (kdos-mount: no kdos-mountd on <socket path> (<reason>)), or bad usage |
The index is re-rendered as a number before it is sent, through atoi, so an argument that is not a
number becomes row 0: kdos-mount mount sdb1 acts on row 0 rather than being refused.
kdos-mount write <index> <image> <disk> writes an image (see
Writing an image): it opens the image itself, as you, and prints the daemon’s
progress lines as they arrive. The other verbs that carry a secret (unlock, format, cifs), and
eject, close and disconnect, are reached from the desktop’s surfaces rather than from
kdos-mount:
| Surface | Uses |
|---|---|
kdos-devices |
list, mount, unmount |
kdos-disks |
list, mount, unmount, unlock, close, format (always as ext4), smart, write |
kdos-connect |
cifs, krb5, shares, browse, disconnect |
kdos-mediad |
subscribe, list, mount, eject |
A request is one line, plus a second frame where a secret is involved.
err too long otherwise). That ceiling is set by
cifs alone: a DNS name may be 253 bytes, a share 80, a username 104 and an NT domain 255, so a
legal corporate share spells a request of about seven hundred.err short frame.write only: the image, attached as SCM_RIGHTS. The
daemon reads frame one with recvmsg so the descriptor is not lost, keeps at most one, and closes
one that arrives with any other verb before it looks at the verb.Every token is checked before it means anything, and the token count is fixed per verb:
err bad request.err unknown command) rather than
ignored. A dispatcher that read an index and discarded the rest would accept
mount 0 rm -rf / as a well-formed mount.err too many arguments.The device list is rescanned on every request rather than cached, so a stick pulled out between two requests is never still offered.
The daemon walks /sys/block, ignoring loop, ram, zram and dm- devices, and considers each
partition of a disk, and the whole disk when none of its partitions qualifies (a stick formatted
without a partition table is the usual case). A device appears in list only
if every one of these holds:
| Rule | Why |
|---|---|
| Its disk is removable, or is on USB | An internal disk is the administrator’s. An external drive in a USB enclosure reports itself as non-removable, so the bus is checked too |
| It carries a filesystem the daemon recognises | See the list below |
It is not named in /etc/fstab, by device path or by LABEL= |
An entry there is a decision somebody already made |
| It is not the medium this system booted from | Offering to unmount the live medium is offering to end the session |
A device that is already mounted is listed, with its mountpoint in the last column. The boot
medium is a device mounted at /, /mnt/iso or /boot, and in a live session (root on an
overlay) any ISO 9660 medium. The live ISO does not appear in the running system’s /proc/mounts
at all, because the initramfs moved the root out from under those mounts, so this broad rule is
the only reliable one; the cost is that a second data CD cannot be mounted while running live.
The label and filesystem type are read directly from the superblock rather than through a detection library. The daemon recognises, in this order:
| On disk | Reported as |
|---|---|
| LUKS (checked first, because a LUKS header written over an old filesystem still carries that filesystem’s superblock) | crypto_LUKS |
| ext2, ext3, ext4 | ext4 |
| FAT12/16/32 | vfat |
| NTFS | ntfs3 |
| exFAT | exfat |
| ISO 9660 | iso9660 |
| btrfs | btrfs |
Kernel support is checked at mount time. Before calling mount(2), the daemon checks the type
against /proc/filesystems and answers err this kernel cannot mount <type> when the driver is
missing, instead of failing halfway.
Mount options. nosuid and nodev always; noexec unless exec = yes is set in
/etc/kdos/mountd.conf. A setuid-root binary on somebody else’s stick is a local root hole, so
executing from removable media is something you opt into. FAT, exFAT and NTFS have no ownership of
their own, so they are mounted with uid= and gid= set to the caller and fmask=0117,dmask=0007;
on a native filesystem the mountpoint itself is given to the caller.
The mountpoint is /media/<user>/<label>, created mode 0700 and owned by the caller, and
removed on unmount. The label is reduced to the characters A–Z a–z 0–9 . _ - before it becomes a
path component, because it is whatever somebody else’s computer wrote into a superblock. A device
with no usable label is named after its kernel name, and failing that disk. A path too long to
be a mountpoint is refused rather than shortened.
The image ships no mountd.conf; create it to change a default. The daemon reads it on each
request that needs it.
| Line | Default when absent | Effect |
|---|---|---|
exec = yes |
noexec |
Removable media and network shares are mounted without noexec, so programs on them can run |
format = yes |
format refused |
The format verb is allowed |
write = yes |
write refused |
The write verb is allowed |
The daemon searches the whole file for the text exec = yes or exec=yes (and likewise for
format and write) anywhere, comments included. A commented-out # exec = yes therefore still
turns the setting on. To turn a setting off, delete the text rather than commenting it out.
The passphrase reaches cryptsetup open on standard input, through --key-file=-, and never in an
argument list: /proc/<pid>/cmdline is readable by every user for the life of the process. One
buffer holds it, and every way out of the request wipes it. A wrong passphrase and a header the
installed cryptsetup will not open give the same answer, err could not unlock, so a failure
says nothing about how close a guess was.
The mapping name is the daemon’s: kdos-<kname>, derived from the row. A client cannot ask for a
mapping called anything else, and close finds the same name from the same row without being told
it. Unlocking a volume that is already open is not an error.
After an unlock, the opened mapping appears in list beside the container it came from, so the
filesystem inside can be mounted like any other row. The daemon finds it by the name it chose
itself rather than by walking /sys/block/dm-*; a mapping this daemon did not open, such as
someone’s own cryptsetup open of a root volume, is not offered. The row keeps the container’s
physical disk, so every destructive verb is still judged by that disk.
smart answers for the whole drive, not the partition: SMART is a property of the drive, so a row
per partition would repeat one answer and point a raw-device tool at an offset nothing owns.
smartctl needs the raw block device, which no session may open. The alternatives, a setuid binary
or a sudo rule, are both wider holes than one daemon answering one question. The daemon runs
smartctl -H -i on the parent disk and filters the output down to three tab-separated fields: the
model (Device Model or Model Number), the serial and the health line (overall-health or
SMART Health Status), with - for a field it did not find. Its exit status is a bitfield rather
than a failure flag: bits 3 to 7 mean the drive is unwell, which is often the answer rather than
the absence of one, so only an empty capture is treated as “nothing learnt”.
cifs mounts an SMB share with a password. mount(2) cannot open an SMB session by itself
(dialect negotiation, authentication and the tree connect all happen inside mount.cifs), so this
verb runs that helper.
. and -, up to 253 characters, not starting or ending with a separator and with no
... Share: letters, digits, ., _, - and $, up to 80. User: letters, digits, ., _,
@ and -, up to 104. Domain: letters, digits, . and -, up to 255, or - for none.
mount.cifs builds its option string by joining fields and escapes nothing but the password, so
a comma in any of these would become a new mount option, and a / or \ in a server would
silently re-aim the mount. What is not on the list is refused rather than quoted.PASSWD_FD=0, with the bytes on the
child’s standard input). The other ways mount.cifs accepts one are worse: an option string is
visible in the process list, an environment value in /proc/<pid>/environ, and a file is a file
somebody has to delete.nsswitch.conf has no effect on musl and there
is no winbind, so a .local name and a bare NetBIOS name are the two shapes getaddrinfo never
answers. The normal resolver is tried first (a name in /etc/hosts is one somebody wrote down,
and a broadcast must not override it); then a .local name goes to avahi-resolve-host-name and
a bare name to nmblookup. An address or a dotted DNS name never triggers a broadcast. Only the
address is substituted: the share mounts under the name that was typed, with the address passed
in ip=, so the mountpoint is one a person recognises. A name nothing answers to is refused
before anything is loaded or created.cifs module is loaded first. Nothing else on the image loads it, and
/proc/filesystems lists only what the kernel already has, so checking support before modprobe
would refuse every first connection./media/<user>/<server>-<share> with
uid=, gid=, file_mode=0600 and dir_mode=0700, because a server without Unix extensions
reports every file as owned by root. nosuid and nodev always; noexec unless exec = yes. A
share that is already mounted answers with its existing mountpoint. When the helper refuses, the
first line of its own error message is the answer.shares lists what /proc/mounts says is connected (type cifs or smb3). No list is held
between requests, so a server that went away, or a share another session mounted, is reported as
it is at that moment. disconnect rebuilds the same list before acting on a row.
browse is two broadcasts, not a directory. There is no browse master to ask (samba is built
without winbind and without a domain controller), so the list is avahi-browse -ptrk _smb._tcp for
machines that advertise the service, plus nmblookup -S -- '*' for machines that answer a NetBIOS
query. Only a machine with a <20> (file server) entry is offered, since one without it is sharing
nothing. At most 32 machines are listed. A browse row is not an index: cifs and krb5 take a
server name.
krb5 mounts with sec=krb5, using the ticket already in the caller’s credential cache. No
secret crosses the socket: the kernel’s cifs module raises a cifs.spnego key request,
request-key runs cifs.upcall against that cache, and the helper hands back the SPNEGO blob.
That is why it is a verb of its own rather than cifs with an empty password.
cruid=<caller>. Without it the upcall would look in root’s credential cache,
which is empty, and the mount would fail with Required key not available, naming no user.- means “no username” and “no domain”, and for Kerberos that is the normal case: the principal
in the ticket already says who you are./usr/sbin/cifs.upcall and /etc/request-key.d/cifs.spnego.conf are checked before the module
is loaded and before the mountpoint is made. Without them the kernel’s error is that same
unhelpful Required key not available, so the daemon names the missing file instead and leaves
no empty directory under /media.eject, unlock, format and write pass through one check before they act. It refuses a device
that is mounted (err unmount it first), a device on the boot medium, and a device node that has changed
since the scan.
The boot medium is refused by the disk, not by the partition. A live USB carries an ISO 9660
partition and a FAT EFI partition beside it. Every per-partition rule would offer the EFI partition
(it is removable, it is FAT, it is unmounted and no fstab names it), and formatting it would destroy
the running session. In a live session any disk carrying an ISO 9660 partition is treated as the
boot disk, whole: err that is the medium this session booted from.
The device node is checked again at the moment of use. Between the scan that built the row and
the call that acts on it, a path could become a symlink or a different device. The daemon opens it
with O_NOFOLLOW and requires a block device whose device number matches the one /sys recorded.
format requires the person to type the device’s kernel name; a flag or the word yes is not
enough. The daemon compares the second frame with the name it put in the list itself, by exact
length and content, so a client cannot send a confirmation it was never shown. A mismatch
answers err type <kname> to confirm.
format is opt-in: without format = yes in /etc/kdos/mountd.conf it answers
err format is off; set `format = yes` in /etc/kdos/mountd.conf. The filesystem is one of four,
checked against a table (anything else answers err unknown filesystem), and every new filesystem
is labelled KDOS:
| Filesystem | Command |
|---|---|
ext4 |
/usr/sbin/mkfs.ext4 -F -L KDOS <node> |
btrfs |
/usr/bin/mkfs.btrfs -f -L KDOS <node> |
vfat |
/usr/sbin/mkfs.vfat -I -n KDOS <node> |
exfat |
/usr/sbin/mkfs.exfat -n KDOS <node> |
write <row> <count> puts a disk image, such as an installer or a live system, over the whole disk
the row is on, and then reads the disk back and compares it with the image. The image arrives as an
open descriptor attached to the request, never as a path. The client opened it as the person asking,
so it can only be a file that person can read; a root daemon that took a path would open it as root,
and write 0 /etc/shadow would copy a file the caller cannot read onto a stick the caller can.
write is opt-in: without write = yes in /etc/kdos/mountd.conf it answers
err write is off; set `write = yes` in /etc/kdos/mountd.conf. It is a key of its own because
turning on formatting says nothing about replacing a whole disk. Beyond the check every destructive
verb passes, it refuses:
| Refusal | Answer |
|---|---|
| No descriptor attached | err no image: the file travels as a descriptor, not a name |
| Any row on the same disk mounted, or an unlocked mapping on it | err unmount <kname> first, err close <kname> first |
Any partition of the disk, or the disk itself, that /etc/fstab names or the running system is mounted from — partitions list leaves out, which the write would replace all the same |
err <name> is in /etc/fstab or holds the running system |
A second frame that is not the disk’s name (sdb for a row on sdb1) |
err type <disk> to confirm |
| A descriptor that is not a regular file with something in it | err the image is not a file with something in it |
| An image larger than the disk | err the image is <n>G and <disk> is <m>G |
| The disk opened by anyone else | err <disk> is in use — unmount and close everything on it |
The disk is opened with O_EXCL, which on a block device is the kernel’s exclusive claim: it fails
while any partition of the disk is mounted, mapped or claimed, and while the write holds it a mount,
a format or a second write of that disk fails in turn. The node is opened with O_NOFOLLOW and its
device number is checked against /sys, like every other verb’s.
The copy runs in a double-forked worker, so the daemon goes on answering every other client for the minutes a write takes. The connection stays open and carries the worker’s progress, one line per step of about one per cent:
progress write 1048576 3000000
progress verify 3000000 3000000
ok sdb 3000000 verified
After the copy the data is synced and the device’s cached pages are dropped (BLKFLSBUF), so the
verify reads what the disk hands back rather than what the worker just wrote; a stick that wraps
writes past its real capacity fails there with err verify failed: <disk> does not hold the image
at byte <n>. Last, the daemon asks the kernel to re-read the partition table (BLKRRPART), which
makes the new partitions appear and sends every subscriber changed. A worker whose client has gone
carries on to the end; its progress lines are dropped.
A disk with no filesystem the daemon recognises is not in list at all, so an image cannot be
written onto it through this verb; and in a live session any disk carrying an ISO 9660 partition is
treated as the boot disk, so a stick that already holds another system’s image is refused there.
Every program it runs is named by absolute path: /usr/bin/eject, /usr/sbin/cryptsetup,
/usr/sbin/smartctl, the mkfs programs above, /sbin/modprobe, /sbin/mount.cifs,
/usr/bin/avahi-resolve-host-name, /usr/bin/avahi-browse and /usr/bin/nmblookup. Otherwise a
root process would look programs up through an inherited PATH. eject, cryptsetup, smartctl,
nmblookup and avahi-resolve-host-name get -- before the device or name, so a leading dash
cannot become an option. The mkfs node and the mount.cifs share are paths the daemon built
itself (/dev/<kname> and //<checked server>/<checked share>), so neither can begin with a dash.
kdos-mediad is the subscriber. It is a name of the kdos-shell binary, started once per session
by the compositor alongside kdos-notifyd:
kdos-mediad sends subscribe and keeps the connection open.NETLINK_KOBJECT_UEVENT), so hotplug
works with no udev rule file and without udev running. It reacts to block-device add,
remove and change events; change is included because that is what a drive reports when a
disc goes into a tray that was already there.add, remove or change event the daemon writes changed to every
subscriber, without checking whether the offered list is different; an internal disk or a
mapping changing sends it too. It cannot say which device, because a row number is only true
of the list it came with, so the subscriber asks again with list and compares.kdos-mediad raises a notification with Open and Eject buttons. When a button is
clicked it reads the list again and finds the device by kernel name, rather than trusting the
row the notification was built from.The daemon only says that something moved; the session decides what it means. The daemon is root,
starts before anybody logs in, and has no session bus to send a notification on. At most four
subscribers are held at once; a fifth is answered err too many subscribers. A subscriber that
hangs up is dropped before anything is written to it.
The panel does not subscribe. A subscription would put a socket read in the panel’s frame loop,
which keeps blocking calls out of the way of drawing (the tray follows the same rule; see
kdos-shell). kdos-devices asks when it is opened.
If the daemon cannot open the hotplug socket it logs
kdos-mountd: no uevent source; subscribe will report nothing and serves everything else.
kdos-mountd --fixture <sys> [dev]
kdos-mountd --fixture-serve <sys> [dev]
--fixture prints what the daemon would offer from a recorded /sys tree (and optionally a /dev
tree), one row per device with its size in bytes and a final <n> eligible, and mounts nothing.
--fixture-serve runs the real request handling over a real socket (KDOS_MOUNTD_SOCKET) with the
fixture’s trees, admits any caller, and prints each command it would run (with its environment and
the byte count, never the bytes, of anything it would feed on standard input) instead of running
it. No name is resolved and nothing is broadcast. That is how a format aimed at the boot medium
is proved to be refused without a disk to lose. A write in these modes prints write <node> and
copies the image into the file KDOS_MOUNTD_SINK names instead of the disk, and verifies against
that file, so the copy and the read-back really run with nothing that looks like a device touched.
Only in these two modes does the daemon read the path overrides KDOS_MOUNTD_SYS,
KDOS_MOUNTD_DEV, KDOS_MOUNTD_MOUNTS, KDOS_MOUNTD_FSTAB, KDOS_MOUNTD_MEDIA,
KDOS_MOUNTD_CONF, KDOS_MOUNTD_SINK and KDOS_MOUNTD_UEVENT (a FIFO standing in for the
kernel’s hotplug socket).
The daemon started by the init script reads none of them: an environment variable that moved its
idea of /dev would be a way to aim a format at any device on the machine.
The committed fixture, testing/fixtures/mountd, is a recorded /sys/block tree with six hand-built
device images. Five are on removable disks and are offered: a FAT32 stick labelled KDOSSTICK, a
LUKS container, a live-USB pair (an ISO 9660 partition and a FAT ESP beside it) and an optical
disc. One is an ext4 filesystem on an internal disk and is never offered. The fixture also carries
an fstab that claims the stick by label, and a mounts-live table from a booted live medium with
one mounted network share. The internal disk carries a real superblock so that a broken removable
check shows up as an extra row rather than as nothing.
The only program on the system that mounts an application pack. A pack is a signed, read-only EROFS image holding an application, a runtime it depends on, or data; how packs are built, verified and combined into a box is in Packs and boxes.
| Verb | Does |
|---|---|
list |
Every pack the machine can see: id, version, kind, state (mounted, installed or available), size in bytes, and origin (store or medium), tab-separated, then ok |
info <id> |
One pack’s metadata, then its state, origin and, when mounted, mountpoint |
mount <id>, unmount <id> |
Mount or release a pack. A pack still used by a box is not unmounted |
compose <box> <id>… |
Build a box’s overlay stack from 1 to 32 packs. Ids past the 32nd are ignored rather than refused |
decompose <box> |
Tear it down |
install <file> |
Verify a pack in the staging directory and move it into the store |
remove <id> |
Take a pack out of the store |
rollback <id> |
Return to a retained earlier version |
graft <id>, ungraft <id> |
Place a data pack’s contents where its consumer looks for them, or remove them (a graft) |
ping |
ok |
status |
Tab-separated lines: store, medium and staging directories, retain count, mount route (file-backed, loop device, or not yet used), packs (installed, available and mounted counts), boxes (how many are composed), and a box line for each composed box (name, merged root, ephemeral or persistent), then ok |
Every verb answers root and wheel only. The clients on the system are kdos-appbox and
kdos doctor. Launching a boxed application sends info for the pack’s environment and compose
for its box; kdos-box start sends compose and kdos-box remove sends decompose.
kdos-appbox genlaunchers --packs sends list and then mount for each installed pack.
kdos app import, kdos-appbox import and kdos-box import copy a pack into the staging
directory and send install. kdos-appbox status and kdos doctor ask for status, and
kdos doctor reports the mount route. kdos app install and kdos app remove build and delete
container images and ask this daemon nothing. No program on the system sends unmount, remove,
rollback, graft or ungraft; those verbs are reached only by a client that speaks the socket
directly.
An id is at most 63 characters from [A-Za-z0-9._-] and does not begin with . or -; anything
else is refused before it is looked up.
Paths:
| Path | What |
|---|---|
/var/lib/kdos/packs |
The store; the current version of a pack is <id>.kpack |
/var/lib/kdos/packs/staging |
Where an unprivileged download lands; mode 01777, set when the daemon starts |
/var/lib/kdos/packs/mnt |
Mountpoints |
/mnt/iso/packs |
Packs on the boot medium |
/var/lib/kdos/pack-manifest |
Every graft made, so an ungraft removes exactly what was added |
/etc/kdos/keys/packs |
The keys packs are verified against, separate from /etc/kdos/keys, which is kpkg’s package-repository ring |
/etc/kdos/packd.conf |
Configuration |
Unlike kdos-mountd’s overrides, the daemon reads these environment variables in normal operation,
not only in fixture mode. The init script sets none of them:
| Variable | Effect |
|---|---|
KDOS_PACK_STORE, KDOS_PACK_MEDIUM, KDOS_PACK_MANIFEST |
Replace the store, the boot-medium pack directory and the graft record |
KDOS_PACK_HOME, KDOS_PACK_SHARE |
Replace the caller’s home directory and /usr/share as graft and box-upper destinations |
KDOS_PACK_RETAIN |
Overrides retain in packd.conf (below) |
KDOS_REQUIRE_SIG |
Refuse every unsigned pack at install (<id> is unsigned and KDOS_REQUIRE_SIG is set), for a machine that installs only what it can attribute |
All of them are also listed in Filesystem and IPC.
The client never names a path, with one exception: install takes a file name inside the
staging directory. A name containing /, the name .., an empty name, or a name with a character
outside [A-Za-z0-9._-] is refused. status publishes the staging directory and the retention
count, so a program writing a download into the store does not have to work either out for itself;
a second definition of where an unprivileged write is allowed is the kind of thing that drifts.
Verification happens in the daemon, where the mount happens, because a client that verified and then asked a daemon to mount would have verified nothing:
PACKAGES index whose signature is good and which lists this
pack, the pack’s SHA-256 must match the index entry; a mismatch is refused
(<id> does not match the signed index)./etc/kdos/keys/packs. A good
signature is accepted; a bad one, a hash mismatch, or a signature by a key the ring does not
hold is refused.KDOS_REQUIRE_SIG
is set.A pack in the store was verified when it was written and only root can write the store, so it is
not re-hashed at each mount. A pack on the boot medium was never installed, so it is verified at its
first mount; there a bad signature, a hash mismatch or a missing key is refused, and an unsigned
pack mounts. Every pack is mounted read-only with nosuid and nodev, and a data pack with
noexec as well.
The list is rescanned on every request, so a medium pulled out between two requests is not still offered. At start-up the daemon adopts whatever packs and boxes are already mounted, so a restart while boxes are running does not lose track of them.
An install replaces the old version safely. If the old version is mounted but not in use, it is
unmounted before the file is swapped, so the next compose reads the new file. If it is composed
into a running box, the install is refused (<id> is composed into <n> box(es) — stop them, then
install), the same rule remove and rollback apply. Without this, a box composed after an update
would put the new application’s layer over the old runtime’s still-mounted files, and the
application would fail on a library only the new runtime carries. The version being replaced is
kept as <id>-<version>.kpack beside the new <id>.kpack.
/etc/kdos/packd.conf sets how many superseded versions of each pack the store keeps:
retain = 1
| Value | Effect |
|---|---|
1 (shipped) |
The most recent update can be undone |
0 |
Nothing is kept; rollback answers err no earlier <id> is kept |
Up to 8 |
That many, newest first by version; higher values are treated as 8 |
The daemon reads retain from the file once, when it first needs it, and keeps that value for the
rest of its life; restart it (service restart packd) after changing the file.
KDOS_PACK_RETAIN, when set, is read on every use.
The sweep that deletes older versions runs after an install and at no other time, so nothing deletes
a rollback while somebody is deciding whether to use it. rollback keeps the current file as
<id>-<version>.kpack, renames the retained version whose version string sorts last (compared as
text) into its place, and answers ok <id> <current> -> <restored>. The version rolled away
from stays in the store under its own version.
kdos-packd --fixture <store> [medium] lists the packs in a scratch store with their signature
state, composes each application pack, grafts each data pack (printing each graft’s destination),
and prints the result, mounting nothing. KDOS_PACKD_VERBOSE shows its log lines; KDOS_KEYS
points it at another key directory. The daemon reads KDOS_KEYS only in fixture mode; the
kdos-pack tool reads it in normal use, to choose the ring its verify and info check against
(default /etc/kdos/keys/packs).
The two mount routes, reference counting and adoption of existing mounts at start-up are described in Packs and boxes.
kdos-boxsock is not a root daemon and has no socket in /run. It is installed in /usr/bin, runs
as the desktop user, one process per box, and gives that box its own Wayland socket so the
compositor always knows which box a window came from.
kdos-boxsock <box> [instance-id]
What it does:
[A-Za-z0-9._-], and not . or ...$XDG_RUNTIME_DIR/kdos-box-<box>@<display>.lock. If another kdos-boxsock for
the same box and compositor already holds it, this one exits 0 and does nothing. The lock, not
the socket file, answers “is this box already served”, because a socket file is also left
behind by a crash.$XDG_RUNTIME_DIR/kdos-box-<box>@<display>.sock, mode 0700, so only this user can
connect.io.kdos.appbox, the box name as the application id, and the instance id (the box name
when no instance is given).Every client that connects on that socket is tagged by the compositor itself; the client never sees the tag, so it cannot forge, choose or drop it. That tag is what the compositor’s sandbox filter reads, what the box chip on a title bar shows, and what lets the panel say which box a window came from.
<display> is taken from $WAYLAND_DISPLAY: its last path component, reduced to
[A-Za-z0-9._-] and cut to twelve characters, or session when nothing is left. It is in the path
because the listener belongs to the compositor this process connected to; a path keyed on the box
alone would send a later launch through a compositor that may already be gone. kdos-appbox
derives the same path from the same variables, so the two agree with nothing passed between them.
If the compositor does not offer the security-context protocol, kdos-boxsock exits 1 with
compositor has no security-context-v1 — not tagging rather than bind an untagged socket, and
kdos-appbox runs the box on the session’s shared socket. The box then starts, unconfined at the
protocol level, instead of failing to start.
It is a separate program for two reasons:
kdos-appbox links only libkbase, libktui, libkcolor and libkxdg, none of which speaks
Wayland. Speaking Wayland would add a client library and generated protocol code to a program
whose short dependency list is a deliberate property.The portal backend, installed as /usr/lib/xdg-desktop-portal-kdos and started on demand over the
session bus. It implements the FileChooser, Settings, AppChooser and Access backend interfaces; the
access question that the Camera, Screenshot and Location portals ask is answered through
kdos-prompt. It is covered in
The session, including the two rules that matter
most: every request is answered, and the bus loop never blocks on a dialog.
The lock screen, in /usr/bin. It is not a root daemon, but it is the other long-lived program
close to privilege. It is an ext-session-lock-v1 client: it covers every output with a lock
surface and checks the password by running kdos-checkpass, handing it the password on standard
input.
Once the compositor confirms the session is locked, kdos-lock prints locked on standard output;
kdos-power suspend waits for that line.
The compositor, not the lock program, owns the locked state. If the lock program crashes, the screens stay covered and a new lock client may take over. See kdos-comp.
kdos-checkpass (/usr/bin, mode 4755) is the only setuid piece of the lock screen: /etc/shadow
is readable only by root, and the lock screen must not run as root.
/etc/shadow directly rather than through the name-service layer, so no NSS module runs
in a setuid process.crypt() runs as the caller, and compares the
result in constant time.kdos-checkpass exit |
Means |
|---|---|
| 0 | Correct |
| 1 | Wrong |
| 2 | Could not tell: no such user, no readable shadow entry, an account with no password or a locked one (an empty, ! or * hash), or an argument was given |
Each wrong answer takes one second before kdos-checkpass exits, which limits how fast a password
can be guessed by driving it directly; the lock screen adds its own pause after a failure. An
account with no password, or a locked one, cannot unlock the lock screen at all.
Two services that are not KDOS’s own and not root keep the login records musl cannot: utmpd
holds who is logged in now and wtmpd appends the history to /var/log/wtmp. Both come from the
utmps port, each run as utmps-utmpd or utmps-wtmpd behind s6-ipcserver (from s6) as the
utmp account, and 06_utmps.sh starts both under ksvc with the supervised names utmpd and
wtmpd. The service name for service and /etc/service.disabled is utmps, and disabling it
leaves who, w, users and last with nothing to report. What each socket and file holds, who
may write to them and why every reader is built against utmps is in
Login records.
A new daemon should meet all of these. Where an existing one does not, the exception is stated in
its section: kdos-mountd breaks rule 6 (see
One request per connection), and kdos-powerd breaks rule 8 for
kdos-bootctl and iw (see accent).
/etc/init.d script through supervise./run, mode 0666, and refuses to serve that socket unless it is
root (or, like kdos-energyd, cannot start at all without root’s access).wheel, plus seat for what the person at
the machine needs without administering it) by calling kb_uid_allowed(), never a copy of that
test, and answers err not permitted otherwise.--fixture mode that decides and prints without acting, or, like kdos-powerd,
flags that run each verb’s rules against a scratch tree.Where the pieces go is fixed as well. The source and its recipe (kpkgbuild and build.sh) are
src/daemons/<name>/, the area that holds the five root daemons above (kdos-boxsock,
xdg-desktop-portal-kdos and kdos-lock are session programs, sources under src/desktop/ and
named under the list’s src-desktop heading); the port is named under the src-daemons heading of
script/phases/50_desktop/packages.txt, the only phase whose PORT_REPO includes src/daemons;
and its init script is fs/etc/init.d/<NN>_<service>.sh, whose number places it after
everything it needs, since rcS starts the scripts in numeric order (the five here are 55 to 59).
rcS starts them and rcK stops themkdos-boxsockPart IV — Programs, chapter 26. Previous: 25. kdos-appbox · Contents · Next: 27. kinstall