diff --git a/bin/omarchy-dev-link b/bin/omarchy-dev-link index b09473b547d..1af65d1d464 100755 --- a/bin/omarchy-dev-link +++ b/bin/omarchy-dev-link @@ -92,7 +92,10 @@ done # Staged and parsed before anything is installed: a sudoers file sudo refuses to # read takes every rule after it down with it, including the %wheel grant, and # the password prompt needed to undo that is on the other side of the breakage. -staged_sudoers=$(mktemp) +# Keep the draft out of world-writable /tmp; visudo -cf reads it before install. +stage_dir="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}" +mkdir -m 700 -p "$stage_dir" +staged_sudoers=$(mktemp "$stage_dir/omarchy-dev-path.XXXXXX") trap 'rm -f "$staged_sudoers"' EXIT { diff --git a/bin/omarchy-hyprland-monitor-watch b/bin/omarchy-hyprland-monitor-watch index 33e8f2c5999..871dc115b82 100755 --- a/bin/omarchy-hyprland-monitor-watch +++ b/bin/omarchy-hyprland-monitor-watch @@ -3,8 +3,10 @@ # omarchy:summary=Watch Hyprland monitor events and recover monitor toggles when a monitor is removed SOCKET="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" -LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-monitor-clamshell.lock" -MODELESS_LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-monitor-modeless.lock" +LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}" +mkdir -m 700 -p "$LOCK_DIR" +LOCK="$LOCK_DIR/omarchy-monitor-clamshell.lock" +MODELESS_LOCK="$LOCK_DIR/omarchy-monitor-modeless.lock" sync_clamshell() { ( diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index 8305cb0fdd7..4e0cfa59763 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -72,10 +72,12 @@ if (( ${#image_dirs[@]} == 0 )); then exit 1 fi -selection_file=$(mktemp) -done_file=$(mktemp) -pending_file=$(mktemp) -pending_video_file=$(mktemp) +ipc_dir="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}" +mkdir -m 700 -p "$ipc_dir" +selection_file=$(mktemp "$ipc_dir/omarchy-menu-images.XXXXXX") +done_file=$(mktemp "$ipc_dir/omarchy-menu-images.XXXXXX") +pending_file=$(mktemp "$ipc_dir/omarchy-menu-images.XXXXXX") +pending_video_file=$(mktemp "$ipc_dir/omarchy-menu-images.XXXXXX") rm -f "$done_file" trap 'rm -f "$selection_file" "$done_file" "$pending_file" "$pending_video_file"' EXIT diff --git a/bin/omarchy-menu-input b/bin/omarchy-menu-input index b61f88e332d..8a2f6114b48 100755 --- a/bin/omarchy-menu-input +++ b/bin/omarchy-menu-input @@ -29,8 +29,10 @@ while (( $# > 0 )); do shift done -selection_file=$(mktemp) -done_file=$(mktemp) +ipc_dir="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}" +mkdir -m 700 -p "$ipc_dir" +selection_file=$(mktemp "$ipc_dir/omarchy-menu-input.XXXXXX") +done_file=$(mktemp "$ipc_dir/omarchy-menu-input.XXXXXX") rm -f "$done_file" trap 'rm -f "$selection_file" "$done_file"' EXIT diff --git a/bin/omarchy-menu-select b/bin/omarchy-menu-select index 1404665f30d..eafc706decf 100755 --- a/bin/omarchy-menu-select +++ b/bin/omarchy-menu-select @@ -67,8 +67,10 @@ if (( ${#options[@]} == 0 )); then exit 1 fi -selection_file=$(mktemp) -done_file=$(mktemp) +ipc_dir="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}" +mkdir -m 700 -p "$ipc_dir" +selection_file=$(mktemp "$ipc_dir/omarchy-menu-select.XXXXXX") +done_file=$(mktemp "$ipc_dir/omarchy-menu-select.XXXXXX") rm -f "$done_file" trap 'rm -f "$selection_file" "$done_file"' EXIT diff --git a/bin/omarchy-system-lock b/bin/omarchy-system-lock index 53e4ca5a0ef..6f5c16a4b07 100755 --- a/bin/omarchy-system-lock +++ b/bin/omarchy-system-lock @@ -12,11 +12,18 @@ hyprctl switchxkblayout all 0 > /dev/null 2>&1 # Ensure 1password is locked. Use timeout because `1password --lock` can # otherwise leave a full Electron helper tree running after each lock. +# Keep the flock out of world-writable /tmp: a held lock there used to make +# `flock -n || exit 0` skip `--lock`. If the private lock file cannot be +# created, still lock. if pgrep -x "1password" >/dev/null && omarchy-cmd-present 1password; then ( - flock -n 9 || exit 0 + lock_dir="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}" + lock_file="$lock_dir/omarchy-1password-lock.lock" + if mkdir -m 700 -p "$lock_dir" && exec 9>"$lock_file"; then + flock -n 9 || exit 0 + fi timeout --kill-after=1s 3s 1password --lock >/dev/null 2>&1 || true - ) 9>"${XDG_RUNTIME_DIR:-/tmp}/omarchy-1password-lock.lock" & + ) & fi # Avoid running screensaver when locked diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index 782b4ba7054..7a2bf7ac112 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -13,7 +13,9 @@ CURRENT_THEME_PATH="$HOME/.local/state/omarchy/current/theme" NEXT_THEME_PATH="$HOME/.local/state/omarchy/current/next-theme" CURRENT_BACKGROUND_LINK="$HOME/.local/state/omarchy/current/background" BACKGROUND_TRANSITION_CACHE="$HOME/.cache/omarchy/background-transitions" -THEME_SET_LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock" +THEME_SET_LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}" +mkdir -m 700 -p "$THEME_SET_LOCK_DIR" +THEME_SET_LOCK="$THEME_SET_LOCK_DIR/omarchy-theme-set.lock" USER_THEMES_PATH="$HOME/.config/omarchy/themes" OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes" diff --git a/test/shell.d/dev-link-test.sh b/test/shell.d/dev-link-test.sh index 748b3780fbb..bb55fa72e04 100644 --- a/test/shell.d/dev-link-test.sh +++ b/test/shell.d/dev-link-test.sh @@ -52,8 +52,26 @@ printf 'reboot\n' >>"$OMARCHY_DEV_LINK_TEST_LOG" SH chmod +x "$stub_bin/omarchy-system-reboot" +cat >"$stub_bin/realpath" <<'SH' +#!/bin/bash +while (( $# )); do + case "$1" in + -e|--canonicalize-existing) shift ;; + -*) shift ;; + *) break ;; + esac +done +[[ -n ${1:-} && -e $1 ]] || exit 1 +printf '%s\n' "$1" +SH +chmod +x "$stub_bin/realpath" + +runtime_dir="$test_tmp/runtime" +mkdir -m 700 -p "$runtime_dir" + run_link() { HOME="$test_tmp/home" \ + XDG_RUNTIME_DIR="$runtime_dir" \ OMARCHY_DEV_LINK_TEST_LOG="$log_file" \ OMARCHY_DEV_LINK_TEST_CONF="$conf_file" \ OMARCHY_DEV_LINK_TEST_SUDOERS="$sudoers_file" \ @@ -88,6 +106,18 @@ grep -Eq $'^sudo\tinstall\t-Dm440\t-o\troot\t-g\troot\t[^\t]+\t/etc/sudoers\\.d/ fail "dev link installs the drop-in root-owned and read-only" "$(cat "$log_file")" pass "dev link installs the drop-in root-owned and read-only" +staged_path=$(awk -F '\t' '$1 == "sudo" && $2 == "install" { print $(NF-1) }' "$log_file") +[[ $staged_path == "$runtime_dir"/omarchy-dev-path.* ]] || + fail "dev link stages sudoers under XDG_RUNTIME_DIR" "$(cat "$log_file")" +pass "dev link stages sudoers under XDG_RUNTIME_DIR" + +if grep -Fq 'staged_sudoers=$(mktemp)' "$ROOT/bin/omarchy-dev-link"; then + fail "dev link must not mktemp sudoers in the default /tmp" +fi +grep -Fq '${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}' "$ROOT/bin/omarchy-dev-link" || + fail "dev link falls back to a 0700 /tmp/omarchy-\$UID directory" +pass "dev link does not stage sudoers in world-writable /tmp" + visudo -cf "$sudoers_file" >/dev/null || fail "dev link writes a sudoers drop-in sudo can parse" "$(<"$sudoers_file")" pass "dev link writes a sudoers drop-in sudo can parse" diff --git a/test/shell.d/menu-ipc-path-test.sh b/test/shell.d/menu-ipc-path-test.sh new file mode 100755 index 00000000000..59e0b293deb --- /dev/null +++ b/test/shell.d/menu-ipc-path-test.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +require_command perl + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +runtime_dir="$tmpdir/runtime" +stub_bin="$tmpdir/bin" +ipc_log="$tmpdir/ipc.log" +mkdir -m 700 -p "$runtime_dir" "$stub_bin" + +cat >"$stub_bin/omarchy-shell" <<'SH' +#!/bin/bash +payload="${@: -1}" +perl -MJSON::PP=decode_json -e ' + my $p = decode_json($ARGV[0]); + my $sel = $p->{selectionFile}; + my $done = $p->{doneFile}; + die "missing ipc paths" unless defined $sel && defined $done; + if (defined $ENV{IPC_LOG}) { + open my $l, ">>", $ENV{IPC_LOG} or die $!; + print $l "$sel\n$done\n"; + close $l; + } + open my $s, ">", $sel or die $!; + print $s "picked\n"; + close $s; + open my $d, ">", $done or die $!; + close $d; +' "$payload" +SH +chmod +x "$stub_bin/omarchy-shell" + +for cmd in omarchy-menu-select omarchy-menu-input omarchy-menu-images; do + if grep -E '^selection_file=\$\(mktemp\)$' "$ROOT/bin/$cmd"; then + fail "$cmd must not mktemp in the default /tmp" + fi + grep -Fq '${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}' "$ROOT/bin/$cmd" || + fail "$cmd stages menu IPC under the runtime directory" +done +pass "menu helpers do not stage IPC files in world-writable /tmp" + +: >"$ipc_log" +PATH="$stub_bin:$PATH" XDG_RUNTIME_DIR="$runtime_dir" IPC_LOG="$ipc_log" \ + "$ROOT/bin/omarchy-menu-select" Prompt one >"$tmpdir/select.out" + +[[ $(<"$tmpdir/select.out") == picked ]] || + fail "menu-select still returns the chosen row" "$(<"$tmpdir/select.out")" +while IFS= read -r path; do + [[ $path == "$runtime_dir"/omarchy-menu-select.* ]] || + fail "menu-select IPC path is under XDG_RUNTIME_DIR" "$path" +done <"$ipc_log" +pass "menu-select stages IPC under XDG_RUNTIME_DIR" + +: >"$ipc_log" +PATH="$stub_bin:$PATH" XDG_RUNTIME_DIR="$runtime_dir" IPC_LOG="$ipc_log" \ + "$ROOT/bin/omarchy-menu-input" Reminder >"$tmpdir/input.out" + +[[ $(<"$tmpdir/input.out") == picked ]] || + fail "menu-input still returns the typed value" "$(<"$tmpdir/input.out")" +while IFS= read -r path; do + [[ $path == "$runtime_dir"/omarchy-menu-input.* ]] || + fail "menu-input IPC path is under XDG_RUNTIME_DIR" "$path" +done <"$ipc_log" +pass "menu-input stages IPC under XDG_RUNTIME_DIR" diff --git a/test/shell.d/monitor-recovery-test.sh b/test/shell.d/monitor-recovery-test.sh index 3401abc18da..ae6f386f937 100755 --- a/test/shell.d/monitor-recovery-test.sh +++ b/test/shell.d/monitor-recovery-test.sh @@ -23,6 +23,16 @@ grep -F 'flock -n 9' "$monitor_watch" >/dev/null grep -F 'omarchy-hyprland-monitor-clamshell' "$monitor_watch" >/dev/null pass "monitor watcher retries internal monitor recovery after removal" +if grep -Fq '${XDG_RUNTIME_DIR:-/tmp}/omarchy-monitor-clamshell.lock' "$monitor_watch" || + grep -Fq '${XDG_RUNTIME_DIR:-/tmp}/omarchy-monitor-modeless.lock' "$monitor_watch"; then + fail "monitor watcher flock files must not fall back to world-writable /tmp" +fi +grep -Fq '${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}' "$monitor_watch" || + fail "monitor watcher flock falls back to a 0700 /tmp/omarchy-\$UID directory" +grep -Fq 'mkdir -m 700 -p "$LOCK_DIR"' "$monitor_watch" || + fail "monitor watcher creates the flock directory with mode 0700" +pass "monitor watcher flock files are not in world-writable /tmp" + grep -F 'monitoradded\>\>*|monitoraddedv2\>\>*)' "$monitor_watch" >/dev/null grep -F 'omarchy-hyprland-monitor-clamshell' "$monitor_watch" >/dev/null pass "monitor watcher disables the internal monitor after closed-lid external hotplug" diff --git a/test/shell.d/system-lock-test.sh b/test/shell.d/system-lock-test.sh index da79eac045e..1ced98ee694 100755 --- a/test/shell.d/system-lock-test.sh +++ b/test/shell.d/system-lock-test.sh @@ -34,3 +34,52 @@ mapfile -t shutdown < <(rg '^(pkill|timeout) ' "$call_log") [[ ${shutdown[2]} == "pkill -f [o]rg.omarchy.screensaver" ]] || fail "system lock closes the screensaver terminal after ttfx exits" "calls: ${shutdown[*]}" pass "system lock waits for ttfx before closing its terminal" + +lock_src="$ROOT/bin/omarchy-system-lock" +if grep -Fq '${XDG_RUNTIME_DIR:-/tmp}/omarchy-1password-lock.lock' "$lock_src"; then + fail "1Password flock must not fall back to world-writable /tmp" +fi +grep -Fq '${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}' "$lock_src" || + fail "1Password flock falls back to a 0700 /tmp/omarchy-\$UID directory" +grep -Fq 'flock -n 9 || exit 0' "$lock_src" || + fail "1Password flock still dedupes an in-progress lock" +grep -Fq 'timeout --kill-after=1s 3s 1password --lock' "$lock_src" || + fail "system lock still issues 1password --lock" +pass "1Password lock file is not in world-writable /tmp" + +runtime_dir="$tmpdir/runtime" +mkdir -m 700 -p "$runtime_dir" +: >"$call_log" + +cat >"$mock_bin/pgrep" <<'SH' +#!/bin/bash +[[ $1 == -x && $2 == 1password ]] +SH +cat >"$mock_bin/omarchy-cmd-present" <<'SH' +#!/bin/bash +[[ $1 == 1password ]] +SH +cat >"$mock_bin/1password" <<'SH' +#!/bin/bash +printf '1password %s\n' "$*" >>"$CALL_LOG" +SH +# Arch has util-linux flock; this suite also runs where it is not on PATH. +cat >"$mock_bin/flock" <<'SH' +#!/bin/bash +exit 0 +SH +chmod +x "$mock_bin"/* + +PATH="$mock_bin:$PATH" CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" \ + "$ROOT/bin/omarchy-system-lock" + +for _ in {1..40}; do + grep -Fq 'timeout --kill-after=1s 3s 1password --lock' "$call_log" && break + sleep 0.05 +done + +grep -Fq 'timeout --kill-after=1s 3s 1password --lock' "$call_log" || + fail "system lock still calls 1password --lock when 1Password is running" "$(cat "$call_log")" +[[ -e $runtime_dir/omarchy-1password-lock.lock ]] || + fail "1Password flock is created under XDG_RUNTIME_DIR" +pass "1Password flock lives in the runtime directory" diff --git a/test/shell.d/theme-set-lock-test.sh b/test/shell.d/theme-set-lock-test.sh new file mode 100755 index 00000000000..792dbf12878 --- /dev/null +++ b/test/shell.d/theme-set-lock-test.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +theme_set="$ROOT/bin/omarchy-theme-set" + +if grep -Fq '${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock' "$theme_set"; then + fail "theme-set flock must not fall back to world-writable /tmp" +fi +grep -Fq '${XDG_RUNTIME_DIR:-/tmp/omarchy-$UID}' "$theme_set" || + fail "theme-set flock falls back to a 0700 /tmp/omarchy-\$UID directory" +grep -Fq 'mkdir -m 700 -p "$THEME_SET_LOCK_DIR"' "$theme_set" || + fail "theme-set creates the flock directory with mode 0700" +grep -Fq 'THEME_SET_LOCK="$THEME_SET_LOCK_DIR/omarchy-theme-set.lock"' "$theme_set" || + fail "theme-set keeps the flock file name" +pass "theme-set flock is not in world-writable /tmp"