diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-28 09:27:56 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-28 09:27:56 +0300 |
| commit | ea9de473e3d6788ea08ca3697722cf89b9d1d107 (patch) | |
| tree | b3a3d6d6bc1ab8bc5ca76b9574a7247da19d7bb7 /src | |
| parent | 86c354932e698d52f23e65a1dafb1b7250f4c1b9 (diff) | |
Extract destructive --clean path guard into config.clean-guard.source.sh
Move the safety-critical rm -rf guard (resolve_dist_dir_path and
validate_clean_dist_dir, including the dangerous-path blocklist) out of
config.validate.source.sh into a dedicated module so the policy that gates
an unconditional rm -rf lives in one isolated place with its own test
surface. This is a pure move refactor: the guard logic, the forbidden
list, and the error messages are byte-identical.
Register the new module in both LIB_SOURCES lists (Justfile and the
src/shuriken.sh marker block) at the same position, right before
config.validate.source.sh, keeping the anti-drift invariant green and
regenerating bin/shuriken.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/config.clean-guard.source.sh | 99 | ||||
| -rw-r--r-- | src/lib/config.validate.source.sh | 88 | ||||
| -rwxr-xr-x | src/shuriken.sh | 2 |
3 files changed, 106 insertions, 83 deletions
diff --git a/src/lib/config.clean-guard.source.sh b/src/lib/config.clean-guard.source.sh new file mode 100644 index 0000000..785a1cd --- /dev/null +++ b/src/lib/config.clean-guard.source.sh @@ -0,0 +1,99 @@ +# Destructive --clean path guard. Extracted from config.validate.source.sh +# (task tr0) so the safety-critical "refuse to rm -rf a dangerous DIST_DIR" +# policy lives in one isolated module with its own test surface, instead of +# being buried among the generic config validators. +# +# This module owns the single source of truth for the dangerous-path blocklist +# (the filesystem root, well-known system trees, HOME, and the current working +# directory) plus the canonicalization needed to apply it safely. It is kept +# separate because a regression here is catastrophic (it gates an unconditional +# rm -rf), so the policy and its guard tests deserve a dedicated home. +# +# It still leans on the generic helpers in config.validate.source.sh +# (require_config_var, config_error, validate_dist_dir); all libs are sourced +# before any code runs, so those call sites resolve at runtime regardless of +# module order. + +# Resolve DIST_DIR to a canonical absolute path so the --clean guard cannot be +# bypassed via "." / trailing slashes / symlinks / relative paths. The directory +# itself may not exist yet (cleaning a stale config), so when it is missing we +# resolve its existing parent and append the basename. Output is printed; the +# caller captures it. Returns non-zero only if even the parent cannot resolve. +resolve_dist_dir_path() { + local -r target_dir="$1"; shift + local parent base + + if [ -d "$target_dir" ]; then + ( cd "$target_dir" && pwd -P ) && return + return 1 + fi + + # DIST_DIR does not exist: canonicalize the deepest existing ancestor and + # re-attach the remaining path so symlinked parents are still resolved. + parent=$(existing_parent_dir "$target_dir") + base=${target_dir#"$parent"} + base=${base#/} + parent=$( cd "$parent" && pwd -P ) || return 1 + if [ -n "$base" ]; then + printf '%s/%s\n' "$parent" "$base" + else + printf '%s\n' "$parent" + fi +} + +# Guard for the destructive --clean action: refuse to "rm -rf" DIST_DIR when it +# resolves to an empty value or a clearly dangerous location (filesystem root, +# the user's HOME, the current working directory, or a well-known system tree). +# This is the single place where the dangerous-path policy lives so it stays in +# sync. NOTE (ln0): this only guards against deleting the wrong tree; leftover +# staging artifacts are a separate concern handled by task ln0. +validate_clean_dist_dir() { + local resolved home_resolved cwd_resolved + local -a forbidden=( + / /home /root /tmp /usr /etc /var /bin /sbin /lib /boot /opt + ) + local entry + + require_config_var DIST_DIR || return + + # Reuse the shared DIST_DIR sanity checks (must be a directory, writable, + # parent writable) before applying the destructive-path policy. + validate_dist_dir || return + + resolved=$(resolve_dist_dir_path "$DIST_DIR") || { + config_error "DIST_DIR $DIST_DIR could not be resolved" + return 1 + } + + if [ -z "$resolved" ]; then + config_error 'refusing to clean an empty DIST_DIR' + return 1 + fi + + # Reject the filesystem root and well-known system directories outright. + for entry in "${forbidden[@]}"; do + if [ "$resolved" = "$entry" ]; then + config_error \ + "refusing to clean DIST_DIR $resolved (dangerous path)" + return 1 + fi + done + + # Reject HOME and the current working directory themselves (deleting either + # would be catastrophic and is never the intended DIST_DIR). + if [ -n "${HOME:-}" ]; then + home_resolved=$( cd "$HOME" 2>/dev/null && pwd -P ) || home_resolved='' + if [ -n "$home_resolved" ] && [ "$resolved" = "$home_resolved" ]; then + config_error \ + "refusing to clean DIST_DIR $resolved (is HOME)" + return 1 + fi + fi + + cwd_resolved=$(pwd -P) + if [ "$resolved" = "$cwd_resolved" ]; then + config_error \ + "refusing to clean DIST_DIR $resolved (is current directory)" + return 1 + fi +} diff --git a/src/lib/config.validate.source.sh b/src/lib/config.validate.source.sh index f0b14bf..4d4466e 100644 --- a/src/lib/config.validate.source.sh +++ b/src/lib/config.validate.source.sh @@ -87,89 +87,11 @@ validate_dist_dir() { fi } -# Resolve DIST_DIR to a canonical absolute path so the --clean guard cannot be -# bypassed via "." / trailing slashes / symlinks / relative paths. The directory -# itself may not exist yet (cleaning a stale config), so when it is missing we -# resolve its existing parent and append the basename. Output is printed; the -# caller captures it. Returns non-zero only if even the parent cannot resolve. -resolve_dist_dir_path() { - local -r target_dir="$1"; shift - local parent base - - if [ -d "$target_dir" ]; then - ( cd "$target_dir" && pwd -P ) && return - return 1 - fi - - # DIST_DIR does not exist: canonicalize the deepest existing ancestor and - # re-attach the remaining path so symlinked parents are still resolved. - parent=$(existing_parent_dir "$target_dir") - base=${target_dir#"$parent"} - base=${base#/} - parent=$( cd "$parent" && pwd -P ) || return 1 - if [ -n "$base" ]; then - printf '%s/%s\n' "$parent" "$base" - else - printf '%s\n' "$parent" - fi -} - -# Guard for the destructive --clean action: refuse to "rm -rf" DIST_DIR when it -# resolves to an empty value or a clearly dangerous location (filesystem root, -# the user's HOME, the current working directory, or a well-known system tree). -# This is the single place where the dangerous-path policy lives so it stays in -# sync. NOTE (ln0): this only guards against deleting the wrong tree; leftover -# staging artifacts are a separate concern handled by task ln0. -validate_clean_dist_dir() { - local resolved home_resolved cwd_resolved - local -a forbidden=( - / /home /root /tmp /usr /etc /var /bin /sbin /lib /boot /opt - ) - local entry - - require_config_var DIST_DIR || return - - # Reuse the shared DIST_DIR sanity checks (must be a directory, writable, - # parent writable) before applying the destructive-path policy. - validate_dist_dir || return - - resolved=$(resolve_dist_dir_path "$DIST_DIR") || { - config_error "DIST_DIR $DIST_DIR could not be resolved" - return 1 - } - - if [ -z "$resolved" ]; then - config_error 'refusing to clean an empty DIST_DIR' - return 1 - fi - - # Reject the filesystem root and well-known system directories outright. - for entry in "${forbidden[@]}"; do - if [ "$resolved" = "$entry" ]; then - config_error \ - "refusing to clean DIST_DIR $resolved (dangerous path)" - return 1 - fi - done - - # Reject HOME and the current working directory themselves (deleting either - # would be catastrophic and is never the intended DIST_DIR). - if [ -n "${HOME:-}" ]; then - home_resolved=$( cd "$HOME" 2>/dev/null && pwd -P ) || home_resolved='' - if [ -n "$home_resolved" ] && [ "$resolved" = "$home_resolved" ]; then - config_error \ - "refusing to clean DIST_DIR $resolved (is HOME)" - return 1 - fi - fi - - cwd_resolved=$(pwd -P) - if [ "$resolved" = "$cwd_resolved" ]; then - config_error \ - "refusing to clean DIST_DIR $resolved (is current directory)" - return 1 - fi -} +# The destructive --clean path guard (resolve_dist_dir_path, +# validate_clean_dist_dir, and the dangerous-path blocklist) lives in +# config.clean-guard.source.sh (task tr0). It is kept in its own module because +# it gates an unconditional rm -rf and warrants an isolated test surface; the +# functions there still reuse the generic validators below at runtime. validate_template_dir_access() { if [[ ! -d "$TEMPLATE_DIR" || ! -r "$TEMPLATE_DIR" \ diff --git a/src/shuriken.sh b/src/shuriken.sh index 2308d90..290a074 100755 --- a/src/shuriken.sh +++ b/src/shuriken.sh @@ -151,6 +151,8 @@ source "$SHURIKEN_SOURCE_DIR/lib/config.print.source.sh" source "$SHURIKEN_SOURCE_DIR/lib/config.sync.source.sh" # shellcheck source=src/lib/config.staging.source.sh source "$SHURIKEN_SOURCE_DIR/lib/config.staging.source.sh" +# shellcheck source=src/lib/config.clean-guard.source.sh +source "$SHURIKEN_SOURCE_DIR/lib/config.clean-guard.source.sh" # shellcheck source=src/lib/config.validate.source.sh source "$SHURIKEN_SOURCE_DIR/lib/config.validate.source.sh" # shellcheck source=src/lib/config.cli.source.sh |
