Model what each parser does with an empty and a dangling directive
Review of the previous commits turned up four places where the predicates and their tests disagreed with the tools they are modelling, each checked against udevadm verify, systemd-analyze verify and visudo -cf rather than against reading of the sources. An empty ExecStop= resets the list, so a unit an administrator neutralised that way runs nothing at shutdown and is no longer ours to remove; the predicate now tracks the last state instead of returning on the first home path it sees. A file whose last line ends in a backslash still carries a live directive for systemd, so the pending logical line is emitted at EOF; udev ignores such a line and sudo rejects the file outright, so this costs those two nothing. The scanner's taint pass now reads += appends, which its own comment already promised: the value of an append is no use, but a name that reaches a user root through one has to be judged on it. Two regression guards passed against the implementations they were written for. The udev continuation fixture put the whole RUN+= below the comment, so it matched whether or not the pending half was carried across; the split now falls inside the RUN+= value. The sudoers one kept its file on the strength of a spec above the comment, so it could not fail either; the hand-written spec now sits below. Both fail against a mutant that discards the pending line. The comment above the second also claimed a continued comment stays a comment, which visudo contradicts.
This commit is contained in:
@@ -67,6 +67,12 @@ active_lines() {
|
|||||||
emit_logical "$logical$line"
|
emit_logical "$logical$line"
|
||||||
logical=""
|
logical=""
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# A file whose last line ends in a backslash still carries a live directive for
|
||||||
|
# systemd: `systemd-analyze verify` resolves an ExecStop= written that way.
|
||||||
|
# udev ignores the dangling line and sudo rejects the file outright, so emitting
|
||||||
|
# it costs those two nothing.
|
||||||
|
emit_logical "$logical"
|
||||||
}
|
}
|
||||||
|
|
||||||
# One logical line, whitespace collapsed so a reformatted copy still compares
|
# One logical line, whitespace collapsed so a reformatted copy still compares
|
||||||
@@ -195,6 +201,7 @@ plymouth_unit_runs_from_home() {
|
|||||||
local home_pattern="^(/home/[^/]+|/root)/\\.local/share/omarchy/bin/$binary\$"
|
local home_pattern="^(/home/[^/]+|/root)/\\.local/share/omarchy/bin/$binary\$"
|
||||||
local line word
|
local line word
|
||||||
local -a words
|
local -a words
|
||||||
|
local matched=1
|
||||||
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
if [[ ! $line =~ $exec_stop_pattern ]]; then
|
if [[ ! $line =~ $exec_stop_pattern ]]; then
|
||||||
@@ -203,6 +210,11 @@ plymouth_unit_runs_from_home() {
|
|||||||
|
|
||||||
read -ra words <<<"${BASH_REMATCH[1]}"
|
read -ra words <<<"${BASH_REMATCH[1]}"
|
||||||
if (( ! ${#words[@]} )); then
|
if (( ! ${#words[@]} )); then
|
||||||
|
# An empty assignment resets the list, so nothing named before it still
|
||||||
|
# runs. `systemd-analyze verify` reports the missing command for a unit
|
||||||
|
# with one ExecStop=, and reports nothing once a bare ExecStop= follows it.
|
||||||
|
# An administrator who neutralised the unit this way is left alone.
|
||||||
|
matched=1
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -214,11 +226,13 @@ plymouth_unit_runs_from_home() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [[ $word =~ $home_pattern || $word == "$HOME/.local/share/omarchy/bin/$binary" ]]; then
|
if [[ $word =~ $home_pattern || $word == "$HOME/.local/share/omarchy/bin/$binary" ]]; then
|
||||||
return 0
|
matched=0
|
||||||
|
else
|
||||||
|
matched=1
|
||||||
fi
|
fi
|
||||||
done < <(active_lines systemd)
|
done < <(active_lines systemd)
|
||||||
|
|
||||||
return 1
|
return $matched
|
||||||
}
|
}
|
||||||
|
|
||||||
# /etc/sudoers.d is 0750 root:root as shipped, and omarchy-migrate runs as the
|
# /etc/sudoers.d is 0750 root:root as shipped, and omarchy-migrate runs as the
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
Executable → Regular
@@ -213,14 +213,16 @@ run_migration
|
|||||||
fail "migration matches the binary the filename promises, not any home path"
|
fail "migration matches the binary the filename promises, not any home path"
|
||||||
pass "migration matches the binary the filename promises, not any home path"
|
pass "migration matches the binary the filename promises, not any home path"
|
||||||
|
|
||||||
# udev resumes a continuation across a comment: `udevadm verify` on
|
# udev resumes a continuation across a comment: `udevadm verify` reports its
|
||||||
# 'SUBSYSTEM=="power_supply" \' + "# c" + ', RUN+="..."' reports its style warning
|
# complaint on line 1 for a rule split this way, so the three lines are one live
|
||||||
# on line 1, so those three lines are one rule and the rule is live.
|
# rule. The split falls inside the RUN+= value on purpose -- with the whole
|
||||||
|
# RUN+= below the comment the assertion passes even against an implementation
|
||||||
|
# that throws the pending half away, which is the shape this guards against.
|
||||||
reset_machine
|
reset_machine
|
||||||
cat >"$power_rule" <<'RULE'
|
cat >"$power_rule" <<'RULE'
|
||||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains" \
|
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --unit=omarchy-power-profile \
|
||||||
# split for readability
|
# split for readability
|
||||||
, RUN+="/home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set"
|
/home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set"
|
||||||
RULE
|
RULE
|
||||||
run_migration
|
run_migration
|
||||||
|
|
||||||
|
|||||||
@@ -176,16 +176,17 @@ declare -A VARS_TAINTED=()
|
|||||||
# isolation would miss in whichever direction it picked.
|
# isolation would miss in whichever direction it picked.
|
||||||
collect_vars() {
|
collect_vars() {
|
||||||
local -n source_lines="$1"
|
local -n source_lines="$1"
|
||||||
local line name value
|
local line name value append
|
||||||
|
|
||||||
VARS=()
|
VARS=()
|
||||||
VARS_TAINTED=()
|
VARS_TAINTED=()
|
||||||
for line in "${source_lines[@]}"; do
|
for line in "${source_lines[@]}"; do
|
||||||
[[ $line =~ ^[[:space:]]*# ]] && continue
|
[[ $line =~ ^[[:space:]]*# ]] && continue
|
||||||
[[ $line =~ ^[[:space:]]*(local|declare|export|readonly|typeset)?[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || continue
|
[[ $line =~ ^[[:space:]]*(local|declare|export|readonly|typeset)?[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)(\+?)=(.*)$ ]] || continue
|
||||||
|
|
||||||
name=${BASH_REMATCH[2]}
|
name=${BASH_REMATCH[2]}
|
||||||
value=${BASH_REMATCH[3]}
|
append=${BASH_REMATCH[3]}
|
||||||
|
value=${BASH_REMATCH[4]}
|
||||||
value=${value%%[[:space:]]#*}
|
value=${value%%[[:space:]]#*}
|
||||||
value=${value%[[:space:]]}
|
value=${value%[[:space:]]}
|
||||||
if [[ $value == \"*\" || $value == \'*\' ]]; then
|
if [[ $value == \"*\" || $value == \'*\' ]]; then
|
||||||
@@ -193,6 +194,11 @@ collect_vars() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
mentions_user_writable_root "$value" && VARS_TAINTED["$name"]=1
|
mentions_user_writable_root "$value" && VARS_TAINTED["$name"]=1
|
||||||
|
|
||||||
|
# An append never wins the value -- an array grown across a file resolves to
|
||||||
|
# nothing useful -- but it does carry the taint, or a name could reach a user
|
||||||
|
# root through += and never be judged on it.
|
||||||
|
[[ -n $append ]] && continue
|
||||||
[[ -v VARS[$name] ]] || VARS["$name"]=$value
|
[[ -v VARS[$name] ]] || VARS["$name"]=$value
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -457,9 +463,8 @@ privileged_destination() {
|
|||||||
fi
|
fi
|
||||||
done < <(command_destinations "$line")
|
done < <(command_destinations "$line")
|
||||||
|
|
||||||
# An elevated write whose destination cannot be resolved counts as
|
# An elevated write whose destination cannot be resolved counts as privileged:
|
||||||
# privileged: sudo tee is not aimed at a user's own dotfile, and assuming
|
# sudo tee is not aimed at a user's own dotfile.
|
||||||
# otherwise is how this bug class survived six reviews.
|
|
||||||
if ((elevated == 0)) && ((${#unresolved[@]} > 0)); then
|
if ((elevated == 0)) && ((${#unresolved[@]} > 0)); then
|
||||||
printf '%s' "${unresolved[0]} (unresolved destination of an elevated write)"
|
printf '%s' "${unresolved[0]} (unresolved destination of an elevated write)"
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -197,21 +197,25 @@ run_migration
|
|||||||
fail "migration keeps a same-named file that never cleaned itself up"
|
fail "migration keeps a same-named file that never cleaned itself up"
|
||||||
pass "migration keeps a same-named file that never cleaned itself up"
|
pass "migration keeps a same-named file that never cleaned itself up"
|
||||||
|
|
||||||
# A rule continued onto the next line is one logical line, and a comment that is
|
# A spec continued onto the next line is one logical line, and a comment's own
|
||||||
# continued stays a comment for the whole of it.
|
# trailing backslash swallows nothing: `visudo -cf` on "# note \" plus a bogus
|
||||||
|
# token reports the error on line 2, so the spec below a commented line is live.
|
||||||
|
# The hand-written spec sits under the comment on purpose -- above it, the file
|
||||||
|
# is kept under either reading and the assertion cannot fail.
|
||||||
reset_machine
|
reset_machine
|
||||||
cat >"$first_run" <<'EOF'
|
cat >"$first_run" <<'EOF'
|
||||||
# Retired, keeping the old grant here for reference: \
|
# Retired, keeping the old grant here for reference: \
|
||||||
installer ALL=(ALL) NOPASSWD: /usr/bin/systemctl
|
installer ALL=(ALL) NOPASSWD: /usr/local/bin/our-own-deploy-script
|
||||||
Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run
|
Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run
|
||||||
installer ALL=(ALL) NOPASSWD: \
|
installer ALL=(ALL) NOPASSWD: \
|
||||||
/usr/local/bin/our-own-deploy-script
|
/usr/bin/systemctl
|
||||||
|
installer ALL=(ALL) NOPASSWD: FIRST_RUN_CLEANUP
|
||||||
EOF
|
EOF
|
||||||
run_migration
|
run_migration
|
||||||
|
|
||||||
[[ -e $first_run ]] ||
|
[[ -e $first_run ]] ||
|
||||||
fail "migration reads a continued line as one rule and a continued comment as comment"
|
fail "migration keeps a spec left live under a commented continuation"
|
||||||
pass "migration reads a continued line as one rule and a continued comment as comment"
|
pass "migration keeps a spec left live under a commented continuation"
|
||||||
|
|
||||||
reset_machine
|
reset_machine
|
||||||
printf 'installer ALL=(ALL) NOPASSWD: %s/.local/bin/tsui\n' "$home_dir" >"$tsui"
|
printf 'installer ALL=(ALL) NOPASSWD: %s/.local/bin/tsui\n' "$home_dir" >"$tsui"
|
||||||
@@ -454,3 +458,49 @@ run_migration
|
|||||||
[[ ! -e $plymouth_unit ]] ||
|
[[ ! -e $plymouth_unit ]] ||
|
||||||
fail "migration removes a unit whose ExecStop continues across a comment" "$(cat "$plymouth_unit")"
|
fail "migration removes a unit whose ExecStop continues across a comment" "$(cat "$plymouth_unit")"
|
||||||
pass "migration removes a unit whose ExecStop continues across a comment"
|
pass "migration removes a unit whose ExecStop continues across a comment"
|
||||||
|
|
||||||
|
# An empty ExecStop= resets the list: `systemd-analyze verify` reports the missing
|
||||||
|
# command for a unit with one ExecStop=, and reports nothing once a bare
|
||||||
|
# ExecStop= follows it. An administrator who neutralised the unit that way runs
|
||||||
|
# nothing at shutdown and keeps their file.
|
||||||
|
reset_machine
|
||||||
|
cat >"$plymouth_unit" <<'EOF'
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/true
|
||||||
|
ExecStop=/home/installer/.local/share/omarchy/bin/omarchy-plymouth-shutdown-sync
|
||||||
|
ExecStop=
|
||||||
|
EOF
|
||||||
|
before=$(cat "$plymouth_unit")
|
||||||
|
run_migration
|
||||||
|
|
||||||
|
[[ -e $plymouth_unit ]] ||
|
||||||
|
fail "migration keeps a unit whose ExecStop list was reset to empty"
|
||||||
|
[[ $(cat "$plymouth_unit") == "$before" ]] ||
|
||||||
|
fail "migration leaves that unit byte for byte"
|
||||||
|
pass "migration keeps a unit whose ExecStop list was reset to empty"
|
||||||
|
|
||||||
|
# A reset followed by a fresh home ExecStop= is live again.
|
||||||
|
reset_machine
|
||||||
|
cat >"$plymouth_unit" <<'EOF'
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/true
|
||||||
|
ExecStop=
|
||||||
|
ExecStop=/home/installer/.local/share/omarchy/bin/omarchy-plymouth-shutdown-sync
|
||||||
|
EOF
|
||||||
|
run_migration
|
||||||
|
|
||||||
|
[[ ! -e $plymouth_unit ]] ||
|
||||||
|
fail "migration removes a unit whose ExecStop is set again after a reset"
|
||||||
|
pass "migration removes a unit whose ExecStop is set again after a reset"
|
||||||
|
|
||||||
|
# systemd honours a directive whose line ends the file mid-continuation:
|
||||||
|
# `systemd-analyze verify` resolves an ExecStop= written that way.
|
||||||
|
reset_machine
|
||||||
|
printf '[Service]\nType=oneshot\nExecStart=/usr/bin/true\nExecStop=/home/installer/.local/share/omarchy/bin/omarchy-plymouth-shutdown-sync \\\n' >"$plymouth_unit"
|
||||||
|
run_migration
|
||||||
|
|
||||||
|
[[ ! -e $plymouth_unit ]] ||
|
||||||
|
fail "migration removes a unit whose last line ends mid-continuation"
|
||||||
|
pass "migration removes a unit whose last line ends mid-continuation"
|
||||||
|
|||||||
Reference in New Issue
Block a user