Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions install/config/locate.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
UPDATEDB_CONF_PATH="${OMARCHY_UPDATEDB_CONF_PATH:-/etc/updatedb.conf}"
UPDATEDB_CONF_PATH=/etc/updatedb.conf

echo "Configuring locate to skip Btrfs snapshots and index Btrfs subvolumes"

[[ -f $UPDATEDB_CONF_PATH ]] || exit 0

# Config values can contain sed syntax, so pass the replacement to a fixed awk
# program strictly as data and publish it only after the rewrite succeeds.
rewrite_updatedb_prunepaths() (
local replacement=$1
local rewritten_conf

rewritten_conf=$(mktemp -- "${UPDATEDB_CONF_PATH}.tmp.XXXXXX") || return 1
trap 'rm -f -- "$rewritten_conf"' EXIT

if ! UPDATEDB_PRUNEPATHS="$replacement" awk '
BEGIN { replacement = ENVIRON["UPDATEDB_PRUNEPATHS"] }
/^[[:space:]]*PRUNEPATHS[[:space:]]*=/ {
printf "PRUNEPATHS = \"%s\"\n", replacement
next
}
{ print }
' "$UPDATEDB_CONF_PATH" >"$rewritten_conf"; then
return 1
fi

cp --attributes-only --preserve=all -- "$UPDATEDB_CONF_PATH" "$rewritten_conf" || return 1
touch -m -- "$rewritten_conf" || return 1
mv -fT -- "$rewritten_conf" "$UPDATEDB_CONF_PATH" || return 1
trap - EXIT
)

# updatedb refuses to run at all on a config that defines a variable twice, so
# every setting here is rewritten where it already stands and only appended
# when the file has no line for it.
Expand All @@ -25,7 +51,7 @@ if grep -qE '^[[:space:]]*PRUNEPATHS[[:space:]]*=' "$UPDATEDB_CONF_PATH"; then
pruned=$(sed -nE 's|^[[:space:]]*PRUNEPATHS[[:space:]]*=[[:space:]]*"([^"]*)".*|\1|p' "$UPDATEDB_CONF_PATH" | tail -n 1)

if [[ " $pruned " != *" /.snapshots "* ]]; then
sed -i -E "s|^[[:space:]]*PRUNEPATHS[[:space:]]*=.*|PRUNEPATHS = \"/.snapshots${pruned:+ $pruned}\"|" "$UPDATEDB_CONF_PATH"
rewrite_updatedb_prunepaths "/.snapshots${pruned:+ $pruned}"
fi
else
printf '%s\n' 'PRUNEPATHS = "/.snapshots"' >>"$UPDATEDB_CONF_PATH"
Expand Down
4 changes: 2 additions & 2 deletions migrations/1784809451.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ echo "Configure locate to skip Btrfs snapshots and index Btrfs subvolumes"

OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
locate_config_script="$OMARCHY_PATH/install/config/locate.sh"
UPDATEDB_CONF_PATH="${OMARCHY_UPDATEDB_CONF_PATH:-/etc/updatedb.conf}"
UPDATEDB_CONF_PATH=/etc/updatedb.conf

as_root() {
if (( EUID == 0 )); then
Expand All @@ -20,7 +20,7 @@ if grep -q '^PRUNE_BIND_MOUNTS = "no"' "$UPDATEDB_CONF_PATH" &&
exit 0
fi

as_root env OMARCHY_UPDATEDB_CONF_PATH="$UPDATEDB_CONF_PATH" bash -euo pipefail "$locate_config_script"
as_root bash -euo pipefail "$locate_config_script"

# Rebuild the index with the new exclusions; pruning /.snapshots turns
# multi-hour runs on snapshot-heavy systems back into one-minute runs. Restart
Expand Down
128 changes: 100 additions & 28 deletions test/shell.d/locate-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,30 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"

config_script="$ROOT/install/config/locate.sh"
locate_migration="$ROOT/migrations/1784809451.sh"
[[ -f $locate_migration ]] || fail "locate migration exists"

test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT

config_path_count=$(grep -cFx 'UPDATEDB_CONF_PATH=/etc/updatedb.conf' "$config_script" || true)
migration_path_count=$(grep -cFx 'UPDATEDB_CONF_PATH=/etc/updatedb.conf' "$locate_migration" || true)
config_path_assignments=$(grep -cE '^[[:space:]]*UPDATEDB_CONF_PATH=' "$config_script" || true)
migration_path_assignments=$(grep -cE '^[[:space:]]*UPDATEDB_CONF_PATH=' "$locate_migration" || true)
(( config_path_count == 1 && config_path_assignments == 1 )) || fail "locate config always selects the system updatedb.conf"
(( migration_path_count == 1 && migration_path_assignments == 1 )) || fail "locate migration always checks the system updatedb.conf"
if grep -qF 'OMARCHY_UPDATEDB_CONF_PATH' "$config_script" "$locate_migration"; then
fail "locate scripts expose no environment-controlled config path"
fi
pass "locate scripts keep the privileged config path fixed"

run_config() {
local config_path=$1

sed "s|^UPDATEDB_CONF_PATH=/etc/updatedb.conf$|UPDATEDB_CONF_PATH=$config_path|" "$config_script" |
bash -euo pipefail
}

stock_conf() {
cat >"$1" <<'CONF'
PRUNE_BIND_MOUNTS = "yes"
Expand All @@ -27,35 +47,36 @@ assert_conf_parses() {
command -v updatedb >/dev/null || return 0

local errors
errors=$(updatedb --config-file "$1" -U "$empty_tree" -o "$test_tmp/plocate.db" 2>&1 >/dev/null | grep -F "$1:" || true)
[[ -z $errors ]] || fail "updatedb accepts the rewritten config" "$errors"
if ! errors=$(updatedb --config-file "$1" --require-visibility no -U "$empty_tree" -o "$test_tmp/plocate.db" 2>&1 >/dev/null); then
fail "updatedb accepts the rewritten config" "$errors"
fi
}

conf="$test_tmp/updatedb.conf"
stock_conf "$conf"

OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
run_config "$conf" >/dev/null

grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config indexes Btrfs subvolume mounts like /home"
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate config prunes /.snapshots"
assert_conf_parses "$conf"
pass "locate config skips Btrfs snapshots and indexes Btrfs subvolumes"

OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
run_config "$conf" >/dev/null

[[ $(grep -o '/\.snapshots' "$conf" | wc -l) -eq 1 ]] || fail "locate config is idempotent"
assert_conf_parses "$conf"
pass "locate config leaves an already-configured file alone"

OMARCHY_UPDATEDB_CONF_PATH="$test_tmp/missing.conf" bash -euo pipefail "$config_script" >/dev/null
run_config "$test_tmp/missing.conf" >/dev/null
pass "locate config tolerates a missing updatedb.conf"

# A hand-edited updatedb.conf may drop the settings entirely, or write them
# without the spaces around the "=" or the quotes that the stock Arch file uses.
conf="$test_tmp/sparse-updatedb.conf"
printf '%s\n' 'PRUNENAMES = ".git .hg .svn"' >"$conf"

OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
run_config "$conf" >/dev/null

grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config adds a missing PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config adds a missing PRUNEPATHS"
Expand All @@ -65,7 +86,7 @@ pass "locate config adds settings a hand-edited updatedb.conf is missing"
conf="$test_tmp/unspaced-updatedb.conf"
printf '%s\n' 'PRUNE_BIND_MOUNTS="yes"' 'PRUNEPATHS="/tmp /var/tmp"' >"$conf"

OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
run_config "$conf" >/dev/null

grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an unspaced PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp /var/tmp"' "$conf" || fail "locate config prunes /.snapshots in an unspaced PRUNEPATHS"
Expand All @@ -79,7 +100,7 @@ conf="$test_tmp/commented-updatedb.conf"
printf '%s\n' ' PRUNE_BIND_MOUNTS = "yes" # subvolumes look like bind mounts' \
'PRUNEPATHS = "/tmp" # scratch' >"$conf"

OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
run_config "$conf" >/dev/null

grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an indented PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp"' "$conf" || fail "locate config keeps the paths a commented PRUNEPATHS already prunes"
Expand All @@ -92,7 +113,7 @@ pass "locate config handles indented settings and trailing comments"
conf="$test_tmp/unquoted-updatedb.conf"
printf '%s\n' 'PRUNEPATHS = /tmp' >"$conf"

OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
run_config "$conf" >/dev/null

grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config repairs an unquoted PRUNEPATHS"
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces an unquoted PRUNEPATHS instead of adding a second one"
Expand All @@ -103,20 +124,53 @@ pass "locate config handles updatedb.conf written without quotes"
conf="$test_tmp/nested-snapshots-updatedb.conf"
printf '%s\n' 'PRUNEPATHS = "/var/lib/machines/.snapshots"' >"$conf"

OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
run_config "$conf" >/dev/null

grep -qFx 'PRUNEPATHS = "/.snapshots /var/lib/machines/.snapshots"' "$conf" || fail "locate config prunes /.snapshots alongside a path that ends in it"
assert_conf_parses "$conf"
pass "locate config tells /.snapshots apart from a path that ends in it"

locate_migration=$(grep -rl 'Configure locate to skip Btrfs snapshots' "$ROOT/migrations" | head -n 1 || true)
[[ -n $locate_migration ]] || fail "locate migration exists"

fake_bin="$test_tmp/bin"
mkdir -p "$fake_bin"

# PRUNEPATHS is untrusted config data. In particular, GNU sed replacement
# escapes and delimiters must never turn the reporter's harmless id marker
# into another sed command.
conf="$test_tmp/injection-updatedb.conf"
proof="$test_tmp/reporter-proof.txt"
rce_payload="\\x22;id > $proof;:|ew "
printf 'PRUNEPATHS = "%s"\n' "$rce_payload" >"$conf"

PATH="$fake_bin:$PATH" run_config "$conf" >/dev/null

[[ ! -e $proof ]] || fail "locate config keeps PRUNEPATHS data out of the sed program"
grep -qFx "PRUNEPATHS = \"/.snapshots $rce_payload\"" "$conf" || fail "locate config preserves a sed-like PRUNEPATHS value as data"
assert_conf_parses "$conf"
pass "locate config cannot execute commands embedded in PRUNEPATHS"

conf="$test_tmp/metachar-updatedb.conf"
metachar_paths='/tmp/amp&ersand /tmp/pipe|name /tmp/back\slash'
printf 'PRUNEPATHS = "%s"\n' "$metachar_paths" >"$conf"
chmod 0640 "$conf"
metadata_before=$(stat -c '%a %u:%g' "$conf")

run_config "$conf" >/dev/null

grep -qFx "PRUNEPATHS = \"/.snapshots $metachar_paths\"" "$conf" || fail "locate config preserves replacement metacharacters"
[[ $(stat -c '%a %u:%g' "$conf") == $metadata_before ]] || fail "locate config preserves updatedb.conf ownership and mode"
assert_conf_parses "$conf"
checksum_before=$(sha256sum "$conf")
run_config "$conf" >/dev/null
[[ $(sha256sum "$conf") == $checksum_before ]] || fail "locate config is byte-idempotent with metacharacters"
pass "locate config safely preserves metacharacters and file metadata"

cat >"$fake_bin/sudo" <<'STUB'
#!/bin/bash
{
printf 'sudo'
printf ' %s' "$@"
printf '\n'
} >>"$TEST_LOG"
exec "$@"
STUB
chmod +x "$fake_bin/sudo"
Expand All @@ -127,42 +181,60 @@ printf 'systemctl %s\n' "$*" >>"$TEST_LOG"
STUB
chmod +x "$fake_bin/systemctl"

conf="$test_tmp/migration-updatedb.conf"
stock_conf "$conf"
trusted_conf="$test_tmp/migration-updatedb.conf"
poison_conf="$test_tmp/poison-updatedb.conf"
migration_trusted_proof="$test_tmp/migration-trusted-proof.txt"
migration_poison_proof="$test_tmp/migration-poison-proof.txt"
migration_trusted_payload="\\x22;id > $migration_trusted_proof;:|ew "
migration_poison_payload="\\x22;id > $migration_poison_proof;:|ew "
printf 'PRUNEPATHS = "%s"\n' "$migration_trusted_payload" >"$trusted_conf"
printf 'PRUNEPATHS = "%s"\n' "$migration_poison_payload" >"$poison_conf"
poison_checksum=$(sha256sum "$poison_conf")

migration_root="$test_tmp/migration-root"
mkdir -p "$migration_root/install/config"
staged_config_script="$migration_root/install/config/locate.sh"
staged_migration="$test_tmp/locate-migration.sh"
sed "s|^UPDATEDB_CONF_PATH=/etc/updatedb.conf$|UPDATEDB_CONF_PATH=$trusted_conf|" "$config_script" >"$staged_config_script"
sed "s|^UPDATEDB_CONF_PATH=/etc/updatedb.conf$|UPDATEDB_CONF_PATH=$trusted_conf|" "$locate_migration" >"$staged_migration"

TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$ROOT" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null

grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate migration rewrites updatedb.conf"
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate migration prunes /.snapshots"
OMARCHY_PATH="$migration_root" \
OMARCHY_UPDATEDB_CONF_PATH="$poison_conf" \
bash -euo pipefail "$staged_migration" >/dev/null

grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$trusted_conf" || fail "locate migration rewrites updatedb.conf"
grep -qFx "PRUNEPATHS = \"/.snapshots $migration_trusted_payload\"" "$trusted_conf" || fail "locate migration prunes /.snapshots without compiling config data"
[[ ! -e $migration_trusted_proof && ! -e $migration_poison_proof ]] || fail "locate migration cannot execute the reporter payload"
[[ $(sha256sum "$poison_conf") == $poison_checksum ]] || fail "locate migration ignores an environment-selected config"
assert_conf_parses "$trusted_conf"
if grep -qF 'OMARCHY_UPDATEDB_CONF_PATH' "$test_tmp/calls.log" || grep -qF "$poison_conf" "$test_tmp/calls.log"; then
fail "locate migration does not forward the untrusted config path through sudo"
fi
grep -qFx 'systemctl restart --no-block plocate-updatedb.service' "$test_tmp/calls.log" || fail "locate migration replaces an in-flight run and rebuilds the index without blocking"
pass "locate migration fixes existing installs and rebuilds the index"

: >"$test_tmp/calls.log"

TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$ROOT" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null
OMARCHY_PATH="$migration_root" \
OMARCHY_UPDATEDB_CONF_PATH="$poison_conf" \
bash -euo pipefail "$staged_migration" >/dev/null

[[ ! -s $test_tmp/calls.log ]] || fail "locate migration skips already-configured installs"
pass "locate migration is a no-op once updatedb.conf is configured"

# A dev checkout carries migrations from a release whose install scripts the
# checked-out tree may not have yet, and omarchy-migrate runs under set -e.
: >"$test_tmp/calls.log"
conf="$test_tmp/no-config-script-updatedb.conf"
stock_conf "$conf"
stock_conf "$trusted_conf"

TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$test_tmp/empty" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null ||
bash -euo pipefail "$staged_migration" >/dev/null ||
fail "locate migration survives a tree without the locate config script"

[[ ! -s $test_tmp/calls.log ]] || fail "locate migration touches nothing without the locate config script"
Expand Down