diff --git a/bin/omarchy-hibernation-setup b/bin/omarchy-hibernation-setup index 7bc57246..1aeb0d75 100755 --- a/bin/omarchy-hibernation-setup +++ b/bin/omarchy-hibernation-setup @@ -30,6 +30,35 @@ MKINITCPIO_CONF="/etc/mkinitcpio.conf.d/omarchy_resume.conf" SWAP_FILE="/swap/swapfile" RESUME_DROP_IN="/etc/limine-entry-tool.d/resume.conf" +install_root_file() { + local source="$1" + local destination="$2" + local mode="$3" + local stage + + stage=$(sudo /usr/bin/mktemp -- "${destination%/*}/.${destination##*/}.omarchy.XXXXXX") || return 1 + safe_stage_path "$stage" "$destination" || return 1 + + if sudo /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage" && + sudo /usr/bin/mv -Tf -- "$stage" "$destination"; then + return 0 + else + safe_stage_path "$stage" "$destination" && sudo /usr/bin/rm -f -- "$stage" + return 1 + fi +} + +safe_stage_path() { + local stage="$1" + local destination="$2" + local prefix suffix + + prefix="${destination%/*}/.${destination##*/}.omarchy." + [[ $stage == "$prefix"* ]] || return 1 + suffix=${stage#"$prefix"} + [[ $suffix =~ ^[[:alnum:]]{6}$ ]] +} + # Check if hibernation is already configured if [[ -f $MKINITCPIO_CONF ]] && grep -q "^HOOKS+=(resume)$" "$MKINITCPIO_CONF"; then # Fix empty resume_offset if btrfs map-swapfile failed during initial setup @@ -83,14 +112,20 @@ if ! swapon --show | grep -q "$SWAP_FILE"; then sudo swapon -p 0 "$SWAP_FILE" fi +# Ensure keyboard backlight doesn't prevent sleep +# Install this before writing the resume marker so a failed install remains +# retryable through the normal setup command. +if ! install_root_file "$OMARCHY_PATH/default/systemd/system-sleep/keyboard-backlight" \ + /usr/lib/systemd/system-sleep/keyboard-backlight 0755; then + echo "Could not install the keyboard-backlight system-sleep hook" >&2 + exit 1 +fi + # Add resume hook to mkinitcpio sudo mkdir -p /etc/mkinitcpio.conf.d echo "Adding resume hook to $MKINITCPIO_CONF" echo "HOOKS+=(resume)" | sudo tee "$MKINITCPIO_CONF" >/dev/null -# Ensure keyboard backlight doesn't prevent sleep -sudo cp -p "$OMARCHY_PATH/default/systemd/system-sleep/keyboard-backlight" /usr/lib/systemd/system-sleep/ - # Add resume= kernel parameters so the initramfs resume hook knows where to find the # hibernation image. Without these, resume happens late (after GPU drivers load) and fails. if [[ ! -f $RESUME_DROP_IN ]]; then diff --git a/bin/omarchy-toggle-hybrid-gpu b/bin/omarchy-toggle-hybrid-gpu index aa7c0582..835c007b 100755 --- a/bin/omarchy-toggle-hybrid-gpu +++ b/bin/omarchy-toggle-hybrid-gpu @@ -3,6 +3,35 @@ # omarchy:summary=Toggle dedicated vs integrated GPU mode via supergfxd (for hybrid gpu laptops, like Asus G14). # omarchy:requires-sudo=true +install_root_file() { + local source="$1" + local destination="$2" + local mode="$3" + local stage + + stage=$(sudo /usr/bin/mktemp -- "${destination%/*}/.${destination##*/}.omarchy.XXXXXX") || return 1 + safe_stage_path "$stage" "$destination" || return 1 + + if sudo /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage" && + sudo /usr/bin/mv -Tf -- "$stage" "$destination"; then + return 0 + else + safe_stage_path "$stage" "$destination" && sudo /usr/bin/rm -f -- "$stage" + return 1 + fi +} + +safe_stage_path() { + local stage="$1" + local destination="$2" + local prefix suffix + + prefix="${destination%/*}/.${destination##*/}.omarchy." + [[ $stage == "$prefix"* ]] || return 1 + suffix=${stage#"$prefix"} + [[ $suffix =~ ^[[:alnum:]]{6}$ ]] +} + if omarchy-cmd-missing supergfxctl; then omarchy-pkg-add supergfxctl @@ -54,18 +83,31 @@ case "$gpu_mode" in ;; "Hybrid") if gum confirm "Use only integrated GPU and reboot?"; then - # Switch to integrated mode and ensure vfio is enabled (needed for sleep/wake trick) - sudo sed -i "s/\"mode\": \".*\"/\"mode\": \"Integrated\"/" /etc/supergfxd.conf - sudo sed -i 's/"vfio_enable": false/"vfio_enable": true/' /etc/supergfxd.conf - - # Force igpu mode after system sleep (or dgpu could get activated) - sudo mkdir -p /usr/lib/systemd/system-sleep - sudo cp -p "$OMARCHY_PATH/default/systemd/system-sleep/force-igpu" /usr/lib/systemd/system-sleep/ - # Delay supergfxd startup to avoid race condition with display manager # that can cause system freeze when booting in Integrated mode sudo mkdir -p /etc/systemd/system/supergfxd.service.d - sudo cp -p "$OMARCHY_PATH/default/systemd/system/supergfxd.service.d/delay-start.conf" /etc/systemd/system/supergfxd.service.d/ + if ! install_root_file "$OMARCHY_PATH/default/systemd/system/supergfxd.service.d/delay-start.conf" \ + /etc/systemd/system/supergfxd.service.d/delay-start.conf 0644; then + echo "Could not install the supergfxd startup-delay override" >&2 + exit 1 + fi + + # Publish the self-guarding sleep hook before enabling Integrated mode. It + # remains inert while the config says Hybrid, so any failed step is safe to + # retry without leaving the GPU config partially switched. + sudo mkdir -p /usr/lib/systemd/system-sleep + if ! install_root_file "$OMARCHY_PATH/default/systemd/system-sleep/force-igpu" \ + /usr/lib/systemd/system-sleep/force-igpu 0755; then + echo "Could not install the force-igpu system-sleep hook" >&2 + exit 1 + fi + + # Switch both settings in one atomic config rewrite only after every + # supporting file has been installed successfully. + sudo sed -i \ + -e 's/"mode": ".*"/"mode": "Integrated"/' \ + -e 's/"vfio_enable": false/"vfio_enable": true/' \ + /etc/supergfxd.conf omarchy-system-reboot fi diff --git a/default/systemd/system-sleep/force-igpu b/default/systemd/system-sleep/force-igpu index 6f129438..8568067e 100644 --- a/default/systemd/system-sleep/force-igpu +++ b/default/systemd/system-sleep/force-igpu @@ -1,29 +1,65 @@ #!/bin/bash +set -e + # Use the Vfio to Integrated trick to turn off NVIDIA dgpu when in integrated mode # without needing to restart the computer. This is needed because computers like the Asus G14 # will wake after suspend in Hybrid mode, even if the system was in Integrated mode before # suspending. +restore_marker=/run/omarchy-force-igpu-integrated +sleep_action=${SYSTEMD_SLEEP_ACTION:-$2} +[[ -x /usr/bin/supergfxctl ]] || exit 0 + +switch_mode() { + local expected="$1" current + + if ! /usr/bin/timeout --kill-after=1s 3s /usr/bin/supergfxctl -m "$expected"; then + echo "Could not request the GPU transition to $expected mode" >&2 + return 1 + fi + for _ in {1..10}; do + if current=$(/usr/bin/timeout --kill-after=1s 2s /usr/bin/supergfxctl -g 2>/dev/null) && + [[ $current == "$expected" ]]; then + return 0 + fi + sleep 1 + done + + echo "Could not confirm the GPU transition to $expected mode" >&2 + return 1 +} + case "$1" in pre) + # Remember the mode this sleep cycle started in. supergfxctl persists the + # temporary hibernate switch to Vfio, so post must not consult that mutable + # value when deciding whether to restore Integrated mode. + if [[ -L $restore_marker ]]; then + exit 1 + elif [[ ! -f $restore_marker ]]; then + /usr/bin/grep -Eq '"mode"[[:space:]]*:[[:space:]]*"Integrated"' /etc/supergfxd.conf 2>/dev/null || exit 0 + /usr/bin/install -m 0600 -o root -g root -T /dev/null "$restore_marker" + fi + # Before hibernating, switch to Vfio so the nvidia driver is detached from the dGPU. # Without this, hibernate resume fails because the nvidia driver can't freeze a # powered-off dGPU (returns -EIO), which aborts the entire resume. - if [[ $2 == "hibernate" ]]; then - /usr/bin/supergfxctl -m Vfio - sleep 1 + if [[ $sleep_action == "hibernate" ]]; then + switch_mode Vfio fi ;; post) + [[ -f $restore_marker && ! -L $restore_marker ]] || exit 0 + # small delay so the device is fully re-enumerated sleep 4 # force-bind dGPU to vfio (fully detached from nvidia) - /usr/bin/supergfxctl -m Vfio - sleep 1 + switch_mode Vfio # then go back to Integrated, which powers it off again - /usr/bin/supergfxctl -m Integrated + switch_mode Integrated + /usr/bin/rm -f -- "$restore_marker" ;; esac diff --git a/default/systemd/system-sleep/keyboard-backlight b/default/systemd/system-sleep/keyboard-backlight index c6fbea1c..014ab4a8 100644 --- a/default/systemd/system-sleep/keyboard-backlight +++ b/default/systemd/system-sleep/keyboard-backlight @@ -3,7 +3,9 @@ # Turn off keyboard backlight before hibernate to prevent hang on power-off. # The ASUS keyboard controller can block S4 shutdown if LEDs are active. -if [[ $1 == "pre" && $2 == "hibernate" ]]; then +sleep_action=${SYSTEMD_SLEEP_ACTION:-$2} + +if [[ $1 == "pre" && $sleep_action == "hibernate" ]]; then device="" for candidate in /sys/class/leds/*kbd_backlight*; do if [[ -e "$candidate" ]]; then diff --git a/docs/file-layout.md b/docs/file-layout.md index 24c21af5..4d4e0da0 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -124,8 +124,7 @@ default/** ──► omarchy-settings /usr/share/omarchy ├─ applications/mimeapps.list /usr/share/applications/mimeapps.list ├─ systemd/user/*.service /usr/lib/systemd/user/ ├─ systemd/user/app.slice.d/10-oomd.conf /usr/lib/systemd/user/app.slice.d/ - ├─ systemd/system-sleep/{force-igpu, - │ keyboard-backlight,unmount-fuse} /usr/lib/systemd/system-sleep/ + ├─ systemd/system-sleep/unmount-fuse /usr/lib/systemd/system-sleep/ ├─ systemd/zram-generator.conf.d/90-omarchy.conf /usr/lib/systemd/zram-generator.conf.d/ ├─ fonts/omarchy/omarchy.ttf /usr/share/fonts/omarchy/ ├─ sddm/omarchy/ /usr/share/sddm/themes/omarchy/ @@ -139,6 +138,8 @@ logo.{txt,svg}, icon.{txt,png} ──► omarchy-settings /usr/share/omarchy /etc/skel/.config/omarchy/branding/{about,screensaver}.txt ``` +The hardware-conditional `force-igpu` and `keyboard-backlight` sources also live under `default/systemd/system-sleep/`, but their setup commands publish root-owned copies only on machines that need them; they are not installed by `omarchy-settings`. + ### Why `etc-overrides/` exists Some files under `/etc/` (`.bashrc` in `/etc/skel`, `nsswitch.conf`, diff --git a/migrations/1788662350.sh b/migrations/1788662350.sh new file mode 100644 index 00000000..c0d4dbf2 --- /dev/null +++ b/migrations/1788662350.sh @@ -0,0 +1,303 @@ +echo "Repair user-owned system-sleep hooks and hybrid GPU service configuration" + +system_sleep_dir=/usr/lib/systemd/system-sleep +supergfxd_drop_in=/etc/systemd/system/supergfxd.service.d/delay-start.conf +quarantine_root=/var/lib/omarchy/migrations/1788662350-system-sleep +reload_needed_marker=/var/lib/omarchy/migrations/1788662350-systemd-reload-needed +keyboard_source="$OMARCHY_PATH/default/systemd/system-sleep/keyboard-backlight" +force_igpu_source="$OMARCHY_PATH/default/systemd/system-sleep/force-igpu" +supergfxd_source="$OMARCHY_PATH/default/systemd/system/supergfxd.service.d/delay-start.conf" +legacy_keyboard_sha256=f313a81e47401f0d38b8602e5997f52c5286d5e97f74027564ddd515b3d16511 +legacy_force_igpu_sha256=d604e7c4903829563e45fc52188fc5602c3f1bc66e247f0a2cc0a974ed6e57db + +as_root() { + if (( EUID == 0 )); then + "$@" + else + sudo "$@" + fi +} + +path_is_root_controlled() { + local path="$1" + local current=/ component candidate file_mode link metadata part status uid gid mode + local missing_depth=0 symlink_count=0 + local -a pending resolved link_components + + [[ $path == /* ]] || return 1 + IFS=/ read -r -a pending <<<"$path" + # A non-root group is harmless when neither it nor everyone else can write. + # Resolve symlinks component by component so an indirect link cannot hide an + # intermediate directory controlled by an unprivileged user. + metadata=$(path_metadata /) || return 1 + read -r file_mode uid gid mode <<<"$metadata" + (( uid == 0 && (8#$mode & 8#022) == 0 )) || return 1 + + while ((${#pending[@]})); do + component=${pending[0]} + pending=("${pending[@]:1}") + [[ -n $component ]] || continue + [[ $component == "." ]] && continue + + if [[ $component == ".." ]]; then + if ((${#resolved[@]})); then + unset 'resolved[-1]' + fi + + current=/ + for part in "${resolved[@]}"; do + if [[ $current == "/" ]]; then + current="/$part" + else + current="$current/$part" + fi + done + if (( missing_depth > 0 && ${#resolved[@]} < missing_depth )); then + missing_depth=0 + fi + continue + fi + + if [[ $current == "/" ]]; then + candidate="/$component" + else + candidate="$current/$component" + fi + + if (( missing_depth > 0 )); then + # The first missing component makes descendants inactive today, but keep + # consuming the lexical suffix. A later .. can escape back into an + # existing user-controlled path that would become active if an + # administrator creates the missing directory. + resolved+=("$component") + current=$candidate + continue + elif metadata=$(path_metadata "$candidate"); then + read -r file_mode uid gid mode <<<"$metadata" + else + status=$? + if (( status == 2 )); then + resolved+=("$component") + current=$candidate + missing_depth=${#resolved[@]} + continue + else + return 1 + fi + fi + if (( (16#$file_mode & 16#f000) == 16#a000 )); then + ((++symlink_count <= 40)) || return 1 + link=$(readlink_with_privilege "$candidate") || return 1 + IFS=/ read -r -a link_components <<<"$link" + pending=("${link_components[@]}" "${pending[@]}") + if [[ $link == /* ]]; then + resolved=() + current=/ + fi + continue + fi + + (( uid == 0 && (8#$mode & 8#022) == 0 )) || return 1 + resolved+=("$component") + current=$candidate + done +} + +path_metadata() { + local path="$1" + local metadata parent + + if /usr/bin/stat -c '%f %u %g %a' -- "$path" 2>/dev/null; then + return 0 + elif [[ ! -e $path && ! -L $path ]]; then + parent=${path%/*} + [[ -n $parent ]] || parent=/ + # Avoid asking for sudo for ordinary ENOENT. If the parent is searchable, + # the absence is conclusive; an inaccessible root-only chain still needs a + # privileged metadata check so safe administrator symlinks are preserved. + [[ -x $parent ]] && return 2 + if metadata=$(as_root /usr/bin/stat -c '%f %u %g %a' -- "$path" 2>/dev/null); then + printf '%s\n' "$metadata" + return 0 + elif as_root /usr/bin/test -x "$parent"; then + # The privileged probe could search the protected parent, so stat's + # failure identifies a target that does not exist yet. + return 2 + else + return 1 + fi + else + as_root /usr/bin/stat -c '%f %u %g %a' -- "$path" + fi +} + +readlink_with_privilege() { + local path="$1" + + if /usr/bin/readlink -- "$path" 2>/dev/null; then + return 0 + else + as_root /usr/bin/readlink -- "$path" + fi +} + +privileged_entry_is_safe() { + local path="$1" + + path_is_root_controlled "$path" +} + +file_matches_source() { + local source="$1" + local destination="$2" + + [[ -f $destination && ! -L $destination ]] || return 1 + + if [[ -r $destination ]]; then + /usr/bin/cmp -s -- "$source" "$destination" + else + as_root /usr/bin/cmp -s -- "$source" "$destination" + fi +} + +file_matches_sha256() { + local destination="$1" + local expected="$2" + local digest + + [[ -f $destination && ! -L $destination ]] || return 1 + if [[ -r $destination ]]; then + digest=$(/usr/bin/sha256sum -- "$destination") || return 1 + else + digest=$(as_root /usr/bin/sha256sum -- "$destination") || return 1 + fi + [[ ${digest%% *} == "$expected" ]] +} + +safe_stage_path() { + local stage="$1" + local destination="$2" + local prefix suffix + + prefix="${destination%/*}/.${destination##*/}.omarchy." + [[ $stage == "$prefix"* ]] || return 1 + suffix=${stage#"$prefix"} + [[ $suffix =~ ^[[:alnum:]]{6}$ ]] +} + +install_root_file() { + local source="$1" + local destination="$2" + local mode="$3" + local stage + + stage=$(as_root /usr/bin/mktemp -- "${destination%/*}/.${destination##*/}.omarchy.XXXXXX") || return 1 + safe_stage_path "$stage" "$destination" || return 1 + + if as_root /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage" && + as_root /usr/bin/mv -Tf -- "$stage" "$destination"; then + return 0 + else + safe_stage_path "$stage" "$destination" && as_root /usr/bin/rm -f -- "$stage" + return 1 + fi +} + +preserve_unsafe_customization() { + local path="$1" + local label="$2" + local backup_dir backup + + if ! as_root /usr/bin/install -d -m 0700 -o root -g root "$quarantine_root"; then + echo "Could not create the root-only system-sleep quarantine at $quarantine_root" >&2 + return 1 + fi + if ! backup_dir=$(as_root /usr/bin/mktemp -d -- "$quarantine_root/${label}.XXXXXX"); then + echo "Could not reserve a quarantine path for $path" >&2 + return 1 + fi + backup="$backup_dir/original" + + if as_root /usr/bin/cp -a --no-dereference -T -- "$path" "$backup"; then + printf '%s\n' "$backup" + else + as_root /usr/bin/rm -rf -- "$backup_dir" + echo "Could not preserve unsafe custom content from $path before repairing it" >&2 + return 1 + fi +} + +repair_unsafe_privileged_entry() { + local source="$1" + local destination="$2" + local mode="$3" + local label="$4" + local legacy_sha256="${5:-}" + local backup current_mode + + [[ -e $destination || -L $destination ]] || return 0 + [[ -f $destination || -L $destination ]] || return 0 + + if file_matches_source "$source" "$destination"; then + current_mode=$(/usr/bin/stat -c '%a' -- "$destination" 2>/dev/null) || + current_mode=$(as_root /usr/bin/stat -c '%a' -- "$destination") || return 1 + if privileged_entry_is_safe "$destination" && [[ $current_mode == "${mode#0}" ]]; then + return 0 + fi + elif [[ -n $legacy_sha256 ]] && file_matches_sha256 "$destination" "$legacy_sha256"; then + : + else + privileged_entry_is_safe "$destination" && return 0 + backup=$(preserve_unsafe_customization "$destination" "$label") || return 1 + fi + + if install_root_file "$source" "$destination" "$mode"; then + if [[ -n ${backup:-} ]]; then + echo "Preserved unsafe custom content from $destination at $backup for administrator review" >&2 + fi + else + if [[ -n ${backup:-} ]]; then + echo "Preserved unsafe custom content from $destination at $backup, but could not repair the active path" >&2 + fi + return 1 + fi +} + +# Replace rather than chown an unsafe destination: its current owner may have +# already changed the contents or kept a writable file descriptor open. The +# root-owned staging inode makes the final rename an atomic trust transition. +repair_unsafe_privileged_entry "$keyboard_source" \ + "$system_sleep_dir/keyboard-backlight" 0755 keyboard-backlight "$legacy_keyboard_sha256" + +force_igpu="$system_sleep_dir/force-igpu" +repair_unsafe_privileged_entry "$force_igpu_source" "$force_igpu" 0755 force-igpu "$legacy_force_igpu_sha256" + +systemd_reload_needed=false +if [[ -e $reload_needed_marker || -L $reload_needed_marker ]]; then + systemd_reload_needed=true +fi + +if [[ -e $supergfxd_drop_in || -L $supergfxd_drop_in ]]; then + if ! privileged_entry_is_safe "$supergfxd_drop_in"; then + # Replacing the drop-in and reloading systemd are one repair. Record the + # second half before changing the file so failure or interruption cannot + # be forgotten when a retry sees only the trusted replacement on disk. + if ! as_root /usr/bin/install -Dm0644 -o root -g root /dev/null "$reload_needed_marker"; then + echo "Could not persist the pending systemd reload for the repaired supergfxd configuration" >&2 + exit 1 + fi + systemd_reload_needed=true + repair_unsafe_privileged_entry "$supergfxd_source" "$supergfxd_drop_in" 0644 delay-start.conf + fi +fi + +if $systemd_reload_needed; then + if ! as_root /usr/bin/systemctl daemon-reload; then + echo "Could not reload systemd after repairing the supergfxd configuration; the migration will retry" >&2 + exit 1 + fi + if ! as_root /usr/bin/rm -f -- "$reload_needed_marker"; then + echo "Could not clear the pending systemd reload marker; the migration will retry" >&2 + exit 1 + fi +fi diff --git a/test/shell.d/system-sleep-ownership-migration-test.sh b/test/shell.d/system-sleep-ownership-migration-test.sh new file mode 100644 index 00000000..36d1c95c --- /dev/null +++ b/test/shell.d/system-sleep-ownership-migration-test.sh @@ -0,0 +1,705 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +migration="$ROOT/migrations/1788662350.sh" +test_tmp=$(mktemp -d -p /tmp) +trap 'rm -rf "$test_tmp"' EXIT + +mock_omarchy="$test_tmp/omarchy" +sleep_dir="$test_tmp/system-sleep" +systemd_dir="$test_tmp/systemd" +drop_in="$systemd_dir/supergfxd.service.d/delay-start.conf" +quarantine="$test_tmp/quarantine" +reload_needed_marker="$test_tmp/reload-needed" +migration_copy="$test_tmp/migration.sh" +stub_bin="$test_tmp/bin" +calls="$test_tmp/calls" + +mkdir -p "$mock_omarchy/default/systemd/system-sleep" \ + "$mock_omarchy/default/systemd/system/supergfxd.service.d" \ + "$sleep_dir" "${drop_in%/*}" "$stub_bin" +cp "$ROOT/default/systemd/system-sleep/keyboard-backlight" \ + "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" +cp "$ROOT/default/systemd/system-sleep/force-igpu" \ + "$mock_omarchy/default/systemd/system-sleep/force-igpu" +cp "$ROOT/default/systemd/system/supergfxd.service.d/delay-start.conf" \ + "$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf" + +[[ $(grep -Fxc 'system_sleep_dir=/usr/lib/systemd/system-sleep' "$migration") == 1 ]] || + fail "migration fixes one literal system-sleep directory" +[[ $(grep -Fxc 'supergfxd_drop_in=/etc/systemd/system/supergfxd.service.d/delay-start.conf' "$migration") == 1 ]] || + fail "migration fixes one literal supergfxd drop-in" + +sed \ + -e "s|system_sleep_dir=/usr/lib/systemd/system-sleep|system_sleep_dir=$sleep_dir|" \ + -e "s|supergfxd_drop_in=/etc/systemd/system/supergfxd.service.d/delay-start.conf|supergfxd_drop_in=$drop_in|" \ + -e "s|quarantine_root=/var/lib/omarchy/migrations/1788662350-system-sleep|quarantine_root=$quarantine|" \ + -e "s|/var/lib/omarchy/migrations/1788662350-systemd-reload-needed|$reload_needed_marker|" \ + -e "s|/usr/bin/stat|$stub_bin/stat|g" \ + -e "s|/usr/bin/readlink|$stub_bin/readlink|g" \ + "$migration" >"$migration_copy" + +cat >"$stub_bin/stat" <<'SH' +#!/bin/bash + +path=${!#} +if [[ :${INACCESSIBLE_AS_USER:-}: == *":$path:"* && ${FAKE_SUDO:-0} == 0 ]]; then + exit 13 +fi + +actual_file_mode=$(/usr/bin/stat -c '%f' -- "$path") || exit 1 +actual_mode=$(/usr/bin/stat -c '%a' -- "$path") || exit 1 + +if [[ :${FAKE_ROOT_DIRS:-}: == *":$path:"* ]]; then + uid=0 + gid=0 + mode=$(printf '%o' "$((8#$actual_mode & ~8#022))") +elif [[ :${FAKE_ROOT_FILES:-}: == *":$path:"* ]]; then + uid=0 + gid=${FAKE_ROOT_GID:-0} + mode=${FAKE_ROOT_MODE:-$actual_mode} +else + exec /usr/bin/stat "$@" +fi + +file_type=$((16#$actual_file_mode & 16#f000)) +file_mode=$(printf '%x' "$((file_type | 8#$mode))") + +case "$*" in + *"%f %u %g %a"*) printf '%s %s %s %s\n' "$file_mode" "$uid" "$gid" "$mode" ;; + *"%u %g %a"*) printf '%s %s %s\n' "$uid" "$gid" "$mode" ;; + *"%a"*) printf '%s\n' "$mode" ;; + *) exec /usr/bin/stat "$@" ;; +esac +SH + +cat >"$stub_bin/readlink" <<'SH' +#!/bin/bash + +path=${!#} +if [[ :${INACCESSIBLE_AS_USER:-}: == *":$path:"* && ${FAKE_SUDO:-0} == 0 ]]; then + exit 13 +fi + +exec /usr/bin/readlink "$@" +SH +chmod +x "$stub_bin/stat" "$stub_bin/readlink" + +cat >"$stub_bin/sudo" <<'SH' +#!/bin/bash + +set -euo pipefail + +printf 'sudo' >>"$CALLS" +printf '\t%s' "$@" >>"$CALLS" +printf '\n' >>"$CALLS" + +case "$1" in + */stat | */readlink) + FAKE_SUDO=1 exec "$@" + ;; + /usr/bin/test) + shift + if [[ $1 == "-x" && :${FAKE_ROOT_DIRS:-}: == *":$2:"* ]]; then + exit 0 + else + exec /usr/bin/test "$@" + fi + ;; + /usr/bin/mktemp | /usr/bin/mv | /usr/bin/chmod | /usr/bin/cp | /usr/bin/rm) + exec "$@" + ;; + /usr/bin/systemctl) + if [[ -n ${SYSTEMCTL_FAIL_ONCE_FILE:-} && -e $SYSTEMCTL_FAIL_ONCE_FILE ]]; then + /usr/bin/rm -f -- "$SYSTEMCTL_FAIL_ONCE_FILE" + exit 1 + fi + exit 0 + ;; + /usr/bin/install) + shift + args=() + while (($#)); do + case "$1" in + -o | -g) + shift 2 + ;; + *) + args+=("$1") + shift + ;; + esac + done + exec /usr/bin/install "${args[@]}" + ;; + *) + printf 'unexpected sudo command: %s\n' "$*" >&2 + exit 97 + ;; +esac +SH +chmod +x "$stub_bin/sudo" + +run_migration() { + local fake_root_dirs + + : >"$calls" + fake_root_dirs="/:/tmp:$test_tmp:$sleep_dir:$systemd_dir:${drop_in%/*}" + [[ -z ${EXTRA_FAKE_ROOT_DIRS:-} ]] || fake_root_dirs+=":$EXTRA_FAKE_ROOT_DIRS" + + CALLS="$calls" \ + FAKE_ROOT_DIRS="$fake_root_dirs" \ + FAKE_ROOT_FILES="${FAKE_ROOT_FILES:-${2:-}}" \ + FAKE_ROOT_MODE="${FAKE_ROOT_MODE:-${3:-}}" \ + FAKE_ROOT_GID="${FAKE_ROOT_GID:-0}" \ + INACCESSIBLE_AS_USER="${INACCESSIBLE_AS_USER:-}" \ + SYSTEMCTL_FAIL_ONCE_FILE="${SYSTEMCTL_FAIL_ONCE_FILE:-}" \ + OMARCHY_PATH="$mock_omarchy" \ + PATH="$stub_bin:$PATH" bash -euo pipefail "$migration_copy" >/dev/null +} + +printf 'attacker keyboard\n' >"$sleep_dir/keyboard-backlight" +printf 'attacker gpu\n' >"$sleep_dir/force-igpu" +printf 'attacker drop-in\n' >"$drop_in" +chmod 0777 "$sleep_dir/keyboard-backlight" "$sleep_dir/force-igpu" +chmod 0666 "$drop_in" +exec 9>>"$sleep_dir/keyboard-backlight" + +run_migration Integrated +printf 'write through stale attacker descriptor\n' >&9 +exec 9>&- + +cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" || + fail "migration replaces the user-owned keyboard hook with trusted content" +cmp -s "$mock_omarchy/default/systemd/system-sleep/force-igpu" "$sleep_dir/force-igpu" || + fail "migration replaces the user-owned GPU hook with trusted content" +cmp -s "$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf" "$drop_in" || + fail "migration replaces the user-owned root service drop-in with trusted content" +[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] || + fail "migration activates the repaired keyboard hook" +[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] || + fail "migration activates force-igpu only in Integrated mode" +[[ $(stat -c '%a' "$drop_in") == 644 ]] || + fail "migration installs the service drop-in as configuration" +grep -Fx $'sudo\t/usr/bin/systemctl\tdaemon-reload' "$calls" >/dev/null || + fail "migration reloads systemd after repairing its root service drop-in" +[[ ! -e $reload_needed_marker ]] || + fail "migration leaves a reload marker after systemd accepted the repaired drop-in" +[[ $(stat -c '%a' "$quarantine") == 700 ]] || + fail "migration keeps preserved unsafe custom content in a root-only directory" +keyboard_backup=$(find "$quarantine" -path '*/keyboard-backlight.*/original' -type f -print -quit) +force_backup=$(find "$quarantine" -path '*/force-igpu.*/original' -type f -print -quit) +drop_in_backup=$(find "$quarantine" -path '*/delay-start.conf.*/original' -type f -print -quit) +grep -Fxq 'attacker keyboard' "$keyboard_backup" || + fail "migration preserves unknown keyboard-hook content before replacing it" +grep -Fxq 'attacker gpu' "$force_backup" || + fail "migration preserves unknown force-iGPU content before replacing it" +grep -Fxq 'attacker drop-in' "$drop_in_backup" || + fail "migration preserves unknown service-drop-in content before replacing it" +pass "migration replaces writable privileged files with trusted root-owned copies" + +backup_count=$(find "$quarantine" -mindepth 2 -maxdepth 2 -name original | wc -l) +FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \ + run_migration Integrated +[[ ! -s $calls ]] || + fail "migration changes already-repaired privileged files on a second run" "$(<"$calls")" +[[ $(find "$quarantine" -mindepth 2 -maxdepth 2 -name original | wc -l) == "$backup_count" ]] || + fail "migration creates duplicate quarantines on a second run" +pass "migration is idempotent after repairing unsafe privileged files" + +printf 'attacker drop-in\n' >"$drop_in" +chmod 0666 "$drop_in" +reload_failure="$test_tmp/fail-systemd-reload-once" +touch "$reload_failure" +set +e +SYSTEMCTL_FAIL_ONCE_FILE="$reload_failure" \ + FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu" \ + run_migration Integrated +reload_status=$? +set -e +(( reload_status != 0 )) || + fail "migration reports success after systemd rejects the repaired drop-in" +cmp -s "$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf" "$drop_in" || + fail "migration does not repair the drop-in before the simulated reload failure" +[[ -e $reload_needed_marker && $(stat -c '%a' "$reload_needed_marker") == 644 ]] || + fail "migration does not persist the reload requirement before replacing the drop-in" + +FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \ + run_migration Integrated +grep -Fx $'sudo\t/usr/bin/systemctl\tdaemon-reload' "$calls" >/dev/null || + fail "migration does not retry a failed reload after the drop-in is already safe" +[[ ! -e $reload_needed_marker ]] || + fail "migration does not clear the reload requirement after a successful retry" + +FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \ + run_migration Integrated +[[ ! -s $calls ]] || + fail "migration repeats a successfully completed reload repair" "$(<"$calls")" +pass "migration persists and retries systemd reload after failure or interruption" + +keyboard_backup_count=$(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l) +cp "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \ + "$sleep_dir/keyboard-backlight" +chmod 0644 "$sleep_dir/keyboard-backlight" +FAKE_ROOT_FILES="$sleep_dir/force-igpu:$drop_in" run_migration Integrated +[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] || + fail "migration does not safely activate a user-owned canonical hook" +[[ $(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l) == "$keyboard_backup_count" ]] || + fail "migration quarantines an exact legacy artifact as administrator content" +pass "migration replaces exact vulnerable installer artifacts without inventing backups" + +legacy_keyboard="$test_tmp/legacy-keyboard-backlight" +cat >"$legacy_keyboard" <<'SH' +#!/bin/bash + +# Turn off keyboard backlight before hibernate to prevent hang on power-off. +# The ASUS keyboard controller can block S4 shutdown if LEDs are active. + +if [[ $1 == "pre" && $2 == "hibernate" ]]; then + device="" + for candidate in /sys/class/leds/*kbd_backlight*; do + if [[ -e "$candidate" ]]; then + device="$(basename "$candidate")" + break + fi + done + + if [[ -n "$device" ]]; then + brightnessctl -d "$device" set 0 >/dev/null 2>&1 + fi +fi +SH +[[ $(sha256sum "$legacy_keyboard" | cut -d' ' -f1) == f313a81e47401f0d38b8602e5997f52c5286d5e97f74027564ddd515b3d16511 ]] || + fail "keyboard-backlight legacy fixture no longer matches the migration fingerprint" +keyboard_backup_count=$(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l) +cp "$legacy_keyboard" "$sleep_dir/keyboard-backlight" +chmod 0644 "$sleep_dir/keyboard-backlight" +FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \ + run_migration Integrated +cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" || + fail "migration does not upgrade the released keyboard-backlight hook" +[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] || + fail "migration leaves the released keyboard-backlight hook non-executable" +[[ $(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l) == "$keyboard_backup_count" ]] || + fail "migration quarantines the released keyboard hook as administrator content" +pass "migration activates the released root-owned keyboard-backlight hook" + +cp "$legacy_keyboard" "$sleep_dir/keyboard-backlight" +chmod 0755 "$sleep_dir/keyboard-backlight" +FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \ + run_migration Integrated +cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" || + fail "migration mistakes executable released hook bytes for a current artifact" +pass "migration refreshes recognized legacy hook contents at the final mode" + +printf 'attacker gpu\n' >"$sleep_dir/force-igpu" +chmod 0777 "$sleep_dir/force-igpu" +run_migration Hybrid +[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] || + fail "migration does not activate the trusted self-guarding force-igpu hook" +pass "migration repairs force-igpu without depending on a live GPU-mode query" + +printf 'administrator customization\n' >"$sleep_dir/keyboard-backlight" +chmod 0755 "$sleep_dir/keyboard-backlight" +run_migration Integrated "$sleep_dir/keyboard-backlight" 755 +grep -Fxq 'administrator customization' "$sleep_dir/keyboard-backlight" || + fail "migration preserves a secure administrator-owned custom hook" + +cp "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" +chmod 0644 "$sleep_dir/keyboard-backlight" +run_migration Integrated "$sleep_dir/keyboard-backlight" 644 +[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] || + fail "migration leaves an exact packaged keyboard hook non-executable" + +printf 'administrator customization\n' >"$sleep_dir/keyboard-backlight" +chmod 0644 "$sleep_dir/keyboard-backlight" +run_migration Integrated "$sleep_dir/keyboard-backlight" 644 +[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 644 ]] || + fail "migration changes the mode of a safe noncanonical administrator hook" +grep -Fxq 'administrator customization' "$sleep_dir/keyboard-backlight" || + fail "migration replaces a safe noncanonical administrator hook" +pass "migration activates only exact packaged hooks while preserving safe custom files" + +legacy_force_igpu="$test_tmp/legacy-force-igpu" +cat >"$legacy_force_igpu" <<'SH' +#!/bin/bash + +# Use the Vfio to Integrated trick to turn off NVIDIA dgpu when in integrated mode +# without needing to restart the computer. This is needed because computers like the Asus G14 +# will wake after suspend in Hybrid mode, even if the system was in Integrated mode before +# suspending. + +case "$1" in + pre) + # Before hibernating, switch to Vfio so the nvidia driver is detached from the dGPU. + # Without this, hibernate resume fails because the nvidia driver can't freeze a + # powered-off dGPU (returns -EIO), which aborts the entire resume. + if [[ $2 == "hibernate" ]]; then + /usr/bin/supergfxctl -m Vfio + sleep 1 + fi + ;; + post) + # small delay so the device is fully re-enumerated + sleep 4 + + # force-bind dGPU to vfio (fully detached from nvidia) + /usr/bin/supergfxctl -m Vfio + sleep 1 + + # then go back to Integrated, which powers it off again + /usr/bin/supergfxctl -m Integrated + ;; +esac +SH +[[ $(sha256sum "$legacy_force_igpu" | cut -d' ' -f1) == d604e7c4903829563e45fc52188fc5602c3f1bc66e247f0a2cc0a974ed6e57db ]] || + fail "force-igpu legacy fixture no longer matches the migration fingerprint" +cp "$legacy_force_igpu" "$sleep_dir/force-igpu" +chmod 0644 "$sleep_dir/force-igpu" +FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \ + run_migration Integrated +cmp -s "$mock_omarchy/default/systemd/system-sleep/force-igpu" "$sleep_dir/force-igpu" || + fail "migration does not upgrade the exact legacy force-igpu hook" +[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] || + fail "migration leaves the exact legacy force-igpu hook non-executable" +pass "migration activates the exact legacy force-igpu artifact with its new guard" + +printf 'wheel-managed customization\n' >"$sleep_dir/keyboard-backlight" +chmod 0755 "$sleep_dir/keyboard-backlight" +FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \ + FAKE_ROOT_GID=10 run_migration Integrated +grep -Fxq 'wheel-managed customization' "$sleep_dir/keyboard-backlight" || + fail "migration replaces a safe root:wheel administrator hook" +[[ ! -s $calls ]] || + fail "migration escalates while preserving safe root:wheel entries" +pass "migration treats non-writable root-owned files as safe regardless of group" + +admin_dir="$test_tmp/admin-hooks" +admin_keyboard="$admin_dir/keyboard" +admin_delay="$admin_dir/delay.conf" +mkdir -p "$admin_dir" +printf 'protected keyboard customization\n' >"$admin_keyboard" +printf 'protected delay customization\n' >"$admin_delay" +chmod 0755 "$admin_keyboard" +chmod 0644 "$admin_delay" +rm -f "$sleep_dir/keyboard-backlight" "$drop_in" +ln -s "$admin_keyboard" "$sleep_dir/keyboard-backlight" +ln -s "$admin_delay" "$drop_in" + +EXTRA_FAKE_ROOT_DIRS="$admin_dir" \ + FAKE_ROOT_FILES="$admin_keyboard:$admin_delay:$sleep_dir/force-igpu" \ + FAKE_ROOT_GID=10 run_migration Integrated +[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$admin_keyboard" ]] || + fail "migration replaces a safe administrator-managed keyboard-hook symlink" +[[ -L $drop_in && $(readlink "$drop_in") == "$admin_delay" ]] || + fail "migration replaces a safe administrator-managed service-drop-in symlink" +[[ ! -s $calls ]] || + fail "migration escalates while preserving safe administrator symlinks" +pass "migration preserves symlinks whose full target paths are root-controlled" + +dangling_target="$admin_dir/future-keyboard" +rm -f "$sleep_dir/keyboard-backlight" "$dangling_target" +ln -s "$dangling_target" "$sleep_dir/keyboard-backlight" + +EXTRA_FAKE_ROOT_DIRS="$admin_dir" \ + FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \ + run_migration Integrated +[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$dangling_target" ]] || + fail "migration replaces a safe dangling administrator symlink" +[[ ! -s $calls ]] || + fail "migration asks for sudo to verify an absent target below a searchable root-controlled directory" +pass "migration handles safe dangling administrator symlinks without sudo" + +escaping_user_dir="$test_tmp/escaping-user-hooks" +escaping_user_hook="$escaping_user_dir/keyboard" +escaping_missing_dir="$admin_dir/future" +escaping_target="$escaping_missing_dir/../../escaping-user-hooks/keyboard" +mkdir -p "$escaping_user_dir" +printf 'future unsafe keyboard customization\n' >"$escaping_user_hook" +chmod 0755 "$escaping_user_hook" +rm -f "$sleep_dir/keyboard-backlight" +ln -s "$escaping_target" "$sleep_dir/keyboard-backlight" + +EXTRA_FAKE_ROOT_DIRS="$admin_dir" \ + FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \ + run_migration Integrated +[[ ! -L $sleep_dir/keyboard-backlight ]] || + fail "migration trusts a dangling symlink whose unresolved suffix escapes to a user-controlled path" +cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \ + "$sleep_dir/keyboard-backlight" || + fail "migration does not replace a future user-controlled dangling symlink" +pass "migration resolves the full dangling-symlink suffix before trusting it" + +protected_dir="$test_tmp/root-only-hooks" +protected_target="$protected_dir/target" +protected_bridge="$protected_dir/bridge" +mkdir -p "$protected_dir" +printf 'root-only administrator customization\n' >"$protected_target" +ln -s "$protected_target" "$protected_bridge" +chmod 0700 "$protected_dir" +rm -f "$sleep_dir/keyboard-backlight" +ln -s "$protected_bridge" "$sleep_dir/keyboard-backlight" + +EXTRA_FAKE_ROOT_DIRS="$admin_dir:$protected_dir" \ + FAKE_ROOT_FILES="$protected_target:$admin_delay:$sleep_dir/force-igpu:$drop_in" \ + INACCESSIBLE_AS_USER="$protected_bridge:$protected_target" \ + run_migration Integrated +[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$protected_bridge" ]] || + fail "migration replaces a safe symlink whose target is hidden by a root-only directory" +grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-hooks/bridge$' "$calls" || + fail "migration does not inspect inaccessible symlink metadata with privilege" +grep -q $'^sudo\t.*/readlink\t--\t.*/root-only-hooks/bridge$' "$calls" || + fail "migration does not resolve an inaccessible administrator symlink with privilege" +grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-hooks/target$' "$calls" || + fail "migration does not inspect an inaccessible administrator target with privilege" +pass "migration preserves root-controlled symlink chains hidden from the invoking user" + +protected_dangling_dir="$test_tmp/root-only-dangling" +protected_dangling_target="$protected_dangling_dir/future-keyboard" +mkdir -p "$protected_dangling_dir" +chmod 0000 "$protected_dangling_dir" +rm -f "$sleep_dir/keyboard-backlight" +ln -s "$protected_dangling_target" "$sleep_dir/keyboard-backlight" + +EXTRA_FAKE_ROOT_DIRS="$admin_dir:$protected_dangling_dir" \ + FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \ + INACCESSIBLE_AS_USER="$protected_dangling_target" \ + run_migration Integrated +[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$protected_dangling_target" ]] || + fail "migration replaces a safe dangling symlink below a root-only directory" +grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-dangling/future-keyboard$' "$calls" || + fail "migration does not inspect a protected dangling target with privilege" +grep -q $'^sudo\t/usr/bin/test\t-x\t.*/root-only-dangling$' "$calls" || + fail "migration does not distinguish a protected missing target from an inaccessible parent" +pass "migration preserves dangling administrator symlinks below root-only directories" + +user_dir="$test_tmp/user-hooks" +user_keyboard="$user_dir/keyboard" +mkdir -p "$user_dir" +printf 'unsafe symlink customization\n' >"$user_keyboard" +chmod 0755 "$user_keyboard" +rm -f "$sleep_dir/keyboard-backlight" +ln -s "$user_keyboard" "$sleep_dir/keyboard-backlight" + +EXTRA_FAKE_ROOT_DIRS="$admin_dir" \ + FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu" run_migration Integrated +[[ ! -L $sleep_dir/keyboard-backlight ]] || + fail "migration leaves a user-controlled keyboard-hook symlink active" +cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \ + "$sleep_dir/keyboard-backlight" || + fail "migration does not replace an unsafe symlink with trusted hook content" +symlink_backup=$(find "$quarantine" -path '*/keyboard-backlight.*/original' -type l -print -quit) +[[ -n $symlink_backup && $(readlink "$symlink_backup") == "$user_keyboard" ]] || + fail "migration discards an unsafe custom symlink instead of preserving it" +pass "migration quarantines unsafe symlinks outside the active systemd directory" + +bridge="$user_dir/bridge" +ln -s "$admin_keyboard" "$bridge" +rm -f "$sleep_dir/keyboard-backlight" +ln -s "$bridge" "$sleep_dir/keyboard-backlight" + +EXTRA_FAKE_ROOT_DIRS="$admin_dir" \ + FAKE_ROOT_FILES="$admin_keyboard:$admin_delay:$sleep_dir/force-igpu" \ + run_migration Integrated +[[ ! -L $sleep_dir/keyboard-backlight ]] || + fail "migration trusts a symlink chain routed through a user-controlled directory" +cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \ + "$sleep_dir/keyboard-backlight" || + fail "migration does not repair an indirectly user-controlled symlink" +pass "migration checks every intermediate component in a symlink chain" + +hook_copy="$test_tmp/force-igpu-hook" +hook_calls="$test_tmp/force-igpu-calls" +hook_queries="$test_tmp/force-igpu-queries" +hook_config="$test_tmp/supergfxd.conf" +hook_marker="$test_tmp/force-igpu-restore" +hook_pending="$test_tmp/force-igpu-pending" +sed \ + -e "s|/usr/bin/supergfxctl|$stub_bin/hook-supergfxctl|g" \ + -e "s|/usr/bin/install|$stub_bin/hook-install|g" \ + -e "s|/etc/supergfxd.conf|$hook_config|g" \ + -e "s|/run/omarchy-force-igpu-integrated|$hook_marker|g" \ + "$ROOT/default/systemd/system-sleep/force-igpu" >"$hook_copy" +cat >"$stub_bin/hook-supergfxctl" <<'SH' +#!/bin/bash + +case "$1" in + -m) + printf '%s\n' "$*" >>"$HOOK_CALLS" + if [[ ${HOOK_BLOCK_MODE:-} == "$2" ]]; then + trap '' TERM + /usr/bin/sleep 30 + fi + current=$(sed -n 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$HOOK_CONFIG") + if [[ $current != "$2" ]]; then + printf '%s %s\n' "$2" "${HOOK_CONFIRM_AFTER:-1}" >"$HOOK_PENDING" + fi + ;; + -g) + printf '%s\n' "$*" >>"$HOOK_QUERIES" + if [[ -f $HOOK_PENDING ]]; then + read -r pending remaining <"$HOOK_PENDING" + if [[ ${HOOK_FAIL_MODE:-} != "$pending" ]]; then + remaining=$((remaining - 1)) + if (( remaining <= 0 )); then + sed -i "s/\"mode\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/\"mode\": \"$pending\"/" "$HOOK_CONFIG" + rm -f -- "$HOOK_PENDING" + else + printf '%s %s\n' "$pending" "$remaining" >"$HOOK_PENDING" + fi + fi + fi + sed -n 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$HOOK_CONFIG" + ;; +esac +SH +cat >"$stub_bin/hook-install" <<'SH' +#!/bin/bash +args=() +while (($#)); do + case "$1" in + -o | -g) + shift 2 + ;; + *) + args+=("$1") + shift + ;; + esac +done +exec /usr/bin/install "${args[@]}" +SH +cat >"$stub_bin/sleep" <<'SH' +#!/bin/bash +: +SH +chmod +x "$stub_bin/hook-supergfxctl" "$stub_bin/hook-install" "$stub_bin/sleep" + +hook_env=( + "HOOK_CALLS=$hook_calls" + "HOOK_QUERIES=$hook_queries" + "HOOK_CONFIG=$hook_config" + "HOOK_PENDING=$hook_pending" + "PATH=$stub_bin:$PATH" +) + +printf '{ "mode": "Hybrid" }\n' >"$hook_config" +env "${hook_env[@]}" bash "$hook_copy" pre suspend +env "${hook_env[@]}" bash "$hook_copy" post suspend +[[ ! -e $hook_calls ]] || fail "force-igpu runs while the root-owned config says Hybrid" +[[ ! -e $hook_marker ]] || fail "force-igpu records restore intent while configured for Hybrid mode" + +rm -f "$hook_config" +env "${hook_env[@]}" bash "$hook_copy" pre suspend +env "${hook_env[@]}" bash "$hook_copy" post suspend +[[ ! -e $hook_calls ]] || fail "force-igpu runs when its mode config is unavailable" +[[ ! -e $hook_marker ]] || fail "force-igpu records restore intent without a mode config" + +printf '{ "mode": "Integrated" }\n' >"$hook_config" +env "${hook_env[@]}" bash "$hook_copy" pre suspend +[[ -f $hook_marker && $(stat -c '%a' "$hook_marker") == 600 ]] || + fail "force-igpu does not securely record Integrated restore intent during pre-suspend" +HOOK_CONFIRM_AFTER=2 env "${hook_env[@]}" bash "$hook_copy" post suspend +[[ $(wc -l <"$hook_calls") == 2 ]] || + fail "force-igpu does not run both GPU transitions in Integrated mode" +grep -Fqx -- '-m Integrated' "$hook_calls" || + fail "force-igpu does not restore Integrated mode after suspend" +[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after suspend" +(( $(wc -l <"$hook_queries") >= 4 )) || + fail "force-igpu does not wait for asynchronous GPU transitions" +pass "force-igpu confirms asynchronous transitions for Integrated sleep cycles" + +: >"$hook_calls" +: >"$hook_queries" +printf '{ "mode": "Integrated" }\n' >"$hook_config" +env "${hook_env[@]}" bash "$hook_copy" pre suspend +set +e +HOOK_CONFIRM_AFTER=2 HOOK_FAIL_MODE=Integrated env "${hook_env[@]}" \ + bash "$hook_copy" post suspend >/dev/null 2>&1 +restore_status=$? +set -e +(( restore_status != 0 )) || fail "force-igpu reports success without confirming Integrated mode" +grep -Fq '"mode": "Vfio"' "$hook_config" || + fail "force-igpu failure test does not leave the transition in Vfio mode" +[[ -f $hook_marker ]] || fail "force-igpu discards restore intent after an asynchronous transition failure" +HOOK_CONFIRM_AFTER=2 env "${hook_env[@]}" bash "$hook_copy" post suspend +grep -Fq '"mode": "Integrated"' "$hook_config" || + fail "force-igpu does not recover the Integrated transition on the next sleep cycle" +[[ ! -e $hook_marker ]] || fail "force-igpu leaves restore intent after a confirmed retry" +pass "force-igpu retains restore intent until Integrated mode is confirmed" + +: >"$hook_calls" +: >"$hook_queries" +printf '{ "mode": "Integrated" }\n' >"$hook_config" +env "${hook_env[@]}" bash "$hook_copy" pre suspend +set +e +HOOK_BLOCK_MODE=Vfio env "${hook_env[@]}" \ + bash "$hook_copy" post suspend >/dev/null 2>&1 +blocked_request_status=$? +set -e +(( blocked_request_status != 0 )) || fail "force-igpu waits forever for a blocked GPU transition request" +[[ -f $hook_marker ]] || fail "force-igpu discards restore intent after a blocked transition request" +[[ ! -s $hook_queries ]] || fail "force-igpu polls before a blocked transition request returns" +env "${hook_env[@]}" bash "$hook_copy" post suspend +[[ ! -e $hook_marker ]] || fail "force-igpu cannot retry after a blocked transition request" +pass "force-igpu bounds blocked transition requests and retains retry intent" + +: >"$hook_calls" +printf '{ "mode": "Integrated" }\n' >"$hook_config" +env "${hook_env[@]}" bash "$hook_copy" pre hibernate +grep -Fq '"mode": "Vfio"' "$hook_config" || + fail "force-igpu test double does not model the pre-hibernate Vfio persistence" +[[ -f $hook_marker ]] || fail "force-igpu loses restore intent during the Vfio transition" +env "${hook_env[@]}" bash "$hook_copy" post hibernate +[[ $(wc -l <"$hook_calls") == 3 ]] || + fail "force-igpu skips the post-hibernate transitions after Vfio changes the config" +[[ $(tail -1 "$hook_calls") == "-m Integrated" ]] || + fail "force-igpu does not finish post-hibernate restoration in Integrated mode" +grep -Fq '"mode": "Integrated"' "$hook_config" || + fail "force-igpu leaves supergfxd configured for Vfio after hibernation" +[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after hibernation" +pass "force-igpu restores Integrated mode after pre-hibernate persists Vfio" + +: >"$hook_calls" +printf '{ "mode": "Integrated" }\n' >"$hook_config" +SYSTEMD_SLEEP_ACTION=suspend env "${hook_env[@]}" bash "$hook_copy" pre suspend-then-hibernate +SYSTEMD_SLEEP_ACTION=suspend env "${hook_env[@]}" bash "$hook_copy" post suspend-then-hibernate +[[ $(wc -l <"$hook_calls") == 2 ]] || + fail "force-igpu does not complete the initial suspend phase of suspend-then-hibernate" +SYSTEMD_SLEEP_ACTION=hibernate env "${hook_env[@]}" bash "$hook_copy" pre suspend-then-hibernate +[[ $(wc -l <"$hook_calls") == 3 && $(tail -1 "$hook_calls") == "-m Vfio" ]] || + fail "force-igpu skips the Vfio transition before compound hibernation" +grep -Fq '"mode": "Vfio"' "$hook_config" || + fail "force-igpu does not detach the dGPU during the hibernate phase" +[[ -f $hook_marker ]] || fail "force-igpu loses restore intent during compound hibernation" +SYSTEMD_SLEEP_ACTION=hibernate env "${hook_env[@]}" bash "$hook_copy" post suspend-then-hibernate +[[ $(wc -l <"$hook_calls") == 5 && $(tail -1 "$hook_calls") == "-m Integrated" ]] || + fail "force-igpu does not restore Integrated mode after compound hibernation" +grep -Fq '"mode": "Integrated"' "$hook_config" || + fail "force-igpu leaves supergfxd configured for Vfio after compound hibernation" +[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after compound hibernation" +pass "force-igpu handles both phases of suspend-then-hibernate" + +keyboard_hook_copy="$test_tmp/keyboard-backlight-hook" +keyboard_calls="$test_tmp/keyboard-backlight-calls" +keyboard_led_dir="$test_tmp/leds" +mkdir -p "$keyboard_led_dir/asus::kbd_backlight" +sed "s|/sys/class/leds/\*kbd_backlight\*|$keyboard_led_dir/*kbd_backlight*|" \ + "$ROOT/default/systemd/system-sleep/keyboard-backlight" >"$keyboard_hook_copy" +cat >"$stub_bin/brightnessctl" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >>"$KEYBOARD_CALLS" +SH +chmod +x "$stub_bin/brightnessctl" + +SYSTEMD_SLEEP_ACTION=suspend KEYBOARD_CALLS="$keyboard_calls" PATH="$stub_bin:$PATH" \ + bash "$keyboard_hook_copy" pre suspend-then-hibernate +[[ ! -e $keyboard_calls ]] || fail "keyboard-backlight runs during the suspend phase of compound sleep" +SYSTEMD_SLEEP_ACTION=hibernate KEYBOARD_CALLS="$keyboard_calls" PATH="$stub_bin:$PATH" \ + bash "$keyboard_hook_copy" pre suspend-then-hibernate +grep -Fqx -- '-d asus::kbd_backlight set 0' "$keyboard_calls" || + fail "keyboard-backlight skips the hibernate phase of compound sleep" +pass "keyboard-backlight handles the hibernate phase of suspend-then-hibernate" diff --git a/test/shell.d/unowned-system-paths-test.sh b/test/shell.d/unowned-system-paths-test.sh index f3bec23c..e2450619 100755 --- a/test/shell.d/unowned-system-paths-test.sh +++ b/test/shell.d/unowned-system-paths-test.sh @@ -132,3 +132,47 @@ if problems: PYTHON pass "no Omarchy script writes a path under /usr that no package owns" + +for script in bin/omarchy-hibernation-setup bin/omarchy-toggle-hybrid-gpu; do + grep -F '"${destination%/*}/.${destination##*/}.omarchy.XXXXXX"' "$ROOT/$script" >/dev/null || + fail "$script reserves a hidden sibling for the privileged replacement" + grep -F 'sudo /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage"' "$ROOT/$script" >/dev/null || + fail "$script prepares privileged files with final root ownership and mode" + grep -F 'sudo /usr/bin/mv -Tf -- "$stage" "$destination"' "$ROOT/$script" >/dev/null || + fail "$script atomically replaces the privileged destination" + if grep -F 'sudo /usr/bin/chmod "$mode" "$destination"' "$ROOT/$script" >/dev/null; then + fail "$script changes mode after publishing the privileged destination" + fi +done + +grep -F ' /usr/lib/systemd/system-sleep/keyboard-backlight 0755' "$ROOT/bin/omarchy-hibernation-setup" >/dev/null || + fail "hibernation setup installs keyboard-backlight as a root-owned executable" + +hook_install_line=$(rg -n '^if ! install_root_file .*keyboard-backlight' "$ROOT/bin/omarchy-hibernation-setup" | cut -d: -f1) +resume_marker_line=$(rg -n '^echo "HOOKS\+=\(resume\)"' "$ROOT/bin/omarchy-hibernation-setup" | cut -d: -f1) +[[ -n $hook_install_line && -n $resume_marker_line ]] || + fail "hibernation setup keeps recognizable hook-install and resume-marker steps" +(( hook_install_line < resume_marker_line )) || + fail "hibernation setup marks completion before a failed hook install can be retried" + +grep -F ' /usr/lib/systemd/system-sleep/force-igpu 0755' "$ROOT/bin/omarchy-toggle-hybrid-gpu" >/dev/null || + fail "hybrid GPU setup installs force-igpu as a root-owned executable" +grep -F ' /etc/systemd/system/supergfxd.service.d/delay-start.conf 0644' "$ROOT/bin/omarchy-toggle-hybrid-gpu" >/dev/null || + fail "hybrid GPU setup installs its root service drop-in as root-owned configuration" + +delay_install_line=$(rg -n '^ if ! install_root_file .*delay-start\.conf' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | cut -d: -f1) +force_install_line=$(rg -n '^ if ! install_root_file .*force-igpu' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | cut -d: -f1) +config_switch_line=$(rg -n '^ sudo sed -i \\' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | tail -1 | cut -d: -f1) +[[ -n $delay_install_line && -n $force_install_line && -n $config_switch_line ]] || + fail "hybrid GPU setup keeps recognizable support-file and config-switch steps" +(( delay_install_line < config_switch_line && force_install_line < config_switch_line )) || + fail "hybrid GPU setup switches config before every required file is installed" + +grep -Fq '/usr/bin/grep -Eq' "$ROOT/default/systemd/system-sleep/force-igpu" || + fail "force-igpu does not guard execution with the configured GPU mode" + +if rg -n 'cp -p.*(system-sleep|supergfxd\.service\.d)' "$ROOT/bin/omarchy-hibernation-setup" "$ROOT/bin/omarchy-toggle-hybrid-gpu"; then + fail "privileged sleep and hybrid GPU files are never copied with source ownership" +fi + +pass "system-sleep hooks and the hybrid GPU drop-in enforce root ownership"