From 84f626e1a79ca81132cb08f997d0e0eb0b38e263 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 21:20:13 +0000 Subject: [PATCH 1/2] fix(upgrade): sudo seglinux-upgrade also builds and installs segctl The from-source upgrade built the seglinux package only, so a system installed before seg existed never received seg, seg game, the seg-game service or the sudoers rule, whatever was merged. It now builds segctl from the same source tarball (tools/seg, as pkg/segctl/Makefile does) and installs both in one pacman -U; a segctl build failure only warns. The CI upgrade job builds from this commit on an Artix container and checks the package, the gamemode group, the sudoers file mode and syntax, the service files and that seg game check and seg game run --dry-run run without a GPU. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0119819Vrp5vJojQCT1CjWog --- .github/workflows/ci.yml | 13 +++++++++++++ CHANGELOG.md | 3 +++ data/system/seglinux-upgrade | 36 +++++++++++++++++++++++++++++++++--- docs/lean-audit.md | 6 ++++-- docs/updating.md | 8 +++++++- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 221a568..b5f18d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,6 +302,19 @@ jobs: SUDO_USER=tester SEGLINUX_SOURCE="$PWD" SEGLINUX_VERSION=0.1.0.999999 \ bash data/system/seglinux-upgrade --noconfirm pacman -Q seglinux | grep -q ' 0\.1\.0\.999999-' + # seg (package segctl) comes with it, with its group, sudoers rule + # and services (pacman -U ran the package's install script). + pacman -Q segctl + seg --version + getent group gamemode + test "$(stat -c %a /etc/sudoers.d/seg-game)" = 440 + visudo -cf /etc/sudoers.d/seg-game + test -x /etc/init.d/seg-game && test -f /etc/runit/sv/seg-game/run && test -f /etc/dinit.d/seg-game + ls /etc/dinit.d/boot.d /etc/runlevels/default 2>/dev/null | grep -E '^seg-' || true + # seg game works on a machine with no GPU: it reports, it does not crash. + seg game check > /tmp/game-check.txt || test $? -eq 1 + grep -q 'max_map_count' /tmp/game-check.txt + seg game run --dry-run -- true test -x /usr/bin/seglinux-upgrade && test -L /usr/bin/seglinux-update test ! -e /usr/local/bin/seglinux-upgrade seglinux-shell --version diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1c678..564aa13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ Measurements and the reasons behind each removal are in `gamemode` group may use `nice -10`. - Shader caches: Mesa 4 GB -> 10 GB; NVIDIA's shader cache set to 10 GB and no longer deleted when full (`/etc/profile.d`, so on every init). +- `sudo seglinux-upgrade` (the from-source path) now also builds and + installs `segctl`, so an older install gets `seg`, `seg game`, the + services and the sudoers rule; CI checks this on an Artix container. - `makepkg` builds with every thread; the I/O scheduler rule skips partitions and covers virtual disks. diff --git a/data/system/seglinux-upgrade b/data/system/seglinux-upgrade index 41f465e..662d15d 100755 --- a/data/system/seglinux-upgrade +++ b/data/system/seglinux-upgrade @@ -178,6 +178,7 @@ latest() { # makepkg does not build as root, so it builds as the account that ran sudo. build_seglinux() { local user home version sha installed work top deps packages + local segctl_dir segctl_version segctl_deps segctl_packages=() user=${SUDO_USER:-} if [[ -z $user || $user == root ]]; then warn "SegLinux itself is built from your account: run sudo seglinux-upgrade as yourself, not as root." @@ -218,10 +219,27 @@ build_seglinux() { tar -C "$work/x" -czf "$work/seglinux-$version.tar.gz" "seglinux-$version" fi cp "$work/x/seglinux-$version/packaging/seglinux/"{PKGBUILD,seglinux.install} "$work/" + # seg, the system tool (package segctl: seg game, the zram and tuning + # services), is built from the same source: its recipe, and its folder as + # the source tarball the recipe names. + segctl_dir= + if [[ -f $work/x/seglinux-$version/pkg/segctl/PKGBUILD ]]; then + segctl_dir=$work/segctl + mkdir "$segctl_dir" + cp "$work/x/seglinux-$version/pkg/segctl/"{PKGBUILD,segctl.install} "$segctl_dir/" + segctl_version=$(sed -n 's/^pkgver=//p' "$segctl_dir/PKGBUILD") + tar -C "$work/x/seglinux-$version" --transform "s,^\./tools/seg,segctl-$segctl_version/tools/seg," \ + -czf "$segctl_dir/segctl-$segctl_version.tar.gz" ./tools/seg + fi rm -rf "$work/x" "$work/source.tar.gz" sed -i "s/^pkgver=.*/pkgver=$version/" "$work/PKGBUILD" chown -R "$user:" "$work" - as_user() { (cd "$work" && runuser -u "$user" -- env HOME="$home" CARGO_HOME="$home/.cache/seglinux/cargo" "$@"); } + as_user_in() { + local dir=$1 + shift + (cd "$dir" && runuser -u "$user" -- env HOME="$home" CARGO_HOME="$home/.cache/seglinux/cargo" "$@") + } + as_user() { as_user_in "$work" "$@"; } # What it needs to build and run, as dependencies (pacman -Qdt lists # them once nothing needs them). deps=$(as_user makepkg --printsrcinfo | sed -n 's/^\t\(make\)\{0,1\}depends = //p') || { @@ -229,8 +247,12 @@ build_seglinux() { warn "SegLinux's build recipe could not be read; SegLinux itself is not upgraded this time." return 0 } + segctl_deps= + if [[ -n $segctl_dir ]]; then + segctl_deps=$(as_user_in "$segctl_dir" makepkg --printsrcinfo | sed -n 's/^\t\(make\)\{0,1\}depends = //p') || segctl_dir= + fi # shellcheck disable=SC2086 # one package name per word - pacman -S --needed --noconfirm --asdeps base-devel $deps || { + pacman -S --needed --noconfirm --asdeps base-devel $deps $segctl_deps || { rm -rf "$work" warn "The tools to build SegLinux could not be installed; SegLinux itself is not upgraded this time." return 0 @@ -240,8 +262,16 @@ build_seglinux() { warn "SegLinux $version did not build; your SegLinux stays as it is. Please report it: $source_url/issues" return 0 fi + # seg is a smaller extra: if it does not build, SegLinux is still upgraded. + if [[ -n $segctl_dir ]]; then + if as_user_in "$segctl_dir" makepkg -f --nocheck --noconfirm; then + segctl_packages=("$segctl_dir"/segctl-*.pkg.tar.zst) + else + warn "seg (the segctl package) did not build; it is not upgraded this time." + fi + fi packages=("$work"/seglinux-"$version"-*.pkg.tar.zst) - pacman -U "$@" "${packages[0]}" || warn "SegLinux $version was not installed." + pacman -U "$@" "${packages[0]}" "${segctl_packages[@]}" || warn "SegLinux $version was not installed." rm -rf "$work" } diff --git a/docs/lean-audit.md b/docs/lean-audit.md index 9e0c70e..2f921e7 100644 --- a/docs/lean-audit.md +++ b/docs/lean-audit.md @@ -176,8 +176,10 @@ Found, not changed: the udev rule starts it whatever the answer says. * The two greetd `config.toml` and `pacman.conf` copies in the root overlays are identical to each other (and the first to `data/greeter/greetd.toml`). -* `seglinux-upgrade` updates only the `seglinux` package, so a system - installed before this work does not get `segctl` that way yet. +* `seglinux-upgrade` updated only the `seglinux` package, so a system + installed before this work did not get `segctl`. Fixed in the gaming work: + the from-source upgrade also builds and installs `segctl` (tested in CI on + an Artix container); the signed repository still carries `seglinux` only. ## Decisions for the maintainers diff --git a/docs/updating.md b/docs/updating.md index 9addded..c3201a0 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -83,7 +83,13 @@ each marked `# Added by seglinux-upgrade.`. It keeps the previous file as When SegLinux is built from source, the source comes over HTTPS from SegLinux's GitHub repository, as the ISO and this command do; pacman installs -the package it builds as a local file, like any AUR package. +the package it builds as a local file, like any AUR package. The same build +also makes `segctl` from the `tools/seg` folder (the `seg` tool: `seg game`, +the zram, tuning and game services, the sudoers rule for the `gamemode` +group) and installs the two together; if only `segctl` fails to build, +SegLinux is still upgraded. SegLinux's signed repository does not carry +`segctl` yet (`.github/workflows/update.yml` builds `seglinux` only), so once +packages are published `seg` still needs `--from-source` or the ISO. ## When an update fails From 4bbb12b15a415f9ed7d399c5654c2b3bc78b9833 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 21:21:17 +0000 Subject: [PATCH 2/2] fix(seg-game): seg game apply sets hugepages to madvise Booting the Lite image in QEMU showed seg game check warning "transparent hugepages: [always]": linux-zen starts with always, and the madvise parameter is only written to GRUB by Setup, so the live image and a system upgraded with seglinux-upgrade never had it. The seg-game service runs seg game apply at boot on every init, so it now changes always to madvise (and leaves madvise and never alone). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0119819Vrp5vJojQCT1CjWog --- CHANGELOG.md | 3 ++ docs/gaming.md | 5 ++- tools/seg/README.md | 2 +- tools/seg/segctl/src/game/apply.rs | 66 +++++++++++++++++++++++++++--- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 564aa13..57e8325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,9 @@ Measurements and the reasons behind each removal are in - Setup installs the **other kernel** (`linux` beside `linux-zen`, or the reverse) as a boot menu entry to fall back on when online (`[performance] fallback_kernel`); the installed kernel is never removed. +- Transparent hugepages go from `always` to `madvise` at boot (`seg-game` + service), so the live image and upgraded systems get it too, not only + fresh installs. - Kernel command line: `transparent_hugepage=madvise` everywhere and `amd_pstate=active` on AMD processors (an answer file's parameter of the same name replaces the default). diff --git a/docs/gaming.md b/docs/gaming.md index 33ffaac..0539507 100644 --- a/docs/gaming.md +++ b/docs/gaming.md @@ -226,7 +226,8 @@ a build container with no GPU and the stock kernel, to show the format: `--games` adds Steam, gamescope, MangoHud (64-bit only), Wine Staging and winetricks; `--lutris` adds Lutris. It also tells you when `[lib32]` is not enabled in `/etc/pacman.conf`. -* `sudo seg game apply` applies the gaming sysctls and makes `/dev/ntsync` +* `sudo seg game apply` applies the gaming sysctls, sets transparent + hugepages to `madvise` (from `always` only) and makes `/dev/ntsync` usable now; the `seg-game` service runs it at boot on every init. * `seg game audio on|off|status` lowers PipeWire's buffer to 256 samples at 48 kHz (about 5 ms), for you only (`~/.config/pipewire/...conf.d`). **Off by @@ -345,7 +346,7 @@ leave it off. | `nmi_watchdog=0`, `nowatchdog`, watchdog modules blacklisted | kernel command line (Setup), `/usr/lib/modprobe.d/seglinux-nowatchdog.conf` | Fewer periodic interrupts | | `vm.compaction_proactiveness = 0` | same | No background memory defragmentation while a game allocates (the default is 20); a program that needs a contiguous block still gets it by compacting on demand. What SteamOS sets. | | `fs.inotify.max_user_watches` | sysctl | Launchers watching many files | -| `transparent_hugepage=madvise` | kernel command line (Setup) | Huge pages only for programs that ask (games, databases); `always` can stall on compaction | +| `transparent_hugepage=madvise` | kernel command line (Setup), and `seg game apply` at boot | Huge pages only for programs that ask (games, databases); `always` can stall on compaction. linux-zen starts with `always`, so the live image and a system upgraded with `seglinux-upgrade` get `madvise` from the `seg-game` service (it changes `always` only; `never` is left alone). Found by booting the Lite image: the check said `[always]` before this. | | `amd_pstate=active` | kernel command line (Setup, AMD processors only) | The CPU picks its frequency itself, guided by the energy hint `seg game run` sets. Kernels or CPUs without it ignore it. | | I/O schedulers: NVMe none, SSD mq-deadline, HDD BFQ | `/usr/lib/udev/rules.d/60-seglinux-ioschedulers.rules` | Low latency while games stream assets | | NTSYNC | `/etc/modules-load.d/10-ntsync.conf`, udev rule | Windows synchronisation in the kernel, used by Wine 11 and Proton | diff --git a/tools/seg/README.md b/tools/seg/README.md index 3000af6..a3cb695 100644 --- a/tools/seg/README.md +++ b/tools/seg/README.md @@ -121,7 +121,7 @@ safety notes are in [docs/gaming.md](../../docs/gaming.md#seg-game). | `seg game check` | PASS/WARN/FAIL/SKIP for Vulkan, GPU driver, lib32 drivers, ntsync, `vm.max_map_count`, open files, kernel, fallback kernel, governor, boost permission, hugepages, split lock, compaction, mitigations; the fix under each; exit 1 if any FAIL | | `seg game setup [--yes] [--games] [--lutris]` | the driver packages for the GPUs in this computer (from `/sys/class/drm` and `lspci`) and the command that installs them; `--yes` runs it | | `seg game profiles [FILE]` | check `~/.config/seg/games.toml` and list what it holds | -| `seg game apply` | the gaming sysctls and `/dev/ntsync` permissions now (the `seg-game` service) | +| `seg game apply` | the gaming sysctls, hugepages `always` -> `madvise`, and `/dev/ntsync` permissions now (the `seg-game` service) | | `seg game audio on\|off\|status` | low-latency PipeWire for the current user | | `seg game unsafe-boost on\|off\|status` | `mitigations=off`; needs `--i-understand` after a warning | diff --git a/tools/seg/segctl/src/game/apply.rs b/tools/seg/segctl/src/game/apply.rs index 37f2327..539068b 100644 --- a/tools/seg/segctl/src/game/apply.rs +++ b/tools/seg/segctl/src/game/apply.rs @@ -1,7 +1,7 @@ //! `seg game apply`, run at boot by the `seg-game` service of every init: -//! the gaming sysctls and the permissions of /dev/ntsync. OpenRC and udev do -//! both too; runit, dinit and s6 images may not, and applying twice is -//! harmless. +//! the gaming sysctls, transparent hugepages on `madvise` and the permissions +//! of /dev/ntsync. OpenRC and udev do the sysctls and the device too; runit, +//! dinit and s6 images may not, and applying twice is harmless. use std::fs; use std::os::unix::fs::PermissionsExt; @@ -15,12 +15,23 @@ pub const HELP: &str = "\ Usage: seg game apply Applies /etc/sysctl.d/80-seglinux-gaming.conf now (vm.max_map_count, -kernel.split_lock_mitigate, vm.compaction_proactiveness, ...) and makes -/dev/ntsync usable by every user, so Wine and Proton can use NTSYNC. Needs -root. The seg-game service runs it at boot. +kernel.split_lock_mitigate, vm.compaction_proactiveness, ...), changes +transparent hugepages from `always` to `madvise` (linux-zen starts with +`always`, which can stall a game on memory compaction; `never` is left as it +is) and makes /dev/ntsync usable by every user, so Wine and Proton can use +NTSYNC. Needs root. The seg-game service runs it at boot. "; pub const SYSCTL_FILE: &str = "etc/sysctl.d/80-seglinux-gaming.conf"; +const THP_FILE: &str = "sys/kernel/mm/transparent_hugepage/enabled"; + +/// The word in brackets of `always [madvise] never`. +fn thp_selected(text: &str) -> Option<&str> { + text.split_once('[')? + .1 + .split_once(']') + .map(|(word, _)| word) +} /// Apply the sysctls of the gaming file below `root`; then fix /dev/ntsync. pub fn apply(root: &Path) -> Result> { @@ -45,6 +56,13 @@ pub fn apply(root: &Path) -> Result> { )); } } + let thp = root.join(THP_FILE); + if let Ok(text) = fs::read_to_string(&thp) + && thp_selected(&text) == Some("always") + { + fs::write(&thp, "madvise").context(format!("writing {}", thp.display()))?; + lines.push("transparent hugepages: always -> madvise".into()); + } let ntsync = root.join("dev/ntsync"); if let Ok(meta) = fs::metadata(&ntsync) { let mode = meta.permissions().mode() & 0o777; @@ -117,6 +135,42 @@ mod tests { assert!(again.iter().all(|l| l.starts_with("note:")), "{again:?}"); } + #[test] + fn hugepages_go_from_always_to_madvise_and_nothing_else_changes() { + let t = TempDir::new(); + t.put(THP_FILE, "[always] madvise never\n"); + let lines = apply(t.path()).unwrap(); + assert!( + lines + .iter() + .any(|l| l == "transparent hugepages: always -> madvise"), + "{lines:?}" + ); + assert_eq!( + fs::read_to_string(t.path().join(THP_FILE)).unwrap(), + "madvise" + ); + // madvise is left alone, and so is never (someone chose it). + for already in ["always [madvise] never\n", "always madvise [never]\n"] { + let t = TempDir::new(); + t.put(THP_FILE, already); + let lines = apply(t.path()).unwrap(); + assert!(lines.iter().all(|l| !l.contains("hugepages")), "{lines:?}"); + assert_eq!( + fs::read_to_string(t.path().join(THP_FILE)).unwrap(), + already + ); + } + } + + #[test] + fn the_hugepage_setting_is_read_from_the_brackets() { + assert_eq!(thp_selected("[always] madvise never"), Some("always")); + assert_eq!(thp_selected("always [madvise] never"), Some("madvise")); + assert_eq!(thp_selected("always madvise never"), None); + assert_eq!(thp_selected(""), None); + } + #[test] fn no_file_and_no_device_is_not_an_error() { let t = TempDir::new();