max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
16 files changed,
+781 insertions,
-0 deletions
| @@ -2918,6 +2918,176 @@ | |||
| 2918 | 2918 | || { echo "$action is not defined; run0 cannot ask polkit for an admin" >&2; exit 1; }; \ | |
| 2919 | 2919 | echo "run0: present, systemd $version, $action defined" | |
| 2920 | 2920 | ||
| 2921 | + | # ===================================================================== | |
| 2922 | + | # Hardening, part 1: the SUID set. | |
| 2923 | + | # | |
| 2924 | + | # Filed out of the 2026-08-21 secureblue review; the adopt/reject list and | |
| 2925 | + | # the reasoning are in wiki `alloy-hardening-posture`. | |
| 2926 | + | # | |
| 2927 | + | # A SUID binary is a program any user can run that becomes root, so each one | |
| 2928 | + | # is a promise that its argument parsing, its environment handling and its | |
| 2929 | + | # error paths are all correct. The base image ships sixteen. Six of them have | |
| 2930 | + | # no caller on a machine with one human account and a console that owns | |
| 2931 | + | # account management, and a promise nobody needs is one worth not making. | |
| 2932 | + | # | |
| 2933 | + | # WHAT IS NOT DONE HERE, deliberately: secureblue re-adds capabilities to | |
| 2934 | + | # some of these so they keep working for unprivileged users. Kicksecure's | |
| 2935 | + | # objection to that is sound — `cap_dac_read_search` on `unix_chkpwd` hands | |
| 2936 | + | # out a dangerous capability to preserve a rarely-used convenience — so the | |
| 2937 | + | # tools simply stop working without root, which is the cheaper answer. | |
| 2938 | + | # | |
| 2939 | + | # WHAT STAYS SUID, and why, because a list of removals without the | |
| 2940 | + | # complement reads as an oversight: | |
| 2941 | + | # - pkexec and polkit-agent-helper-1: the console's inline authorize flow | |
| 2942 | + | # depends on the helper (crates/alloy/src/polkit.rs), and the block below | |
| 2943 | + | # already asserts it is setuid. | |
| 2944 | + | # - sudo and su: Alloy names run0 as the way to root (see above) and has | |
| 2945 | + | # never removed sudo. Scripts across this tree call it. Removing it is a | |
| 2946 | + | # separate decision with its own blast radius, not a line in a hardening | |
| 2947 | + | # pass. | |
| 2948 | + | # - passwd and unix_chkpwd: a user changing their own password, and the | |
| 2949 | + | # lock screen checking it. | |
| 2950 | + | # - mount, umount, mount.nfs, fusermount3: `alloy disk` and removable media. | |
| 2951 | + | # - grub2-set-bootflag: the bootloader's one-shot flag. | |
| 2952 | + | # | |
| 2953 | + | # The result is asserted rather than assumed: chmod is silent about a file | |
| 2954 | + | # that was already not setuid, and about one this list misspelled. | |
| 2955 | + | # ===================================================================== | |
| 2956 | + | RUN set -eux; \ | |
| 2957 | + | stripped=""; \ | |
| 2958 | + | for bin in chfn chsh newgrp gpasswd chage pam_timestamp_check; do \ | |
| 2959 | + | path=$(command -v "$bin" 2>/dev/null || true); \ | |
| 2960 | + | [ -n "$path" ] || { echo "$bin is not in the image; this list names a binary the base no longer ships" >&2; exit 1; }; \ | |
| 2961 | + | [ -u "$path" ] || { echo "$path is already not setuid; the base changed and this line is now inert" >&2; exit 1; }; \ | |
| 2962 | + | chmod u-s "$path"; \ | |
| 2963 | + | [ -u "$path" ] && { echo "chmod u-s did not take on $path" >&2; exit 1; }; \ | |
| 2964 | + | stripped="$stripped $bin"; \ | |
| 2965 | + | done; \ | |
| 2966 | + | for keep in /usr/bin/sudo /usr/bin/passwd /usr/bin/mount /usr/bin/umount; do \ | |
| 2967 | + | [ -u "$keep" ] || { echo "$keep lost its setuid bit; the loop above is stripping more than it names" >&2; exit 1; }; \ | |
| 2968 | + | done; \ | |
| 2969 | + | echo "suid: stripped$stripped; $(find /usr/bin /usr/sbin /usr/libexec -perm -4000 -type f 2>/dev/null | wc -l) setuid binaries remain" | |
| 2970 | + | ||
| 2971 | + | # ===================================================================== | |
| 2972 | + | # Hardening, part 2: faillock. | |
| 2973 | + | # | |
| 2974 | + | # etc/security/faillock.conf carries the numbers (50 attempts, 24 hours). | |
| 2975 | + | # That file is inert on its own: what reads it is pam_faillock, and pam_faillock | |
| 2976 | + | # is only in the stack if authselect's `with-faillock` feature is on. Same | |
| 2977 | + | # shape as the fingerprint block above, and asserted the same way, because | |
| 2978 | + | # "the file is present" and "the file is doing something" are different | |
| 2979 | + | # claims and only the second one is worth making. | |
| 2980 | + | # | |
| 2981 | + | # Applies on both profiles. sshd takes keys only, so this is about the | |
| 2982 | + | # console, the greeter and the lock screen — all of which exist on a server | |
| 2983 | + | # too, in the form of whoever is standing at it. | |
| 2984 | + | # ===================================================================== | |
| 2985 | + | RUN set -eux; \ | |
| 2986 | + | conf=/etc/security/faillock.conf; \ | |
| 2987 | + | [ -f "$conf" ] || { echo "$conf did not land from the config tree" >&2; exit 1; }; \ | |
| 2988 | + | grep -q '^deny = 50' "$conf" \ | |
| 2989 | + | || { echo "$conf does not set the deny count this image documents" >&2; exit 1; }; \ | |
| 2990 | + | authselect enable-feature with-faillock; \ | |
| 2991 | + | authselect apply-changes; \ | |
| 2992 | + | grep -q 'pam_faillock\.so' /etc/pam.d/system-auth \ | |
| 2993 | + | || { echo "with-faillock did not reach system-auth; $conf would never be read" >&2; exit 1; }; \ | |
| 2994 | + | grep -q 'pam_faillock\.so' /etc/pam.d/password-auth \ | |
| 2995 | + | || { echo "with-faillock reached system-auth but not password-auth; half the stack counts failures" >&2; exit 1; }; \ | |
| 2996 | + | echo "faillock: $(grep -c pam_faillock /etc/pam.d/system-auth) pam_faillock line(s) in system-auth, deny=50 unlock_time=86400" | |
| 2997 | + | ||
| 2998 | + | # ===================================================================== | |
| 2999 | + | # Hardening, part 3: the clock. | |
| 3000 | + | # | |
| 3001 | + | # etc/chrony.d/10-alloy-nts.conf names four NTS servers. Two things have to | |
| 3002 | + | # be true for it to matter, and neither is true of the file on its own: | |
| 3003 | + | # /etc/chrony.conf has to read the directory, and it has to stop preferring | |
| 3004 | + | # the unauthenticated pool it ships with. | |
| 3005 | + | # | |
| 3006 | + | # Both are edits to a file the chrony rpm owns. /etc is writable on a bootc | |
| 3007 | + | # deployment so this is legal, and it is the same arrangement as the Firefox | |
| 3008 | + | # pref file: the package's file, edited in place, with the edit stated here. | |
| 3009 | + | # The pool line is commented rather than deleted so an operator reading the | |
| 3010 | + | # file can see what was turned off and why. | |
| 3011 | + | # | |
| 3012 | + | # `chronyd -p` parses the configuration and exits, which is what makes this | |
| 3013 | + | # an assertion instead of a hope. Without it, a typo in a server name would | |
| 3014 | + | # be discovered by a machine that silently never synchronises. | |
| 3015 | + | # ===================================================================== | |
| 3016 | + | RUN set -eux; \ | |
| 3017 | + | conf=/etc/chrony.conf; \ | |
| 3018 | + | drop=/etc/chrony.d/10-alloy-nts.conf; \ | |
| 3019 | + | [ -f "$drop" ] || { echo "$drop did not land from the config tree" >&2; exit 1; }; \ | |
| 3020 | + | grep -q '^pool ' "$conf" \ | |
| 3021 | + | || { echo "$conf has no pool line; the base changed and this edit no longer describes it" >&2; exit 1; }; \ | |
| 3022 | + | sed -i 's|^pool |# Commented by Alloy: unauthenticated NTP. See /etc/chrony.d/10-alloy-nts.conf.\n# pool |' "$conf"; \ | |
| 3023 | + | printf '\n# Added by Alloy: read /etc/chrony.d for the NTS sources.\nconfdir /etc/chrony.d\n' >> "$conf"; \ | |
| 3024 | + | ! grep -q '^pool ' "$conf" \ | |
| 3025 | + | || { echo "the pool line survived the edit; the machine would still take unauthenticated time" >&2; exit 1; }; \ | |
| 3026 | + | chronyd -p >/dev/null \ | |
| 3027 | + | || { echo "chronyd rejects the configuration after the Alloy edits" >&2; exit 1; }; \ | |
| 3028 | + | sources=$(chronyd -p 2>/dev/null | grep -c '^server .* nts$' || true); \ | |
| 3029 | + | [ "$sources" -ge 4 ] \ | |
| 3030 | + | || { echo "chronyd parsed the config and sees $sources NTS sources; the confdir is not being read" >&2; exit 1; }; \ | |
| 3031 | + | echo "chrony: pool commented, confdir /etc/chrony.d, $sources NTS sources" | |
| 3032 | + | ||
| 3033 | + | # ===================================================================== | |
| 3034 | + | # Hardening, part 4: assert the drop-in files are not inert. | |
| 3035 | + | # | |
| 3036 | + | # Six files landed from the config tree with nothing else to turn them on. | |
| 3037 | + | # Each one has a way of being present and doing nothing, and every one of | |
| 3038 | + | # those ways is silent on a built image: | |
| 3039 | + | # | |
| 3040 | + | # - a sysctl file that loses to a higher-numbered one, so its values never | |
| 3041 | + | # take. The numbering is the whole defence and it is checked, not trusted. | |
| 3042 | + | # - a modprobe file using `blacklist` where it means `install`, which stops | |
| 3043 | + | # autoload and permits a direct modprobe. | |
| 3044 | + | # - a kargs file whose new entries did not survive template rendering. | |
| 3045 | + | # - a sway directive in a file the compositor does not include. | |
| 3046 | + | # - a tmpfiles line naming a source that is not in the image, so the flatpak | |
| 3047 | + | # overrides never reach the only path flatpak reads. | |
| 3048 | + | # ===================================================================== | |
| 3049 | + | RUN set -eux; \ | |
| 3050 | + | hard=/usr/lib/sysctl.d/90-alloy-hardening.conf; \ | |
| 3051 | + | core=/usr/lib/sysctl.d/95-alloy-coredump.conf; \ | |
| 3052 | + | [ -f "$hard" ] && [ -f "$core" ] \ | |
| 3053 | + | || { echo "the sysctl drop-ins did not land" >&2; exit 1; }; \ | |
| 3054 | + | last=$(ls /usr/lib/sysctl.d/*.conf /etc/sysctl.d/*.conf 2>/dev/null \ | |
| 3055 | + | | xargs -n1 basename \ | |
| 3056 | + | | grep -v -e '^9[05]-alloy-' -e '^99-sysctl\.conf$' \ | |
| 3057 | + | | sort | tail -1); \ | |
| 3058 | + | [ "$(printf '%s\n%s\n' "$last" 90-alloy-hardening.conf | sort | tail -1)" = 90-alloy-hardening.conf ] \ | |
| 3059 | + | || { echo "$last sorts after the Alloy sysctl drop-ins and would win any key they share" >&2; exit 1; }; \ | |
| 3060 | + | grep -q '^kernel.core_pattern=|/bin/false' "$core" \ | |
| 3061 | + | || { echo "$core does not disable cores; an empty core_pattern means 'default name', not 'off'" >&2; exit 1; }; \ | |
| 3062 | + | mod=/usr/lib/modprobe.d/50-alloy-blacklist.conf; \ | |
| 3063 | + | [ -f "$mod" ] || { echo "$mod did not land" >&2; exit 1; }; \ | |
| 3064 | + | ! grep -q '^blacklist ' "$mod" \ | |
| 3065 | + | || { echo "$mod uses 'blacklist', which a direct modprobe ignores; it must use 'install <mod> /bin/false'" >&2; exit 1; }; \ | |
| 3066 | + | grep -q '^install squashfs' "$mod" \ | |
| 3067 | + | && { echo "$mod blacklists squashfs; the installer ISO boots a live root out of one and would stop booting" >&2; exit 1; }; \ | |
| 3068 | + | echo "modprobe: $(grep -c '^install ' "$mod") modules refused, squashfs left loadable for the ISO"; \ | |
| 3069 | + | kargs=/usr/lib/bootc/kargs.d/10-alloy.toml; \ | |
| 3070 | + | [ -f "$kargs" ] || { echo "$kargs did not land from the render stage" >&2; exit 1; }; \ | |
| 3071 | + | for karg in init_on_free=1 slab_nomerge page_alloc.shuffle=1 randomize_kstack_offset=on vsyscall=none debugfs=off; do \ | |
| 3072 | + | grep -q "\"$karg\"" "$kargs" \ | |
| 3073 | + | || { echo "$kargs is missing $karg; the render dropped it" >&2; exit 1; }; \ | |
| 3074 | + | done; \ | |
| 3075 | + | swayconf=/etc/sway/config.d/00-alloy.conf; \ | |
| 3076 | + | grep -q '^xwayland disable' "$swayconf" \ | |
| 3077 | + | || { echo "$swayconf does not disable Xwayland" >&2; exit 1; }; \ | |
| 3078 | + | tmpf=/usr/lib/tmpfiles.d/50-alloy-flatpak.conf; \ | |
| 3079 | + | src=/usr/share/alloy/flatpak/global; \ | |
| 3080 | + | [ -f "$tmpf" ] && [ -f "$src" ] \ | |
| 3081 | + | || { echo "the flatpak override or its tmpfiles rule did not land" >&2; exit 1; }; \ | |
| 3082 | + | grep -q "^C .*$src\$" "$tmpf" \ | |
| 3083 | + | || { echo "$tmpf does not copy $src; flatpak reads /var/lib/flatpak/overrides/global and nothing else" >&2; exit 1; }; \ | |
| 3084 | + | systemd-tmpfiles --dry-run --create "$tmpf" >/dev/null \ | |
| 3085 | + | || { echo "systemd-tmpfiles rejects $tmpf" >&2; exit 1; }; \ | |
| 3086 | + | dns=/etc/systemd/resolved.conf.d/10-alloy-dns.conf; \ | |
| 3087 | + | grep -q '^DNSOverTLS=opportunistic' "$dns" \ | |
| 3088 | + | || { echo "$dns does not set DNSOverTLS, or sets 'yes' before anyone measured it against MagicDNS" >&2; exit 1; }; \ | |
| 3089 | + | echo "hardening drop-ins: sysctl, modprobe, kargs, sway, flatpak, resolved — all present and non-inert" | |
| 3090 | + | ||
| 2921 | 3091 | # ===================================================================== | |
| 2922 | 3092 | # Systemd presets — shipped via etc/systemd/{system,user}-preset/ | |
| 2923 | 3093 | # in the config tree above. Split across system-preset (greetd, |
| @@ -66,6 +66,14 @@ | |||
| 66 | 66 | ||
| 67 | 67 | Rejected: Niri (Alloy's original pick; its scrolling-column model was the specific thing rejected in the 2026-07-17 pivot, see [MANIFESTO.md](MANIFESTO.md#status)), Hyprland (governance contested, animation-forward against Alloy's understatement), river (tag-based/dwm-shaped, Zig, further from i3 than sway), dwl (tiny but too bare for a curated default). | |
| 68 | 68 | ||
| 69 | + | ### Xwayland: **disabled**, ruled 2026-08-21 | |
| 70 | + | ||
| 71 | + | The session ships `xwayland disable`. X11 has no isolation between clients: any X client can read every other client's keystrokes, window contents and clipboard, because that was the design. Running an X server inside a Wayland session hands that model back to anything that asks, and "anything that asks" includes a Flathub app that Alloy otherwise presents as sandboxed. | |
| 72 | + | ||
| 73 | + | Breaking Steam, most Electron applications and older Java toolkits is the intent rather than a cost to be mitigated. Alloy has been a Wayland-only session since the pivot, and this makes the stack honest about it. | |
| 74 | + | ||
| 75 | + | The package stays in the image. `xwayland enable` in `~/.config/sway/config.d/` restores it, which makes the escape hatch a config line rather than a rebuild, and the manual says so (chapter 7). Rationale for this and the rest of the 2026-08-21 hardening pass: wiki `alloy-hardening-posture`. | |
| 76 | + | ||
| 69 | 77 | ## Authored toolkit | |
| 70 | 78 | ||
| 71 | 79 | **ratatui.** Alloy authors one thing, the `alloy` console and its `alloy_tui` design-system crate, and it authors in ratatui. Immediate-mode: every frame the UI code runs top to bottom from current state, no bindings, no observables, no retained UI state to diverge. Picked because **Alloy's principle is to avoid the reactive pattern** (staged appearance, async UI trickling, state-divergence bugs, the "web-shaped feel"), and immediate-mode rules those out by construction. The mountaineer-sysop `sysop-tui` pattern carries over directly as the seed of `alloy_tui`: palette, themed widgets, footer chrome, reserved keys, mock-or-real backend detection. See [CONSOLE.md](CONSOLE.md). |
| @@ -106,6 +106,48 @@ | |||
| 106 | 106 | Alloy ships no graphical file manager, so this is where one comes from if you | |
| 107 | 107 | want one. | |
| 108 | 108 | ||
| 109 | + | ## What a sandboxed app does not get | |
| 110 | + | ||
| 111 | + | Alloy trims flatpak's default permissions before an app ever asks. The image | |
| 112 | + | ships system-wide overrides that remove three grants: X11 access, raw access to | |
| 113 | + | every device node, and blanket access to your home directory and the host | |
| 114 | + | filesystem. GPU access stays, because a video player or a browser without it is | |
| 115 | + | not usable. | |
| 116 | + | ||
| 117 | + | That is a smaller sandbox than Flathub assumes, so some apps will misbehave. Give | |
| 118 | + | back what one actually needs, per app: | |
| 119 | + | ||
| 120 | + | flatpak override --user org.example.SomeApp --filesystem=~/Projects | |
| 121 | + | ||
| 122 | + | The list of what an app currently has: | |
| 123 | + | ||
| 124 | + | flatpak info --show-permissions org.example.SomeApp | |
| 125 | + | ||
| 126 | + | The defaults live in `/var/lib/flatpak/overrides/global`. It is yours to edit, | |
| 127 | + | and an image update will not overwrite it once it exists. | |
| 128 | + | ||
| 129 | + | ## X11 applications do not run | |
| 130 | + | ||
| 131 | + | There is no X server. Alloy is a Wayland-only session and the sway config ships | |
| 132 | + | `xwayland disable`, so an application that can only speak X11 exits saying it | |
| 133 | + | cannot open a display. | |
| 134 | + | ||
| 135 | + | This affects Steam, most Electron applications, some screen-sharing paths, and | |
| 136 | + | older Java toolkits. Anything Wayland-native is unaffected, and most Electron | |
| 137 | + | apps can be told to use Wayland with `--ozone-platform=wayland`. | |
| 138 | + | ||
| 139 | + | The reason is that X11 gives every client the ability to read every other | |
| 140 | + | client's keystrokes and window contents. A sandboxed app with X11 access is not | |
| 141 | + | sandboxed in any way that matters. | |
| 142 | + | ||
| 143 | + | If you need it back, put this in `~/.config/sway/config.d/` and log in again: | |
| 144 | + | ||
| 145 | + | xwayland enable | |
| 146 | + | ||
| 147 | + | Your file wins over the shipped one. The Xwayland package stays in the image so | |
| 148 | + | that this works without rebuilding. Turning it on is a real cost, not a | |
| 149 | + | formality: it applies to the whole session, not to the one app you wanted. | |
| 150 | + | ||
| 109 | 151 | ## Which one, in practice | |
| 110 | 152 | ||
| 111 | 153 | - **A CLI tool you use daily and Fedora has it.** Layer it, or put it in a |
| @@ -36,6 +36,25 @@ | |||
| 36 | 36 | `Ctrl+Alt+F2` gets you a virtual console where you can edit | |
| 37 | 37 | `~/.config/sway/config` and log back in. | |
| 38 | 38 | ||
| 39 | + | ### An app exits saying it cannot open a display | |
| 40 | + | ||
| 41 | + | It is an X11 application, and there is no X server. See chapter 7: Alloy ships | |
| 42 | + | `xwayland disable`, and the way back is `xwayland enable` in | |
| 43 | + | `~/.config/sway/config.d/`. | |
| 44 | + | ||
| 45 | + | Try the Wayland flag first if it is an Electron app: | |
| 46 | + | ||
| 47 | + | org.example.SomeApp --ozone-platform=wayland | |
| 48 | + | ||
| 49 | + | ### I mistyped my password too many times and now it will not take the right one | |
| 50 | + | ||
| 51 | + | Fifty consecutive failures locks the account for 24 hours. From another session, | |
| 52 | + | or a virtual console logged in as another user: | |
| 53 | + | ||
| 54 | + | run0 faillock --user <name> --reset | |
| 55 | + | ||
| 56 | + | `faillock --user <name>` on its own shows the count without clearing it. | |
| 57 | + | ||
| 39 | 58 | ### I edited the sway config and the session will not start | |
| 40 | 59 | ||
| 41 | 60 | Log in on a virtual console and check it: | |
| @@ -268,6 +287,57 @@ | |||
| 268 | 287 | ||
| 269 | 288 | --- | |
| 270 | 289 | ||
| 290 | + | ## Diagnostics that used to work | |
| 291 | + | ||
| 292 | + | The 2026-08-21 hardening pass turned three things off that a person debugging | |
| 293 | + | software expects to be on. Each has a way back; two of them are a command, and | |
| 294 | + | the third is a line in your image. | |
| 295 | + | ||
| 296 | + | ### dmesg says operation not permitted | |
| 297 | + | ||
| 298 | + | The kernel ring buffer takes a capability to read now | |
| 299 | + | (`kernel.dmesg_restrict=1`). Two ways to the same text: | |
| 300 | + | ||
| 301 | + | run0 dmesg | |
| 302 | + | journalctl -k | |
| 303 | + | ||
| 304 | + | `journalctl -k` is the better habit: it is the same messages, it needs no | |
| 305 | + | privilege, and it survives a reboot. | |
| 306 | + | ||
| 307 | + | ### A program crashed and left no core file | |
| 308 | + | ||
| 309 | + | Alloy writes no cores. `kernel.core_pattern` is pointed at `/bin/false` and the | |
| 310 | + | hard limit is zero. A core is the process's memory written to disk, including | |
| 311 | + | whatever it had decrypted, which on this machine is keys and tokens. | |
| 312 | + | ||
| 313 | + | Getting one back takes root twice over, which is the intended friction: the | |
| 314 | + | pattern is a system setting, and the per-process hard limit is zero, so raising | |
| 315 | + | it needs `CAP_SYS_RESOURCE`. | |
| 316 | + | ||
| 317 | + | run0 sysctl -w kernel.core_pattern=core | |
| 318 | + | run0 bash -c 'ulimit -c unlimited; ./the-program-that-crashes' | |
| 319 | + | ||
| 320 | + | The core lands in the working directory, owned by root, and the program ran as | |
| 321 | + | root to produce it. Reboot when you are done, or set the pattern back by hand. | |
| 322 | + | `coredumpctl` is not the path here; `systemd-coredump` is not installed. | |
| 323 | + | ||
| 324 | + | ### modprobe refuses a module that exists | |
| 325 | + | ||
| 326 | + | A short list of filesystem drivers, legacy network protocols and the FireWire | |
| 327 | + | stack cannot be loaded at all, rather than merely being blocked from | |
| 328 | + | autoloading. The list and the reason for each entry are in | |
| 329 | + | `/usr/lib/modprobe.d/50-alloy-blacklist.conf`. | |
| 330 | + | ||
| 331 | + | If you need one back, delete its line from that file in your image checkout and | |
| 332 | + | rebuild. Editing it on the running machine does not last: `/usr/lib` is replaced | |
| 333 | + | on every update. | |
| 334 | + | ||
| 335 | + | Attaching a debugger is unaffected. `gdb -p` and `perf record -p` against your | |
| 336 | + | own processes still work; Alloy deliberately did not take the setting that | |
| 337 | + | would have stopped them. | |
| 338 | + | ||
| 339 | + | --- | |
| 340 | + | ||
| 271 | 341 | ## Reporting something | |
| 272 | 342 | ||
| 273 | 343 | Alloy is pre-v0 and this manual describes a moving system. If something here is |
| @@ -16,3 +16,32 @@ | |||
| 16 | 16 | # does, from the panel's own EDID (crates/alloy/src/display.rs, `detect_panel`), | |
| 17 | 17 | # so the machine's scale is written where its owner can change it rather than | |
| 18 | 18 | # into a root-owned file naming one laptop. | |
| 19 | + | ||
| 20 | + | # --------------------------------------------------------------------- | |
| 21 | + | # Xwayland off. Ruled by Max 2026-08-21. | |
| 22 | + | # | |
| 23 | + | # X11 has no isolation between clients: any X client can read every other | |
| 24 | + | # client's keystrokes, window contents and clipboard, because the protocol | |
| 25 | + | # was designed when that was the point. Running an X server inside a Wayland | |
| 26 | + | # session hands that model back to anything that asks for it, and an X11 | |
| 27 | + | # client is a common thing for a browser extension, an Electron app or a | |
| 28 | + | # random AppImage to be. | |
| 29 | + | # | |
| 30 | + | # WHAT BREAKS, plainly: Steam, most Electron applications, some | |
| 31 | + | # screen-sharing paths, older Java toolkits, and anything shipping an X11-only | |
| 32 | + | # binary. Wayland-native software is untouched. | |
| 33 | + | # | |
| 34 | + | # That breakage is the intent rather than a cost to be mitigated. Alloy is a | |
| 35 | + | # Wayland-only session (docs/STACK.md) and has been since the pivot; an | |
| 36 | + | # Electron app declining to run is the stack being honest about what it is. | |
| 37 | + | # | |
| 38 | + | # THE WAY BACK is this line. Delete it, or set `xwayland enable` in | |
| 39 | + | # ~/.config/sway/config.d/, and the next session has an X server again — a | |
| 40 | + | # user's file wins over this directory by the include order documented at the | |
| 41 | + | # top of this file. The package stays in the image precisely so that works; | |
| 42 | + | # removing it would make the escape hatch a rebuild. It is a few megabytes and | |
| 43 | + | # it buys a supported answer for the person who needs Steam. | |
| 44 | + | # | |
| 45 | + | # Rationale and the review this came out of: wiki `alloy-hardening-posture`. | |
| 46 | + | # --------------------------------------------------------------------- | |
| 47 | + | xwayland disable |
| @@ -33,9 +33,49 @@ | |||
| 33 | 33 | # retyping a byte. | |
| 34 | 34 | # --------------------------------------------------------------------- | |
| 35 | 35 | ||
| 36 | + | # --------------------------------------------------------------------- | |
| 37 | + | # Kernel hardening arguments (KSPP), added 2026-08-21. | |
| 38 | + | # | |
| 39 | + | # The self-protection settings that can only be asked for on the command | |
| 40 | + | # line. Their sysctl-shaped siblings live in | |
| 41 | + | # /usr/lib/sysctl.d/90-alloy-hardening.conf; rationale for the whole set is | |
| 42 | + | # wiki `alloy-hardening-posture`. | |
| 43 | + | # | |
| 44 | + | # `init_on_alloc=1` is deliberately absent: Fedora's kernel is built with | |
| 45 | + | # CONFIG_INIT_ON_ALLOC_DEFAULT_ON, so setting it here would state a default | |
| 46 | + | # twice and imply the other half is off when it is not. | |
| 47 | + | # | |
| 48 | + | # `lockdown=confidentiality` is also absent, and that is an open decision | |
| 49 | + | # rather than an omission (GoingsOn alloy `17489bac`). It costs hibernation | |
| 50 | + | # and unsigned module loading, and the aarch64 NVIDIA work is exactly the | |
| 51 | + | # case that would collide with it. | |
| 52 | + | # | |
| 53 | + | # The two with a measurable throughput cost are `init_on_free` and | |
| 54 | + | # `slab_nomerge`, and this machine is also a build host. If a build gets | |
| 55 | + | # slower after this lands, these are the lines to measure rather than the | |
| 56 | + | # ones to quietly drop. | |
| 57 | + | # --------------------------------------------------------------------- | |
| 58 | + | ||
| 36 | 59 | kargs = [ | |
| 37 | 60 | "quiet", | |
| 38 | 61 | "loglevel=3", | |
| 62 | + | # Free memory is wiped on release, so a use-after-free reads zeroes rather | |
| 63 | + | # than the previous occupant's data. | |
| 64 | + | "init_on_free=1", | |
| 65 | + | # Slab caches of the same size stay separate, so an overflow in one object | |
| 66 | + | # type cannot land on another type's object. | |
| 67 | + | "slab_nomerge", | |
| 68 | + | # The free lists start shuffled, so heap layout is not a constant an | |
| 69 | + | # exploit can assume. | |
| 70 | + | "page_alloc.shuffle=1", | |
| 71 | + | # A random offset per syscall on the kernel stack. | |
| 72 | + | "randomize_kstack_offset=on", | |
| 73 | + | # The legacy vsyscall page is a fixed-address, executable mapping in every | |
| 74 | + | # process. Nothing built this decade needs it; a pre-2013 glibc binary does. | |
| 75 | + | "vsyscall=none", | |
| 76 | + | # debugfs exposes kernel internals to root and is a recurring source of | |
| 77 | + | # information leaks. Nothing in this image reads it. | |
| 78 | + | "debugfs=off", | |
| 39 | 79 | "vt.default_red=@{vt.red}", | |
| 40 | 80 | "vt.default_grn=@{vt.grn}", | |
| 41 | 81 | "vt.default_blu=@{vt.blu}", |