diff --git a/bin/omarchy-brightness-display-ddc b/bin/omarchy-brightness-display-ddc index aab91278605..35afbaf8006 100755 --- a/bin/omarchy-brightness-display-ddc +++ b/bin/omarchy-brightness-display-ddc @@ -9,7 +9,30 @@ step="${2:-}" [[ -n $monitor ]] || exit 1 -cache_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-ddc" +# Bus/range cache must not land at a fixed name in world-writable /tmp. +# Prefer XDG_RUNTIME_DIR; otherwise a private 0700 owner-checked state dir. +private_cache_root() { + local root mode + + if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then + printf '%s\n' "$XDG_RUNTIME_DIR" + return 0 + fi + + root="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy" + mkdir -p "$root" || return 1 + chmod 700 "$root" || return 1 + [[ -O $root ]] || return 1 + mode=$(stat -c '%a' "$root" 2>/dev/null || stat -f '%Lp' "$root") + [[ $mode == 700 ]] || return 1 + printf '%s\n' "$root" +} + +cache_root=$(private_cache_root) || { + echo "Failed to resolve a private cache directory for DDC brightness." >&2 + exit 1 +} +cache_dir="$cache_root/omarchy-brightness-display-ddc" cache_name="${monitor//[^[:alnum:]_.-]/_}" cache_file="$cache_dir/$cache_name.bus" unavailable_cache_seconds=60 diff --git a/bin/omarchy-capture-region b/bin/omarchy-capture-region index 50b7e246e1c..800b3d113c5 100755 --- a/bin/omarchy-capture-region +++ b/bin/omarchy-capture-region @@ -20,8 +20,32 @@ # --match-monitor print "monitor:NAME" instead when the picked geometry # exactly matches a monitor -FULLSCREEN_MARKER="${XDG_RUNTIME_DIR:-/tmp}/omarchy-capture-region-fullscreen" -WINDOW_MARKER="${XDG_RUNTIME_DIR:-/tmp}/omarchy-capture-region-window" +# Fullscreen/window markers must not sit at fixed names in world-writable /tmp. +# Prefer XDG_RUNTIME_DIR; otherwise a private 0700 owner-checked state dir. +private_marker_root() { + local root mode + + if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then + printf '%s\n' "$XDG_RUNTIME_DIR" + return 0 + fi + + root="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy" + mkdir -p "$root" || return 1 + chmod 700 "$root" || return 1 + [[ -O $root ]] || return 1 + mode=$(stat -c '%a' "$root" 2>/dev/null || stat -f '%Lp' "$root") + [[ $mode == 700 ]] || return 1 + printf '%s\n' "$root" +} + +marker_root=$(private_marker_root) || { + echo "Failed to resolve a private directory for capture-region markers." >&2 + exit 1 +} +mkdir -p "$marker_root" +FULLSCREEN_MARKER="$marker_root/omarchy-capture-region-fullscreen" +WINDOW_MARKER="$marker_root/omarchy-capture-region-window" # accounting for portrait/transformed displays JQ_MONITOR_GEO=' diff --git a/bin/omarchy-debug b/bin/omarchy-debug index 59fbc4fce48..b7b28b0e02d 100755 --- a/bin/omarchy-debug +++ b/bin/omarchy-debug @@ -26,7 +26,24 @@ while (( $# > 0 )); do esac done -LOG_FILE="/tmp/omarchy-debug.log" +# /tmp is world-writable, so a fixed name there is created world-readable by +# the default umask and left behind until reboot -- and this one holds +# `sudo dmesg`, the journal, and a full hardware inventory that the invoking +# user's own uid otherwise cannot read. Keep it under the per-user runtime +# directory (0700) instead, and fall back to the state directory where +# Omarchy keeps everything else of its own when there is no session runtime dir. +log_dir="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}" +LOG_FILE="$log_dir/omarchy-debug.log" + +# Nothing here runs under `set -e`, so an unusable log_dir -- a stale +# XDG_RUNTIME_DIR, a full tmpfs, a read-only home -- would leave `--print` +# exiting 0 having printed nothing at all. -T is what makes a directory at +# LOG_FILE an error; without it `install` treats one as a destination +# directory, writes $LOG_FILE/null, and reports success. +if ! mkdir -p "$log_dir" || ! install -T -m 600 /dev/null "$LOG_FILE"; then + echo "Error: Failed to create $LOG_FILE" >&2 + exit 1 +fi if [[ $NO_SUDO = "true" ]]; then DMESG_OUTPUT="(skipped - --no-sudo flag used)" @@ -34,7 +51,7 @@ else DMESG_OUTPUT="$(sudo dmesg)" fi -cat > "$LOG_FILE" < "$LOG_FILE" </dev/null || pacman -Q omarchy 2>/dev/null || echo "unknown") @@ -59,6 +76,10 @@ INSTALLED PACKAGES ========================================= $({ expac -S '%n %v (%r)' $(pacman -Qqe) 2>/dev/null; comm -13 <(pacman -Sql | sort) <(pacman -Qqe | sort) | xargs -r expac -Q '%n %v (AUR)'; } | sort) EOF +then + echo "Error: Failed to write $LOG_FILE" >&2 + exit 1 +fi if [[ $PRINT_ONLY = "true" ]]; then cat "$LOG_FILE" 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-menu-share b/bin/omarchy-menu-share index 57d94bea109..49acc8b7f00 100755 --- a/bin/omarchy-menu-share +++ b/bin/omarchy-menu-share @@ -14,8 +14,19 @@ fi MODE="$1" shift +TEMP_FILE="" +cleanup() { + [[ -n ${TEMP_FILE:-} && -f $TEMP_FILE ]] && rm -f "$TEMP_FILE" +} +trap cleanup EXIT + if [[ $MODE == "clipboard" ]]; then - TEMP_FILE=$(mktemp --suffix=.txt) + # Private runtime dir — shared /tmp left clipboard contents readable to peers. + share_dir="${XDG_RUNTIME_DIR:-$HOME/.local/state/omarchy}" + mkdir -p "$share_dir" + chmod 700 "$share_dir" 2>/dev/null || true + TEMP_FILE=$(mktemp "$share_dir/omarchy-share.XXXXXX.txt") + chmod 600 "$TEMP_FILE" wl-paste >"$TEMP_FILE" FILE_ARRAY=("$TEMP_FILE") elif (($# > 0)); then @@ -41,10 +52,11 @@ else readarray -t FILE_ARRAY <<<"$picked" fi -# Run LocalSend in its own systemd service (detached from terminal) -systemd-run --user --quiet --collect localsend --headless send "${FILE_ARRAY[@]}" - -# Note: Temporary file will remain until system cleanup for clipboard mode -# This ensures the file content is available for the LocalSend GUI +if [[ $MODE == "clipboard" ]]; then + # Wait so LocalSend can read the private temp before EXIT removes it. + systemd-run --user --quiet --collect --wait localsend --headless send "${FILE_ARRAY[@]}" +else + systemd-run --user --quiet --collect localsend --headless send "${FILE_ARRAY[@]}" +fi exit 0 diff --git a/bin/omarchy-reminder b/bin/omarchy-reminder index d12871018b2..6e339502508 100755 --- a/bin/omarchy-reminder +++ b/bin/omarchy-reminder @@ -44,7 +44,7 @@ open_interactive() { show_reminders() { local timer next remaining reminder reminder_minutes body="" - local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders" + local reminder_dir="${XDG_RUNTIME_DIR:-$HOME/.local/state}/omarchy-reminders" local reminder_message="" local now=$(date +%s) @@ -72,7 +72,7 @@ show_reminders() { show_json() { local timer next remaining reminder reminder_minutes unit reminder_message label item_json reminders_json="[]" - local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders" + local reminder_dir="${XDG_RUNTIME_DIR:-$HOME/.local/state}/omarchy-reminders" local now=$(date +%s) local count=0 local tooltip="Set Reminder" @@ -119,7 +119,7 @@ show_json() { clear_reminders() { local units - local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders" + local reminder_dir="${XDG_RUNTIME_DIR:-$HOME/.local/state}/omarchy-reminders" units=$(systemctl --user list-timers --all --no-legend --no-pager "omarchy-reminder-*.timer" 2>/dev/null | awk '{ print $(NF - 1), $NF }') @@ -182,12 +182,13 @@ fi set_at=$(date +%s) remind_at=$(date -d "+${minutes} minutes" +%H:%M) unit="omarchy-reminder-${minutes}m-$set_at" -reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders" +reminder_dir="${XDG_RUNTIME_DIR:-$HOME/.local/state}/omarchy-reminders" message_file="$reminder_dir/$unit.message" confirmation="You'll be reminded at $remind_at" confirmation_title="Reminder set for ${minutes} minutes" mkdir -p "$reminder_dir" +chmod 700 "$reminder_dir" if [[ -n $custom_message ]]; then printf "%s" "$custom_message" >"$message_file" 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 e440ee438a6..8545251f5fc 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -15,7 +15,9 @@ NEXT_THEME_PATH="$HOME/.local/state/omarchy/current/next-theme" CURRENT_BACKGROUND_LINK="$HOME/.local/state/omarchy/current/background" THEME_BACKGROUND_STATE_PATH="$HOME/.local/state/omarchy/theme-backgrounds" 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/bin/omarchy-update b/bin/omarchy-update index 45305ef6d77..a418ac7587b 100755 --- a/bin/omarchy-update +++ b/bin/omarchy-update @@ -41,9 +41,30 @@ cleanup_update() { omarchy_security_exit_with_revoked_sudo "$status" } +# Transcript must not land on a world-writable predictable path. A local user +# who pre-creates /tmp/omarchy-update.log as a symlink can redirect script(1) +# into ~/.bashrc or ~/.ssh/authorized_keys. Prefer the private runtime dir; +# fall back to a 0700 cache directory under $HOME when XDG_RUNTIME_DIR is unset. +omarchy_update_log_path() { + local dir + if [[ -n ${XDG_RUNTIME_DIR:-} && -d $XDG_RUNTIME_DIR && ! -L $XDG_RUNTIME_DIR ]]; then + dir="$XDG_RUNTIME_DIR/omarchy-update" + else + dir="${HOME}/.cache/omarchy/update" + fi + mkdir -p -m 700 "$dir" + printf '%s' "$dir/update.log" +} + if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then script_command=$(printf '%q ' "$0" "$@") - exec env OMARCHY_UPDATE_LOGGED=1 OMARCHY_UPDATE_USER_PATH="$user_path" script -qefc "$script_command" "/tmp/omarchy-update.log" + update_log=$(omarchy_update_log_path) + # Refuse to follow a planted symlink at the log path. + if [[ -L $update_log ]]; then + rm -f "$update_log" + fi + exec env OMARCHY_UPDATE_LOGGED=1 OMARCHY_UPDATE_USER_PATH="$user_path" OMARCHY_UPDATE_LOG="$update_log" \ + script -qefc "$script_command" "$update_log" fi if ! omarchy-update-lock held; then diff --git a/bin/omarchy-update-analyze-logs b/bin/omarchy-update-analyze-logs index 1febd99d823..7ed87238312 100755 --- a/bin/omarchy-update-analyze-logs +++ b/bin/omarchy-update-analyze-logs @@ -2,7 +2,25 @@ # omarchy:summary=Check the update log for known failure conditions -update_log="/tmp/omarchy-update.log" +set -euo pipefail + +omarchy_update_log_path() { + local dir + if [[ -n ${XDG_RUNTIME_DIR:-} && -d $XDG_RUNTIME_DIR && ! -L $XDG_RUNTIME_DIR ]]; then + dir="$XDG_RUNTIME_DIR/omarchy-update" + else + dir="${HOME}/.cache/omarchy/update" + fi + printf '%s' "$dir/update.log" +} + +# Prefer the path the current update exported; otherwise resolve the same +# private location omarchy-update uses for new transcripts. +update_log="${OMARCHY_UPDATE_LOG:-$(omarchy_update_log_path)}" + +if [[ ! -f $update_log || -L $update_log ]]; then + exit 0 +fi # Check for initramfs generation failure if grep -q "Updating linux initcpios" "$update_log"; then diff --git a/bin/omarchy-update-lock b/bin/omarchy-update-lock index ba3c97b6404..5482766820f 100755 --- a/bin/omarchy-update-lock +++ b/bin/omarchy-update-lock @@ -6,7 +6,9 @@ set -e -lock_dir="${XDG_RUNTIME_DIR:-/tmp}" +# Prefer the private runtime dir; never fall back to shared /tmp where +# another user could pre-create the lock path. +lock_dir="${XDG_RUNTIME_DIR:-$HOME/.local/state/omarchy}" lock_path="$lock_dir/omarchy-update.lock" lock_is_held() { @@ -31,6 +33,7 @@ case "${1:-}" in fi mkdir -p "$lock_dir" 2>/dev/null || true + chmod 700 "$lock_dir" 2>/dev/null || true exec {OMARCHY_UPDATE_LOCK_FD}>"$lock_path" if ! flock -n "$OMARCHY_UPDATE_LOCK_FD"; then echo "An Omarchy update is already running." diff --git a/bin/omarchy-update-orphan-pkgs b/bin/omarchy-update-orphan-pkgs index 7869671a46c..892ff010ef0 100755 --- a/bin/omarchy-update-orphan-pkgs +++ b/bin/omarchy-update-orphan-pkgs @@ -12,7 +12,9 @@ echo -e "\e[32m\nOrphan system packages\e[0m" printf ' %s\n' "${orphans[@]}" echo -if [[ ! -t 0 || ! -t 1 ]]; then +# omarchy update -y keeps a TTY but promises not to ask. Treat unattended +# the same as a non-interactive pipe: report and move on. +if [[ ${OMARCHY_UPDATE_UNATTENDED:-} == 1 || ! -t 0 || ! -t 1 ]]; then echo "${#orphans[@]} orphaned package(s) found. Re-run omarchy-update-orphan-pkgs in a terminal to review/remove them." echo exit 0 diff --git a/bin/omarchy-upload-log b/bin/omarchy-upload-log index 134d9174180..375651deb6c 100755 --- a/bin/omarchy-upload-log +++ b/bin/omarchy-upload-log @@ -5,8 +5,19 @@ # omarchy:hidden=true LOG_TYPE="${1:-install}" -TEMP_LOG="/tmp/upload-log.txt" -SYSTEM_INFO="/tmp/system-info.txt" + +# What lands in these two files is the whole journal, the package list and a +# hardware inventory, and the first of them is then published to a paste. A +# fixed name in /tmp is created world-readable by the default umask, so any +# other local account can read the payload before it is sent -- and one that +# creates the name first makes every write here fail closed while `curl` still +# uploads their file and prints the URL as this user's log. mktemp gives this +# run a directory only its owner can enter (0700), which is the actual +# boundary -- the files created inside it still land at the default umask. +staging_dir=$(mktemp -d) || exit 1 +trap 'rm -rf "$staging_dir"' EXIT +TEMP_LOG="$staging_dir/upload-log.txt" +SYSTEM_INFO="$staging_dir/system-info.txt" # Get system information if fastfetch is available if omarchy-cmd-present fastfetch; then diff --git a/default/agents/skills/diagnose-crash/reporting.md b/default/agents/skills/diagnose-crash/reporting.md index 15bd08bc54d..02f98127963 100644 --- a/default/agents/skills/diagnose-crash/reporting.md +++ b/default/agents/skills/diagnose-crash/reporting.md @@ -87,8 +87,10 @@ gh issue create --repo omacom/omarchy --title "..." --body "..." Include what happened, what was expected, steps to reproduce, system details from `omarchy version`, and diagnostics from `omarchy debug --no-sudo --print` (which -also writes `/tmp/omarchy-debug.log`; the interactive `omarchy debug` can upload -it and print a shareable URL worth including). +also writes `$XDG_RUNTIME_DIR/omarchy-debug.log`, or +`${XDG_STATE_HOME:-~/.local/state}/omarchy/omarchy-debug.log` when there is no +session runtime directory; the interactive `omarchy debug` can upload it and +print a shareable URL worth including). `gh` cannot attach media. If a screenshot would help, save one and give the user the path to drag into the web form. diff --git a/default/agents/skills/omarchy/contributing.md b/default/agents/skills/omarchy/contributing.md index c76c12d61f8..9d8ea4a6630 100644 --- a/default/agents/skills/omarchy/contributing.md +++ b/default/agents/skills/omarchy/contributing.md @@ -22,7 +22,8 @@ description with steps to reproduce, and diagnostics. Gather them: ```bash omarchy version -# Generate the diagnostic log (also written to /tmp/omarchy-debug.log) +# Generate the diagnostic log (also written to $XDG_RUNTIME_DIR/omarchy-debug.log, +# or ${XDG_STATE_HOME:-~/.local/state}/omarchy/omarchy-debug.log without a session) omarchy debug --no-sudo --print # Interactive variant: `omarchy debug` offers to upload the log to diff --git a/docs/update-process.md b/docs/update-process.md index d486c4c9076..0e7365a3ae4 100644 --- a/docs/update-process.md +++ b/docs/update-process.md @@ -23,7 +23,7 @@ The design goal is: | --- | --- | --- | | `${XDG_RUNTIME_DIR:-/tmp}/omarchy-update.lock` | user | Prevent overlapping update runs. Owned by `omarchy-update-lock`; compatibility wrappers inherit/respect it. | | `${XDG_RUNTIME_DIR}/omarchy-update-stay-awake/` | user | Private mode-0700 inhibitor coordination state. If no runtime directory is available, the helper uses the validated mode-0700 `/tmp/omarchy-$UID/` fallback. | -| `/tmp/omarchy-update.log` | user | Transcript of `omarchy update`, used by `omarchy-update-analyze-logs`. | +| `${XDG_RUNTIME_DIR}/omarchy-update/update.log (or ~/.cache/omarchy/update/update.log)` | user | Transcript of `omarchy update`, used by `omarchy-update-analyze-logs`. | | `~/.local/state/omarchy/current/` | user | Generated active theme, selected theme name, and current background symlink. | | `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. | | `~/.local/state/omarchy/reboot-required` | user | Optional reboot marker checked by `omarchy-update-restart`. | @@ -120,7 +120,7 @@ High-level flow: ```text omarchy-update - ├─ ensure transcript logging through script(1) → /tmp/omarchy-update.log + ├─ ensure transcript logging through script(1) → ${XDG_RUNTIME_DIR}/omarchy-update/update.log (or ~/.cache/omarchy/update/update.log) ├─ omarchy-update-lock │ └─ acquire the update lock and run omarchy-update inside it ├─ omarchy-update-requires-free-space @@ -163,7 +163,7 @@ Important behavior: check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check. - `omarchy update` checks/runs migrations in the same visible terminal via `omarchy-migrate` after pacman finishes. -- A failure should leave enough output in `/tmp/omarchy-update.log` and the +- A failure should leave enough output in `${XDG_RUNTIME_DIR}/omarchy-update/update.log (or ~/.cache/omarchy/update/update.log)` and the terminal transcript to debug. ## Path 2: direct `sudo pacman -Syu` attempt @@ -306,7 +306,7 @@ scripts. | `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. | | `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. | | `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. | -| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. | +| `omarchy-update-analyze-logs` | Scans `${XDG_RUNTIME_DIR}/omarchy-update/update.log (or ~/.cache/omarchy/update/update.log)` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. | | `omarchy-update-restart` | Restarts components selected by `restart-*-required` markers, always restarts the shell, and prompts for reboot after kernel/Hyprland updates. Internal phase flags let the update finish sudo-capable restarts before user hooks and defer only the unprivileged reboot prompt. | **Keep.** Important final step; may eventually include service-restart checks. | | `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. | | `omarchy-update-time` | Restarts `systemd-timesyncd`. | **Question.** Not really an update command. Consider renaming/moving under system/time maintenance. | diff --git a/migrations/1788398495.sh b/migrations/1788398495.sh new file mode 100644 index 00000000000..4a2fa8e39fd --- /dev/null +++ b/migrations/1788398495.sh @@ -0,0 +1,26 @@ +echo "Remove leftover world-readable diagnostics files from /tmp" + +# omarchy-debug and omarchy-upload-log used to write these under /tmp, where the +# default umask leaves them mode 0644. The sticky bit does not stop another local +# account from reading them for as long as they remain. The scripts now stage +# under $XDG_RUNTIME_DIR / mktemp -d; drop any leftovers this user still owns. +# +# Only regular files owned by the current uid, and never through a symlink: -f +# and -O both follow one, so a link this user left at the name would otherwise be +# unlinked in place of a leftover. Overridable for tests. + +tmp_root=${OMARCHY_LEGACY_DIAGNOSTICS_TMP:-/tmp} + +remove_owned_legacy() { + local path=$1 + + # -e follows symlinks; -L catches a dangling or live symlink at the name. + [[ -L $path ]] && return 0 + [[ -f $path ]] || return 0 + [[ -O $path ]] || return 0 + rm -f -- "$path" +} + +remove_owned_legacy "$tmp_root/omarchy-debug.log" +remove_owned_legacy "$tmp_root/upload-log.txt" +remove_owned_legacy "$tmp_root/system-info.txt" diff --git a/test/shell.d/brightness-display-ddc-cache-path-test.sh b/test/shell.d/brightness-display-ddc-cache-path-test.sh new file mode 100755 index 00000000000..b2124a6309a --- /dev/null +++ b/test/shell.d/brightness-display-ddc-cache-path-test.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +script="$ROOT/bin/omarchy-brightness-display-ddc" + +if grep -Fq '${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-ddc' "$script"; then + fail "DDC brightness no longer caches under world-writable /tmp" +fi + +grep -Fq 'private_cache_root' "$script" || fail "DDC brightness resolves a private cache root" +grep -Fq 'XDG_STATE_HOME' "$script" || fail "DDC brightness falls back to XDG_STATE_HOME when runtime dir is unset" +grep -Fq 'chmod 700' "$script" || fail "DDC brightness enforces mode 0700 on the private cache root" +pass "DDC brightness cache path avoids world-writable /tmp" + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +stub_bin="$tmp/bin" +mkdir -p "$stub_bin" "$tmp/home" "$tmp/state" +cat >"$stub_bin/ddcutil" <<'SH' +#!/bin/bash +exit 1 +SH +chmod +x "$stub_bin/ddcutil" + +export PATH="$stub_bin:$PATH" +export HOME="$tmp/home" +unset XDG_RUNTIME_DIR +export XDG_STATE_HOME="$tmp/state" + +bash "$script" DP-1 >/dev/null 2>&1 || true + +[[ -d $XDG_STATE_HOME/omarchy/omarchy-brightness-display-ddc ]] || + fail "creates cache under XDG_STATE_HOME/omarchy" "$(find "$tmp" -type d)" +mode=$(stat -c '%a' "$XDG_STATE_HOME/omarchy" 2>/dev/null || stat -f '%Lp' "$XDG_STATE_HOME/omarchy") +[[ $mode == 700 ]] || fail "private cache root is mode 0700" "mode=$mode" +[[ ! -e /tmp/omarchy-brightness-display-ddc ]] || + fail "does not create /tmp/omarchy-brightness-display-ddc" +pass "DDC brightness stages cache under a private state directory" diff --git a/test/shell.d/capture-region-marker-path-test.sh b/test/shell.d/capture-region-marker-path-test.sh new file mode 100755 index 00000000000..a3e6c68094d --- /dev/null +++ b/test/shell.d/capture-region-marker-path-test.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +script="$ROOT/bin/omarchy-capture-region" + +if grep -Fq '${XDG_RUNTIME_DIR:-/tmp}/omarchy-capture-region-fullscreen' "$script"; then + fail "capture-region fullscreen marker no longer falls back to /tmp" +fi +if grep -Fq '${XDG_RUNTIME_DIR:-/tmp}/omarchy-capture-region-window' "$script"; then + fail "capture-region window marker no longer falls back to /tmp" +fi + +grep -Fq 'private_marker_root' "$script" || fail "capture-region resolves a private marker root" +grep -Fq 'XDG_STATE_HOME' "$script" || fail "capture-region falls back to XDG_STATE_HOME when runtime dir is unset" +grep -Fq 'chmod 700' "$script" || fail "capture-region enforces mode 0700 on the private marker root" +pass "capture-region marker paths avoid world-writable /tmp" 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/diagnostics-staging-test.sh b/test/shell.d/diagnostics-staging-test.sh new file mode 100755 index 00000000000..7ce1a89e45b --- /dev/null +++ b/test/shell.d/diagnostics-staging-test.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +stub_bin="$tmp_dir/bin" +mkdir -p "$stub_bin" "$tmp_dir/run" "$tmp_dir/home" + +for stub in inxi journalctl expac pacman comm sort ping fastfetch uname; do + printf '#!/bin/bash\nexit 0\n' >"$stub_bin/$stub" +done + +cat >"$stub_bin/sudo" <<'SH' +#!/bin/bash +exec "$@" +SH + +cat >"$stub_bin/dmesg" <<'SH' +#!/bin/bash +printf '%s\n' "secret kernel ring buffer" +SH + +# The upload is the point of the staging file, so capture what curl was handed +# rather than sending anything. +# The staging directory is removed when the upload exits, so the mode has to be +# read here, while the payload still exists. +cat >"$stub_bin/curl" <<'SH' +#!/bin/bash +for arg in "$@"; do + if [[ $arg == file=@* ]]; then + payload="${arg#file=@}" + printf '%s\n' "$payload" >"$OMARCHY_TEST_UPLOAD_PATH" + stat -c '%a' "${payload%/*}" >"$OMARCHY_TEST_UPLOAD_MODE" 2>/dev/null || + stat -f '%Lp' "${payload%/*}" >"$OMARCHY_TEST_UPLOAD_MODE" 2>/dev/null || true + fi +done +printf 'https://logs.omarchy.org/test\n' +SH + +chmod +x "$stub_bin"/* +export PATH="$stub_bin:$ROOT/bin:$PATH" +export HOME="$tmp_dir/home" +export XDG_RUNTIME_DIR="$tmp_dir/run" +export OMARCHY_TEST_UPLOAD_PATH="$tmp_dir/upload-path" +export OMARCHY_TEST_UPLOAD_MODE="$tmp_dir/upload-mode" + +# omarchy-debug: the log holds sudo dmesg and the journal, so it must not be +# staged under a name every account on the machine can predict and read. +"$ROOT/bin/omarchy-debug" --print >/dev/null + +[[ -f $XDG_RUNTIME_DIR/omarchy-debug.log ]] || + fail "omarchy-debug writes its log under the per-user runtime directory" "$(ls -a "$XDG_RUNTIME_DIR")" +pass "omarchy-debug writes its log under the per-user runtime directory" + +mode=$(stat -c '%a' "$XDG_RUNTIME_DIR/omarchy-debug.log" 2>/dev/null || stat -f '%Lp' "$XDG_RUNTIME_DIR/omarchy-debug.log") +[[ $mode == "600" ]] || + fail "the debug log is readable only by its owner" "mode: $mode" +pass "the debug log is readable only by its owner" + +grep -Fq 'secret kernel ring buffer' "$XDG_RUNTIME_DIR/omarchy-debug.log" || + fail "the debug log still collects what it collected before" +pass "the debug log still collects what it collected before" + +if grep -q '/tmp/omarchy-debug.log' "$ROOT/bin/omarchy-debug"; then + fail "omarchy-debug no longer names a world-writable path" +fi +pass "omarchy-debug no longer names a world-writable path" + +# omarchy-upload-log stages the payload it publishes; the same reasoning +# applies, and there the file is also swappable between the last write and the +# upload. +"$ROOT/bin/omarchy-upload-log" system-info >/dev/null + +uploaded=$(<"$OMARCHY_TEST_UPLOAD_PATH") +staging_dir=${uploaded%/*} + +# mktemp still puts its directory under /tmp; the boundary is that the +# directory is unpredictable and only its owner can enter it, not that the +# path leaves /tmp behind. +[[ $uploaded != "/tmp/upload-log.txt" && $staging_dir != "/tmp" ]] || + fail "the uploaded payload is not staged at a shared, predictable name" "uploaded: $uploaded" +pass "the uploaded payload is not staged at a shared, predictable name" + +staging_mode=$(<"$OMARCHY_TEST_UPLOAD_MODE") +[[ $staging_mode == "700" ]] || + fail "the staging directory is reachable only by its owner" "mode: ${staging_mode:-unreadable}" +pass "the staging directory is reachable only by its owner" + +if [[ -d $staging_dir ]]; then + fail "the staging directory is cleaned up after the upload" "left behind: $staging_dir" +fi +pass "the staging directory is cleaned up after the upload" + +# A directory at the log path is the one shape `install` accepts without +# producing the file, so the guard has to be the thing that catches it -- +# otherwise `--print` exits 0 having printed nothing, which is what a caller +# reads as a successful empty diagnostic. +blocked_runtime="$tmp_dir/blocked" +mkdir -p "$blocked_runtime/omarchy-debug.log" +blocked_out=$(XDG_RUNTIME_DIR="$blocked_runtime" "$ROOT/bin/omarchy-debug" --no-sudo --print 2>/dev/null) && blocked_status=0 || blocked_status=$? + +if (( blocked_status == 0 )) || [[ -n $blocked_out ]]; then + fail "omarchy-debug fails loudly when the log file cannot be created" "status: $blocked_status, bytes: ${#blocked_out}" +fi +pass "omarchy-debug fails loudly when the log file cannot be created" + +# Simulate ENOSPC after creation: install succeeds, but the body writer fails. +cat >"$stub_bin/cat" <<'SH' +#!/bin/bash +exit 1 +SH +chmod +x "$stub_bin/cat" +write_out=$("$ROOT/bin/omarchy-debug" --no-sudo --print 2>"$tmp_dir/write-error") && write_status=0 || write_status=$? +if (( write_status == 0 )) || [[ -n $write_out ]]; then + fail "omarchy-debug rejects a failed diagnostic body write" +fi +grep -Fq 'Failed to write' "$tmp_dir/write-error" || fail "body write failure reports an error" +pass "omarchy-debug rejects a failed diagnostic body write" diff --git a/test/shell.d/legacy-diagnostics-tmp-migration-test.sh b/test/shell.d/legacy-diagnostics-tmp-migration-test.sh new file mode 100755 index 00000000000..b7234615620 --- /dev/null +++ b/test/shell.d/legacy-diagnostics-tmp-migration-test.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +migration=$(grep -rl 'Remove leftover world-readable diagnostics files from /tmp' "$ROOT/migrations" | head -n 1 || true) +[[ -n $migration ]] || fail "legacy diagnostics /tmp migration exists" + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +run_migration() { + OMARCHY_LEGACY_DIAGNOSTICS_TMP="$tmp" bash -euo pipefail "$migration" >/dev/null +} + +# Own leftovers are removed. +: >"$tmp/omarchy-debug.log" +: >"$tmp/upload-log.txt" +: >"$tmp/system-info.txt" +chmod 644 "$tmp/omarchy-debug.log" "$tmp/upload-log.txt" "$tmp/system-info.txt" +run_migration +[[ ! -e $tmp/omarchy-debug.log ]] || fail "removes omarchy-debug.log" +[[ ! -e $tmp/upload-log.txt ]] || fail "removes upload-log.txt" +[[ ! -e $tmp/system-info.txt ]] || fail "removes system-info.txt" +pass "removes owned legacy diagnostics files from the staging dir" + +# Idempotent when nothing is left. +run_migration +pass "no-ops when the legacy files are already gone" + +# Unrelated files in the same directory stay put. +: >"$tmp/omarchy-debug.log" +: >"$tmp/keep-me.txt" +run_migration +[[ ! -e $tmp/omarchy-debug.log ]] || fail "still removes the legacy name" +[[ -f $tmp/keep-me.txt ]] || fail "leaves unrelated files alone" +pass "leaves unrelated files in the same directory alone" + +# Symlinks at the legacy names are left alone (do not follow / unlink the target). +: >"$tmp/real-target.txt" +ln -s "$tmp/real-target.txt" "$tmp/omarchy-debug.log" +run_migration +[[ -L $tmp/omarchy-debug.log ]] || fail "leaves a symlink at the legacy name" +[[ -f $tmp/real-target.txt ]] || fail "does not unlink a symlink target" +pass "refuses to remove a symlink at a legacy name" + +# A non-owned regular file is left alone when we can create one (skip if not root +# and the filesystem forbids alien ownership — the common case is a no-op create). +other="$tmp/upload-log.txt" +: >"$other" +if chown nobody "$other" 2>/dev/null; then + run_migration + [[ -f $other ]] || fail "leaves a file owned by another user" + pass "leaves a legacy file owned by another user" +else + rm -f "$other" + pass "skips other-owner check when chown is unavailable" +fi 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" diff --git a/test/shell.d/update-log-path-test.sh b/test/shell.d/update-log-path-test.sh new file mode 100644 index 00000000000..221bb7c40a4 --- /dev/null +++ b/test/shell.d/update-log-path-test.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +export HOME="$test_tmp/home" +export XDG_RUNTIME_DIR="$test_tmp/run" +mkdir -p "$HOME" "$XDG_RUNTIME_DIR" + +# Source the path helper the same way the scripts resolve it. +path_from_update=$(bash -c ' + omarchy_update_log_path() { + local dir + if [[ -n ${XDG_RUNTIME_DIR:-} && -d $XDG_RUNTIME_DIR && ! -L $XDG_RUNTIME_DIR ]]; then + dir="$XDG_RUNTIME_DIR/omarchy-update" + else + dir="${HOME}/.cache/omarchy/update" + fi + mkdir -p -m 700 "$dir" + printf "%s" "$dir/update.log" + } + omarchy_update_log_path +') + +[[ $path_from_update == "$XDG_RUNTIME_DIR/omarchy-update/update.log" ]] || + fail "update log resolves under XDG_RUNTIME_DIR" "$path_from_update" +pass "update log resolves under XDG_RUNTIME_DIR" + +# Analyze-logs must ignore a symlink at the log path (the attack /tmp used to allow). +mkdir -p "$XDG_RUNTIME_DIR/omarchy-update" +ln -s "$HOME/.bashrc" "$XDG_RUNTIME_DIR/omarchy-update/update.log" +touch "$HOME/.bashrc" +OMARCHY_UPDATE_LOG="$XDG_RUNTIME_DIR/omarchy-update/update.log" \ + bash "$ROOT/bin/omarchy-update-analyze-logs" +# Still a symlink — analyze refused to read it; bashrc untouched content-wise. +[[ -L $XDG_RUNTIME_DIR/omarchy-update/update.log ]] || + fail "analyze-logs must not replace a planted symlink" +pass "analyze-logs refuses to read a planted symlink" + +# Without XDG_RUNTIME_DIR, fall back under $HOME/.cache, never /tmp. +unset XDG_RUNTIME_DIR +fallback=$(bash -c ' + omarchy_update_log_path() { + local dir + if [[ -n ${XDG_RUNTIME_DIR:-} && -d $XDG_RUNTIME_DIR && ! -L $XDG_RUNTIME_DIR ]]; then + dir="$XDG_RUNTIME_DIR/omarchy-update" + else + dir="${HOME}/.cache/omarchy/update" + fi + mkdir -p -m 700 "$dir" + printf "%s" "$dir/update.log" + } + omarchy_update_log_path +') +[[ $fallback == "$HOME/.cache/omarchy/update/update.log" ]] || + fail "update log falls back under \$HOME/.cache" "$fallback" +[[ $fallback != /tmp/* ]] || fail "update log must not fall back under /tmp" "$fallback" +pass "update log falls back under \$HOME/.cache, not /tmp" + +if grep -E '^[^#]* /tmp/omarchy-update\.log' "$ROOT/bin/omarchy-update" >/dev/null; then + fail "omarchy-update still hard-codes /tmp/omarchy-update.log" +fi +pass "omarchy-update no longer hard-codes /tmp/omarchy-update.log" diff --git a/test/shell.d/update-orphan-test.sh b/test/shell.d/update-orphan-test.sh index 7d2b05cdc83..1521b294387 100644 --- a/test/shell.d/update-orphan-test.sh +++ b/test/shell.d/update-orphan-test.sh @@ -23,7 +23,7 @@ SH } run_orphan_checker() { - HOME="$test_home" PATH="$stub_bin:$PATH" "$ROOT/bin/omarchy-update-orphan-pkgs" + HOME="$test_home" PATH="$stub_bin:$PATH" "$BASH" "$ROOT/bin/omarchy-update-orphan-pkgs" } write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then printf "old-lib\nunused-tool\n"; exit 0; fi; exit 1' @@ -39,3 +39,27 @@ write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then exit 0; fi; exit 1' run_orphan_checker >"$test_tmp/none.out" 2>"$test_tmp/none.err" [[ ! -s $test_tmp/none.out ]] || fail "orphan checker stays quiet when no orphans exist" pass "orphan checker stays quiet without orphans" + +# -y leaves stdin/stdout as TTYs in a real terminal, so the unattended flag +# has to be checked explicitly or gum confirm blocks forever. +write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then printf "old-lib\n"; exit 0; fi; exit 1' +write_stub gum 'echo "gum should not be called under -y" >&2; exit 99' +# Match update-package-conflict-test.sh: script gives both streams a PTY. +# Its command syntax differs on macOS, where these shell tests also run. +cat >"$test_tmp/terminal.sh" <<'SH' +[[ -t 0 && -t 1 ]] || exit 70 +exec "$BASH" "$ROOT/bin/omarchy-update-orphan-pkgs" +SH +export ORPHAN_PTY_RUNNER="$test_tmp/terminal.sh" +if [[ $(uname -s) == "Darwin" ]]; then + HOME="$test_home" PATH="$stub_bin:$PATH" OMARCHY_UPDATE_UNATTENDED=1 \ + script -q "$test_tmp/unattended.out" "$BASH" "$ORPHAN_PTY_RUNNER" >/dev/null 2>&1 +else + HOME="$test_home" PATH="$stub_bin:$PATH" OMARCHY_UPDATE_UNATTENDED=1 \ + script -qec 'bash "$ORPHAN_PTY_RUNNER"' "$test_tmp/unattended.out" >/dev/null 2>&1 +fi +if grep -q 'gum should not be called' "$test_tmp/unattended.out"; then + fail "unattended orphan step invoked gum on a terminal" +fi +grep -q 'Re-run omarchy-update-orphan-pkgs in a terminal' "$test_tmp/unattended.out" || fail "unattended orphan step must not prompt" +pass "orphan checker skips the prompt under OMARCHY_UPDATE_UNATTENDED"