In any modern codebase `O_CLOEXEC` should be the default and file
descriptor inheritance must be explicitly enabled. Flip the switch and
port everyone to our internal open helpers as well.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
sd-event can only wait for fd readiness. A fiber that receives from a socket
waits until the socket is readable and then calls recv() itself. io_uring can
run the recv() in the kernel and report its result, but sd-event has no way
to submit an SQE or to deliver its completion.
Let's give each event loop an io_uring instance, and let's add
future_new_io_uring(), which submits an SQE and returns a future that
resolves with the res of the CQE. The loop creates the ring the first time
event_io_uring_available() is called. If the setup fails,
event_io_uring_available() returns false, and the caller waits for fd
readiness instead. Event loops that never submit an SQE never call
io_uring_setup(). A seccomp filter that kills the process on
io_uring_setup() doesn't affect them. They also never get the EINTR that the
kernel causes in a thread when a ring that the thread used is freed.
io_uring is opt-in per event loop, with event_set_use_io_uring(). io_uring
runs an operation that has to block, for example on a regular file, in a
worker thread of the process. While a worker thread exists,
unshare(CLONE_NEWUSER) and setns() into a user or mount namespace fail with
EINVAL. Every worker thread also counts against the task limit of the
process. A program should only enable io_uring if it doesn't call unshare()
or setns() for a namespace like that, and if extra threads don't push it
over its task limit.
The ring fd is added to the epoll fd, and the loop processes CQEs when epoll
reports the ring fd as readable. Event sources keep using epoll, because
io_uring has no level-triggered poll.
The loop resolves the future of an SQE as soon as it processes the CQE.
sd_future_resolve() only enables the event sources of the callbacks and of
the waiting fibers, so the fibers still run in the priority order of their
event sources.
Cancelling the future submits a cancel SQE but doesn't resolve the future.
The future resolves when the CQE of the original SQE arrives, with
-ECANCELED, or with the result of the operation if it completed first. The
buffers that the SQE references stay valid until then, because a fiber waits
for the future in sd_future_cancel_wait_unref().
On exit, the loop submits a cancel-all SQE and waits for the CQEs of the
inflight SQEs before it dispatches the exit sources. A fiber that waits for
an io_uring future then runs its cleanup from its exit source, in the
priority order of the exit sources, like a fiber that waits for an epoll
future. A fiber that was created on another fiber behaves differently. Its
parent normally cancels it. The cancel-all resolves its io_uring future with
-ECANCELED, though, and the fiber then resumes from its own exit source
before its parent cancels it.
While the loop waits for the CQEs on exit, it waits in io_uring_enter()
instead of epoll_wait(), so no timer or other source is processed after the
exit. A nested event loop on a fiber waits on the ring fd in that state as
well. Timers and fds that the loop doesn't service anymore can keep the epoll
fd readable, and a nested loop that waits on the epoll fd would spin.
We keep the API internal. Handing out io_uring SQEs comes with contracts we
can't support as a stable API across kernels and opcodes. The buffers that
the SQE references have to stay valid until the CQE arrives, and the SQE has
to post exactly one CQE, which rules out multishot operations.
SYSTEMD_EVENT_IO_URING= overrides the opt-in for tests. If it's true, every
event loop uses io_uring. If it's false, no event loop uses io_uring.
Add a new parameter to the sd_notify FD store interface that allows
units to easily wipe the content of their own store, without having
to call into D-Bus.
With FileDescriptorStorePreserve=yes we propagate the FD Stores of a
user session to the system manager, but running CleanUnit was not
propagated so the FD Stores of a cleaned unit remained in the upper
store and were restored.
Follow-up for 5608575e01
The packaging references are part of mkosi's configuration, so tine can't
build a checkout with the same pinned spec. Let's share the pins in one
JSON file keyed by distro and keep mkosi, the packaging updater and
Packit using it.
So far the variable was set as soon as one of the SMBIOS structures
(types 1, 2 and 11) was measured, and a later failure on another only
logged.The original intent was to prevent sd-stub from measuring these
things a second time after sd-boot already did.
This PR adds a second consumer: in a confidential VM, PID 1 takes the
variable as confirmation that the SMBIOS OEM strings are covered by
PCR 1 before it imports credentials from them. Make the loader provide
that guarantee and leave the variable unset if any structure could not
be measured, which also matches how the documentation already describes
it.
The effect on the existing consumer is limited to boots with a
measurement failure: when sd-boot fails to measure some of the structs
and then chainloads sd-stub, sd-stub measures the structs sd-boot was
able to measure a second time. Previously it would have left the
incomplete measurement as is. In that situation PCR 1 already didn't
match the expected value before (events missing), and after this change
it still won't (additional events), so anything bound to it, such as a
pcrlock policy, does not match either way.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
The policy requires a warning before a ban, so this throws in two
messages that we can copy + paste so we don't have to draft a bespoke
warning each time someone violates the rule
salsa is very flaky and CI jobs constantly fail when pulling from it.
Switch to github repo in our org so that it is more reliable
(if it's down, the CI jobs are down as well by definition, so no
combinatorial probability of outages
This introduces environment variables $SYSTEMD_NETWORK_MAX_ADDRESSES_PER_LINK
and $SYSTEMD_NETWORK_STATIC_ADDRESSES_PER_NETWORK to make these resource
limits adjustable via the environment.
The policy requires a warning before a ban, so this throws in two
messages that we can copy + paste so we don't have to draft a bespoke
warning each time someone violates the rule
dispatch_verb!(), command_print_help_name!(), command_print_verb_help!()
and introspect_cli!() each declare the bounds the linker defines for
the SYSTEMD_VERBS and SYSTEMD_OPTIONS sections and hand them to an
unsafe backend as four raw pointers. foreach_option!() does the same
and needs OptionParser::__parser() only so that it can evaluate its
argument outside of the unsafe block.
Instead, wrap the bounds in a Section<T> that only __verbs_section!()
and __options_section!() create. Creating it is the unsafe part, so the
backends become safe functions and each table macro is a single call.
foreach_option!() calls a safe OptionParser::__next() now and
__parser() is gone.
The verbs get the userdata as it is though. So __dispatch_verb() stays
unsafe and dispatch_verb!() expands to the call directly. IOW, the
caller vouches for the userdata in an unsafe block of its own instead
of the macro hiding that in its unsafe block. clippy's
not_unsafe_ptr_arg_deref caught this once the bounds were gone.
The new code takes the address of the bounds with &raw const,
addr_of!() is soft-deprecated.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
The previous documentation did not correctly exclude 127 (DEL) and the C1 control character
range 128-159 from the control codepoints.
However, the function unichar_is_control() defined on src/basic/utf8.c includes the range
0x7F (i.e. 127) to 0x9F (i.e. 159) as control codepoints. This commit explicitly documents the
127-159 range as control.
Fixes#44202.
Today, whether dissect-image insists on a valid dm-verity signature
often
depends on the image instead of on the host. With the default policies,
an
image without a signature mounts fine. The same image fails to mount if
it
contains a malformed signature partition or a signature that userspace
validation can't parse. If a root hash is configured, it also fails if
it
has no signature partition at all.
The image is input we don't trust. Whoever builds it controls whether it
contains a signature and what the signature contains. Checks that depend
on
the image contents give no protection, because anyone who wants to get
past
them can remove the signature. They only break images that work fine as
plain
Verity images. Whether a signature is required has to come from the
host's
image policy. A policy that allows "signed" but not "verity", such as
`RootImagePolicy=root=signed`, requires a valid signature. With the
default
policies, the signature is optional.
Making that the rule exposed two ways to bypass a policy that requires a
signature: reusing a shared Verity device that was set up without a
validated
signature, and mounting a partition directly when no root hash is known.
The
PR fixes those first, so every commit keeps signed-only policies intact.
Behavior changes worth calling out:
- With the default policies, images whose signature is missing,
malformed or
can't be validated are set up with Verity alone.
- Under a policy that requires a signature, a shared Verity device is
only
reused if the kernel validated a signature for it. If we have a
signature
and the policy allows "signed", we set up our own device instead of
reusing
one without a validated signature. A device with a validated signature
is
reused even if the caller has no signature.
- A root or usr partition whose policy allows neither "unprotected" nor
"unused" is refused if we can't set up Verity for it, instead of being
mounted directly. As a result, a DDI with Verity for both the root and
the
usr partition is refused under a policy that requires Verity for usr,
such
as systemd-mountfsd's policy for untrusted images.
The measurement fix and the mountfsd device sharing fix from earlier
versions
of this PR were merged in #44386 and #44387. The veritysetup and
gpt-auto
changes, the mountfsd polkit changes and the portable changes will
follow as
separate PRs on top of this one.
Lennart asked in #44114 to split the preparatory stuff out so it can
be merged in smaller PRs. So this is a stack of five, each based on
the one before it and each showing only its own commits:
1. #44323, the C helpers (merged)
2. #44324, support for programs written in Rust (merged)
3. #44420, bindings.h lists only the headers Rust code uses (merged)
4. #44325, the systemd_shared wrappers systemd-keyring-setup needs
5. #44114, systemd-keyring-setup itself
This one is 4. Each wrapper commit adds the headers it needs to
bindings.h.
This adds the systemd_shared wrappers systemd-keyring-setup is written
against, so #44114 is left with just the program.
The wrappers follow the pattern of the crate: thin, fallible, with the
C names and anchored at file descriptors. They cover
- the file and directory helpers: the chase_and_*at() family,
read_full_file_full() into the buffer C allocated, which is erased on
drop when it was read with READ_FULL_FILE_SECURE, the linkable
temporary files of tmpfile-util.h, readdir_all(), the credentials
directory resolved below a root and loop_write() and fchmod()
- the keyring-util helpers
- the VOA lookup, with ConfFiles owning the array a lookup returns
- the X.509 helpers of crypto-util behind HAVE_OPENSSL, with X509
owning the certificate and giving access to the extension and key
usage flags, the subject key identifier, the serial number and the
DER and PEM encodings
- format-table
- module-util, with the libkmod context freed on drop
- .note.dlopen ELF notes. elf_note_dlopen!() writes the same note
SD_ELF_NOTE_DLOPEN() writes, so a Rust program declares its dlopen()
dependencies the way the C programs do and systemd-analyze
dlopen-metadata sees them.
Flag combinations the C helpers assert on fail with -EINVAL instead.
test-rust-shared covers all of it.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
The man pages don't say what happens when an image policy allows
"verity" as well as "signed" and the signature can't be validated.
systemd.exec(5) even claims that RootHashSignature= only opens the
volume if the kernel validates the signature, which isn't true with the
default policy.
Let's document that only a policy that allows "signed" without "verity"
requires a valid signature, in systemd.image-policy(7),
systemd.exec(5), systemd-dissect(1) and for
$SYSTEMD_DISSECT_VERITY_SIGNATURE, and add a NEWS entry for the changed
behavior.
Packaging tools read the dlopen() dependencies of a program from its
.note.dlopen ELF notes. elf_note_dlopen!() writes the same note
SD_ELF_NOTE_DLOPEN() writes into a static in that section. LLVM makes
it an SHT_NOTE section because of its name. The static is referenced
from the function that uses it so --gc-sections keeps the note for as
long as the function, just like the anchored C variant does.
libcrypto_note!() and libkmod_note!() are LIBCRYPTO_NOTE() and
LIBKMOD_NOTE().
SD_ELF_NOTE_DLOPEN_TYPE is spelled with UINT32_C() which bindgen can't
evaluate so it gets a const helper.
HACKING.md asks programs to declare their dlopen() dependencies with
these macros.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Unprivileged mountfsd clients can select the root hash of a shared Verity
device and read its contents without opening the original image.
Let's require the same backing file when non-root clients reuse Verity
devices through mountfsd. Privileged clients and other callers keep
sharing by root hash without needing loop device metadata.
So far bindings.h listed every header of src/basic/, src/shared/ and
src/systemd/ and the crate bound everything libsystemd-shared exports.
That buys nothing. Nothing of it ends up in a binary and no Rust code
reaches it. It does cost though: the generated crate is 2.7 MB and the
rlib 15 MB, every regeneration takes 5 s, rust-analyzer chews on
all of it, and whenever bindgen chokes on a header nobody uses the
Rust build breaks. Two of the four bindgen workarounds in meson.build
exist only because of that, the packed journal objects and the PE
header.
Instead, list the headers the Rust code uses, 18 right now, and let
every wrapper that needs another one add it. That's how the kernel does
it with bindings_helper.h and QEMU with wrapper.h. The libc and kernel
constants keep arriving by family through the allowlist, bindings.h
lists their headers so that doesn't hang on what the tree's headers
happen to include. The generated crate shrinks to 260 KB and
regenerating it takes a second. The binaries don't change.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
C code keeps secrets in buffers that it frees with erase_and_free().
That's a convention every caller has to remember. With the allocator
being part of the type Rust code can make it a guarantee instead.
Add Erasing which allocates like Malloc but frees memory with
erase_and_free(), which erases it first. It never uses realloc() as
that may leave the old contents behind in the memory it frees. Growing
allocates anew, copies and erases the old memory instead. So a buffer
holding a key or a passphrase is simply a Vec<u8, Erasing>.
Note that elements a Vec drops early via pop() or truncate() stay in
its memory until it is freed.
HACKING.md and CODING_STYLE.md point secrets at it.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
HACKING.md describes how a program is declared and written with the
command line framework of the C programs, what the toolchain
requirements and the per-distribution packages are, what a Rust program
costs and what it may not do (std, external crates), how the build
treats Rust code (lint set, overflow checks, panic=abort, remapped
paths, no CET markers) and where the tests, clippy, rustdoc and the
rust-analyzer project file are.
CODING_STYLE.md gets the rules for Rust code:
- rustfmt and the enforced lint set
- define_main!()
- the systemd_shared crate as the only way to talk to the rest of
systemd
- names mirroring the C names
- SAFETY comments and Safety sections
- BorrowedFd for file descriptors
- no_std
- the error handling conventions
- #[expect] over #[allow]
- allocation failure never panicking or aborting, like in the kernel
- no pointless copies of strings
ARCHITECTURE.md explains where the Rust code lives. README lists rustc
and bindgen among the optional build dependencies, and NEWS tells
packagers that builds with --auto-features=enabled now need them or
-Drust=disabled.
CODING_STYLE.md also spells out the rules the systemd maintainers
agreed on for Rust: it is only used for new leaf components and
existing C code isn't ported, it's built with meson and without cargo,
only the core, alloc and libc crates are permitted, and Rust code only
talks to the rest of systemd through C linkage.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Credentials can reach a system through the credential stores. E.g.
`systemd-imds --import` writes cloud provided credentials to
`/run/credstore/` in the initrd. `ImportCredential=` already finds them
there, but `ConditionCredential=` and `AssertCredential=` only checked
the credentials passed into the service manager itself. Hence, a
credential in the store could configure a service, but not decide
whether a unit runs.
Make the credential condition search the same credential store
directories as `ImportCredential=`. Only the existence of a credential
is checked, encrypted ones are not decrypted.
This is a behaviour change, see NEWS. As before, non-root callers which
can't access a credential store treat these as absent, otherwise this
would break running e.g. `systemd-analyze` or `sysetmd-sysupdate` as
user.
Signed-off-by: Martin Pitt <martin@amutable.com>
Allow marking Varlink interfaces, methods, errors, types, fields and
enum values as experimental, for documentation and discoverability.
Calls are not refused.
- One SD_VARLINK_EXPERIMENTAL flag works for interface, symbol and
field flags; _FULL variants of the definition macros take flags.
Interface and symbol flags are now validated, and the internal
_SD_VARLINK_*_FLAGS_MAX are replaced by _SD_VARLINK_*_FLAGS_ALL.
- The IDL output shows experimental items with a "# [Experimental]"
comment line, and sd_varlink_idl_parse() turns it back into the flag,
so the information survives GetInterfaceDescription.
- varlink_idl_consistent() refuses non-experimental fields referring to
experimental types, non-nullable experimental fields in
non-experimental symbols, and experimental values in
non-experimental enums. These rules are deliberately strict, so that
they can be relaxed later without breaking anything.
- varlinkctl highlights experimental methods in list-methods and shows
a notice when calling one (only when on a terminal).
- docs/PORTABILITY_AND_STABILITY.md now excludes experimental parts of
Varlink interfaces from the stability promise.
- Fixes resolution of named types within anonymous types in the parser,
which the new checks ran into.
- io.systemd.PCRLock is marked experimental as a first user; other
experimental components will follow once the design is settled.
Open questions:
- Should experimental values in non-experimental enums be permitted in
some cases, e.g. for enums only used as input?
- Should experimental errors be allowed in non-experimental interfaces?
The IDL does not link errors to methods, so this is documentation
only.
So far the constraints a file system declares about itself via the
user.validatefs.* xattrs were only checked by systemd-validatefs@.service
for file systems mounted via .mount units. Let's validate them also when
mounting partitions of DDIs, i.e. for systemd-dissect, systemd-nspawn,
portabled, sysext, RootImage= and mountfsd.
Any constraint that is set must be fulfilled in full: if a file system
declares GPT label or type constraints but isn't placed on a GPT
partition (for example because it is a bare file system image, or on an
MBR partition table) mounting is refused. If the partition is Verity
protected, the constraints must be fulfilled by the Verity hash
partition too, which matches the behaviour of systemd-validatefs.
Validation is done when the superblock is created, i.e. on the mountfsd
side if mountfsd is used. It can be turned off via
$SYSTEMD_DISSECT_VALIDATEFS=0. If validation fails, mounting returns
-EHWPOISON.
nss-systemd always passes USERDB_EXCLUDE_NSS to userdb. That flag skips
the io.systemd.NameServiceSwitch and io.systemd.Multiplexer services. A
sandbox can bind mount the host's userdb sockets into
/run/systemd/userdb/,
but nss-systemd in that sandbox still cannot resolve the host's regular
users and groups.
Add $SYSTEMD_NSS_SERVER_SIDE_NSS. If it is set, nss-systemd queries the
server-side NSS services too. Client-side NSS stays off, because
nss-systemd itself runs as a glibc NSS module.
nss-systemd always passes USERDB_EXCLUDE_NSS to userdb. That flag skips
the io.systemd.NameServiceSwitch and io.systemd.Multiplexer services. A
sandbox can bind mount the host's userdb sockets into /run/systemd/userdb/,
but nss-systemd in that sandbox still cannot resolve the host's regular
users and groups.
Add $SYSTEMD_NSS_SERVER_SIDE_NSS. If it is set, nss-systemd queries the
server-side NSS services too. Client-side NSS stays off, because
nss-systemd itself runs as a glibc NSS module.
The Trusted Computing Group has assigned the systemd project an NV index
range, let's document properly which one that is, and how we use it.
Thank you so much, TCG!
musl libc got a overhaul in it's locale support funded by NLnet,
as part the default search paths were set to search in
/etc/musl/locale, /usr/share/musl/locale, /share/musl/locale/.
Also change the behaviour on musl to search for multiple directories,
and include the old path as a fallback.
See https://www.openwall.com/lists/musl/2026/10/02/1
If the booted UKI carries no PCR public key or no signed policy with
the "initrd" reference, initialize NvPCRs in the initrd with a
PolicyPCR init policy bound to the current value of PCR 11 instead of
not initializing them at all.
The resulting NvPCR names depend on PCR 11 and hence change with every
UKI update, so this is only useful where PCR 11 can be predicted.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Set DISSECT_IMAGE_TPM2_UNLOCK where unlocking an image's LUKS partitions
via the local TPM2 is wanted and the caller is trusted:
* systemd-dissect, an admin tool operating on an image the invoking user
can read anyway;
* systemd-nspawn --image=, which already prompts for a passphrase
interactivly, so TPM2 first and passphrase second is the natural
order;
* systemd-sysext/systemd-confext, which decrypt without any passphrase,
so extension images sealed to the host can be activated without user
interaction.
For sysext/confext setting the flag at merge time is not enough: before
merging, the extension-release metadata is read via image_read_metadata()
in discover-image.c, which uses its own flag set and would already fail
with -ENOKEY on an encrypted image. Give it an extra_flags parameter and
pass the flag from there, too; the other callers (machined, portabled)
pass 0 and keep their behaviour.
Two units need adjusting for this to work when started by the service
manager: systemd-nspawn@.service has DevicePolicy=closed, so allow
/dev/tpmrm0 there, and the sysext/confext services are not ordered
against the TPM2 device showing up, so order them after tpm2.target
like the pcr* services. The initrd variants cannot be ordered the same
way: they run before local-fs-pre.target, while the software TPM fallback
only becomes available after the early ESP mount, which is ordered after
local-fs-pre.target, so a static After=tpm2.target would be an ordering
cycle with systemd.tpm2_software_fallback=1. Hence systemd-tpm2-generator
adds the ordering as a drop-in instead, unless the fallback is in use.
That drop-in also orders them after systemd-pcrphase-initrd.service:
nothing else guarantees the "enter-initrd" phase has been measured into
PCR 11 before the initrd extension units merge their images (the barrier's
Before=systemd-sysext.service is a leftover from when that unit was also
used in the initrd), so tokens bound to a signed PCR 11 policy or a
pcrlock policy would race the measurement.
Deliberately not enabled for now: RootImage=/ExtensionImages=/MountImages=
in units, portabled and gpt-auto. For those an unconditional default is
probably not wanted but a per-unit or per-image switch, which is left for
a follow-up. systemd-mountfsd, which unprivileged systemd-dissect and
systemd-nspawn delegate to, keeps its own flag set and does not honour
the flag, hence TPM2 unlocking is only available when running privileged;
the man pages say so.
Document the behaviour and $SYSTEMD_DISSECT_TPM2_UNLOCK= in the dissect,
nspawn and sysext man pages and in docs/ENVIRONMENT.md.
Replace the inotify + /run/udev/watch/ symlink machinery with fanotify inode
marks (FAN_CLOSE_WRITE). The point is not merely a different backend; fanotify
removes several structural costs that inotify imposed on udevd:
- An inotify event only identifies the watch by a watch descriptor, which udevd
had to translate back to a device. That mapping was maintained as a table of
symlinks under /run/udev/watch/. A fanotify event instead carries the watched
inode's file handle (FAN_REPORT_FID), which is resolved to the device directly
via open_by_handle_at()+fstat(). The wd->device translation, and with it
/run/udev/watch/ entirely, is no longer needed.
- Without that shared symlink store to coordinate, a worker can add and remove
its own device marks directly on the fanotify group fd. Previously the watch
state was owned by the manager, and workers had to request (un)registration
over the notification socket (sd_notify()+SIGUSR1). Dropping that round-trip
both simplifies the code and is expected to be faster.
- A fanotify mark lives on the inode and is dropped by the kernel once the inode
is gone, so the special handling for removed device nodes is no longer needed.
The group fd is handed over across restarts via the service manager's fd store
and the kernel keeps the marks, so nothing is rebuilt in-process; the fd is also
propagated to each worker. Watches left behind by an older, inotify-based udevd
under /run/udev/watch/ are migrated to fanotify marks on first start, then the
directory is removed.
Device watch falls back to inotify on kernels < v5.13, when CONFIG_SHMEM=n
(devtmpfs backed by ramfs), or when CONFIG_FANOTIFY is disabled. This fallback
can also be forced via $SYSTEMD_UDEV_USE_INOTIFY.
The enablement has two levels:
- at compilation time, setting -Dexperimental=disabled disables the
features completely. The idea is that we can compile code out if
that is set. -Dexperimental=enabled disables the check completely.
-Dexperimental=envvar allows use if the variable is set.
-Dexperimental=auto is translated to envvar in developer mode, and
envvar otherwise.
- at runtime, if not disabled, the envvar is checked first
(always has priority over the compiled-in default), and then the
compile-time default is used.
If denied, the user gets an error message.
This is just meson and build.h plumbing + docs.
Code in the tree handles an error in an if (r < 0) block that returns, and
continues with the success path after it. CODING_STYLE.md does not mention
the pattern. Document it with an example.
Inspired by the text from https://github.com/jj-vcs/jj/blob/main/AI_POLICY.md.
- We add a section on not using LLM's to communicate.
- We relax the language on using AI to design/write code a bit. The main emphasis
is put on understanding what you're submitting. It's fine if AI designed it
and wrote it, but you as the author are supposed to fully understand and be able
to answer questions about it.
Let's maintain the list of subsystem maintainers
in CODEOWNERS, which github knows about and can do
useful stuff with like automatically adding those
usernames as reviewers.