copyfail-defense

Latest: v2.0.1 EL8 / EL9 / EL10 x86_64 CVE-2026-31431 cf2 / Dirty Frag GPL v2

Defense-in-depth toolkit for the Copy Fail bug class: three live LPE chains that share the same splice() → MSG_SPLICE_PAGES page-cache write primitive.

ClassCVEKernel sinkPrivilege
cf1 CVE-2026-31431 algif_aead AEAD scratch-write none
cf2 (no CVE yet) esp_input skip_cow path CAP_NET_ADMIN via unshare(NEWUSER|NEWNET)
Dirty Frag-ESP (embargo broken) same as cf2 same as cf2
Dirty Frag-RxRPC (no CVE, no upstream patch) rxkad_verify_packet_1 in-place pcbc(fcrypt) none

v2.0.1 stacks four defensive primitives into a single dnf install: an LD_PRELOAD shim (cf1 primary), a modprobe blacklist for the cf-class entry-point modules (cf1 + cf2 + Dirty Frag), kernel-enforced systemd drop-ins (RestrictAddressFamilies=~AF_ALG ~AF_RXRPC + RestrictNamespaces=~user ~net), and a read-only host posture auditor that reports per-class coverage in JSON for SIEM ingest.

v2.0.1 adds workload auto-detection. At install time, %posttrans inspects the host for IPsec, AFS, and rootless containers. Conflicting drop-ins are suppressed automatically; every other layer stays active. copyfail-redetect re-runs after fleet changes; /etc/copyfail/force-full bypasses detection. See §Auto-detection below for the full signal list, suppression matrix, and how to inspect the decision.

🔍 Full writeup: Copy Fail (CVE-2026-31431) on rfxn.com/research covers the cf1 kernel mechanics; cf2 and Dirty Frag extend the same splice() → in-place-crypto primitive to two more sinks.

Upgrading from afalg-defense v1.0.x or copyfail-defense v2.0.0? dnf upgrade copyfail-defense performs the rename swap + file-layout split automatically. v2.0.0 monolithic conf files are preserved as .rpmsave-v2.0.1 next to the new split files so any operator hand-edits survive. Auto-detection runs once at upgrade; review journalctl -t copyfail-defense after the transaction to see what was suppressed. Shim activation remains the explicit operator step (copyfail-shim-enable).

Install

A single copyfail.repo file works for EL8/EL9/EL10: dnf expands $releasever + $basearch per host. RPMs are GPG-signed and the metadata itself is verified via detached repomd.xml.asc (gpgcheck=1 + repo_gpgcheck=1).

sudo curl -sSL https://rfxn.github.io/copyfail/copyfail.repo \
  -o /etc/yum.repos.d/copyfail.repo
sudo dnf install -y copyfail-defense
sudo /usr/sbin/copyfail-shim-enable

The meta pulls four subpackages:

SubpackageCoverage
copyfail-defense-shim LD_PRELOAD AF_ALG block (cf1 primary)
copyfail-defense-modprobe kernel-module entry-point cuts (cf1 + cf2 + Dirty Frag)
copyfail-defense-systemd per-unit RestrictAddressFamilies=~AF_ALG ~AF_RXRPC + RestrictNamespaces=~user ~net on user@/sshd/cron/crond/atd + opt-in container-runtime examples
copyfail-defense-auditor read-only host posture auditor with per-class coverage report

Coverage matrix

Which rung blocks which bug class. = primary mitigation; · = not applicable; superscripts mark caveated coverage (notes below).

Mitigation rungcf1cf2DF-ESPDF-RxRPC
LD_PRELOAD shim (AF_ALG hook) · ·¹
modprobe algif_aead family ²· ··
modprobe esp4 esp6 xfrm_user xfrm_algo · ·
modprobe rxrpc ·· ·
systemd RestrictAddressFamilies=~AF_ALG · ··
systemd RestrictAddressFamilies=~AF_RXRPC ·· ·
systemd RestrictNamespaces=~user ~net · ·

1 Catches the cksum step in the public DF-RxRPC PoC, not the kernel sink itself; defense-in-depth, not a primary stop.
2 No-op on RHEL stock kernels (CRYPTO_USER_API* is built-in, so the blacklist line cannot prevent load). Listed for completeness on custom or non-RHEL kernels where algif_aead ships modular.

Reference: kernel patches and audit signatures

ClassUpstream patchAudit signature
cf1a664bf3dsocket(a0=38)
cf2f4c50a4034unshare(NEWUSER)
DF-ESPf4c50a4034unshare(NEWUSER)
DF-RxRPCnone upstreamadd_key("rxrpc",...)

The auditor probes page-cache integrity for /usr/bin/su and the PAM stacks every class targets, as a cached-IOC for an in-flight exploit. See --json posture.bug_classes[*].kernel_sink.

Operator-applied (auditor-recommended)

The auditor flags these via --emit-remediation; no subpackage applies them, since each can break legitimate workloads on a busy fleet. Review before pasting.

ActionTargetsWhen recommended
chmod 4750 /usr/bin/su && chgrp wheel /usr/bin/su cf2, DF-ESP Suppressed when non-wheel/admin interactive users exist (cPanel-style tenant fleets); chmod 4750 would break their su workflow.
auditd rule cf_userns (unshare(CLONE_NEWUSER)) cf2, DF-ESP Hosts where auditd is tuned for userns events (otherwise high alert noise).
auditd rule cf_addkey (add_key("rxrpc",...)) DF-RxRPC Always; rxrpc keyring activity is rare enough that the false-positive rate stays low.
auditd rule afalg_attempt (socket(a0=38)) cf1 Hosts already running auditd; pairs with the LD_PRELOAD shim as a tripwire.

Auto-detection of conflicting workloads

v2.0.1+ inspects the host in %posttrans for workloads the default cuts would break, and suppresses the conflicting drop-in only while every other layer stays active. The intent is "do no harm to running production"; nothing else relaxes. The cf1 LD_PRELOAD shim and the auditor are never suppressed, so CVE-2026-31431 coverage is unchanged on every host.

Why each workload triggers a carve-out

WorkloadWhy the default would break it
IPsec (strongSwan, libreswan, openswan) The kernel xfrm/esp path is the cf2 / Dirty Frag-ESP kernel sink. Blacklisting esp4/esp6/xfrm_user/xfrm_algo disables IPsec tunnels entirely (no SA install, no encrypted traffic). Suppression keeps the modprobe drop off and leans on systemd RestrictNamespaces=~user ~net to block the unshare(NEWUSER|NEWNET) step that gates cf2.
AFS (openafs, kafs) rxrpc is both the DF-RxRPC kernel sink and the transport AFS itself rides on; blacklisting it breaks openafs-client. The per-unit RestrictAddressFamilies=~AF_RXRPC would also break AFS userspace tooling (aklog, kinit-style PAGs) when invoked from any of the five tenant units.
Rootless containers (rootless podman/buildah) Rootless podman/buildah needs CLONE_NEWUSER under the calling user@.service. Our default RestrictNamespaces=~user ~net on user@.service turns the unshare(2) into EPERM, which kills every rootless container. Suppression strips the userns drop-in on user@.service only: sshd/cron/crond/atd retain it.

Detection signals

WorkloadDetection signals (any of)Suppresses
IPsec systemctl is-enabled returns enabled for strongswan/strongswan-starter/strongswan-swanctl/ipsec/libreswan/openswan/pluto; OR /etc/ipsec.conf has a conn stanza; OR non-empty *.conf in /etc/swanctl/conf.d/, /etc/ipsec.d/, /etc/strongswan/conf.d/, or /etc/strongswan.d/. 99-cf2-xfrm.conf (esp4, esp6, xfrm_user, xfrm_algo blacklist)
AFS systemctl is-enabled for openafs-client/openafs-server/kafs/afsd; OR /etc/openafs/CellServDB or /etc/openafs/ThisCell exists; OR /etc/krb5.conf.d/openafs* present; OR /proc/fs/afs/ registered. 99-rxrpc.conf (rxrpc modprobe blacklist) AND 12-rxrpc-af.conf (RestrictAddressFamilies=~AF_RXRPC on all 5 tenant units)
Rootless containers /home/*/.local/share/containers/storage/overlay-containers/ present with mtime within 180d; OR /var/lib/containers/storage/ non-empty with mtime <90d; OR /run/user/<UID>/containers/ present for any UID ≥ 1000 (live rootless tmpfs); OR podman.socket enabled (system-wide or any per-user instance). 15-userns.conf on user@.service.d only

False-positive guards baked into the detector: /etc/subuid populated by useradd is not a rootless signal — shadow-utils auto-populates subuid for every regular user regardless of container intent, which produced near-100% FPs on cPanel-shaped fleets in v2.0.1 rev 1. The rootful /var/lib/containers/storage signal is gated on mtime < 90d so a long-purged podman install doesn't keep triggering suppression. The /home walk is bounded (maxdepth 6, mtime -180) so a pathological tenant home tree can't stall %posttrans. The /run/user/<UID>/containers signal requires UID ≥ 1000 so system-account artifacts under /run/user/0 don't trip detection.

What stays protected after a suppression

TriggerLayer droppedLayers still active
IPsec 99-cf2-xfrm.conf cf1 LD_PRELOAD shim · cf1 modprobe (algif_aead family) · RestrictNamespaces=~user ~net on all 5 tenant units (still blocks cf2/DF-ESP via the unshare gate) · RestrictAddressFamilies=~AF_ALG ~AF_RXRPC · 99-rxrpc.conf blacklist
AFS 99-rxrpc.conf + 12-rxrpc-af.conf (5 units) cf1 LD_PRELOAD shim · cf1 modprobe · 99-cf2-xfrm.conf blacklist · RestrictNamespaces=~user ~net on all 5 units (cf2/DF-ESP) · RestrictAddressFamilies=~AF_ALG
Rootless containers 15-userns.conf on user@.service only sshd/cron/crond/atd keep RestrictNamespaces=~user ~net (system-tier cf2/DF-ESP defense intact) · cf1 shim · all modprobe blacklists · AF_ALG/AF_RXRPC restricts everywhere

Inspect the decision

%posttrans writes a versioned JSON report to /var/lib/copyfail-defense/auto-detect.json after every install/upgrade and after every copyfail-redetect run:

{
  "schema_version": "2",
  "tool_version": "2.0.1",
  "force_full": false,
  "detected": {
    "ipsec":               { "present": true,  "signals": ["systemctl: strongswan.service enabled"] },
    "afs":                 { "present": false, "signals": [] },
    "rootless_containers": { "present": true,  "signals": ["systemctl --user (alice): podman.socket enabled"] }
  },
  "suppressed": {
    "modprobe_cf2_xfrm":      true,
    "modprobe_rxrpc":         false,
    "systemd_rxrpc_af":       false,
    "systemd_userns_user_at": true
  },
  "applied": { "modprobe_cf1": true, "systemd_userns_sshd": true, "...": "..." }
}

Other inspection paths:

# Per-action log lines from %posttrans / copyfail-redetect
sudo journalctl -t copyfail-defense-detect --since today

# Auditor surface (SIEM-friendly)
sudo copyfail-local-check --json | jq '.posture.auto_detect'
# {"available": true, "suppressed_modprobe": ["cf2_xfrm"], "suppressed_systemd": ["userns_user_at"]}

Re-detect or force full coverage

# Re-run detection after enabling IPsec/AFS/rootless post-install
sudo /usr/sbin/copyfail-redetect
sudo systemctl daemon-reload
sudo systemctl try-reload-or-restart sshd.service

# Skip detection entirely - apply every drop-in unconditionally
sudo mkdir -p /etc/copyfail
sudo touch /etc/copyfail/force-full
sudo /usr/sbin/copyfail-redetect

The auditor reports force-full sentinel active when the sentinel is on. Remove the file and re-run copyfail-redetect to re-engage detection.

Verify

# cf1: AF_ALG socket creation should fail
python3 -c 'import socket; socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)'
# expect: PermissionError [Errno 1] Operation not permitted

# Holistic per-class coverage report
sudo copyfail-local-check
# tail-of-output:
#   Surface area / mitigation matrix:
#     Class                Sink reachable?  Mitigated?  Active layers
#     cf1 (CVE-2026-31431) YES              yes         ld_preload_shim, systemd_af_alg, modprobe_blacklist
#     cf2 (xfrm-ESP)       YES              yes         modprobe_blacklist, systemd_restrict_ns
#     Dirty Frag-ESP       YES              yes         modprobe_blacklist, systemd_restrict_ns
#     Dirty Frag-RxRPC     YES              yes         modprobe_blacklist, systemd_af_rxrpc

Override paths

The default install applies RestrictNamespaces=~user ~net to user@.service. This breaks rootless podman/buildah under user@.service. Override:

sudo install -d /etc/systemd/system/user@.service.d
sudo tee /etc/systemd/system/user@.service.d/20-override.conf >/dev/null <<'EOF'
[Service]
RestrictNamespaces=
RestrictAddressFamilies=
EOF
sudo systemctl daemon-reload

The 10- prefix on package drop-ins and 99- prefix on the modprobe drop are deliberate: any 20-*.conf or numerically-later operator file overrides ours.

If your fleet legitimately uses IPsec (strongSwan, libreswan, FRRouting), the modprobe blacklist breaks those workloads. Either skip the modprobe subpackage:

sudo dnf install copyfail-defense-shim copyfail-defense-systemd copyfail-defense-auditor
# (omit copyfail-defense and copyfail-defense-modprobe)

…or remove the relevant entries from /etc/modprobe.d/99-copyfail-defense.conf (the file is %config(noreplace), so your edit survives package upgrade). Same logic for AFS (openafs, kafs) and the rxrpc blacklist line.

Auditor JSON (v2.0 schema)

{
  "schema_version": "2.0",
  "covers": ["CVE-2026-31431", "cf2-xfrm-esp", "dirtyfrag-esp", "dirtyfrag-rxrpc"],
  "posture": {
    "verdict": "vulnerable_kernel_userspace_mitigated",
    "bug_classes_covered": ["cf1", "cf2", "dirtyfrag-esp"],
    "bug_classes": {
      "cf1":             { "applicable": true, "mitigated": true,  "kernel_sink": "...", "layers": {...} },
      "cf2":             { "applicable": true, "mitigated": true,  "kernel_sink": "...", "layers": {...} },
      "dirtyfrag-esp":   { "applicable": true, "mitigated": true,  "kernel_sink": "...", "layers": {...} },
      "dirtyfrag-rxrpc": { "applicable": true, "mitigated": false, "kernel_sink": "...", "layers": {...} }
    },
    "layers": { ... }
  }
}

bug_classes_covered is the SIEM-ergonomic single filter ("is this host hardened?"). The bug_classes map exposes per-layer breakdown for finer dashboards. Exit codes 0 clean, 2 VULN, 3 VULN-but-mitigated, 4 hardening recommendations only (unchanged from v1.0.x).

Direct downloads

Each release on github.com/rfxn/copyfail/releases ships per-EL binary RPMs, an SRPM, the .repo file, and the public signing key as release assets. The same files are published in the dnf tree below. v2.0.0 and v1.0.1 RPMs are retained alongside v2.0.1 to support clean dnf upgrade across the rename chain (afalg-defensecopyfail-defense).

The .repo file (works on EL8/EL9/EL10):

sudo curl -sSL https://rfxn.github.io/copyfail/copyfail.repo \
  -o /etc/yum.repos.d/copyfail.repo

And the public signing key (dnf imports this automatically on first install, but you can pre-import it):

sudo curl -sSL https://rfxn.github.io/copyfail/RPM-GPG-KEY-copyfail \
  | sudo rpm --import /dev/stdin

Per-EL binary RPMs are independently compiled against each distribution's glibc (EL8: 2.28 with split libdl; EL9/EL10: 2.34+ with merged libdl); do not cross-install across ELs.

EL8 (RHEL/Alma/Rocky/CentOS Stream 8)

Filesha256
copyfail-defense-2.0.1-1.el8.x86_64.rpm 164ae08518fb2e0e23b9d9a58aa587a0dc33141caceb7bda11d62c751666f921
copyfail-defense-shim-2.0.1-1.el8.x86_64.rpm fe87110e81ecf21af7361074e00f4350b6fed88cefa51cbf1eee920a4d62f9fa
copyfail-defense-modprobe-2.0.1-1.el8.noarch.rpm 4f21f228283e7a2484568aa58a7b68a623c9323c4a1a88cbdbe0d7e94b9d335a
copyfail-defense-systemd-2.0.1-1.el8.noarch.rpm d9ccb7cd46f63e876dda97e8f896da56eb7e328536f0eff6c73c248aafdb6846
copyfail-defense-auditor-2.0.1-1.el8.noarch.rpm 67a9a664a4be0bafb020fd1fd89c1ebaaba776e09fa966903167843f75e92426
copyfail-defense-2.0.1-1.el8.src.rpm 0a0c17f31a9689996b00d21235fef98b186dc774b3ea3d9c0168573401db70a7
v2.0.0 (retained for upgrade-cycle)
copyfail-defense-2.0.0-1.el8.x86_64.rpm 48cd6c0c0ad5d81d7c995c8116540dc27f37347733841b78812905315978c0d1
copyfail-defense-shim-2.0.0-1.el8.x86_64.rpm 808b8bde6e7ff0401b7aebae45a85bad13cc2ead751acf4e713e829c492fa391
copyfail-defense-modprobe-2.0.0-1.el8.noarch.rpm 1be80b66a5869fa9bfa9a4890784691c938fe9d3987293ea39f4b815a4f209c0
copyfail-defense-systemd-2.0.0-1.el8.noarch.rpm a982573aee5d2945db4b4939a8106e80393b0b2256c6e9fbaffd34329571f1ef
copyfail-defense-auditor-2.0.0-1.el8.noarch.rpm 7aeda780a8474eeb343170a1dcc8512c9fc617066882e5e1880ecfb377937e68
copyfail-defense-2.0.0-1.el8.src.rpm e558257a57fe027c459128e6a6cd0a66b5aeb30fd85ad04a76b3a4e98d147458

EL9 (RHEL/Alma/Rocky/CentOS Stream 9)

Filesha256
copyfail-defense-2.0.1-1.el9.x86_64.rpm 4ebd832796fc691e9a95922347365a33276129b7c92fa3b9d1e6d98fdcfa358d
copyfail-defense-shim-2.0.1-1.el9.x86_64.rpm 23b7e64d4cf133c6d9d5876fffdc960a00ddf64c35796341408e8adac5082d16
copyfail-defense-modprobe-2.0.1-1.el9.noarch.rpm 767862499adde09e5a9e4c6e81fb72dec1dd614e6574549fc94e55381eb9f4c3
copyfail-defense-systemd-2.0.1-1.el9.noarch.rpm e3dda02c33978d5834ffd3e3c656460f88a009562f853998b6f0a0697c88558d
copyfail-defense-auditor-2.0.1-1.el9.noarch.rpm 77d96907c75f19f41a3c4fcac7d126e3f60b775d4efb44292732c9e71fcb0bb6
copyfail-defense-2.0.1-1.el9.src.rpm 6b17ae2d00283ef87d343a75ac93b91f36d3db015f904b2ff01aaabc5489dbe9
v2.0.0 (retained for upgrade-cycle)
copyfail-defense-2.0.0-1.el9.x86_64.rpm b096765a794f4a493a5c95791010d91fcd2dd6b2eec089d5299149c29e41c3e9
copyfail-defense-shim-2.0.0-1.el9.x86_64.rpm eeee02be1013315155dbc83c154473da3856670b08e78ca8824610cedb48d01d
copyfail-defense-modprobe-2.0.0-1.el9.noarch.rpm 396af862d697221dd264ad0d8f3ac452698b50a92f53f188cf2816b5009081f7
copyfail-defense-systemd-2.0.0-1.el9.noarch.rpm 85dc7ed2b43002e065b3ea473d2f4bd60b8d152d347bb403e41e1f49a25f924a
copyfail-defense-auditor-2.0.0-1.el9.noarch.rpm a202f54eacbe65aaabc1c3b5e9f086286aa23ed7ca8204fee9eff35517acd9c7
copyfail-defense-2.0.0-1.el9.src.rpm 66a0aec618cac8f84799b10e676e2590700cac8a6bc37cbf500091fb50861372

EL10 (RHEL 10 / CentOS Stream 10 / AlmaLinux Kitten)

Filesha256
copyfail-defense-2.0.1-1.el10.x86_64.rpm c5e5896f37f5211739e44fe399e8fb09e28b7fc5e970b0289222599692f9a127
copyfail-defense-shim-2.0.1-1.el10.x86_64.rpm 9ac88af79b4a062a9143d9d267678c9b8f03052364127aff062d6670fccaea22
copyfail-defense-modprobe-2.0.1-1.el10.noarch.rpm e13b2eb8554e7b7b7b6c177c673cb43b02f525c96a562a01a53f3a481a0128e9
copyfail-defense-systemd-2.0.1-1.el10.noarch.rpm 0aa4ec14a4ecb3f8bdc421f839c70877be8dc8747be150974f27594f08b145bc
copyfail-defense-auditor-2.0.1-1.el10.noarch.rpm b8a63b55f0bfe582f4ef3722deeb525320d1046b81fe6fb3656f95412d272107
copyfail-defense-2.0.1-1.el10.src.rpm a63c1d43e190271b63c29a12322a14658539ddca5056d3cf2acb7f71c23d8e4e
v2.0.0 (retained for upgrade-cycle)
copyfail-defense-2.0.0-1.el10.x86_64.rpm 9bd847d2183545548f745765ac59291b1b724a178ebeb0090da3a9a133a4d4c7
copyfail-defense-shim-2.0.0-1.el10.x86_64.rpm 1e6b492dfd27e9bd795315730afee6d945e1cd9c9bea76dc8815c75847fdfbd6
copyfail-defense-modprobe-2.0.0-1.el10.noarch.rpm fc6d69aaeae344fe67a2527eec42dec4711ef8b7841e520c9227445de97e3494
copyfail-defense-systemd-2.0.0-1.el10.noarch.rpm 97560d682f7089aaf6fe95ec5671a104b31f3925417683229e95a4424950cbdc
copyfail-defense-auditor-2.0.0-1.el10.noarch.rpm 4a7ab18217f6b11f1f968ea0b8b1755542ef9f38e1f9008561d1af63d87df6e8
copyfail-defense-2.0.0-1.el10.src.rpm 18e44228bdc0d3db48572714276c682d5df54095e56422c903641a6f8ca68b12

v1.0.1 RPMs (legacy afalg-defense family) are retained in the repo trees through the 2.0.x release line for clean dnf upgrade compatibility. Removed in v2.0.1.

Signing

v1.0.1 and later are signed by the Copyfail Project Signing Key:

fingerprint: 6001 1CDC EA2F F52D 975A  FDEE 6D30 F32C D5E8 0F80
uid:         Copyfail Project Signing Key <proj@rfxn.com>
key file:    /RPM-GPG-KEY-copyfail

The copyfail.repo sets both gpgcheck=1 (verifies each RPM) and repo_gpgcheck=1 (verifies repomd.xml via the detached repomd.xml.asc we publish alongside it). dnf imports the public key from gpgkey= on first use.

Verify out-of-band:

curl -sSL https://rfxn.github.io/copyfail/RPM-GPG-KEY-copyfail | gpg --import
gpg --fingerprint proj@rfxn.com
# expect: 6001 1CDC EA2F F52D 975A  FDEE 6D30 F32C D5E8 0F80
rpm --import https://rfxn.github.io/copyfail/RPM-GPG-KEY-copyfail
rpm -K /path/to/copyfail-defense-2.0.1-1.el9.x86_64.rpm
# expect: digests signatures OK

Defense in depth

Each rung defeats the bug by a different mechanism, so an attack that defeats one does not necessarily defeat the next.

RungWhere it failsWhat the next rung covers
Kernel patch (vendor) EL7 EOL; rollout lags disclosure days-to-weeks; reboot may not be available; Dirty Frag-RxRPC has no upstream patch Userspace cuts close the window without a reboot
modprobe blacklist No-op when relevant module is builtin (RHEL algif_aead is); no effect on already-resident modules Functional for esp4/esp6/xfrm_user/xfrm_algo/rxrpc on stock RHEL kernels
systemd RestrictAddressFamilies / RestrictNamespaces Reaches only services systemd starts post-restriction. Misses cron-as-root, sshd-pre-restriction, container payloads with own pid 1 LD_PRELOAD shim covers every dyn-linked process regardless of init
LD_PRELOAD shim Static binaries; direct syscall instruction; SUID strip seccomp at unit/runtime level catches direct-syscall path
seccomp filter Per-service. Operationally heavy: each unit/runtime needs explicit policy This package's systemd subpackage ships a one-line filter for the highest-leverage tenant units

Reporting issues

Bugs, packaging problems, false positives, missing distros: please file a GitHub issue. Include the auditor's --json output if the report is about detection, and the output of uname -a + cat /etc/os-release for any runtime question.