# ANCIENT-MOD: running on old kernels (target: 4.19) Upstream requires kernel ≥ 5.10 (see `README`, "REQUIREMENTS") and dropped the fallback code for older kernels. This branch restores or adds fallbacks so the system can at least boot on 4.19. Code touched by this effort is marked with `ANCIENT-MOD` comments where practical. Legend: **revert** = reverse-applied an upstream commit (possibly adjusted), **new** = new code because upstream code was refactored since the fallback was removed. ## Boot-critical | Area | Kernel feature | Change | Where | |---|---|---|---| | Mount point checks (`is_mount_point_at()`, `path_is_mount_point()`), used by early `mount_setup()` for `/proc`, `/sys`, `/dev` with `MNT_FATAL` | `STATX_ATTR_MOUNT_ROOT` (5.8) | **new**: `xstatx_full()` emulates the attribute when it is required but missing: compares the mount id (from `/proc/self/fdinfo`) of the inode with its parent's (`..` for directories, parent path for others); same inode as parent ⇒ root ⇒ mount point. Replaces upstream fallback removed in `a98a6eb95c`. | `src/basic/stat-util.c` (`xstatx_emulate()`) | | `chase()` (used everywhere), `path_get_mnt_id_at()`, `fds_inode_and_mount_same()`, `XSTATX_MNT_ID_BEST` | `STATX_MNT_ID` (5.8/5.10) | **new**: same emulation fills `stx_mnt_id` from `/proc/self/fdinfo/` `mnt_id:` (available since 3.15). Replaces removals in `7ecfa87c56`, `e5a3127339`. | `src/basic/stat-util.c` | | cgroup2 mount | `memory_recursiveprot` (5.7) | **revert** (adjusted) of `80c5b63db5`: option is probed again instead of always passed; if it cannot be probed because `fsopen()` is missing (< 5.2) it is not used. | `src/shared/mount-setup.c` | | `mount_option_supported()` | `fsopen()` (5.2) | **revert** of part of `c2198d0c3f`: return `-EAGAIN` ("don't know") when the new mount API is unavailable. | `src/basic/mountpoint-util.c` | | `close_all_fds*()` (every fork/exec, `systemd-executor`) | `close_range()` (5.9) | **revert** of `c471cca093` and `d58c826c54`: fall back to iterating `/proc/self/fd` (or the frugal loop). | `src/basic/fd-util.c`, `src/basic/fd-util.h`, `src/test/test-fd-util.c` | | `pidref_set_pid()` (every process PID1 tracks, `safe_fork()`, `posix_spawn_wrapper()`) | `pidfd_open()` (5.3) | **revert** (adjusted) of part of `6e14c46bac`: ENOSYS ⇒ PidRef without fd (plain PID). | `src/basic/pidref.c` | | `pidfd_open()` shim (all pidfd users), `pidfd_waitid()` | `waitid(P_PIDFD)` (5.4) | **new**: vendor kernels (Android 4.19) backport `pidfd_open()` but not `waitid(P_PIDFD)`, so every wait failed with EINVAL (PID1 froze after generators). The shim probes once on a pidfd of self that `pidfd_send_signal()` and `poll()` work (without pidfd poll support a live process reports readable); if not, `pidfd_open()` returns ENOSYS so the plain-PID fallbacks above apply. Waits go through `pidfd_waitid()`, which probes `waitid(P_PIDFD)` (ECHILD = supported, EINVAL = not) and otherwise uses `waitid(P_PID)` with the known PID (safe for children: the PID cannot be reused before reaping). Keeping pidfds is required by logind, which since v258 refuses sessions whose leader has no pidfd (`NoSessionPIDFD`). | `src/libc/pidfd.c`, `src/include/override/sys/pidfd.h`, `src/basic/pidref.c`, `src/libsystemd/sd-event/sd-event.c` | | `pidref_wait_for_terminate_full()` with timeout | pidfd (5.3) | **new**: without pidfd poll `waitid(P_PID, …, WNOHANG)` every 10 ms instead of returning `-ENOMEDIUM`. | `src/basic/pidref.c` | | `pidfd_check_pidfs()`, `pidfd_info_mask_is_supported()`, `pidfd_get_inode_id_self_cached()` | pidfd (5.3) | **revert**/new: ENOSYS ⇒ "not supported" instead of hard error. | `src/basic/pidfd-util.c` | | sd-event child sources (`sd_event_add_child()`, used by udevd, logind, …) | pidfd (5.3), `waitid(P_PIDFD)` (5.4) | **new** (upstream removed the non-pidfd path in `6e14c46bac` and later made the code pidfd-only): if `pidfd_open()` is unsupported the source works on the PID via `waitid(P_PID)` + SIGCHLD signalfd. SIGCHLD is blocked automatically in that case (upstream no longer requires callers to block it). `send_child_signal()` falls back to `kill()`/`rt_sigqueueinfo()`; `set_child_pidfd_own()` returns `-EOPNOTSUPP` without pidfd. | `src/libsystemd/sd-event/sd-event.c` | | `pam_systemd` session creation | pidfd (5.3) | **revert** of part of `6e14c46bac`: fall back to `CreateSession` (PID based). Ineffective on its own: logind (Varlink and D-Bus) rejects leaders without pidfd, so sessions need a kernel with `pidfd_open()` + pidfd poll. | `src/login/pam_systemd.c` | | Unit cgroup-empty tracking (`unit_check_cgroup_events()`) | `frozen` key in `cgroup.events` (5.2) | **new**: if the key is missing, read only `populated`. Without this every cgroup event read failed and stopped units were never noticed as empty. | `src/core/cgroup.c` | | Freeze/thaw | cgroup v2 freezer (5.2) | **revert** of `3c86a935ad`: `cg_freezer_supported()` restored; freeze is `-EOPNOTSUPP` / no-op when `cgroup.freeze` is absent. | `src/basic/cgroup-util.[ch]`, `src/core/cgroup.c`, `src/core/unit.c` | | Credentials (`LoadCredential=`, `SetCredential=`, `ImportCredential=` — used by many stock units) | new mount API `fsopen`/`fsmount`/`move_mount` (5.2) | **new**: if the credentials fs cannot be created because the API is missing, use the existing "plain directory" fallback (previously only used on EPERM). | `src/core/exec-credential.c` | | `ProtectProc=` / `ProcSubset=` (used by logind, networkd, timesyncd, hostnamed, …) | per-instance procfs, textual `hidepid=`, `subset=` (5.8) | **new**: on kernels < 5.8 the options are ignored (they would either fail with EINVAL or, worse, change the host's shared `/proc` superblock). `/proc/sys` & friends are then still protected by the normal ProtectKernel* mounts. Upstream removed the old fallback in `0cba5bdcf1`. | `src/core/namespace.c` | | `RootDirectory=` | `open_tree()` (5.2) | **new**: tolerate ENOSYS when cloning the root directory mount and use the classic bind mount path. | `src/core/namespace.c` | | `futimens_opath()`, `access_fd()` | `AT_EMPTY_PATH` for `utimensat()`/`faccessat()` (5.8) | **revert** of `52bf232e82` (manually): fall back to `/proc/self/fd/`. | `src/basic/fs-util.c` | ## Networking | Area | Kernel feature | Change | Where | |---|---|---|---| | `socket_bind_to_ifindex()` (DHCP client, resolved, networkd, …) | `SO_BINDTOIFINDEX` (5.1) | **revert** of `ddbbc98bd5` (manually): fall back to `SO_BINDTODEVICE`. | `src/basic/socket-util.c` | | `rtnl_resolve_ifname_full()` (alternative names) | `IFLA_ALT_IFNAME` lookup (5.5) | **revert** of `42b09813f4`: treat EINVAL like ENODEV. | `src/libsystemd/sd-netlink/netlink-util.c` | | networkd startup enumeration | nexthop (5.3) | **revert** of `034b69d8b8` (manually): ignore `-EOPNOTSUPP` from nexthop enumeration, otherwise networkd refuses to start. | `src/network/networkd-manager.c` | | networkd permanent MAC address | `IFLA_PERM_ADDRESS` (5.6) | **revert** of `04f05d9bd7`: fall back to ethtool. | `src/network/networkd-link.c` | | IPv6 DNAT (nspawn port forwarding / firewall-util) | nft concatenation fix (5.8) | **revert** of `58fb46ecd5` (manually): `-EOVERFLOW` ⇒ `-EOPNOTSUPP`. | `src/shared/firewall-util.c` | ## Other | Area | Kernel feature | Change | Where | |---|---|---|---| | `SurviveFinalKillSignal=` | `user.*` xattrs on cgroupfs (5.7) | **revert** of `8292b859f3`: fall back to `trusted.survive_final_kill_signal`. | `src/core/cgroup.c`, `src/shared/killall.c` | | Loop devices via `LOOP_SET_STATUS64` fallback | buffer cache invalidation bug (fixed in 5.0) | **revert** of `5e3887e6c1`: issue `BLKFLSBUF` when offset/size is set. | `src/shared/loop-util.c` | ## Known remaining gaps (not addressed) - **cgroup v1 is gone upstream**: the kernel must boot with a cgroup v2 hierarchy. 4.19's cgroup v2 lacks the `cpuset` (5.0) and `freezer` (5.2) controllers, `cgroup.kill` (5.14, already graceful). - **PSI** (4.20): `systemd-oomd` does not work; memory-pressure event sources are already optional. - **BPF**: features needing BPF links / BPF LSM / bounded loops (5.3–5.7) — `RestrictFileSystems=`, `RestrictNetworkInterfaces=`, `SocketBind*=`, `BindNetworkInterface=`, … — report "not supported" (upstream probing, `ef12eadfc8`). - **New mount API without fallback** (5.2/5.12): `PrivateBPF=`, `ExtensionImages=`/`ExtensionDirectories=` (overlay via `fsopen`), `MountImages=`, runtime `systemctl bind`/`mount-image`, idmapped mounts, mountfsd, nsresourced, sysext/confext, homed (also needs fscrypt v2 / 5.4), coredump's mount of the crashed process' root. - **`MS_NOSYMFOLLOW`** (5.10) is passed as a legacy `mount(2)` flag; older kernels silently ignore unknown flags, so the mount just lacks `nosymfollow`. - **Credentials** use a plain directory instead of a dedicated (noswap) tmpfs, i.e. less isolation. - **Per-device IO weight** (`IODeviceWeight=`, BFQ per-device weight, 5.4): upstream dropped the EINVAL workaround (`00c4851af0`), expect warnings when used. - **udev net_id** for IP-over-InfiniBand (`dev_port`, 4.20): `d3d375a686` not reverted; multi-port IPoIB devices may get colliding names. - `test-event`'s `test_pidfd` inherently requires pidfds and fails without them; it and `test_child_pidfd_wnowait` also call `waitid(P_PIDFD)` directly and fail without it. - logind sessions require pidfds (see `pam_systemd` row); on kernels without `pidfd_open()` no login session can be created. - Anything else newer than 5.10 listed in `README` as "recommended baseline" is expected to fall back gracefully upstream, but that has not been verified on a real old kernel. ## Testing done - Full native test suite (`meson test -C build`) on a 7.x kernel. - Selected tests (`test-stat-util`, `test-mountpoint-util`, `test-chase`, `test-fs-util`, `test-fd-util`, `test-pidref`, `test-process-util`, `test-socket-util`, `test-mount-util`, `test-udev-spawn`, `test-async`, `test-event` except `test_pidfd`) and `systemd --test --system` (with the built generators) under an `LD_PRELOAD` shim that simulates an old kernel: strips `STATX_MNT_ID*`/`STATX_ATTR_MOUNT_ROOT` from `statx()` results and makes `pidfd_open`, `pidfd_send_signal`, `pidfd_spawn`, `close_range` and the new mount API return ENOSYS. - `test-pidref`, `test-process-util`, `test-event` (except `test_pidfd`, `test_child_pidfd_wnowait`) under an `LD_PRELOAD` shim that only makes `waitid(P_PIDFD)` fail with EINVAL (Android 4.19 vendor kernel case: pidfds stay enabled, waits use the `P_PID` fallback). - Booted on an Android 4.19 vendor kernel (Xiaomi vince): reaches `graphical.target`; login sessions not verified yet.