8669 lines
379 KiB
Bash
8669 lines
379 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# Regression coverage for the source-safe local recovery hardware contract.
|
|
set -Eeuo pipefail
|
|
|
|
readonly REPOSITORY_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)"
|
|
readonly LIBRARY_PATH="${REPOSITORY_ROOT}/scripts/lib/k3s-local-recovery.sh"
|
|
readonly CONTRACT_PATH="${REPOSITORY_ROOT}/infrastructure/security/k3s/local-recovery.env"
|
|
readonly VALIDATOR_PATH="${REPOSITORY_ROOT}/scripts/validate/k3s-local-recovery.sh"
|
|
readonly FEASIBILITY_PATH="${REPOSITORY_ROOT}/scripts/validate/k3s-local-recovery-feasibility.sh"
|
|
readonly PREPARE_WRAPPER_PATH="${REPOSITORY_ROOT}/scripts/bootstrap/prepare-k3s-local-recovery.sh"
|
|
readonly OPEN_WRAPPER_PATH="${REPOSITORY_ROOT}/scripts/bootstrap/open-k3s-local-recovery.sh"
|
|
readonly CLOSE_WRAPPER_PATH="${REPOSITORY_ROOT}/scripts/bootstrap/close-k3s-local-recovery.sh"
|
|
|
|
fail() {
|
|
printf 'TEST FAILURE: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
assert_eq() {
|
|
local expected="$1" actual="$2" description="$3"
|
|
[[ "$actual" == "$expected" ]] || fail "$description (expected ${expected}, got ${actual})"
|
|
}
|
|
|
|
assert_succeeds() {
|
|
"$@" || fail "expected success: $*"
|
|
}
|
|
|
|
assert_fails() {
|
|
if "$@"; then
|
|
fail "expected failure: $*"
|
|
fi
|
|
}
|
|
|
|
task5b_focus_selector_guard() {
|
|
local focus="${1-}"
|
|
(( $# == 1 )) || return 2
|
|
case "$focus" in
|
|
''|gate0|api_state|tty_seam|action1) return 0 ;;
|
|
*)
|
|
printf 'TEST FAILURE: unsupported TASK5B_FOCUS: %s\n' "$focus" >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# A nonempty Task 5B selector is closed before fixture creation, sourcing, or
|
|
# any Task 5A/older/full-suite assertion can run.
|
|
task5b_focus_selector_guard "${TASK5B_FOCUS:-}" || exit $?
|
|
|
|
task4_review2_focus_selector_guard() {
|
|
local focus="${1-}"
|
|
(( $# == 1 )) || return 2
|
|
case "$focus" in
|
|
''|critical|capture|atomic|show_protected|feas_show_protected|feas_attachment_listing) return 0 ;;
|
|
*)
|
|
printf 'TEST FAILURE: unsupported TASK4_REVIEW2_FOCUS: %s\n' "$focus" >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# A Task 1 selector is closed before any optional full-suite branch can run.
|
|
task4_review2_focus_selector_guard "${TASK4_REVIEW2_FOCUS:-}" || exit $?
|
|
|
|
fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/k3s-local-recovery-test.XXXXXX")"
|
|
cleanup() {
|
|
case "$fixture_root" in
|
|
/tmp/k3s-local-recovery-test.*|"${TMPDIR:-/tmp}"/k3s-local-recovery-test.*)
|
|
rm -rf -- "$fixture_root"
|
|
;;
|
|
*)
|
|
fail 'refusing to remove an unexpected fixture directory'
|
|
;;
|
|
esac
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
write_fixture() {
|
|
local name="$1" destination
|
|
destination="${fixture_root}/${name}"
|
|
cp -- "$CONTRACT_PATH" "$destination"
|
|
case "$name" in
|
|
missing_key.env)
|
|
sed -i '/^K3SLR_OWNER_GID=/d' "$destination"
|
|
;;
|
|
duplicate_key.env)
|
|
printf '%s\n' 'K3SLR_OWNER_GID=1000' >>"$destination"
|
|
;;
|
|
unknown_key.env)
|
|
printf '%s\n' 'K3SLR_UNSUPPORTED=value' >>"$destination"
|
|
;;
|
|
command_substitution.env)
|
|
printf 'K3SLR_OUTER_MOUNT=$(touch %s)\n' "${fixture_root}/evaluated" >"$destination.tmp"
|
|
sed '/^K3SLR_OUTER_MOUNT=/d' "$destination" >>"$destination.tmp"
|
|
mv -- "$destination.tmp" "$destination"
|
|
;;
|
|
relative_device.env)
|
|
sed -i 's|^K3SLR_RECOVERY_DISK_BY_ID=.*|K3SLR_RECOVERY_DISK_BY_ID=dev/disk/by-id/wwn-0x500a0751e6aa6254|' "$destination"
|
|
;;
|
|
parent_path.env)
|
|
sed -i 's|^K3SLR_DATABASE_RELATIVE=.*|K3SLR_DATABASE_RELATIVE=HyeonworksRecovery/..|' "$destination"
|
|
;;
|
|
*)
|
|
fail "unknown fixture: $name"
|
|
;;
|
|
esac
|
|
printf '%s\n' "$destination"
|
|
}
|
|
|
|
load_fixture() {
|
|
_k3slr_load_contract "$(write_fixture "$1")"
|
|
}
|
|
|
|
# The production change this catches is a missing or unsafe source-only contract
|
|
# library. The RED run before implementation asserted that source failed.
|
|
# shellcheck source=/dev/null
|
|
source "$LIBRARY_PATH"
|
|
|
|
assert_succeeds _k3slr_load_contract "$CONTRACT_PATH"
|
|
assert_eq /dev/disk/by-id/wwn-0x500a0751e6aa6254 \
|
|
"$( _k3slr_config_value "$CONTRACT_PATH" K3SLR_RECOVERY_DISK_BY_ID )" \
|
|
'approved recovery disk identity is available without sourcing config'
|
|
assert_eq 'K3s Recovery LUKS' "$K3SLR_KEEPASS_ENTRY" 'KeePass entry is library-owned'
|
|
assert_eq 'luks-header-backup.bin' "$K3SLR_KEEPASS_HEADER_ATTACHMENT" 'header attachment is library-owned'
|
|
assert_fails _k3slr_config_value "$CONTRACT_PATH" K3SLR_UNSUPPORTED
|
|
|
|
assert_fails load_fixture missing_key.env
|
|
assert_fails load_fixture duplicate_key.env
|
|
assert_fails load_fixture unknown_key.env
|
|
assert_fails load_fixture command_substitution.env
|
|
[[ ! -e "${fixture_root}/evaluated" ]] || fail 'contract parser evaluated command substitution'
|
|
assert_fails load_fixture relative_device.env
|
|
assert_fails load_fixture parent_path.env
|
|
assert_eq 12884901891 "$( _k3slr_required_bytes 858993460 1 )" '25 percent padding and two phases'
|
|
assert_fails _k3slr_required_bytes -1 0
|
|
assert_fails _k3slr_required_bytes 1 9223372036854775807
|
|
assert_fails _k3slr_required_bytes 1844674407370955161 0
|
|
assert_succeeds _k3slr_inner_capacity_fits 12884901891 1 858993460
|
|
assert_fails _k3slr_inner_capacity_fits 12884901890 1 858993460
|
|
assert_fails _k3slr_inner_capacity_fits 12884901891 1x 858993460
|
|
assert_succeeds _k3slr_outer_capacity_fits 1000000000000 400000000000 34359738368
|
|
assert_fails _k3slr_outer_capacity_fits 1000000000000 250000000000 34359738368
|
|
assert_fails _k3slr_outer_capacity_fits 1000 1001 1
|
|
|
|
validator_device_evidence() {
|
|
local fixture="$1"
|
|
local recovery_partition=/dev/fixture-recovery-partition
|
|
local recovery_partition_parent=/dev/fixture-recovery-disk
|
|
local recovery_fs_uuid=4EA0196C0C5FA27E recovery_partuuid=4670aa9f-9045-4bce-930d-9e84dfec9f38
|
|
local recovery_model=CT1000MX500SSD1 recovery_serial=2306E6AA6254
|
|
local recovery_wwn=0x500a0751e6aa6254 k3s_major_minor=8:32
|
|
local smart_health=PASSED smart_reallocated=0 smart_pending=0 smart_uncorrectable=0 ntfs_probe=pass
|
|
case "$fixture" in
|
|
wrong_by_id_target) recovery_partition_parent=/dev/fixture-wrong-disk ;;
|
|
wrong_fs_uuid) recovery_fs_uuid=AAAAAAAAAAAAAAAA ;;
|
|
wrong_partuuid) recovery_partuuid=aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa ;;
|
|
wrong_model) recovery_model=WRONGMODEL ;;
|
|
wrong_serial) recovery_serial=WRONGSERIAL ;;
|
|
wrong_wwn) recovery_wwn=0xaaaaaaaaaaaaaaaa ;;
|
|
same_parent_disk) k3s_major_minor=8:0 ;;
|
|
smart_health_failed) smart_health=FAILED ;;
|
|
smart_reallocated_nonzero) smart_reallocated=1 ;;
|
|
smart_pending_nonzero) smart_pending=1 ;;
|
|
smart_uncorrectable_nonzero) smart_uncorrectable=1 ;;
|
|
ntfs_dirty_or_hibernated) ntfs_probe=fail ;;
|
|
esac
|
|
cat <<EOF
|
|
recovery_partition=$recovery_partition
|
|
recovery_disk=/dev/fixture-recovery-disk
|
|
recovery_partition_parent=$recovery_partition_parent
|
|
recovery_fs_uuid=$recovery_fs_uuid
|
|
recovery_partuuid=$recovery_partuuid
|
|
recovery_type=ntfs
|
|
recovery_model=$recovery_model
|
|
recovery_serial=$recovery_serial
|
|
recovery_wwn=$recovery_wwn
|
|
recovery_major_minor=8:0
|
|
k3s_partition=/dev/fixture-k3s-partition
|
|
k3s_disk=/dev/fixture-k3s-disk
|
|
k3s_partition_parent=/dev/fixture-k3s-disk
|
|
k3s_fs_uuid=b86086ef-2b3c-4638-abcf-fc7f137dcb97
|
|
k3s_partuuid=b081b955-d6bc-442c-ac0f-db76560a5245
|
|
k3s_type=ext4
|
|
k3s_model=CT250MX500SSD1
|
|
k3s_serial=21132DF28BD1
|
|
k3s_wwn=0x500a07512df28bd1
|
|
k3s_major_minor=$k3s_major_minor
|
|
smart_health=$smart_health
|
|
smart_reallocated=$smart_reallocated
|
|
smart_pending=$smart_pending
|
|
smart_uncorrectable=$smart_uncorrectable
|
|
ntfs_probe=$ntfs_probe
|
|
EOF
|
|
}
|
|
|
|
validator_state_evidence() {
|
|
local fixture="$1" mode="$2"
|
|
if [[ "$mode" == open ]]; then
|
|
local outer_source=/dev/fixture-recovery-partition
|
|
local outer_options=rw,nodev,nosuid,noexec,uid=1000,gid=1000,umask=077
|
|
local mapping_type=LUKS2 mapping_device=/dev/fixture-loop inner_label=K3S_RECOVERY
|
|
case "$fixture" in
|
|
open_wrong_outer_source) outer_source=/dev/fixture-other-partition ;;
|
|
open_plain_mapping) mapping_type=plain ;;
|
|
open_wrong_backing_file) mapping_device=/dev/fixture-other-loop ;;
|
|
open_wrong_inner_label) inner_label=WRONG_LABEL ;;
|
|
open_wrong_mount_options) outer_options=rw,nodev,nosuid,uid=1000,gid=1000,umask=077 ;;
|
|
esac
|
|
cat <<EOF
|
|
outer_mounted=1
|
|
outer_source=$outer_source
|
|
outer_fstype=ntfs3
|
|
outer_options=$outer_options
|
|
inner_mounted=1
|
|
inner_source=/dev/mapper/k3s-recovery
|
|
inner_fstype=ext4
|
|
inner_options=rw,nodev,nosuid,noexec
|
|
container_kind=regular
|
|
container_symlink=0
|
|
container_uid=1000
|
|
container_gid=1000
|
|
container_mode=600
|
|
container_size=34359738368
|
|
container_allocated=34359738368
|
|
loop_count=1
|
|
loop_device=/dev/fixture-loop
|
|
mapping_present=1
|
|
mapping_type=$mapping_type
|
|
mapping_device=$mapping_device
|
|
proof_mapping_present=0
|
|
inner_type=ext4
|
|
inner_label=$inner_label
|
|
inner_root_uid=0
|
|
inner_root_gid=0
|
|
inner_root_mode=700
|
|
inner_root_kind=directory
|
|
container_chain_pinned=1
|
|
container_inode=4242
|
|
container_device=8:3
|
|
loop_back_inode=4242
|
|
loop_back_major_minor=8:3
|
|
outer_major_minor=8:3
|
|
loop_major_minor=7:0
|
|
mapping_loop_major_minor=7:0
|
|
mapping_major_minor=253:0
|
|
inner_major_minor=253:0
|
|
loop_offset=0
|
|
loop_sizelimit=0
|
|
snapshot_stable=1
|
|
EOF
|
|
else
|
|
local loop_count=0 mapping_present=0
|
|
[[ "$fixture" == unexpected_loop ]] && loop_count=1
|
|
[[ "$fixture" == unexpected_mapping ]] && mapping_present=1
|
|
cat <<'EOF'
|
|
outer_mounted=0
|
|
inner_mounted=0
|
|
EOF
|
|
printf 'loop_count=%s\nmapping_present=%s\n' "$loop_count" "$mapping_present"
|
|
cat <<'EOF'
|
|
proof_mapping_present=0
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
validator_outputs_are_sanitized() {
|
|
local stdout_file="$1" stderr_file="$2" protected
|
|
for protected in \
|
|
/dev/disk/by-id/wwn-0x500a0751e6aa6254 /dev/disk/by-id/wwn-0x500a0751e6aa6254-part3 \
|
|
/dev/disk/by-id/wwn-0x500a07512df28bd1 /dev/disk/by-id/wwn-0x500a07512df28bd1-part1 \
|
|
4EA0196C0C5FA27E 4670aa9f-9045-4bce-930d-9e84dfec9f38 CT1000MX500SSD1 2306E6AA6254 0x500a0751e6aa6254 \
|
|
b86086ef-2b3c-4638-abcf-fc7f137dcb97 b081b955-d6bc-442c-ac0f-db76560a5245 CT250MX500SSD1 21132DF28BD1 0x500a07512df28bd1 \
|
|
/dev/fixture-recovery-partition /dev/fixture-recovery-disk /dev/fixture-wrong-disk /dev/fixture-other-partition \
|
|
/dev/fixture-k3s-partition /dev/fixture-k3s-disk /dev/fixture-loop /dev/fixture-other-loop \
|
|
/dev/mapper/k3s-recovery /dev/mapper/k3s-recovery-proof \
|
|
/mnt/k3s-recovery-ssd /srv/recovery/k3s \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault/hyeonworks-recovery.kdbx \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/volume.env \
|
|
/srv/recovery/k3s/.latest-post-bundle.env \
|
|
/srv/recovery/k3s/k3s-secrets-encryption-20260801T000000Z/post \
|
|
HyeonworksRecovery HyeonworksRecovery/vault/hyeonworks-recovery.kdbx \
|
|
HyeonworksRecovery/containers/k3s-recovery.luks HyeonworksRecovery/volume.env \
|
|
hyeonworks-recovery.kdbx k3s-recovery.luks k3s-recovery-proof k3s-recovery K3S_RECOVERY \
|
|
.latest-post-bundle.env bundle.env verification.manifest k3s-secrets-encryption- \
|
|
AAAAAAAAAAAAAAAA aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa WRONGMODEL WRONGSERIAL \
|
|
0xaaaaaaaaaaaaaaaa WRONG_LABEL 8:0 8:32 8:3 8:9 7:0 7:9 253:0 34359738368; do
|
|
if /usr/bin/grep -Fq -- "$protected" "$stdout_file"; then
|
|
return 1
|
|
fi
|
|
if /usr/bin/grep -Fq -- "$protected" "$stderr_file"; then
|
|
return 1
|
|
fi
|
|
done
|
|
return 0
|
|
}
|
|
|
|
assert_validator_case() {
|
|
local fixture="$1" expected_rc="$2" expected_state="$3"
|
|
shift 3
|
|
local stdout_file="${fixture_root}/${fixture}.stdout" stderr_file="${fixture_root}/${fixture}.stderr"
|
|
local log_file="${fixture_root}/${fixture}.argv" output rc
|
|
set +e
|
|
run_validator_command_fixture "$fixture" "$stdout_file" "$stderr_file" "$log_file" "$@"
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq "$expected_rc" ]] || fail "$fixture exit code (expected $expected_rc, got $rc)"
|
|
if (( expected_rc == 0 )); then
|
|
[[ ! -s "$stderr_file" ]] || fail "$fixture wrote unexpected stderr"
|
|
output="$(<"$stdout_file")"
|
|
assert_eq "Recovery device: match
|
|
Recovery state: ${expected_state}
|
|
Lineage: match
|
|
Latest bundle: $([[ "$*" == *--check-latest-bundle* ]] && printf verified || printf not_checked)" \
|
|
"$output" "$fixture success output"
|
|
fi
|
|
validator_outputs_are_sanitized "$stdout_file" "$stderr_file" || fail "$fixture leaked protected material"
|
|
return 0
|
|
}
|
|
|
|
review_focus="${K3SLR_REVIEW_FOCUS:-}"
|
|
|
|
probe_review1_ntfs_probe_argv() (
|
|
local argv_log="${fixture_root}/review1-ntfs-probe.argv"
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
_k3slrv_one_line() {
|
|
case "${*: -1}" in
|
|
"$K3SLR_RECOVERY_PARTITION_BY_ID") printf '%s\n' /dev/fixture-recovery-partition ;;
|
|
"$K3SLR_RECOVERY_DISK_BY_ID") printf '%s\n' /dev/fixture-recovery-disk ;;
|
|
"$K3SLR_K3S_PARTITION_BY_ID") printf '%s\n' /dev/fixture-k3s-partition ;;
|
|
"$K3SLR_K3S_DISK_BY_ID") printf '%s\n' /dev/fixture-k3s-disk ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
_k3slrv_lsblk() { printf '%s\n' fixture; }
|
|
_k3slrv_blkid() { printf '%s\n' fixture; }
|
|
_k3slrv_smart_evidence() {
|
|
printf '%s\n' smart_health=PASSED smart_reallocated=0 smart_pending=0 smart_uncorrectable=0
|
|
}
|
|
_k3slrv_findmnt_optional() { :; }
|
|
_k3slrv_findmnt_source_optional() { :; }
|
|
_k3slrv_root() { printf '%s\n' "$*" >"$argv_log"; }
|
|
_k3slrv_collect_device_evidence device_ready >/dev/null
|
|
[[ "$(<"$argv_log")" == '/usr/bin/ntfs-3g.probe --readwrite /dev/fixture-recovery-partition' ]]
|
|
)
|
|
|
|
probe_review1_blkid_argv() (
|
|
local argv_log="${fixture_root}/review1-blkid.argv"
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
_k3slrv_one_line_root() {
|
|
printf '%s\n' "$*" >"$argv_log"
|
|
printf '%s\n' fixture
|
|
}
|
|
_k3slrv_blkid UUID /dev/fixture-recovery-partition >/dev/null
|
|
[[ "$(<"$argv_log")" == '/usr/sbin/blkid --output value --match-tag UUID -- /dev/fixture-recovery-partition' ]]
|
|
)
|
|
|
|
probe_review1_ntfs3_effective_masks() (
|
|
local device_raw state_raw
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
device_raw="$(validator_device_evidence open_exact_lineage)"
|
|
state_raw="$(validator_state_evidence open_exact_lineage open)"
|
|
state_raw="${state_raw/umask=077/dmask=0077,fmask=0077}"
|
|
_k3slrv_validate_state open "$state_raw" "$device_raw"
|
|
)
|
|
|
|
probe_review1_initial_guard() (
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
declare -F _k3slrv_initial_guard >/dev/null || return 1
|
|
_k3slrv_initial_guard 1000 hB || return 1
|
|
! _k3slrv_initial_guard 0 hB
|
|
)
|
|
|
|
probe_review1_bash_x_rejected() (
|
|
local stdout_file="${fixture_root}/review1-xtrace.stdout"
|
|
local stderr_file="${fixture_root}/review1-xtrace.stderr" rc
|
|
set +e
|
|
/usr/bin/bash -x "$VALIDATOR_PATH" --help >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq 1 && ! -s "$stdout_file" ]] || return 1
|
|
[[ "$(<"$stderr_file")" == *'Recovery validation refused'* ]] || return 1
|
|
[[ "$(<"$stderr_file")" != *'K3SLR_RECOVERY_FS_UUID'* ]]
|
|
)
|
|
|
|
probe_review1_manifest_symlink_escape() (
|
|
local root="${fixture_root}/review1-manifest-symlink" bundle outside hash
|
|
root="$(mktemp -d "${root}.XXXXXX")"
|
|
bundle="${root}/bundle"
|
|
outside="${root}/outside"
|
|
mkdir -p "$bundle/data" "$outside/parent"
|
|
chmod 0700 "$bundle" "$bundle/data" "$outside" "$outside/parent"
|
|
printf '%s\n' payload >"$bundle/data/payload"
|
|
printf '%s\n' escaped >"$outside/leaf"
|
|
printf '%s\n' escaped-parent >"$outside/parent/payload"
|
|
hash="$(/usr/bin/sha256sum "$bundle/data/payload")"; hash="${hash%% *}"
|
|
printf '%s %s\n' "$hash" './data/payload' >"$bundle/verification.manifest"
|
|
chmod 0600 "$bundle/verification.manifest" "$bundle/data/payload" "$outside/leaf" "$outside/parent/payload"
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
declare -F _k3slrv_verify_manifest_targets >/dev/null || return 1
|
|
_k3slrv_root() { "$@"; }
|
|
_k3slrv_verify_manifest_targets "$bundle" "$bundle/verification.manifest" || return 1
|
|
ln -s "$outside/leaf" "$bundle/leaf-link"
|
|
hash="$(/usr/bin/sha256sum "$outside/leaf")"; hash="${hash%% *}"
|
|
printf '%s %s\n' "$hash" './leaf-link' >"$bundle/verification.manifest"
|
|
! _k3slrv_verify_manifest_targets "$bundle" "$bundle/verification.manifest" || return 1
|
|
ln -s "$outside/parent" "$bundle/parent-link"
|
|
hash="$(/usr/bin/sha256sum "$outside/parent/payload")"; hash="${hash%% *}"
|
|
printf '%s %s\n' "$hash" './parent-link/payload' >"$bundle/verification.manifest"
|
|
! _k3slrv_verify_manifest_targets "$bundle" "$bundle/verification.manifest"
|
|
)
|
|
|
|
probe_review1_partition_mounted_elsewhere() (
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
_k3slrv_one_line() {
|
|
case "${*: -1}" in
|
|
"$K3SLR_RECOVERY_PARTITION_BY_ID") printf '%s\n' /dev/fixture-recovery-partition ;;
|
|
"$K3SLR_RECOVERY_DISK_BY_ID") printf '%s\n' /dev/fixture-recovery-disk ;;
|
|
"$K3SLR_K3S_PARTITION_BY_ID") printf '%s\n' /dev/fixture-k3s-partition ;;
|
|
"$K3SLR_K3S_DISK_BY_ID") printf '%s\n' /dev/fixture-k3s-disk ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
_k3slrv_lsblk() { printf '%s\n' fixture; }
|
|
_k3slrv_blkid() { printf '%s\n' fixture; }
|
|
_k3slrv_smart_evidence() {
|
|
printf '%s\n' smart_health=PASSED smart_reallocated=0 smart_pending=0 smart_uncorrectable=0
|
|
}
|
|
_k3slrv_findmnt_optional() { :; }
|
|
_k3slrv_findmnt_source_optional() { printf '%s\n' /mnt/fixture-unapproved; }
|
|
_k3slrv_root() { :; }
|
|
! _k3slrv_collect_device_evidence device_ready >/dev/null
|
|
)
|
|
|
|
probe_review1_open_lineage_snapshot() (
|
|
local good mutation key current line candidate device_raw state_raw
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
declare -F _k3slrv_validate_open_snapshot >/dev/null || return 1
|
|
good=$'container_chain_pinned=1\ncontainer_inode=4242\ncontainer_device=8:3\nloop_back_inode=4242\nloop_back_major_minor=8:3\nouter_major_minor=8:3\nloop_major_minor=7:0\nmapping_loop_major_minor=7:0\nmapping_major_minor=253:0\ninner_major_minor=253:0\nloop_offset=0\nloop_sizelimit=0\nsnapshot_stable=1'
|
|
_k3slrv_validate_open_snapshot "$good" || { printf 'review1 snapshot good failed\n' >&2; return 1; }
|
|
for mutation in \
|
|
'container_chain_pinned=0' \
|
|
'container_device=8:9' \
|
|
'loop_back_inode=9999' \
|
|
'loop_back_major_minor=8:9' \
|
|
'mapping_loop_major_minor=7:9' \
|
|
'mapping_major_minor=253:9' \
|
|
'loop_offset=1' \
|
|
'loop_sizelimit=1' \
|
|
'snapshot_stable=0'; do
|
|
key="${mutation%%=*}"
|
|
current=''
|
|
while IFS= read -r line; do [[ "$line" == "$key="* ]] && current="$line"; done <<<"$good"
|
|
[[ -n "$current" ]] || return 1
|
|
candidate="${good/"$current"/"$mutation"}"
|
|
! _k3slrv_validate_open_snapshot "$candidate" || { printf 'review1 snapshot mutation accepted: %s\n' "$mutation" >&2; return 1; }
|
|
done
|
|
device_raw="$(validator_device_evidence open_exact_lineage)"
|
|
state_raw="$(validator_state_evidence open_exact_lineage open)"
|
|
_k3slrv_validate_state open "$state_raw" "$device_raw" || { printf 'review1 state snapshot good failed\n' >&2; return 1; }
|
|
! _k3slrv_validate_state open "${state_raw/snapshot_stable=1/snapshot_stable=0}" "$device_raw" || {
|
|
printf 'review1 state snapshot drift accepted\n' >&2
|
|
return 1
|
|
}
|
|
)
|
|
|
|
probe_review1_predefined_parser_cannot_bypass() (
|
|
local marker="${fixture_root}/review1-parser-attacked" stdout_file="${fixture_root}/review1-parser.stdout"
|
|
local stderr_file="${fixture_root}/review1-parser.stderr" rc
|
|
export -f validator_device_evidence validator_state_evidence
|
|
set +e
|
|
/usr/bin/bash -c '
|
|
source "$1"
|
|
_k3slr_load_contract() { printf attacked >"$2"; return 1; }
|
|
_k3slrv_verify_trusted_binaries() { :; }
|
|
_k3slrv_require_cached_sudo() { :; }
|
|
_k3slrv_collect_device_evidence() { validator_device_evidence exact_device_ready; }
|
|
_k3slrv_collect_state_evidence() { validator_state_evidence closed_no_residue device_ready; }
|
|
k3slr_local_recovery_main --expect-device-ready
|
|
' review1-parser "$VALIDATOR_PATH" "$marker" >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq 0 && ! -e "$marker" && ! -s "$stderr_file" ]] || return 1
|
|
[[ "$(<"$stdout_file")" == *'Recovery state: device_ready'* ]]
|
|
)
|
|
|
|
probe_review2_exported_source_cannot_intercept() (
|
|
local marker="${fixture_root}/review2-source-attacked"
|
|
local stdout_file="${fixture_root}/review2-source.stdout"
|
|
local stderr_file="${fixture_root}/review2-source.stderr" rc
|
|
export -f validator_device_evidence validator_state_evidence
|
|
source() {
|
|
printf '%s' attacked >"$K3SLR_SOURCE_ATTACK_MARKER"
|
|
return 1
|
|
}
|
|
export -f source
|
|
set +e
|
|
K3SLR_SOURCE_ATTACK_MARKER="$marker" /usr/bin/bash -c '
|
|
builtin source -- "$1"
|
|
_k3slrv_verify_trusted_binaries() { :; }
|
|
_k3slrv_require_cached_sudo() { :; }
|
|
_k3slrv_collect_device_evidence() { validator_device_evidence exact_device_ready; }
|
|
_k3slrv_collect_state_evidence() { validator_state_evidence closed_no_residue device_ready; }
|
|
k3slr_local_recovery_main --expect-device-ready
|
|
' review2-source "$VALIDATOR_PATH" >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq 0 && ! -e "$marker" && ! -s "$stderr_file" ]] || return 1
|
|
[[ "$(<"$stdout_file")" == $'Recovery device: match\nRecovery state: device_ready\nLineage: match\nLatest bundle: not_checked' ]]
|
|
)
|
|
|
|
validator_command_fake() {
|
|
local scope="$1" command="$2" argument field='' device='' tag='' format='' target='' mapping value occurrence
|
|
local stat_mode=600 stat_size=34359738368 stat_blocks=67108864 stat_device=8:3
|
|
local smart_health=PASSED smart_reallocated=0 smart_pending=0 smart_uncorrectable=0
|
|
shift 2
|
|
{
|
|
printf '%s\t%s' "$scope" "$command"
|
|
for argument in "$@"; do printf '\t%s' "$argument"; done
|
|
printf '\n'
|
|
} >>"$K3SLR_FAKE_LOG"
|
|
case "$command" in
|
|
/usr/bin/test)
|
|
if [[ "${1-}" == -L ]]; then return 1; fi
|
|
return 0
|
|
;;
|
|
/usr/bin/stat)
|
|
while (( $# > 0 )); do
|
|
case "$1" in
|
|
--format=*) format="${1#--format=}"; shift ;;
|
|
--) shift; target="$1"; shift ;;
|
|
*) target="$1"; shift ;;
|
|
esac
|
|
done
|
|
case "$format|$target" in
|
|
'%u|%a|%F|'*) printf '%s\n' '0|755|regular file' ;;
|
|
'%d:%i|%F|/mnt/k3s-recovery-ssd') printf '%s\n' '2051:100|directory' ;;
|
|
'%d:%i|%F|/mnt/k3s-recovery-ssd/HyeonworksRecovery') printf '%s\n' '2051:101|directory' ;;
|
|
'%d:%i|%F|/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers') printf '%s\n' '2051:102|directory' ;;
|
|
'%d:%i|%F|/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks') printf '%s\n' '2051:4242|regular file' ;;
|
|
'%F|%u|%g|%a|%s|%b|%i|/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks'|\
|
|
'%F|%u|%g|%a|%s|%b|%i|%Hd:%Ld|/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks')
|
|
occurrence="$(/usr/bin/grep -Fc -- $'\t/usr/bin/stat\t--format=%F|%u|%g|%a|%s|%b|%i' "$K3SLR_FAKE_LOG")"
|
|
if (( occurrence >= 2 )); then
|
|
case "$K3SLR_FAKE_CASE" in
|
|
open_drift_container_mode) stat_mode=640 ;;
|
|
open_drift_container_size) stat_size=34359734272 ;;
|
|
open_drift_container_allocated) stat_blocks=67108856 ;;
|
|
esac
|
|
fi
|
|
if [[ "$format" == *'%Hd:%Ld' ]]; then
|
|
printf 'regular file|1000|1000|%s|%s|%s|4242|%s\n' \
|
|
"$stat_mode" "$stat_size" "$stat_blocks" "$stat_device"
|
|
else
|
|
printf 'regular file|1000|1000|%s|%s|%s|4242\n' \
|
|
"$stat_mode" "$stat_size" "$stat_blocks"
|
|
fi
|
|
;;
|
|
'%u|%g|%a|%F|/srv/recovery/k3s')
|
|
occurrence="$(/usr/bin/grep -Fc -- $'\t/usr/bin/stat\t--format=%u|%g|%a|%F\t--\t/srv/recovery/k3s' "$K3SLR_FAKE_LOG")"
|
|
if [[ "$K3SLR_FAKE_CASE" == open_drift_inner_root && "$occurrence" -ge 2 ]]; then
|
|
printf '%s\n' '0|0|750|directory'
|
|
else
|
|
printf '%s\n' '0|0|700|directory'
|
|
fi
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
/usr/bin/sudo)
|
|
[[ "$*" == '--non-interactive --validate' ]]
|
|
;;
|
|
/usr/bin/readlink)
|
|
device="${*: -1}"
|
|
case "$device" in
|
|
/dev/disk/by-id/wwn-0x500a0751e6aa6254-part3) printf '%s\n' /dev/fixture-recovery-partition ;;
|
|
/dev/disk/by-id/wwn-0x500a0751e6aa6254) printf '%s\n' /dev/fixture-recovery-disk ;;
|
|
/dev/disk/by-id/wwn-0x500a07512df28bd1-part1) printf '%s\n' /dev/fixture-k3s-partition ;;
|
|
/dev/disk/by-id/wwn-0x500a07512df28bd1) printf '%s\n' /dev/fixture-k3s-disk ;;
|
|
*) printf '%s\n' "$device" ;;
|
|
esac
|
|
;;
|
|
/usr/bin/lsblk)
|
|
while (( $# > 0 )); do
|
|
if [[ "$1" == --output ]]; then field="$2"; shift 2; else device="$1"; shift; fi
|
|
done
|
|
case "$field|$device" in
|
|
'PKNAME|/dev/fixture-recovery-partition')
|
|
if [[ "$K3SLR_FAKE_CASE" == wrong_by_id_target ]]; then printf '%s\n' /dev/fixture-wrong-disk; else printf '%s\n' /dev/fixture-recovery-disk; fi
|
|
;;
|
|
'PKNAME|/dev/fixture-k3s-partition') printf '%s\n' /dev/fixture-k3s-disk ;;
|
|
'MODEL|/dev/fixture-recovery-disk')
|
|
if [[ "$K3SLR_FAKE_CASE" == wrong_model ]]; then printf '%s\n' WRONGMODEL; else printf '%s\n' CT1000MX500SSD1; fi
|
|
;;
|
|
'SERIAL|/dev/fixture-recovery-disk')
|
|
if [[ "$K3SLR_FAKE_CASE" == wrong_serial ]]; then printf '%s\n' WRONGSERIAL; else printf '%s\n' 2306E6AA6254; fi
|
|
;;
|
|
'WWN|/dev/fixture-recovery-disk')
|
|
if [[ "$K3SLR_FAKE_CASE" == wrong_wwn ]]; then printf '%s\n' 0xaaaaaaaaaaaaaaaa; else printf '%s\n' 0x500a0751e6aa6254; fi
|
|
;;
|
|
'MAJ:MIN|/dev/fixture-recovery-disk') printf '%s\n' 8:0 ;;
|
|
'MODEL|/dev/fixture-k3s-disk') printf '%s\n' CT250MX500SSD1 ;;
|
|
'SERIAL|/dev/fixture-k3s-disk') printf '%s\n' 21132DF28BD1 ;;
|
|
'WWN|/dev/fixture-k3s-disk') printf '%s\n' 0x500a07512df28bd1 ;;
|
|
'MAJ:MIN|/dev/fixture-k3s-disk')
|
|
if [[ "$K3SLR_FAKE_CASE" == same_parent_disk ]]; then printf '%s\n' 8:0; else printf '%s\n' 8:32; fi
|
|
;;
|
|
'MAJ:MIN|/dev/fixture-loop') printf '%s\n' 7:0 ;;
|
|
'MAJ:MIN|/dev/fixture-other-loop') printf '%s\n' 7:9 ;;
|
|
'MAJ:MIN|/dev/mapper/k3s-recovery') printf '%s\n' 253:0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
/usr/sbin/blkid)
|
|
while (( $# > 0 )); do
|
|
if [[ "$1" == --match-tag ]]; then tag="$2"; shift 2; else device="$1"; shift; fi
|
|
done
|
|
case "$tag|$device" in
|
|
'UUID|/dev/fixture-recovery-partition')
|
|
if [[ "$K3SLR_FAKE_CASE" == wrong_fs_uuid ]]; then printf '%s\n' AAAAAAAAAAAAAAAA; else printf '%s\n' 4EA0196C0C5FA27E; fi
|
|
;;
|
|
'PARTUUID|/dev/fixture-recovery-partition')
|
|
if [[ "$K3SLR_FAKE_CASE" == wrong_partuuid ]]; then printf '%s\n' aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa; else printf '%s\n' 4670aa9f-9045-4bce-930d-9e84dfec9f38; fi
|
|
;;
|
|
'TYPE|/dev/fixture-recovery-partition') printf '%s\n' ntfs ;;
|
|
'UUID|/dev/fixture-k3s-partition') printf '%s\n' b86086ef-2b3c-4638-abcf-fc7f137dcb97 ;;
|
|
'PARTUUID|/dev/fixture-k3s-partition') printf '%s\n' b081b955-d6bc-442c-ac0f-db76560a5245 ;;
|
|
'TYPE|/dev/fixture-k3s-partition') printf '%s\n' ext4 ;;
|
|
'TYPE|/dev/mapper/k3s-recovery') printf '%s\n' ext4 ;;
|
|
'LABEL|/dev/mapper/k3s-recovery')
|
|
occurrence="$(/usr/bin/grep -Fc -- $'\t/usr/sbin/blkid\t--output\tvalue\t--match-tag\tLABEL\t--\t/dev/mapper/k3s-recovery' "$K3SLR_FAKE_LOG")"
|
|
if [[ "$K3SLR_FAKE_CASE" == open_wrong_inner_label ||
|
|
( "$K3SLR_FAKE_CASE" == open_drift_inner_label && "$occurrence" -ge 2 ) ]]; then
|
|
printf '%s\n' WRONG_LABEL
|
|
else
|
|
printf '%s\n' K3S_RECOVERY
|
|
fi
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
/usr/sbin/smartctl)
|
|
case "$K3SLR_FAKE_CASE" in
|
|
smart_health_failed) smart_health=FAILED ;;
|
|
smart_reallocated_nonzero) smart_reallocated=1 ;;
|
|
smart_pending_nonzero) smart_pending=1 ;;
|
|
smart_uncorrectable_nonzero) smart_uncorrectable=1 ;;
|
|
esac
|
|
printf '%s\n' \
|
|
"SMART overall-health self-assessment test result: ${smart_health}" \
|
|
" 5 Reallocated_Sector_Ct 0x0033 100 100 010 Pre-fail Always - ${smart_reallocated}" \
|
|
"197 Current_Pending_Sector 0x0012 100 100 000 Old_age Always - ${smart_pending}" \
|
|
"198 Offline_Uncorrectable 0x0010 100 100 000 Old_age Offline - ${smart_uncorrectable}"
|
|
;;
|
|
/usr/bin/findmnt)
|
|
while (( $# > 0 )); do
|
|
case "$1" in
|
|
--mountpoint|--source) field="$1"; target="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
if [[ "$field" == --source ]]; then
|
|
[[ "$K3SLR_FAKE_CASE" == open_* ]] || return 1
|
|
printf '%s\n' /mnt/k3s-recovery-ssd
|
|
elif [[ "$field" == --mountpoint && "$K3SLR_FAKE_CASE" == open_* ]]; then
|
|
case "$target" in
|
|
/mnt/k3s-recovery-ssd)
|
|
if [[ "$K3SLR_FAKE_CASE" == open_wrong_outer_source ]]; then device=/dev/fixture-other-partition; else device=/dev/fixture-recovery-partition; fi
|
|
if [[ "$K3SLR_FAKE_CASE" == open_wrong_mount_options ]]; then
|
|
value='rw,nodev,nosuid,uid=1000,gid=1000,dmask=0077,fmask=0077'
|
|
else
|
|
value='rw,nodev,nosuid,noexec,uid=1000,gid=1000,dmask=0077,fmask=0077'
|
|
fi
|
|
if [[ "$K3SLR_FAKE_CASE" == open_wrong_container_device ]]; then field=8:9; else field=8:3; fi
|
|
printf '%s %s %s %s %s\n' "$device" ntfs3 "$value" 41 "$field"
|
|
;;
|
|
/srv/recovery/k3s) printf '%s\n' '/dev/mapper/k3s-recovery ext4 rw,nodev,nosuid,noexec 42 253:0' ;;
|
|
*) return 1 ;;
|
|
esac
|
|
else
|
|
return 1
|
|
fi
|
|
;;
|
|
/usr/bin/ntfs-3g.probe)
|
|
[[ "$#" -eq 2 && "$1" == --readwrite && "$2" == /dev/fixture-recovery-partition ]] || return 2
|
|
[[ "$K3SLR_FAKE_CASE" != ntfs_dirty_or_hibernated ]]
|
|
;;
|
|
/usr/sbin/losetup)
|
|
if [[ "$K3SLR_FAKE_CASE" == open_* ]]; then
|
|
if [[ "$K3SLR_FAKE_CASE" == open_wrong_container_device ]]; then field=8:9; else field=8:3; fi
|
|
printf '/dev/fixture-loop 4242 %s 7:0 0 0\n' "$field"
|
|
elif [[ "$K3SLR_FAKE_CASE" == unexpected_loop ]]; then
|
|
printf '%s\n' /dev/fixture-loop
|
|
fi
|
|
;;
|
|
/usr/sbin/cryptsetup)
|
|
mapping="${*: -1}"
|
|
if [[ "$mapping" == k3s-recovery-proof ]]; then return 4; fi
|
|
if [[ "$K3SLR_FAKE_CASE" == open_* || "$K3SLR_FAKE_CASE" == unexpected_mapping ]]; then
|
|
if [[ "$K3SLR_FAKE_CASE" == open_plain_mapping ]]; then value=plain; else value=LUKS2; fi
|
|
if [[ "$K3SLR_FAKE_CASE" == open_wrong_backing_file ]]; then device=/dev/fixture-other-loop; else device=/dev/fixture-loop; fi
|
|
printf '%s\n' "/dev/mapper/k3s-recovery is active and is in use." " type: ${value}" " device: ${device}"
|
|
else
|
|
return 4
|
|
fi
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
run_validator_command_fixture() {
|
|
local fixture="$1" stdout_file="$2" stderr_file="$3" log_file="$4"
|
|
shift 4
|
|
: >"$log_file"
|
|
export -f validator_command_fake
|
|
K3SLR_FAKE_CASE="$fixture" K3SLR_FAKE_LOG="$log_file" /usr/bin/bash -c '
|
|
source "$1"; shift
|
|
_k3slrv_exec() { validator_command_fake "$@"; }
|
|
k3slr_local_recovery_main "$@"
|
|
' review1-command "$VALIDATOR_PATH" "$@" >"$stdout_file" 2>"$stderr_file"
|
|
}
|
|
|
|
probe_review1_command_boundary_device_ready() (
|
|
local stdout_file="${fixture_root}/review1-command.stdout" stderr_file="${fixture_root}/review1-command.stderr"
|
|
local log_file="${fixture_root}/review1-command.log"
|
|
run_validator_command_fixture exact_device_ready "$stdout_file" "$stderr_file" "$log_file" --expect-device-ready || return 1
|
|
[[ ! -s "$stderr_file" && "$(<"$stdout_file")" == *'Recovery state: device_ready'* ]] || return 1
|
|
/usr/bin/grep -Fqx $'root\t/usr/bin/ntfs-3g.probe\t--readwrite\t/dev/fixture-recovery-partition' "$log_file" || return 1
|
|
/usr/bin/grep -Fqx $'root\t/usr/sbin/blkid\t--output\tvalue\t--match-tag\tUUID\t--\t/dev/fixture-recovery-partition' "$log_file"
|
|
)
|
|
|
|
probe_review1_command_boundary_open() (
|
|
local stdout_file="${fixture_root}/review1-command-open.stdout" stderr_file="${fixture_root}/review1-command-open.stderr"
|
|
local log_file="${fixture_root}/review1-command-open.log"
|
|
run_validator_command_fixture open_exact_lineage "$stdout_file" "$stderr_file" "$log_file" --expect-open || return 1
|
|
[[ ! -s "$stderr_file" && "$(<"$stdout_file")" == *'Recovery state: open'* ]] || return 1
|
|
/usr/bin/grep -Fqx $'root\t/usr/bin/findmnt\t--noheadings\t--raw\t--output\tSOURCE,FSTYPE,OPTIONS,ID,MAJ:MIN\t--mountpoint\t/mnt/k3s-recovery-ssd' "$log_file" || return 1
|
|
/usr/bin/grep -Fqx $'root\t/usr/bin/findmnt\t--noheadings\t--raw\t--output\tTARGET\t--source\t/dev/fixture-recovery-partition' "$log_file" || return 1
|
|
/usr/bin/grep -Fqx $'root\t/usr/sbin/losetup\t--list\t--associated\t/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks\t--noheadings\t--raw\t--output\tNAME,BACK-INO,BACK-MAJ:MIN,MAJ:MIN,OFFSET,SIZELIMIT' "$log_file" || return 1
|
|
/usr/bin/grep -Fqx $'root\t/usr/sbin/cryptsetup\tstatus\t--\tk3s-recovery' "$log_file" || return 1
|
|
/usr/bin/grep -Fqx $'root\t/usr/sbin/cryptsetup\tstatus\t--\tk3s-recovery-proof' "$log_file"
|
|
)
|
|
|
|
prepare_review1_latest_bundle() {
|
|
local case_name="$1" root="$2" inner relative bundle outside identity payload_hash metadata_hash
|
|
inner="${root}/inner"
|
|
relative='k3s-secrets-encryption-20260801T000000Z/post'
|
|
bundle="${inner}/${relative}"
|
|
outside="${root}/outside-payload"
|
|
mkdir -p -- "$bundle"
|
|
printf '%s\n' 'encrypted payload fixture' >"${bundle}/payload.bin"
|
|
printf '%s\n' \
|
|
'schema=platform-k3s-bundle-v1' \
|
|
'bundle_id=01234567-89ab-4cde-8fab-0123456789ab' \
|
|
'phase=post' \
|
|
'k3s_version=v1.36.2+k3s1' \
|
|
'datastore=sqlite' \
|
|
'created_at_utc=2026-08-01T00:00:00Z' \
|
|
'secret_count=1' >"${bundle}/bundle.env"
|
|
case "$case_name" in
|
|
malformed)
|
|
sed -i 's/^schema=.*/schema=not-the-approved-schema/' "${bundle}/bundle.env"
|
|
;;
|
|
duplicate)
|
|
printf '%s\n' 'phase=post' >>"${bundle}/bundle.env"
|
|
;;
|
|
valid|symlink_escape|hash_mismatch) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
payload_hash="$(/usr/bin/sha256sum -- "${bundle}/payload.bin")"; payload_hash="${payload_hash%% *}"
|
|
metadata_hash="$(/usr/bin/sha256sum -- "${bundle}/bundle.env")"; metadata_hash="${metadata_hash%% *}"
|
|
if [[ "$case_name" == symlink_escape ]]; then
|
|
printf '%s\n' 'escaped payload fixture' >"$outside"
|
|
ln -s -- "$outside" "${bundle}/escaped-link"
|
|
payload_hash="$(/usr/bin/sha256sum -- "$outside")"; payload_hash="${payload_hash%% *}"
|
|
printf '%s %s\n' "$payload_hash" './escaped-link' >"${bundle}/verification.manifest"
|
|
else
|
|
[[ "$case_name" != hash_mismatch ]] || payload_hash='0000000000000000000000000000000000000000000000000000000000000000'
|
|
printf '%s %s\n%s %s\n' \
|
|
"$payload_hash" './payload.bin' "$metadata_hash" './bundle.env' >"${bundle}/verification.manifest"
|
|
fi
|
|
chmod 0700 -- "$bundle"
|
|
chmod 0600 -- "${bundle}/bundle.env" "${bundle}/verification.manifest" "${bundle}/payload.bin"
|
|
identity="$(/usr/bin/stat --format='%d:%i' -- "$bundle")"
|
|
printf '%s\n' \
|
|
'schema=k3slr-latest-post-bundle-v1' \
|
|
"relative_path=${relative}" \
|
|
"directory_identity=${identity}" >"${inner}/.latest-post-bundle.env"
|
|
chmod 0600 -- "${inner}/.latest-post-bundle.env"
|
|
}
|
|
|
|
run_review1_latest_bundle_case() (
|
|
local case_name="$1" expected_rc="$2" root inner rc
|
|
root="${fixture_root}/review1-latest-${case_name}"
|
|
mkdir -p -- "$root"
|
|
prepare_review1_latest_bundle "$case_name" "$root" || return 1
|
|
inner="${root}/inner"
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
K3SLR_INNER_MOUNT="$inner"
|
|
_k3slrv_exec() {
|
|
local scope="$1" command="$2" argument mode_kind
|
|
shift 2
|
|
[[ "$scope" == user || "$scope" == root ]] || return 1
|
|
if [[ "$command" == /usr/bin/stat && " $* " == *" --format=%u:%g:%a:%F "* ]]; then
|
|
mode_kind="$($command "${@/--format=%u:%g:%a:%F/--format=%a:%F}")" || return 1
|
|
printf '0:0:%s\n' "$mode_kind"
|
|
return 0
|
|
fi
|
|
"$command" "$@"
|
|
}
|
|
set +e
|
|
_k3slrv_verify_latest_bundle
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq "$expected_rc" ]]
|
|
)
|
|
|
|
probe_review1_latest_bundle_actual() {
|
|
assert_succeeds run_review1_latest_bundle_case valid 0
|
|
assert_succeeds run_review1_latest_bundle_case malformed 1
|
|
assert_succeeds run_review1_latest_bundle_case duplicate 1
|
|
assert_succeeds run_review1_latest_bundle_case symlink_escape 1
|
|
assert_succeeds run_review1_latest_bundle_case hash_mismatch 1
|
|
}
|
|
|
|
run_review2_latest_main_case() (
|
|
local fixture_name="$1" expected_rc="$2" bundle_case="$3"
|
|
local root="${fixture_root}/review2-latest-main-${fixture_name}" inner
|
|
local stdout_file="${fixture_root}/review2-latest-main-${fixture_name}.stdout"
|
|
local stderr_file="${fixture_root}/review2-latest-main-${fixture_name}.stderr" rc output
|
|
mkdir -p -- "$root"
|
|
prepare_review1_latest_bundle "$bundle_case" "$root" || return 1
|
|
inner="${root}/inner"
|
|
export -f validator_device_evidence validator_state_evidence
|
|
set +e
|
|
K3SLR_TEST_INNER_MOUNT="$inner" /usr/bin/bash -c '
|
|
builtin source -- "$1"
|
|
_k3slrv_exec() {
|
|
local scope="$1" command="$2" mode_kind
|
|
shift 2
|
|
[[ "$scope" == user || "$scope" == root ]] || return 1
|
|
if [[ "$command" == /usr/bin/stat && " $* " == *" --format=%u:%g:%a:%F "* ]]; then
|
|
mode_kind="$($command "${@/--format=%u:%g:%a:%F/--format=%a:%F}")" || return 1
|
|
printf "0:0:%s\\n" "$mode_kind"
|
|
return 0
|
|
fi
|
|
"$command" "$@"
|
|
}
|
|
_k3slrv_verify_trusted_binaries() { :; }
|
|
_k3slrv_require_cached_sudo() { K3SLR_INNER_MOUNT="$K3SLR_TEST_INNER_MOUNT"; }
|
|
_k3slrv_collect_device_evidence() { validator_device_evidence open_exact_lineage; }
|
|
_k3slrv_collect_state_evidence() { validator_state_evidence open_exact_lineage open; }
|
|
k3slr_local_recovery_main --expect-open --check-latest-bundle
|
|
' review2-latest-main "$VALIDATOR_PATH" >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -ne "$expected_rc" ]]; then
|
|
printf 'task4 feasibility diagnostic: case=%s expected=%s actual=%s stderr=%s\n' \
|
|
"$fixture_case" "$expected_rc" "$rc" "$(<"$stderr_file")" >&2
|
|
/usr/bin/tail -n 30 "$TASK4_FEASIBILITY_COMMAND_LOG" >&2
|
|
return 1
|
|
fi
|
|
if (( expected_rc == 0 )); then
|
|
[[ ! -s "$stderr_file" ]] || return 1
|
|
output="$(<"$stdout_file")"
|
|
[[ "$output" == $'Recovery device: match\nRecovery state: open\nLineage: match\nLatest bundle: verified' ]] || return 1
|
|
else
|
|
[[ ! -s "$stdout_file" && "$(<"$stderr_file")" == 'Recovery validation failed' ]] || return 1
|
|
fi
|
|
validator_outputs_are_sanitized "$stdout_file" "$stderr_file"
|
|
)
|
|
|
|
probe_review2_latest_main_integration() {
|
|
assert_succeeds run_review2_latest_main_case latest_manifest_valid 0 valid
|
|
assert_succeeds run_review2_latest_main_case latest_manifest_invalid 1 hash_mismatch
|
|
}
|
|
|
|
probe_review1_record_preserving_evidence() (
|
|
local raw
|
|
local -A parsed=()
|
|
# shellcheck source=/dev/null
|
|
source "$VALIDATOR_PATH"
|
|
_k3slrv_exec() { printf 'fixture-value\n\n'; }
|
|
! _k3slrv_one_line /usr/bin/readlink -f -- /fixture || return 1
|
|
declare -F _k3slrv_capture >/dev/null || return 1
|
|
trailing_collector() { printf 'alpha=one\n\n'; }
|
|
_k3slrv_capture raw trailing_collector || return 1
|
|
! _k3slrv_parse_evidence "$raw" parsed alpha || return 1
|
|
! _k3slrv_parse_evidence $'alpha=one\nalpha=two' parsed alpha || return 1
|
|
! _k3slrv_parse_evidence $'alpha=one\nunknown=two' parsed alpha || return 1
|
|
! _k3slrv_parse_evidence $'alpha=one\r' parsed alpha || return 1
|
|
! _k3slrv_parse_evidence $'alpha=one\ncontinued' parsed alpha
|
|
)
|
|
|
|
probe_review1_leak_sentinels() (
|
|
local stdout_file="${fixture_root}/review1-leak.stdout" stderr_file="${fixture_root}/review1-leak.stderr"
|
|
: >"$stdout_file"
|
|
: >"$stderr_file"
|
|
validator_outputs_are_sanitized "$stdout_file" "$stderr_file" || return 1
|
|
printf '%s\n' '4670aa9f-9045-4bce-930d-9e84dfec9f38' >"$stdout_file"
|
|
! validator_outputs_are_sanitized "$stdout_file" "$stderr_file" || return 1
|
|
: >"$stdout_file"
|
|
printf '%s\n' '/srv/recovery/k3s/.latest-post-bundle.env' >"$stderr_file"
|
|
! validator_outputs_are_sanitized "$stdout_file" "$stderr_file"
|
|
)
|
|
|
|
assert_validator_case exact_device_ready 0 device_ready --expect-device-ready
|
|
assert_validator_case wrong_by_id_target 1 device_ready --expect-device-ready
|
|
assert_validator_case wrong_fs_uuid 1 device_ready --expect-device-ready
|
|
assert_validator_case wrong_partuuid 1 device_ready --expect-device-ready
|
|
assert_validator_case wrong_model 1 device_ready --expect-device-ready
|
|
assert_validator_case wrong_serial 1 device_ready --expect-device-ready
|
|
assert_validator_case wrong_wwn 1 device_ready --expect-device-ready
|
|
assert_validator_case same_parent_disk 1 device_ready --expect-device-ready
|
|
assert_validator_case smart_health_failed 1 device_ready --expect-device-ready
|
|
assert_validator_case smart_reallocated_nonzero 1 device_ready --expect-device-ready
|
|
assert_validator_case smart_pending_nonzero 1 device_ready --expect-device-ready
|
|
assert_validator_case smart_uncorrectable_nonzero 1 device_ready --expect-device-ready
|
|
assert_validator_case ntfs_dirty_or_hibernated 1 device_ready --expect-device-ready
|
|
assert_validator_case closed_no_residue 0 closed --expect-closed
|
|
assert_validator_case unexpected_loop 1 closed --expect-closed
|
|
assert_validator_case unexpected_mapping 1 closed --expect-closed
|
|
assert_validator_case open_exact_lineage 0 open --expect-open
|
|
assert_validator_case open_wrong_outer_source 1 open --expect-open
|
|
assert_validator_case open_plain_mapping 1 open --expect-open
|
|
assert_validator_case open_wrong_backing_file 1 open --expect-open
|
|
assert_validator_case open_wrong_inner_label 1 open --expect-open
|
|
assert_validator_case open_wrong_mount_options 1 open --expect-open
|
|
assert_validator_case open_wrong_container_device 1 open --expect-open
|
|
assert_validator_case open_drift_container_mode 1 open --expect-open
|
|
assert_validator_case open_drift_container_size 1 open --expect-open
|
|
assert_validator_case open_drift_container_allocated 1 open --expect-open
|
|
assert_validator_case open_drift_inner_label 1 open --expect-open
|
|
assert_validator_case open_drift_inner_root 1 open --expect-open
|
|
assert_validator_case exact_device_ready 2 device_ready
|
|
assert_validator_case exact_device_ready 2 device_ready --expect-open --expect-closed
|
|
assert_validator_case open_exact_lineage 2 open --expect-closed --check-latest-bundle
|
|
|
|
if [[ -z "$review_focus" || "$review_focus" == 1 ]]; then
|
|
assert_succeeds probe_review1_ntfs_probe_argv
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 2 ]]; then
|
|
assert_succeeds probe_review1_blkid_argv
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 3 ]]; then
|
|
assert_succeeds probe_review1_ntfs3_effective_masks
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 4 ]]; then
|
|
assert_succeeds probe_review1_initial_guard
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 5 ]]; then
|
|
assert_succeeds probe_review1_bash_x_rejected
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 6 ]]; then
|
|
assert_succeeds probe_review1_manifest_symlink_escape
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 7 ]]; then
|
|
assert_succeeds probe_review1_partition_mounted_elsewhere
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 8 ]]; then
|
|
assert_succeeds probe_review1_open_lineage_snapshot
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 9 ]]; then
|
|
assert_succeeds probe_review1_predefined_parser_cannot_bypass
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == r2_9 ]]; then
|
|
assert_succeeds probe_review2_exported_source_cannot_intercept
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == r2_latest ]]; then
|
|
assert_succeeds probe_review2_latest_main_integration
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == 10 ]]; then
|
|
assert_succeeds probe_review1_command_boundary_device_ready
|
|
assert_succeeds probe_review1_command_boundary_open
|
|
assert_succeeds probe_review1_latest_bundle_actual
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == m1 ]]; then
|
|
assert_succeeds probe_review1_record_preserving_evidence
|
|
fi
|
|
if [[ -z "$review_focus" || "$review_focus" == m2 ]]; then
|
|
assert_succeeds probe_review1_leak_sentinels
|
|
fi
|
|
|
|
task4_pipeline_contract() (
|
|
declare -F _k3slr_luks_format_from_keepass >/dev/null || return 1
|
|
declare -F _k3slr_luks_open_from_keepass >/dev/null || return 1
|
|
declare -F _k3slr_password_pipe_preflight >/dev/null || return 1
|
|
declare -F _k3slr_keepass_database_lineage_matches >/dev/null || return 1
|
|
)
|
|
|
|
task4_tty_and_secret_api_contract() (
|
|
local stdout_file="${fixture_root}/task4-nontty.stdout"
|
|
local stderr_file="${fixture_root}/task4-nontty.stderr"
|
|
declare -F _k3slr_keepass_password_stdout >/dev/null || return 1
|
|
! _k3slr_keepass_password_stdout /nonexistent "$K3SLR_KEEPASS_ENTRY" \
|
|
</dev/null >"$stdout_file" 2>"$stderr_file" || return 1
|
|
[[ ! -s "$stdout_file" ]] || return 1
|
|
declare -F _k3slr_keepass_password >/dev/null && return 1
|
|
declare -F _k3slr_read_keepass_password >/dev/null && return 1
|
|
if /usr/bin/rg -n \
|
|
'(^|[[:space:]])(local|declare|typeset)[^#]*(secret|password|passphrase)|read[^#]*(secret|password|passphrase)|KEEPASS[^[:space:]]*PASSWORD=|LUKS[^[:space:]]*PASSWORD=' \
|
|
"$LIBRARY_PATH" "$FEASIBILITY_PATH" >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
task4_feasibility_command_fake() {
|
|
local command="$1" operation path metadata process_identity=''
|
|
shift
|
|
if [[ ( "${TASK4_FEASIBILITY_CASE:-}" == signal_* ||
|
|
"${TASK4_FEASIBILITY_CASE:-}" == anchor_* ) &&
|
|
! -e "${TASK4_FEASIBILITY_MAIN_PID_FILE:-/nonexistent}" ]]; then
|
|
printf '%s\n' "$BASHPID" >"$TASK4_FEASIBILITY_MAIN_PID_FILE"
|
|
fi
|
|
printf '%s %s\n' "$command" "$*" >>"$TASK4_FEASIBILITY_COMMAND_LOG"
|
|
case "$command" in
|
|
/usr/bin/test)
|
|
if [[ "${1-}" == -t ]]; then return 0; fi
|
|
path="${*: -1}"
|
|
if [[ "$path" == /tmp/k3slr-feasibility.* ]]; then /usr/bin/test "$@"; else return 0; fi
|
|
;;
|
|
/usr/bin/stat)
|
|
path="${*: -1}"
|
|
if [[ "$path" == /tmp/k3slr-feasibility.* ]]; then
|
|
metadata="$(/usr/bin/stat "$@")" || return 1
|
|
if [[ -e "${TASK4_ATOMIC_MUTATED_MARKER:-/nonexistent}" &&
|
|
"$TASK4_FEASIBILITY_CASE" == atomic_wrong_owner && "$path" == */synthetic.kdbx ]]; then
|
|
printf '99999|%s\n' "${metadata#*|}"
|
|
elif [[ -e "${TASK4_ATOMIC_MUTATED_MARKER:-/nonexistent}" &&
|
|
"$TASK4_FEASIBILITY_CASE" == atomic_parent_drift && "$path" == "$TASK4_FEASIBILITY_REQUESTED_DIR" &&
|
|
! -s "${TASK4_ATOMIC_PARENT_STAT_LOG:-/nonexistent}" ]]; then
|
|
printf x >"$TASK4_ATOMIC_PARENT_STAT_LOG"
|
|
printf '%s|99:99\n' "${metadata%|*}"
|
|
else
|
|
printf '%s\n' "$metadata"
|
|
fi
|
|
else
|
|
printf '0|755|regular file\n'
|
|
fi
|
|
;;
|
|
/usr/bin/id|/usr/bin/mawk|/usr/bin/od|/usr/bin/ps|/usr/bin/sleep|/usr/bin/sha256sum|/usr/bin/cmp|/usr/bin/pkill)
|
|
if [[ -n "${TASK4_FEASIBILITY_ROLE_LOG:-}" &&
|
|
( "$command" == /usr/bin/mawk || "$command" == /usr/bin/sha256sum ) ]]; then
|
|
_k3slr_process_identity process_identity "$BASHPID" || return 1
|
|
printf '%s|%s\n' "${command##*/}" "$process_identity" >>"$TASK4_FEASIBILITY_ROLE_LOG"
|
|
fi
|
|
"$command" "$@"
|
|
;;
|
|
/usr/bin/readlink)
|
|
printf '%s\n' "${*: -1}"
|
|
;;
|
|
/usr/bin/dpkg-query)
|
|
case "${*: -1}" in
|
|
keepassxc)
|
|
case "$TASK4_FEASIBILITY_CASE" in
|
|
package_missing_lf) printf 'ii |2.7.6+dfsg.1-1build3' ;;
|
|
package_double_lf) printf 'ii |2.7.6+dfsg.1-1build3\n\n' ;;
|
|
*) printf 'ii |2.7.6+dfsg.1-1build3\n' ;;
|
|
esac
|
|
;;
|
|
cryptsetup-bin) printf 'ii |2:2.7.0-1ubuntu4.2\n' ;;
|
|
*) return 92 ;;
|
|
esac
|
|
;;
|
|
/usr/bin/mktemp)
|
|
/usr/bin/mkdir -- "$TASK4_FEASIBILITY_REQUESTED_DIR" || return 1
|
|
/usr/bin/chmod 0700 -- "$TASK4_FEASIBILITY_REQUESTED_DIR" || return 1
|
|
printf '%s\n' "$TASK4_FEASIBILITY_REQUESTED_DIR"
|
|
;;
|
|
/usr/bin/rm)
|
|
if [[ "$TASK4_FEASIBILITY_CASE" == cleanup_failure && "${1-}" == --recursive ]]; then
|
|
return 9
|
|
fi
|
|
/usr/bin/rm "$@"
|
|
;;
|
|
/usr/bin/sudo)
|
|
[[ "${1-}" == --non-interactive && "${2-}" == --validate ]]
|
|
;;
|
|
/usr/bin/keepassxc-cli)
|
|
operation="$1"
|
|
if [[ -n "${TASK4_FEASIBILITY_ROLE_LOG:-}" ]]; then
|
|
_k3slr_process_identity process_identity "$BASHPID" || return 1
|
|
printf 'keepass|%s\n' "$process_identity" >>"$TASK4_FEASIBILITY_ROLE_LOG"
|
|
fi
|
|
if [[ -n "${TASK4_CHILD_CMDLINE_LOG:-}" ]]; then
|
|
/usr/bin/tr '\0' '\n' <"/proc/${BASHPID}/cmdline" >>"$TASK4_CHILD_CMDLINE_LOG"
|
|
/usr/bin/tr '\0' '\n' <"/proc/${BASHPID}/environ" >>"$TASK4_CHILD_ENVIRON_LOG"
|
|
fi
|
|
if [[ "$TASK4_FEASIBILITY_CASE" == signal_* && "$operation" == db-create ]]; then
|
|
printf '%s\n' "$BASHPID" >"$TASK4_FEASIBILITY_CHILD_PID_FILE"
|
|
trap 'printf terminated >"$TASK4_FEASIBILITY_CHILD_TERM_FILE"; exit 143' TERM INT
|
|
while :; do /usr/bin/sleep 1; done
|
|
fi
|
|
if [[ "$TASK4_FEASIBILITY_CASE" == anchor_* && "$operation" == db-create ]]; then
|
|
printf '%s\n' "$BASHPID" >"$TASK4_FEASIBILITY_CHILD_PID_FILE"
|
|
trap '' TERM
|
|
printf ready >"$TASK4_FEASIBILITY_TERM_IGNORE_READY"
|
|
while :; do /usr/bin/sleep 1; done
|
|
fi
|
|
/usr/bin/mawk '{ next } END { exit 0 }' || return 1
|
|
case "$operation" in
|
|
db-create)
|
|
path="${*: -1}"
|
|
( set -o noclobber; printf 'synthetic-database\n' >"$path" ) || return 1
|
|
/usr/bin/chmod 0600 -- "$path"
|
|
;;
|
|
add)
|
|
path="${@: -2:1}"
|
|
printf 'synthetic-database-after-add\n' >"${path}.atomic"
|
|
if [[ "$TASK4_FEASIBILITY_CASE" == atomic_wrong_mode ]]; then
|
|
/usr/bin/chmod 0644 -- "${path}.atomic"
|
|
else
|
|
/usr/bin/chmod 0600 -- "${path}.atomic"
|
|
fi
|
|
/usr/bin/mv -- "${path}.atomic" "$path"
|
|
if [[ "$TASK4_FEASIBILITY_CASE" == atomic_symlink ]]; then
|
|
/usr/bin/unlink "$path"
|
|
/usr/bin/ln -s -- /dev/null "$path"
|
|
fi
|
|
if [[ -n "${TASK4_ATOMIC_MUTATED_MARKER:-}" ]]; then printf x >"$TASK4_ATOMIC_MUTATED_MARKER"; fi
|
|
;;
|
|
show)
|
|
[[ "$TASK4_FEASIBILITY_CASE" != synthetic_failure ]] || return 7
|
|
if (( $# == 6 )) && [[ "${2-}" == --show-protected &&
|
|
"${3-}" == --attributes && "${4-}" == Password &&
|
|
"${5-}" == */synthetic.kdbx && "${6-}" == "$K3SLR_KEEPASS_ENTRY" ]]; then
|
|
printf '%s\n' 'Zz9Yy8Xx7Ww6Vv5Uu4Tt3Ss2Rr1Qq0Pp8Oo7Nn6M'
|
|
elif (( $# == 7 )) && [[ "${2-}" == --quiet &&
|
|
"${3-}" == --attributes && "${4-}" == Title &&
|
|
"${5-}" == --show-attachments && "${6-}" == */synthetic.kdbx &&
|
|
"${7-}" == "$K3SLR_KEEPASS_ENTRY" ]]; then
|
|
if [[ -f "${6}.attachment-state" ]]; then
|
|
printf 'K3s Recovery LUKS\n\nAttachments:\n fixture.bin (30.0 B)\n'
|
|
else
|
|
printf 'K3s Recovery LUKS\n\nNo attachments present.\n'
|
|
fi
|
|
else
|
|
return 91
|
|
fi
|
|
;;
|
|
attachment-import)
|
|
path="$3"
|
|
printf 'synthetic-database-after-attachment\n' >"${path}.atomic"
|
|
/usr/bin/chmod 0600 -- "${path}.atomic"
|
|
/usr/bin/mv -- "${path}.atomic" "$path"
|
|
: >"${path}.attachment-state"
|
|
;;
|
|
attachment-export)
|
|
path="${*: -1}"
|
|
printf '%s\n' 'k3slr-attachment-roundtrip-v1' >"$path"
|
|
;;
|
|
*) return 91 ;;
|
|
esac
|
|
;;
|
|
*) return 90 ;;
|
|
esac
|
|
}
|
|
|
|
run_task4_feasibility_fixture() (
|
|
local fixture_case="$1" expected_rc="$2" requested_dir="$3"
|
|
local stdout_file="$4" stderr_file="$5" rc=0
|
|
# shellcheck source=/dev/null
|
|
source "$FEASIBILITY_PATH"
|
|
export TASK4_FEASIBILITY_CASE="$fixture_case"
|
|
export TASK4_FEASIBILITY_REQUESTED_DIR="$requested_dir"
|
|
export TASK4_FEASIBILITY_COMMAND_LOG="${fixture_root}/task4-${fixture_case}.commands"
|
|
: >"$TASK4_FEASIBILITY_COMMAND_LOG"
|
|
_k3slr_command() { task4_feasibility_command_fake "$@"; }
|
|
set +e
|
|
k3slr_local_recovery_feasibility_main --execute </dev/ptmx >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -ne "$expected_rc" ]]; then /usr/bin/tail -n 50 "$TASK4_FEASIBILITY_COMMAND_LOG" >&2; return 1; fi
|
|
if [[ "$fixture_case" == cleanup_failure ]]; then
|
|
[[ -d "$requested_dir" ]] || return 1
|
|
/usr/bin/rm -rf -- "$requested_dir"
|
|
else
|
|
if [[ -e "$requested_dir" ]]; then printf 'task4 feasibility diagnostic: fixture remains\n' >&2; return 1; fi
|
|
fi
|
|
if /usr/bin/grep -Fq -- 'Zz9Yy8Xx7Ww6Vv5Uu4Tt3Ss2Rr1Qq0Pp8Oo7Nn6M' "$stdout_file" "$stderr_file"; then
|
|
printf 'task4 feasibility diagnostic: sentinel output leak\n' >&2
|
|
return 1
|
|
fi
|
|
if [[ "$fixture_case" == success ]]; then
|
|
/usr/bin/grep -Fx -- "/usr/bin/keepassxc-cli db-create --quiet --set-password ${requested_dir}/synthetic.kdbx" "$TASK4_FEASIBILITY_COMMAND_LOG" >/dev/null || return 1
|
|
/usr/bin/grep -Fx -- "/usr/bin/keepassxc-cli add --quiet --generate --length 40 --lower --upper --numeric --every-group ${requested_dir}/synthetic.kdbx ${K3SLR_KEEPASS_ENTRY}" "$TASK4_FEASIBILITY_COMMAND_LOG" >/dev/null || return 1
|
|
/usr/bin/grep -Fx -- "/usr/bin/keepassxc-cli show --show-protected --attributes Password ${requested_dir}/synthetic.kdbx ${K3SLR_KEEPASS_ENTRY}" "$TASK4_FEASIBILITY_COMMAND_LOG" >/dev/null || return 1
|
|
[[ "$(/usr/bin/grep -Fxc -- "/usr/bin/keepassxc-cli show --quiet --attributes Title --show-attachments ${requested_dir}/synthetic.kdbx ${K3SLR_KEEPASS_ENTRY}" "$TASK4_FEASIBILITY_COMMAND_LOG")" -eq 2 ]] || return 1
|
|
/usr/bin/grep -Fx -- "/usr/bin/keepassxc-cli attachment-import --quiet ${requested_dir}/synthetic.kdbx ${K3SLR_KEEPASS_ENTRY} fixture.bin ${requested_dir}/input.bin" "$TASK4_FEASIBILITY_COMMAND_LOG" >/dev/null || return 1
|
|
/usr/bin/grep -E -- "^/usr/bin/keepassxc-cli attachment-export --quiet ${requested_dir}/synthetic\\.kdbx ${K3SLR_KEEPASS_ENTRY} fixture\\.bin /proc/[0-9]+/fd/[0-9]+$" "$TASK4_FEASIBILITY_COMMAND_LOG" >/dev/null || return 1
|
|
if [[ "$(( $(/usr/bin/wc -l <"$stdout_file") ))" -ne 1 ]]; then
|
|
printf 'task4 diagnostic: success stdout line count mismatch\n' >&2
|
|
return 1
|
|
fi
|
|
if [[ "$(<"$stdout_file")" != 'Local recovery feasibility: pass' ]]; then
|
|
printf 'task4 diagnostic: success stdout classification mismatch\n' >&2
|
|
return 1
|
|
fi
|
|
if [[ -s "$stderr_file" ]]; then
|
|
printf 'task4 diagnostic: success stderr is nonempty\n' >&2
|
|
return 1
|
|
fi
|
|
elif [[ "$fixture_case" == synthetic_failure || "$fixture_case" == cleanup_failure ]]; then
|
|
[[ ! -s "$stdout_file" && "$(<"$stderr_file")" == 'Local recovery feasibility failed' ]] || return 1
|
|
fi
|
|
return 0
|
|
)
|
|
|
|
task4_review1_blocking_child_is_reaped() (
|
|
local signal_name expected_rc requested_dir stdout_file stderr_file child_pid index rc killer_pid
|
|
local role pid ppid pgid start_time recorded_roles
|
|
# shellcheck source=/dev/null
|
|
source "$FEASIBILITY_PATH"
|
|
_k3slr_lifecycle_boundary() {
|
|
local phase="$1" tracked_pid="${2-}" tracked_identity=''
|
|
if [[ "$phase" == feasibility-pid-published && -n "${TASK4_FEASIBILITY_ROLE_LOG:-}" ]]; then
|
|
_k3slr_process_identity tracked_identity "$tracked_pid" || return 1
|
|
printf 'supervisor|%s\n' "$tracked_identity" >>"$TASK4_FEASIBILITY_ROLE_LOG"
|
|
fi
|
|
}
|
|
for signal_name in TERM INT; do
|
|
requested_dir="/tmp/k3slr-feasibility.signal${signal_name}${BASHPID}${RANDOM}"
|
|
stdout_file="${fixture_root}/task4-signal-${signal_name}.stdout"
|
|
stderr_file="${fixture_root}/task4-signal-${signal_name}.stderr"
|
|
export TASK4_FEASIBILITY_CASE="signal_${signal_name}"
|
|
export TASK4_FEASIBILITY_REQUESTED_DIR="$requested_dir"
|
|
export TASK4_FEASIBILITY_COMMAND_LOG="${fixture_root}/task4-signal-${signal_name}.commands"
|
|
export TASK4_FEASIBILITY_CHILD_PID_FILE="${fixture_root}/task4-signal-${signal_name}.pid"
|
|
export TASK4_FEASIBILITY_CHILD_TERM_FILE="${fixture_root}/task4-signal-${signal_name}.terminated"
|
|
export TASK4_FEASIBILITY_MAIN_PID_FILE="${fixture_root}/task4-signal-${signal_name}.main-pid"
|
|
export TASK4_FEASIBILITY_ROLE_LOG="${fixture_root}/task4-signal-${signal_name}.roles"
|
|
: >"$TASK4_FEASIBILITY_COMMAND_LOG"
|
|
: >"$TASK4_FEASIBILITY_ROLE_LOG"
|
|
_k3slr_command() { task4_feasibility_command_fake "$@"; }
|
|
(
|
|
for ((index=0; index<100; index++)); do
|
|
[[ -s "$TASK4_FEASIBILITY_CHILD_PID_FILE" && -s "$TASK4_FEASIBILITY_MAIN_PID_FILE" ]] && break
|
|
/usr/bin/sleep 0.05
|
|
done
|
|
[[ -s "$TASK4_FEASIBILITY_CHILD_PID_FILE" && -s "$TASK4_FEASIBILITY_MAIN_PID_FILE" ]] || exit 1
|
|
kill -"$signal_name" "$(<"$TASK4_FEASIBILITY_MAIN_PID_FILE")"
|
|
) &
|
|
killer_pid=$!
|
|
set +e
|
|
k3slr_local_recovery_feasibility_main --execute </dev/ptmx >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
wait "$killer_pid"
|
|
set -e
|
|
child_pid="$(<"$TASK4_FEASIBILITY_CHILD_PID_FILE")"
|
|
if [[ "$signal_name" == TERM ]]; then expected_rc=143; else expected_rc=130; fi
|
|
if [[ "$rc" -ne "$expected_rc" || ! -e "$TASK4_FEASIBILITY_CHILD_TERM_FILE" || -e "$requested_dir" ]] ||
|
|
kill -0 "$child_pid" 2>/dev/null; then
|
|
printf 'task4 signal diagnostic: signal=%s rc=%s expected=%s term=%s fixture=%s live=%s\n' \
|
|
"$signal_name" "$rc" "$expected_rc" "$([[ -e "$TASK4_FEASIBILITY_CHILD_TERM_FILE" ]] && printf yes || printf no)" \
|
|
"$([[ -e "$requested_dir" ]] && printf yes || printf no)" "$([[ -e /proc/$child_pid ]] && printf yes || printf no)" >&2
|
|
return 1
|
|
fi
|
|
recorded_roles=0
|
|
while IFS='|' read -r role pid start_time ppid pgid; do
|
|
[[ "$pid" =~ ^[1-9][0-9]*$ && "$ppid" =~ ^[0-9]+$ && "$pgid" =~ ^[1-9][0-9]*$ &&
|
|
"$start_time" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
! kill -0 "$pid" 2>/dev/null || return 1
|
|
recorded_roles=$((recorded_roles + 1))
|
|
done <"$TASK4_FEASIBILITY_ROLE_LOG"
|
|
(( recorded_roles >= 3 )) || return 1
|
|
done
|
|
)
|
|
|
|
task4_feasibility_cleanup_and_leak_contract() (
|
|
local case_name expected_rc output error temporary uid
|
|
export TASK4_CHILD_CMDLINE_LOG="${fixture_root}/task4-child.cmdline"
|
|
export TASK4_CHILD_ENVIRON_LOG="${fixture_root}/task4-child.environ"
|
|
: >"$TASK4_CHILD_CMDLINE_LOG"
|
|
: >"$TASK4_CHILD_ENVIRON_LOG"
|
|
[[ -f "$FEASIBILITY_PATH" && ! -L "$FEASIBILITY_PATH" ]] || return 1
|
|
for case_name in success synthetic_failure cleanup_failure; do
|
|
case "$case_name" in
|
|
success) expected_rc=0 ;;
|
|
synthetic_failure|cleanup_failure) expected_rc=1 ;;
|
|
esac
|
|
temporary="/tmp/k3slr-feasibility.task4${BASHPID}${RANDOM}"
|
|
output="${fixture_root}/task4-${case_name}.stdout"
|
|
error="${fixture_root}/task4-${case_name}.stderr"
|
|
assert_succeeds run_task4_feasibility_fixture "$case_name" "$expected_rc" "$temporary" "$output" "$error"
|
|
done
|
|
uid="$(/usr/bin/id -u)"
|
|
if /usr/bin/find /tmp /run -xdev -user "$uid" \
|
|
-name '*Zz9Yy8Xx7Ww6Vv5Uu4Tt3Ss2Rr1Qq0Pp8Oo7Nn6M*' \
|
|
-print -quit 2>/dev/null | /usr/bin/grep -q .; then
|
|
return 1
|
|
fi
|
|
if /usr/bin/find /tmp /run -xdev -user "$uid" -type f -readable \
|
|
! -path "${fixture_root}/*" \
|
|
-exec /usr/bin/grep -IlF -- 'Zz9Yy8Xx7Ww6Vv5Uu4Tt3Ss2Rr1Qq0Pp8Oo7Nn6M' '{}' \; \
|
|
-print -quit 2>/dev/null | /usr/bin/grep -q .; then
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
task4_review1_noclobber_lifecycle() (
|
|
local collision fixture_dir target
|
|
# shellcheck source=/dev/null
|
|
source "$FEASIBILITY_PATH"
|
|
export TASK4_FEASIBILITY_CASE=success
|
|
export TASK4_FEASIBILITY_COMMAND_LOG="${fixture_root}/task4-noclobber.commands"
|
|
_k3slr_command() { task4_feasibility_command_fake "$@"; }
|
|
for collision in preexisting symlink; do
|
|
fixture_dir="/tmp/k3slr-feasibility.noclobber${collision}${BASHPID}${RANDOM}"
|
|
/usr/bin/mkdir -m 0700 -- "$fixture_dir"
|
|
: >"$TASK4_FEASIBILITY_COMMAND_LOG"
|
|
if [[ "$collision" == preexisting ]]; then
|
|
target="${fixture_dir}/input.bin"
|
|
printf 'owned\n' >"$target"
|
|
else
|
|
target="${fixture_dir}/synthetic.kdbx"
|
|
/usr/bin/ln -s -- /dev/null "$target"
|
|
fi
|
|
K3SLRF_ACTIVE_CHILD_PID=''
|
|
assert_fails _k3slrf_run_synthetic "$fixture_dir"
|
|
[[ -e "$target" || -L "$target" ]] || return 1
|
|
! /usr/bin/grep -Fq -- '/usr/bin/keepassxc-cli ' "$TASK4_FEASIBILITY_COMMAND_LOG" || return 1
|
|
/usr/bin/rm -rf -- "$fixture_dir"
|
|
done
|
|
)
|
|
|
|
task4_review2_atomic_save_transition() (
|
|
local requested_dir="/tmp/k3slr-feasibility.atomic${BASHPID}${RANDOM}" atomic_case
|
|
local stdout_file="${fixture_root}/task4-review2-atomic.stdout" stderr_file="${fixture_root}/task4-review2-atomic.stderr"
|
|
export TASK4_ATOMIC_MUTATED_MARKER="${fixture_root}/task4-review2-atomic-mutated"
|
|
export TASK4_ATOMIC_PARENT_STAT_LOG="${fixture_root}/task4-review2-parent-stat"
|
|
/usr/bin/unlink "$TASK4_ATOMIC_MUTATED_MARKER" 2>/dev/null || true
|
|
: >"$TASK4_ATOMIC_PARENT_STAT_LOG"
|
|
assert_succeeds run_task4_feasibility_fixture success 0 "$requested_dir" "$stdout_file" "$stderr_file"
|
|
for atomic_case in atomic_symlink atomic_wrong_owner atomic_wrong_mode atomic_parent_drift; do
|
|
/usr/bin/unlink "$TASK4_ATOMIC_MUTATED_MARKER" 2>/dev/null || true
|
|
: >"$TASK4_ATOMIC_PARENT_STAT_LOG"
|
|
requested_dir="/tmp/k3slr-feasibility.atomicfault${BASHPID}${RANDOM}"
|
|
stdout_file="${fixture_root}/task4-review2-${atomic_case}.stdout"
|
|
stderr_file="${fixture_root}/task4-review2-${atomic_case}.stderr"
|
|
assert_succeeds run_task4_feasibility_fixture "$atomic_case" 1 "$requested_dir" "$stdout_file" "$stderr_file"
|
|
done
|
|
)
|
|
|
|
task4_review1_raw_package_record() (
|
|
local record_case
|
|
# shellcheck source=/dev/null
|
|
source "$FEASIBILITY_PATH"
|
|
export TASK4_FEASIBILITY_COMMAND_LOG="${fixture_root}/task4-package.commands"
|
|
: >"$TASK4_FEASIBILITY_COMMAND_LOG"
|
|
_k3slr_command() { task4_feasibility_command_fake "$@"; }
|
|
TASK4_FEASIBILITY_CASE=success
|
|
assert_succeeds _k3slrf_package_version_is_exact keepassxc '2.7.6+dfsg.1-1build3'
|
|
for record_case in package_missing_lf package_double_lf; do
|
|
TASK4_FEASIBILITY_CASE="$record_case"
|
|
assert_fails _k3slrf_package_version_is_exact keepassxc '2.7.6+dfsg.1-1build3'
|
|
done
|
|
)
|
|
|
|
task4_review1_command_fake() {
|
|
local command="$1" format path role pgid caller_pgid early_signal role_identity='' start_time=''
|
|
shift
|
|
printf '%s %s\n' "$command" "$*" >>"$TASK4_REVIEW1_COMMAND_LOG"
|
|
case "$command" in
|
|
/usr/bin/test)
|
|
if [[ "${1-}" == -t && "${TASK4_REVIEW3_SIGNAL_CASE:-}" == 1 &&
|
|
! -e "${TASK4_REVIEW3_CALLER_PID_FILE:-/nonexistent}" ]]; then
|
|
printf '%s\n' "$BASHPID" >"$TASK4_REVIEW3_CALLER_PID_FILE"
|
|
caller_pgid="$(/usr/bin/ps -o pgid= -p "$BASHPID")" || return 1
|
|
caller_pgid="${caller_pgid//[[:space:]]/}"
|
|
printf '%s\n' "$caller_pgid" >"$TASK4_REVIEW3_CALLER_PGID_FILE"
|
|
fi
|
|
if [[ "${1-}" == -t && "${TASK4_REVIEW2_REQUIRE_REAL_TTY:-}" == 1 ]]; then
|
|
printf 'tty-check pid=%s result=%s\n' "$BASHPID" "$([[ -t 0 ]] && printf tty || printf pipe)" >>"$TASK4_REVIEW1_COMMAND_LOG"
|
|
/usr/bin/test -t 0
|
|
return
|
|
fi
|
|
if [[ "${1-}" == -t && "${TASK4_REVIEW1_VALIDATION_CASE:-}" == non_tty ]]; then return 1; fi
|
|
if [[ "${1-}" == '!' && "${2-}" == -L && "${3-}" == "${K3SLR_OUTER_MOUNT}/${K3SLR_ROOT_RELATIVE}/vault" &&
|
|
"${TASK4_REVIEW1_VALIDATION_CASE:-}" == parent_symlink ]]; then return 1; fi
|
|
return 0
|
|
;;
|
|
/usr/bin/id)
|
|
[[ "$*" == '-u' ]] || return 97
|
|
printf '1000\n'
|
|
;;
|
|
/usr/bin/readlink)
|
|
printf '%s\n' "${*: -1}"
|
|
;;
|
|
/usr/bin/stat)
|
|
format="$1"
|
|
path="${*: -1}"
|
|
case "$format" in
|
|
--format=%u\|%a\|%F) printf '0|755|regular file\n' ;;
|
|
--format=%F) printf 'block special file\n' ;;
|
|
--format=%d:%i\|%F\|%u\|%a)
|
|
case "$path" in
|
|
"$K3SLR_OUTER_MOUNT") printf '8:10|directory|1000|700\n' ;;
|
|
"${K3SLR_OUTER_MOUNT}/${K3SLR_ROOT_RELATIVE}") printf '8:11|directory|1000|700\n' ;;
|
|
"${K3SLR_OUTER_MOUNT}/${K3SLR_ROOT_RELATIVE}/vault") printf '8:12|directory|1000|700\n' ;;
|
|
"${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}")
|
|
if [[ "${TASK4_REVIEW1_VALIDATION_CASE:-}" == post_drift ]]; then
|
|
printf 'x\n' >>"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
if [[ "$(/usr/bin/wc -l <"$TASK4_REVIEW1_DB_STAT_LOG")" -gt 1 ]]; then
|
|
printf '8:99|regular file|1000|600\n'
|
|
else
|
|
printf '8:13|regular file|1000|600\n'
|
|
fi
|
|
else
|
|
printf '8:13|regular file|1000|600\n'
|
|
fi
|
|
;;
|
|
*) return 96 ;;
|
|
esac
|
|
;;
|
|
*) return 95 ;;
|
|
esac
|
|
;;
|
|
/usr/bin/keepassxc-cli)
|
|
if [[ -n "${TASK4_REVIEW4_IDENTITY_ROLE_LOG:-}" ]]; then
|
|
_k3slr_process_identity role_identity "$BASHPID" || return 1
|
|
printf 'keepass|%s\n' "$role_identity" >>"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
/usr/bin/sleep 0.05
|
|
fi
|
|
if [[ -n "${TASK4_CHILD_CMDLINE_LOG:-}" ]]; then
|
|
/usr/bin/tr '\0' '\n' <"/proc/${BASHPID}/cmdline" >>"$TASK4_CHILD_CMDLINE_LOG"
|
|
/usr/bin/tr '\0' '\n' <"/proc/${BASHPID}/environ" >>"$TASK4_CHILD_ENVIRON_LOG"
|
|
fi
|
|
printf '%s\n' "$*" >>"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
if [[ "${TASK4_REVIEW2_REQUIRE_REAL_TTY:-}" == 1 ]]; then
|
|
/usr/bin/test -t 0 || return 88
|
|
fi
|
|
if [[ "$TASK4_REVIEW1_PRODUCER_CASE" == blocking_signal ]]; then
|
|
exec /usr/bin/bash -c '
|
|
pgid="$(/usr/bin/ps -o pgid= -p "$$")" || exit 1
|
|
pgid="${pgid//[[:space:]]/}"
|
|
IFS= read -r stat_record <"/proc/$$/stat" || exit 1
|
|
stat_tail="${stat_record##*) }"
|
|
read -r -a stat_fields <<<"$stat_tail"
|
|
start_time="${stat_fields[19]}"
|
|
printf "keepass %s %s %s %s\n" "$$" "$PPID" "$pgid" "$start_time" >>"$TASK4_REVIEW3_ROLE_LOG"
|
|
trap "exit 143" TERM
|
|
trap "exit 130" INT
|
|
while :; do :; done
|
|
'
|
|
fi
|
|
if [[ "$TASK4_REVIEW1_PRODUCER_CASE" == term_ignoring_descendant ]]; then
|
|
exec /usr/bin/bash -c '
|
|
trap "" TERM
|
|
printf ready >"$TASK4_REVIEW5_TERM_IGNORE_READY"
|
|
while :; do :; done
|
|
'
|
|
fi
|
|
case "$TASK4_REVIEW1_PRODUCER_CASE" in
|
|
partial_failure) printf '%s' partial; return 7 ;;
|
|
zero_failure) return 8 ;;
|
|
zero_success) return 0 ;;
|
|
missing_lf) printf '%s' 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N' ;;
|
|
double_lf) printf '%s\n\n' 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N' ;;
|
|
invalid_class) printf '%s\n' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' ;;
|
|
malformed_length) printf '%s\n' 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm' ;;
|
|
oversized_success) /usr/bin/mawk 'BEGIN { for (i=0; i<200000; i++) printf "X"; printf "\n" }' ;;
|
|
success) printf '%s\n' 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N' ;;
|
|
*) return 94 ;;
|
|
esac
|
|
;;
|
|
/usr/bin/sudo)
|
|
if [[ "${1-}" == --non-interactive && "${2-}" == --validate ]]; then return 0; fi
|
|
printf 'dispatch\n' >>"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
/usr/bin/mawk 'END { exit 0 }'
|
|
;;
|
|
/usr/bin/pkill)
|
|
/usr/bin/pkill "$@"
|
|
;;
|
|
/usr/bin/ps)
|
|
if [[ "${TASK4_REVIEW3_EARLY_SIGNAL:-}" =~ ^(INT|TERM)$ && "${1-} ${2-}" == '-o stat=' ]]; then
|
|
early_signal="$TASK4_REVIEW3_EARLY_SIGNAL"
|
|
TASK4_REVIEW3_EARLY_SIGNAL=''
|
|
printf '%s\n' "${*: -1}" >"$TASK4_REVIEW3_SUPERVISOR_PID_FILE"
|
|
/usr/bin/ps -o pgid= -p "${*: -1}" | /usr/bin/tr -d '[:space:]' >"$TASK4_REVIEW3_SUPERVISOR_PGID_FILE"
|
|
kill -"$early_signal" "$(<"$TASK4_REVIEW3_CALLER_PID_FILE")"
|
|
fi
|
|
/usr/bin/ps "$@"
|
|
;;
|
|
/usr/bin/sleep)
|
|
/usr/bin/sleep "$@"
|
|
;;
|
|
/usr/bin/od|/usr/bin/mawk|/usr/bin/dd)
|
|
if [[ -n "${TASK4_REVIEW4_IDENTITY_ROLE_LOG:-}" && "$command" != /usr/bin/dd ]]; then
|
|
role="${command##*/}"
|
|
_k3slr_process_identity role_identity "$BASHPID" || return 1
|
|
printf '%s|%s\n' "$role" "$role_identity" >>"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
fi
|
|
if [[ "$TASK4_REVIEW1_PRODUCER_CASE" == blocking_signal && "$command" != /usr/bin/dd ]]; then
|
|
role="${command##*/}"
|
|
pgid="$(/usr/bin/ps -o pgid= -p "$BASHPID")" || return 1
|
|
pgid="${pgid//[[:space:]]/}"
|
|
_k3slr_process_identity role_identity "$BASHPID" || return 1
|
|
IFS='|' read -r _ start_time _ _ <<<"$role_identity"
|
|
printf '%s %s %s %s %s\n' "$role" "$BASHPID" "$PPID" "$pgid" "$start_time" >>"$TASK4_REVIEW3_ROLE_LOG"
|
|
exec "$command" "$@"
|
|
fi
|
|
"$command" "$@"
|
|
;;
|
|
*) return 93 ;;
|
|
esac
|
|
}
|
|
|
|
task4_review1_producer_must_finish_before_consumer() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}"
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${fixture_root}/task4-review1-consumer.log"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${fixture_root}/task4-review1-producer.log"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${fixture_root}/task4-review1-command.log"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${fixture_root}/task4-review1-db-stat.log"
|
|
export TASK4_CHILD_CMDLINE_LOG="${fixture_root}/task4-child.cmdline"
|
|
export TASK4_CHILD_ENVIRON_LOG="${fixture_root}/task4-child.environ"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
: >"$TASK4_CHILD_CMDLINE_LOG"
|
|
: >"$TASK4_CHILD_ENVIRON_LOG"
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
|
|
TASK4_REVIEW1_PRODUCER_CASE=partial_failure
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
assert_fails _k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME"
|
|
[[ ! -s "$TASK4_REVIEW1_CONSUMER_LOG" ]] || return 1
|
|
|
|
TASK4_REVIEW1_PRODUCER_CASE=zero_failure
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
assert_fails _k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME"
|
|
[[ ! -s "$TASK4_REVIEW1_CONSUMER_LOG" ]] || return 1
|
|
|
|
TASK4_REVIEW1_PRODUCER_CASE=success
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
if ! _k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME"; then
|
|
printf 'task4 critical diagnostic: producer=%s consumer=%s\n' \
|
|
"$(/usr/bin/wc -l <"$TASK4_REVIEW1_PRODUCER_LOG")" \
|
|
"$(/usr/bin/wc -l <"$TASK4_REVIEW1_CONSUMER_LOG")" >&2
|
|
/usr/bin/tail -n 20 "$TASK4_REVIEW1_COMMAND_LOG" >&2
|
|
return 1
|
|
fi
|
|
[[ "$(/usr/bin/wc -l <"$TASK4_REVIEW1_CONSUMER_LOG")" -eq 1 ]] || return 1
|
|
[[ "$(/usr/bin/tail -n 1 "$TASK4_REVIEW1_PRODUCER_LOG")" == \
|
|
"show --show-protected --attributes Password ${database} ${K3SLR_KEEPASS_ENTRY}" ]]
|
|
)
|
|
|
|
task4_review3_cleanup_recorded_roles() {
|
|
local role pid ppid pgid start_time expected_identity current_identity caller_pgid cleanup_rc=0
|
|
[[ -f "$1" ]] || return 0
|
|
caller_pgid="$(/usr/bin/ps -o pgid= -p "$BASHPID")" || return 1
|
|
caller_pgid="${caller_pgid//[[:space:]]/}"
|
|
while read -r role pid ppid pgid start_time; do
|
|
[[ "$pid" =~ ^[1-9][0-9]*$ ]] || continue
|
|
expected_identity="${pid}|${start_time}|${ppid}|${pgid}"
|
|
if _k3slr_process_identity current_identity "$pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$expected_identity" && "$pgid" != "$caller_pgid" ]]; then
|
|
builtin kill -TERM "$pid" 2>/dev/null || cleanup_rc=1
|
|
fi
|
|
done <"$1"
|
|
/usr/bin/sleep 0.1
|
|
while read -r role pid ppid pgid start_time; do
|
|
[[ "$pid" =~ ^[1-9][0-9]*$ ]] || continue
|
|
expected_identity="${pid}|${start_time}|${ppid}|${pgid}"
|
|
if _k3slr_process_identity current_identity "$pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$expected_identity" && "$pgid" != "$caller_pgid" ]]; then
|
|
builtin kill -KILL "$pid" 2>/dev/null || cleanup_rc=1
|
|
fi
|
|
done <"$1"
|
|
return "$cleanup_rc"
|
|
}
|
|
|
|
task4_review3_nested_pipeline_signal_cleanup() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}"
|
|
local signal_name expected_rc iteration prefix killer_pid rc started_ns ended_ns elapsed_ms
|
|
local role pid ppid pgid start_time leaked dedicated_pgid caller_pgid
|
|
export TASK4_REVIEW2_REQUIRE_REAL_TTY=1
|
|
export TASK4_REVIEW3_SIGNAL_CASE=1
|
|
TASK4_REVIEW1_PRODUCER_CASE=blocking_signal
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
for signal_name in INT TERM; do
|
|
if [[ "$signal_name" == INT ]]; then expected_rc=130; else expected_rc=143; fi
|
|
for iteration in 1 2; do
|
|
prefix="${fixture_root}/task4-review3-${signal_name}-${iteration}"
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${prefix}.consumer"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${prefix}.producer"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${prefix}.commands"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${prefix}.db-stat"
|
|
export TASK4_REVIEW3_CALLER_PID_FILE="${prefix}.caller-pid"
|
|
export TASK4_REVIEW3_CALLER_PGID_FILE="${prefix}.caller-pgid"
|
|
export TASK4_REVIEW3_ROLE_LOG="${prefix}.roles"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
: >"$TASK4_REVIEW3_ROLE_LOG"
|
|
/usr/bin/unlink "$TASK4_REVIEW3_CALLER_PID_FILE" 2>/dev/null || true
|
|
/usr/bin/unlink "$TASK4_REVIEW3_CALLER_PGID_FILE" 2>/dev/null || true
|
|
(
|
|
for ((probe=0; probe<200; probe++)); do
|
|
if [[ -s "$TASK4_REVIEW3_CALLER_PID_FILE" &&
|
|
"$(/usr/bin/wc -l <"$TASK4_REVIEW3_ROLE_LOG")" -ge 3 ]]; then break; fi
|
|
/usr/bin/sleep 0.01
|
|
done
|
|
[[ -s "$TASK4_REVIEW3_CALLER_PID_FILE" &&
|
|
"$(/usr/bin/wc -l <"$TASK4_REVIEW3_ROLE_LOG")" -ge 3 ]] || exit 1
|
|
kill -"$signal_name" "$(<"$TASK4_REVIEW3_CALLER_PID_FILE")"
|
|
) &
|
|
killer_pid=$!
|
|
started_ns="$(/usr/bin/date +%s%N)"
|
|
set +e
|
|
_k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx
|
|
rc=$?
|
|
wait "$killer_pid"
|
|
set -e
|
|
ended_ns="$(/usr/bin/date +%s%N)"
|
|
elapsed_ms=$(((ended_ns - started_ns) / 1000000))
|
|
leaked=0
|
|
dedicated_pgid=''
|
|
caller_pgid="$(<"$TASK4_REVIEW3_CALLER_PGID_FILE")"
|
|
while read -r role pid ppid pgid start_time; do
|
|
[[ "$role" == keepass || "$role" == od || "$role" == mawk ]] || continue
|
|
if [[ -z "$dedicated_pgid" ]]; then dedicated_pgid="$pgid"; fi
|
|
[[ "$pgid" == "$dedicated_pgid" ]] || leaked=$((leaked + 1))
|
|
if kill -0 "$pid" 2>/dev/null; then leaked=$((leaked + 1)); fi
|
|
done <"$TASK4_REVIEW3_ROLE_LOG"
|
|
if [[ "$rc" -ne "$expected_rc" || "$elapsed_ms" -ge 3000 ||
|
|
-s "$TASK4_REVIEW1_CONSUMER_LOG" || "$leaked" -ne 0 ||
|
|
-z "$dedicated_pgid" || "$dedicated_pgid" == "$caller_pgid" ]]; then
|
|
printf 'task4 review3 signal diagnostic: signal=%s iteration=%s rc=%s elapsed_ms=%s leaked=%s\n' \
|
|
"$signal_name" "$iteration" "$rc" "$elapsed_ms" "$leaked" >&2
|
|
/usr/bin/sed -n '1,20p' "$TASK4_REVIEW3_ROLE_LOG" >&2
|
|
/usr/bin/tail -n 40 "$TASK4_REVIEW1_COMMAND_LOG" >&2
|
|
while read -r role pid ppid pgid start_time; do
|
|
/usr/bin/ps -o pid=,ppid=,pgid=,stat=,args= -p "$pid" >&2 || true
|
|
done <"$TASK4_REVIEW3_ROLE_LOG"
|
|
task4_review3_cleanup_recorded_roles "$TASK4_REVIEW3_ROLE_LOG"
|
|
return 1
|
|
fi
|
|
done
|
|
done
|
|
)
|
|
|
|
task4_review3_launch_window_signal_cleanup() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}"
|
|
local signal_name expected_rc prefix rc started_ns ended_ns elapsed_ms supervisor_pid supervisor_pgid
|
|
local supervisor_identity current_identity caller_pgid cleanup_rc=0
|
|
export TASK4_REVIEW2_REQUIRE_REAL_TTY=1
|
|
export TASK4_REVIEW3_SIGNAL_CASE=1
|
|
TASK4_REVIEW1_PRODUCER_CASE=blocking_signal
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
_k3slr_lifecycle_boundary() {
|
|
local phase="$1" recorded_pgid='' recorded_identity=''
|
|
if [[ "$phase" == pid-published && "${TASK4_REVIEW3_EARLY_SIGNAL:-}" =~ ^(INT|TERM)$ ]]; then
|
|
printf '%s\n' "$producer_pid" >"$TASK4_REVIEW3_SUPERVISOR_PID_FILE"
|
|
recorded_pgid="$(/usr/bin/ps -o pgid= -p "$producer_pid")" || return 1
|
|
recorded_pgid="${recorded_pgid//[[:space:]]/}"
|
|
printf '%s\n' "$recorded_pgid" >"$TASK4_REVIEW3_SUPERVISOR_PGID_FILE"
|
|
_k3slr_process_identity recorded_identity "$producer_pid" || return 1
|
|
printf '%s\n' "$recorded_identity" >"$TASK4_REVIEW3_SUPERVISOR_IDENTITY_FILE"
|
|
kill -"$TASK4_REVIEW3_EARLY_SIGNAL" "$(<"$TASK4_REVIEW3_CALLER_PID_FILE")"
|
|
TASK4_REVIEW3_EARLY_SIGNAL=''
|
|
fi
|
|
}
|
|
for signal_name in INT TERM; do
|
|
if [[ "$signal_name" == INT ]]; then expected_rc=130; else expected_rc=143; fi
|
|
prefix="${fixture_root}/task4-review3-early-${signal_name}"
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${prefix}.consumer"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${prefix}.producer"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${prefix}.commands"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${prefix}.db-stat"
|
|
export TASK4_REVIEW3_CALLER_PID_FILE="${prefix}.caller-pid"
|
|
export TASK4_REVIEW3_CALLER_PGID_FILE="${prefix}.caller-pgid"
|
|
export TASK4_REVIEW3_ROLE_LOG="${prefix}.roles"
|
|
export TASK4_REVIEW3_SUPERVISOR_PID_FILE="${prefix}.supervisor-pid"
|
|
export TASK4_REVIEW3_SUPERVISOR_PGID_FILE="${prefix}.supervisor-pgid"
|
|
export TASK4_REVIEW3_SUPERVISOR_IDENTITY_FILE="${prefix}.supervisor-identity"
|
|
export TASK4_REVIEW3_EARLY_SIGNAL="$signal_name"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
: >"$TASK4_REVIEW3_ROLE_LOG"
|
|
/usr/bin/unlink "$TASK4_REVIEW3_CALLER_PID_FILE" 2>/dev/null || true
|
|
/usr/bin/unlink "$TASK4_REVIEW3_CALLER_PGID_FILE" 2>/dev/null || true
|
|
started_ns="$(/usr/bin/date +%s%N)"
|
|
set +e
|
|
_k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx
|
|
rc=$?
|
|
set -e
|
|
ended_ns="$(/usr/bin/date +%s%N)"
|
|
elapsed_ms=$(((ended_ns - started_ns) / 1000000))
|
|
supervisor_pid="$(<"$TASK4_REVIEW3_SUPERVISOR_PID_FILE")"
|
|
supervisor_pgid="$(<"$TASK4_REVIEW3_SUPERVISOR_PGID_FILE")"
|
|
supervisor_identity="$(<"$TASK4_REVIEW3_SUPERVISOR_IDENTITY_FILE")"
|
|
if [[ "$rc" -ne "$expected_rc" || "$elapsed_ms" -ge 3000 ||
|
|
-s "$TASK4_REVIEW1_CONSUMER_LOG" || -s "$TASK4_REVIEW3_ROLE_LOG" ]] ||
|
|
kill -0 "$supervisor_pid" 2>/dev/null; then
|
|
printf 'task4 review3 early diagnostic: signal=%s rc=%s elapsed_ms=%s supervisor=%s pgid=%s\n' \
|
|
"$signal_name" "$rc" "$elapsed_ms" "$supervisor_pid" "$supervisor_pgid" >&2
|
|
caller_pgid="$(/usr/bin/ps -o pgid= -p "$BASHPID")" || cleanup_rc=1
|
|
caller_pgid="${caller_pgid//[[:space:]]/}"
|
|
if _k3slr_process_identity current_identity "$supervisor_pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$supervisor_identity" && "$supervisor_pgid" != "$caller_pgid" ]]; then
|
|
builtin kill -KILL "$supervisor_pid" 2>/dev/null || cleanup_rc=1
|
|
fi
|
|
(( cleanup_rc == 0 )) || printf 'task4 review3 early cleanup failed\n' >&2
|
|
return 1
|
|
fi
|
|
done
|
|
)
|
|
|
|
task4_review4_stale_cached_group_boundary() (
|
|
local child_a unrelated_pid unrelated_pgid unrelated_identity='' caller_pgid supplied_identity=''
|
|
local current_identity='' current_pgid='' boundary_rc=0 killed=0 cleanup_rc=0 wait_rc
|
|
/usr/bin/setsid /usr/bin/sleep 30 &
|
|
unrelated_pid=$!
|
|
/usr/bin/sleep 0.05
|
|
_k3slr_process_identity unrelated_identity "$unrelated_pid" || return 1
|
|
unrelated_pgid="$(/usr/bin/ps -o pgid= -p "$unrelated_pid")" || return 1
|
|
unrelated_pgid="${unrelated_pgid//[[:space:]]/}"
|
|
caller_pgid="$(/usr/bin/ps -o pgid= -p "$BASHPID")" || return 1
|
|
caller_pgid="${caller_pgid//[[:space:]]/}"
|
|
[[ "$unrelated_pgid" =~ ^[1-9][0-9]*$ && "$unrelated_pgid" != "$caller_pgid" ]] || return 1
|
|
/usr/bin/sleep 30 &
|
|
child_a=$!
|
|
_k3slr_process_identity supplied_identity "$child_a" || return 1
|
|
set +e
|
|
_k3slr_terminate_and_reap "$child_a" "$unrelated_pgid" "$supplied_identity"
|
|
boundary_rc=$?
|
|
set -e
|
|
if ! kill -0 "$unrelated_pid" 2>/dev/null; then killed=1; fi
|
|
if kill -0 "$unrelated_pid" 2>/dev/null; then
|
|
_k3slr_process_identity current_identity "$unrelated_pid" 2>/dev/null || current_identity=''
|
|
current_pgid="$(/usr/bin/ps -o pgid= -p "$unrelated_pid" 2>/dev/null || true)"
|
|
current_pgid="${current_pgid//[[:space:]]/}"
|
|
caller_pgid="$(/usr/bin/ps -o pgid= -p "$BASHPID" 2>/dev/null || true)"
|
|
caller_pgid="${caller_pgid//[[:space:]]/}"
|
|
if [[ "$current_identity" == "$unrelated_identity" && "$current_pgid" == "$unrelated_pgid" &&
|
|
"$caller_pgid" =~ ^[1-9][0-9]*$ && "$current_pgid" != "$caller_pgid" ]]; then
|
|
builtin kill -TERM -- "-${unrelated_pgid}" 2>/dev/null || cleanup_rc=1
|
|
fi
|
|
fi
|
|
if wait "$unrelated_pid" 2>/dev/null; then wait_rc=0; else wait_rc=$?; fi
|
|
(( wait_rc != 127 )) || cleanup_rc=1
|
|
if _k3slr_process_identity current_identity "$child_a" 2>/dev/null &&
|
|
[[ "$current_identity" == "$supplied_identity" ]]; then
|
|
builtin kill -TERM "$child_a" 2>/dev/null || cleanup_rc=1
|
|
fi
|
|
if wait "$child_a" 2>/dev/null; then wait_rc=0; else wait_rc=$?; fi
|
|
(( wait_rc != 127 )) || cleanup_rc=1
|
|
(( cleanup_rc == 0 )) || return 1
|
|
if [[ "$killed" -ne 0 ]]; then
|
|
printf 'task4 review4 stale group RED: child=%s supplied_group=%s unrelated=killed boundary_rc=%s\n' \
|
|
"$child_a" "$unrelated_pgid" "$boundary_rc" >&2
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
task4_review4_direct_fallback_boundaries() (
|
|
local mode child_pid child_identity='' child_group='' child_state='' attempt original_group_function
|
|
for mode in caller_group query_failure; do
|
|
( kill -STOP "$BASHPID"; printf 'unexpected child continuation\n' >&2 ) &
|
|
child_pid=$!
|
|
child_identity=''
|
|
for ((attempt=0; attempt<200; attempt++)); do
|
|
if _k3slr_process_record child_identity child_state "$child_pid" && [[ "$child_state" == T ]]; then break; fi
|
|
/usr/bin/sleep 0.01
|
|
done
|
|
[[ -n "$child_identity" && "$child_state" == T ]] || return 1
|
|
IFS='|' read -r _ _ _ child_group <<<"$child_identity"
|
|
if [[ "$mode" == query_failure ]]; then
|
|
original_group_function="$(declare -f _k3slr_process_group_for_pid)"
|
|
_k3slr_process_group_for_pid() { return 1; }
|
|
fi
|
|
_k3slr_terminate_and_reap "$child_pid" "$child_group" "$child_identity" || return 1
|
|
if [[ "$mode" == query_failure ]]; then eval "$original_group_function"; fi
|
|
if kill -0 "$child_pid" 2>/dev/null; then return 1; fi
|
|
done
|
|
)
|
|
|
|
task4_review4_term_ignoring_group_escalates() (
|
|
local supervisor_pid supervisor_pgid='' supervisor_identity='' descendant_pid='' state='' attempt
|
|
local descendant_file="${fixture_root}/task4-review4-term-ignoring-descendant"
|
|
: >"$descendant_file"
|
|
set -m
|
|
(
|
|
kill -STOP "$BASHPID"
|
|
trap '' TERM
|
|
( trap '' TERM; while :; do :; done ) &
|
|
printf '%s\n' "$!" >"$descendant_file"
|
|
wait
|
|
) &
|
|
supervisor_pid=$!
|
|
_k3slr_wait_for_stopped_group supervisor_pgid supervisor_identity "$supervisor_pid" || {
|
|
set +m
|
|
return 1
|
|
}
|
|
set +m
|
|
kill -CONT "$supervisor_pid" || return 1
|
|
for ((attempt=0; attempt<200; attempt++)); do
|
|
[[ -s "$descendant_file" ]] && break
|
|
/usr/bin/sleep 0.01
|
|
done
|
|
descendant_pid="$(<"$descendant_file")"
|
|
[[ "$descendant_pid" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
_k3slr_terminate_and_reap "$supervisor_pid" "$supervisor_pgid" "$supervisor_identity" || return 1
|
|
! kill -0 "$supervisor_pid" 2>/dev/null || return 1
|
|
! kill -0 "$descendant_pid" 2>/dev/null || return 1
|
|
! _k3slr_group_exists "$supervisor_pgid"
|
|
)
|
|
|
|
task4_review4_wait_clear_is_atomic() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}"
|
|
local rc role pid ppid pgid start_time sentinel_pid sentinel_identity='' current_identity=''
|
|
local seen_supervisor=0 seen_process_sub=0 seen_keepass=0 seen_od=0 seen_mawk=0
|
|
export TASK4_REVIEW2_REQUIRE_REAL_TTY=1 TASK4_REVIEW3_SIGNAL_CASE=1
|
|
export TASK4_REVIEW1_PRODUCER_CASE=success
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${fixture_root}/task4-review4-wait-clear.consumer"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${fixture_root}/task4-review4-wait-clear.producer"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${fixture_root}/task4-review4-wait-clear.commands"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${fixture_root}/task4-review4-wait-clear.db-stat"
|
|
export TASK4_REVIEW3_CALLER_PID_FILE="${fixture_root}/task4-review4-wait-clear.caller"
|
|
export TASK4_REVIEW3_CALLER_PGID_FILE="${fixture_root}/task4-review4-wait-clear.caller-pgid"
|
|
export TASK4_REVIEW4_IDENTITY_ROLE_LOG="${fixture_root}/task4-review4-wait-clear.roles"
|
|
export TASK4_REVIEW4_NEGATIVE_KILL_LOG="${fixture_root}/task4-review4-wait-clear.negative-kill"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
: >"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
: >"$TASK4_REVIEW4_NEGATIVE_KILL_LOG"
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
_k3slr_lifecycle_boundary() {
|
|
local phase="$1" boundary_pid="${2-}" boundary_identity=''
|
|
case "$phase" in
|
|
pid-published)
|
|
_k3slr_process_identity boundary_identity "$boundary_pid" || return 1
|
|
printf 'supervisor|%s\n' "$boundary_identity" >>"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
;;
|
|
validator-stage-published)
|
|
_k3slr_process_identity boundary_identity "$boundary_pid" || return 1
|
|
printf 'process_sub|%s\n' "$boundary_identity" >>"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
;;
|
|
wait-reaped-before-clear)
|
|
builtin kill -TERM "$(<"$TASK4_REVIEW3_CALLER_PID_FILE")"
|
|
;;
|
|
esac
|
|
}
|
|
kill() {
|
|
local argument
|
|
for argument in "$@"; do
|
|
if [[ "$argument" =~ ^-[1-9][0-9]*$ ]]; then printf '%s\n' "$argument" >>"$TASK4_REVIEW4_NEGATIVE_KILL_LOG"; fi
|
|
done
|
|
builtin kill "$@"
|
|
}
|
|
/usr/bin/setsid /usr/bin/sleep 30 &
|
|
sentinel_pid=$!
|
|
/usr/bin/sleep 0.02
|
|
_k3slr_process_identity sentinel_identity "$sentinel_pid" || return 1
|
|
set +e
|
|
_k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx
|
|
rc=$?
|
|
set -e
|
|
[[ "$rc" -eq 143 && ! -s "$TASK4_REVIEW1_CONSUMER_LOG" &&
|
|
! -s "$TASK4_REVIEW4_NEGATIVE_KILL_LOG" ]] || return 1
|
|
while IFS='|' read -r role pid start_time ppid pgid; do
|
|
[[ "$pid" =~ ^[1-9][0-9]*$ && "$ppid" =~ ^[0-9]+$ && "$pgid" =~ ^[1-9][0-9]*$ &&
|
|
"$start_time" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
case "$role" in
|
|
supervisor) seen_supervisor=1 ;;
|
|
process_sub) seen_process_sub=1 ;;
|
|
keepass) seen_keepass=1 ;;
|
|
od) seen_od=1 ;;
|
|
mawk) seen_mawk=1 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
! kill -0 "$pid" 2>/dev/null || return 1
|
|
done <"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
(( seen_supervisor && seen_process_sub && seen_keepass && seen_od && seen_mawk )) || return 1
|
|
_k3slr_process_identity current_identity "$sentinel_pid" || return 1
|
|
[[ "$current_identity" == "$sentinel_identity" ]] || return 1
|
|
builtin kill -TERM "$sentinel_pid"
|
|
wait "$sentinel_pid" 2>/dev/null || true
|
|
)
|
|
|
|
task4_review4_signal_phase_matrix() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}"
|
|
local signal_name expected_rc injection_phase phase rc prefix supervisor_pid='' supervisor_pgid=''
|
|
local supervisor_identity='' sentinel_pid sentinel_identity='' current_identity='' role pid ppid pgid
|
|
local sentinel_start_time='' sentinel_parent='' sentinel_pgid='' fixture_cleanup_rc=0
|
|
local -a phases=(coproc-launch-before pid-published stop-query-complete cont-before cont-after \
|
|
wait-reaped-before-clear pre-consumer)
|
|
export TASK4_REVIEW2_REQUIRE_REAL_TTY=1 TASK4_REVIEW3_SIGNAL_CASE=1
|
|
/usr/bin/setsid /usr/bin/sleep 60 &
|
|
sentinel_pid=$!
|
|
/usr/bin/sleep 0.02
|
|
_k3slr_process_identity sentinel_identity "$sentinel_pid" || return 1
|
|
IFS='|' read -r sentinel_pid sentinel_start_time sentinel_parent sentinel_pgid <<<"$sentinel_identity"
|
|
_task4_review4_phase_sentinel_cleanup() {
|
|
if _k3slr_process_identity current_identity "$sentinel_pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$sentinel_identity" ]]; then
|
|
_k3slr_signal_pid TERM "$sentinel_pid" "$sentinel_identity" "$sentinel_parent" \
|
|
"$sentinel_pgid" 2>/dev/null || return 1
|
|
if _k3slr_wait_child "$sentinel_pid"; then :; else [[ "$?" -ne 127 ]] || return 1; fi
|
|
fi
|
|
}
|
|
_task4_review4_phase_exit_cleanup() {
|
|
local original_status="$1" cleanup_status=0
|
|
trap - EXIT
|
|
if [[ -n "$prefix" ]]; then
|
|
_task4_review5_phase_fixture_cleanup "$prefix" || cleanup_status=1
|
|
fi
|
|
_task4_review4_phase_sentinel_cleanup || cleanup_status=1
|
|
(( cleanup_status == 0 )) || exit 1
|
|
exit "$original_status"
|
|
}
|
|
trap '_task4_review4_phase_exit_cleanup "$?"' EXIT
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
_k3slr_lifecycle_boundary() {
|
|
local boundary_phase="$1" boundary_pid="${2-}" recorded_pgid=''
|
|
if [[ "$boundary_phase" == pid-published ]]; then
|
|
printf '%s\n' "$boundary_pid" >"${prefix}.supervisor-pid"
|
|
_k3slr_process_identity supervisor_identity "$boundary_pid" || return 1
|
|
printf '%s\n' "$supervisor_identity" >"${prefix}.supervisor-identity"
|
|
recorded_pgid="$(/usr/bin/ps -o pgid= -p "$boundary_pid")" || return 1
|
|
recorded_pgid="${recorded_pgid//[[:space:]]/}"
|
|
printf '%s\n' "$recorded_pgid" >"${prefix}.supervisor-pgid"
|
|
fi
|
|
if [[ "$boundary_phase" == "$injection_phase" ]]; then
|
|
printf 'injected\n' >"${prefix}.injected"
|
|
builtin kill -"$signal_name" "$(<"$TASK4_REVIEW3_CALLER_PID_FILE")"
|
|
fi
|
|
}
|
|
for signal_name in INT TERM; do
|
|
if [[ "$signal_name" == INT ]]; then expected_rc=130; else expected_rc=143; fi
|
|
for phase in "${phases[@]}"; do
|
|
injection_phase="$phase"
|
|
prefix="${fixture_root}/task4-review4-phase-${signal_name}-${phase}"
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${prefix}.consumer"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${prefix}.producer"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${prefix}.commands"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${prefix}.db-stat"
|
|
export TASK4_REVIEW3_CALLER_PID_FILE="${prefix}.caller-pid"
|
|
export TASK4_REVIEW3_CALLER_PGID_FILE="${prefix}.caller-pgid"
|
|
export TASK4_REVIEW3_ROLE_LOG="${prefix}.roles"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
: >"$TASK4_REVIEW3_ROLE_LOG"
|
|
/usr/bin/unlink "${prefix}.injected" "${prefix}.supervisor-pid" \
|
|
"${prefix}.supervisor-pgid" "${prefix}.supervisor-identity" 2>/dev/null || true
|
|
if [[ "$phase" == wait-reaped-before-clear || "$phase" == pre-consumer ]]; then
|
|
TASK4_REVIEW1_PRODUCER_CASE=success
|
|
else
|
|
TASK4_REVIEW1_PRODUCER_CASE=blocking_signal
|
|
fi
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
set +e
|
|
_k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -ne "$expected_rc" || ! -s "${prefix}.injected" ||
|
|
-s "$TASK4_REVIEW1_CONSUMER_LOG" ]]; then
|
|
printf 'task4 review4 phase diagnostic: signal=%s phase=%s rc=%s expected=%s injected=%s consumer=%s\n' \
|
|
"$signal_name" "$phase" "$rc" "$expected_rc" "$([[ -s "${prefix}.injected" ]] && printf yes || printf no)" \
|
|
"$([[ -s "$TASK4_REVIEW1_CONSUMER_LOG" ]] && printf yes || printf no)" >&2
|
|
return 1
|
|
fi
|
|
if [[ "$phase" == coproc-launch-before || "$phase" == pid-published ||
|
|
"$phase" == stop-query-complete || "$phase" == cont-before ]] &&
|
|
[[ -s "$TASK4_REVIEW3_ROLE_LOG" ]]; then
|
|
printf 'task4 review5 pre-CONT worker dispatch: signal=%s phase=%s\n' \
|
|
"$signal_name" "$phase" >&2
|
|
return 1
|
|
fi
|
|
if [[ -s "${prefix}.supervisor-pid" ]]; then
|
|
supervisor_pid="$(<"${prefix}.supervisor-pid")"
|
|
supervisor_pgid="$(<"${prefix}.supervisor-pgid")"
|
|
if kill -0 "$supervisor_pid" 2>/dev/null || _k3slr_group_exists "$supervisor_pgid"; then
|
|
printf 'task4 review4 phase leak: signal=%s phase=%s supervisor=%s pgid=%s\n' \
|
|
"$signal_name" "$phase" "$supervisor_pid" "$supervisor_pgid" >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
while read -r role pid ppid pgid start_time; do
|
|
[[ "$pid" =~ ^[1-9][0-9]*$ ]] || continue
|
|
! kill -0 "$pid" 2>/dev/null || return 1
|
|
done <"$TASK4_REVIEW3_ROLE_LOG"
|
|
_k3slr_process_identity current_identity "$sentinel_pid" || return 1
|
|
[[ "$current_identity" == "$sentinel_identity" ]] || return 1
|
|
done
|
|
done
|
|
return 0
|
|
)
|
|
|
|
task4_review5_exact_proc_record_contract() (
|
|
local raw_file="${fixture_root}/task4-review5-proc-stat" raw_record='' identity='' state='' live_identity=''
|
|
local valid='123 (comm with ) and ) delimiter) T 0 456 456 0 -1 4194560 1 2 3 4 5 6 7 8 9 10 11 12 18446744073709551615'
|
|
printf '%s\n' "$valid" >"$raw_file"
|
|
_k3slr_read_exact_record raw_record "$raw_file" || return 1
|
|
[[ "$raw_record" == "$valid" ]] || return 1
|
|
_k3slr_parse_proc_stat_record identity state 123 "$raw_record" || return 1
|
|
[[ "$identity" == '123|18446744073709551615|0|456' && "$state" == T ]] || return 1
|
|
_k3slr_parse_proc_stat_record identity state 123 \
|
|
'123 (lower stopped) t 7 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 999999999999999999999999' || return 1
|
|
[[ "$identity" == '123|999999999999999999999999|7|456' && "$state" == t ]] || return 1
|
|
_k3slr_parse_proc_stat_record identity state 123 \
|
|
'123 (idle) I 7 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' || return 1
|
|
_k3slr_parse_proc_stat_record identity state 123 \
|
|
'123 (paging) W 7 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' || return 1
|
|
_k3slr_parse_proc_stat_record identity state 123 \
|
|
'123 (consecutive ))) close) R 8 457 457 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 14' || return 1
|
|
[[ "$identity" == '123|14|8|457' && "$state" == R ]] || return 1
|
|
_k3slr_parse_proc_stat_record identity state 123 \
|
|
'123 (decoy ) R 91 777 777 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 15 still comm) S 7 458 458 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 16' || return 1
|
|
[[ "$identity" == '123|16|7|458' && "$state" == S ]] || return 1
|
|
_k3slr_parse_proc_stat_record identity state 123 \
|
|
'123 (comm ending in )) T 6 459 459 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 17' || return 1
|
|
[[ "$identity" == '123|17|6|459' && "$state" == T ]] || return 1
|
|
_k3slr_process_identity live_identity "$BASHPID" || return 1
|
|
[[ "$live_identity" == "${BASHPID}|"* ]] || return 1
|
|
|
|
local raw_line=sentinel resolved_record=sentinel queried_identity=sentinel
|
|
local raw_proc_record=sentinel parsed_state=sentinel collision_identity=sentinel
|
|
printf '%s\n' "$valid" >"$raw_file"
|
|
_k3slr_read_exact_record raw_line "$raw_file" || return 1
|
|
[[ "$raw_line" == "$valid" ]] || return 1
|
|
_k3slr_proc_stat_record resolved_record "$BASHPID" || return 1
|
|
[[ "$resolved_record" == "${BASHPID} "* ]] || return 1
|
|
_k3slr_process_identity queried_identity "$BASHPID" || return 1
|
|
[[ "$queried_identity" == "${BASHPID}|"* ]] || return 1
|
|
_k3slr_process_record raw_proc_record parsed_state "$BASHPID" || return 1
|
|
[[ "$raw_proc_record" == "${BASHPID}|"* && "$parsed_state" != sentinel ]] || return 1
|
|
local identity_destination=sentinel process_state=sentinel collision_state=sentinel collision_identity=sentinel
|
|
_k3slr_parse_proc_stat_record identity_destination collision_state 123 "$valid" || return 1
|
|
[[ "$identity_destination" == '123|18446744073709551615|0|456' && "$collision_state" == T ]] || return 1
|
|
_k3slr_parse_proc_stat_record collision_identity process_state 123 "$valid" || return 1
|
|
[[ "$collision_identity" == '123|18446744073709551615|0|456' && "$process_state" == T ]] || return 1
|
|
local destination_name=sentinel resolved_group=sentinel child_pid=sentinel
|
|
local identity_destination=sentinel state_destination=sentinel group_destination=sentinel
|
|
_k3slr_capture_one_line destination_name /usr/bin/bash -c 'printf "capture-matrix\n"' || return 1
|
|
[[ "$destination_name" == capture-matrix ]] || return 1
|
|
_k3slr_process_group_for_pid resolved_group "$BASHPID" || return 1
|
|
[[ "$resolved_group" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
_k3slr_proc_stat_record child_pid "$BASHPID" || return 1
|
|
[[ "$child_pid" == "${BASHPID} "* ]] || return 1
|
|
_k3slr_process_identity destination_name "$BASHPID" || return 1
|
|
[[ "$destination_name" == "${BASHPID}|"* ]] || return 1
|
|
_k3slr_process_record identity_destination state_destination "$BASHPID" || return 1
|
|
[[ "$identity_destination" == "${BASHPID}|"* && "$state_destination" != sentinel ]] || return 1
|
|
! _k3slr_parse_proc_stat_record identity identity 123 "$valid" || return 1
|
|
! _k3slr_process_record identity identity "$BASHPID" || return 1
|
|
! _k3slr_wait_for_stopped_group identity identity "$BASHPID" || return 1
|
|
! _k3slr_parse_proc_stat_record _k3slr_parse_state state 123 "$valid" || return 1
|
|
|
|
printf '%s' "$valid" >"$raw_file"
|
|
! _k3slr_read_exact_record raw_record "$raw_file" || return 1
|
|
printf '%s\n\n' "$valid" >"$raw_file"
|
|
! _k3slr_read_exact_record raw_record "$raw_file" || return 1
|
|
printf '%s\r\n' "$valid" >"$raw_file"
|
|
! _k3slr_read_exact_record raw_record "$raw_file" || return 1
|
|
|
|
local malformed
|
|
for malformed in \
|
|
$'123 (cr\rcomm) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
$'123 (embedded\nrecord) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
$'123\t(tab prefix) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
$'123 (tab delimiter)\tT 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123 (double-space prefix) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123(no-space prefix) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123 (double-space delimiter) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123 (no-space delimiter)T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123 (decoy ) R 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13 real end) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 14' \
|
|
'123 (tail close) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 ) 13' \
|
|
'124 (wrong pid) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'0123 (leading pid) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123 (leading ppid) T 00 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123 (leading pgid) T 0 0456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123 (leading start) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 00' \
|
|
'123 (zero start) T 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 0' \
|
|
'123 (bad state) ? 0 456 456 0 -1 1 1 2 3 4 5 6 7 8 9 10 11 12 13' \
|
|
'123 (short) T 0 456'; do
|
|
identity=identity-sentinel
|
|
state=state-sentinel
|
|
! _k3slr_parse_proc_stat_record identity state 123 "$malformed" || return 1
|
|
[[ "$identity" == identity-sentinel && "$state" == state-sentinel ]] || return 1
|
|
done
|
|
)
|
|
|
|
task4_review5_cleanup_exact_recorded_roles() {
|
|
local log_file="$1" role pid start_time ppid pgid expected_identity current_identity caller_group
|
|
_k3slr_process_group_for_pid caller_group "$BASHPID" || return 1
|
|
while IFS='|' read -r role pid start_time ppid pgid; do
|
|
[[ "$pid" =~ ^[1-9][0-9]*$ && "$pgid" =~ ^[1-9][0-9]*$ ]] || continue
|
|
expected_identity="${pid}|${start_time}|${ppid}|${pgid}"
|
|
if _k3slr_process_identity current_identity "$pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$expected_identity" && "$pgid" != "$caller_group" ]]; then
|
|
builtin kill -KILL "$pid" 2>/dev/null || return 1
|
|
fi
|
|
done <"$log_file"
|
|
}
|
|
|
|
_task4_review5_phase_fixture_cleanup() {
|
|
local prefix="$1" supervisor_pid='' supervisor_pgid='' supervisor_identity=''
|
|
local current_identity='' observed_group='' caller_group='' identity_pid='' start_time=''
|
|
local parent_pid='' identity_group='' role pid ppid pgid expected_identity signal_name wait_rc
|
|
local cleanup_rc=0
|
|
if [[ -s "${prefix}.supervisor-pid" && -s "${prefix}.supervisor-pgid" &&
|
|
-s "${prefix}.supervisor-identity" ]]; then
|
|
supervisor_pid="$(<"${prefix}.supervisor-pid")"
|
|
supervisor_pgid="$(<"${prefix}.supervisor-pgid")"
|
|
supervisor_identity="$(<"${prefix}.supervisor-identity")"
|
|
IFS='|' read -r identity_pid start_time parent_pid identity_group <<<"$supervisor_identity"
|
|
if _k3slr_process_identity current_identity "$supervisor_pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$supervisor_identity" && "$identity_pid" == "$supervisor_pid" &&
|
|
"$identity_group" == "$supervisor_pgid" ]]; then
|
|
_k3slr_process_group_for_pid observed_group "$supervisor_pid" 2>/dev/null || observed_group=''
|
|
_k3slr_process_group_for_pid caller_group "$BASHPID" 2>/dev/null || caller_group=''
|
|
if [[ "$observed_group" == "$supervisor_pgid" && "$observed_group" != "$caller_group" ]]; then
|
|
_k3slr_terminate_and_reap "$supervisor_pid" "$supervisor_pgid" \
|
|
"$supervisor_identity" 2>/dev/null || :
|
|
fi
|
|
fi
|
|
if _k3slr_process_identity current_identity "$supervisor_pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$supervisor_identity" ]]; then
|
|
_k3slr_process_group_for_pid observed_group "$supervisor_pid" 2>/dev/null || observed_group=''
|
|
_k3slr_process_group_for_pid caller_group "$BASHPID" 2>/dev/null || caller_group=''
|
|
if [[ "$observed_group" == "$supervisor_pgid" && "$observed_group" != "$caller_group" ]]; then
|
|
_k3slr_signal_pid KILL "$supervisor_pid" "$supervisor_identity" "$parent_pid" \
|
|
"$supervisor_pgid" 2>/dev/null || :
|
|
if _k3slr_wait_child "$supervisor_pid"; then wait_rc=0; else wait_rc=$?; fi
|
|
(( wait_rc != 127 )) || cleanup_rc=1
|
|
fi
|
|
fi
|
|
fi
|
|
if [[ -f "${prefix}.roles" ]]; then
|
|
for signal_name in TERM KILL; do
|
|
while read -r role pid ppid pgid start_time; do
|
|
[[ "$pid" =~ ^[1-9][0-9]*$ && "$ppid" =~ ^[0-9]+$ &&
|
|
"$pgid" =~ ^[1-9][0-9]*$ && "$start_time" =~ ^[1-9][0-9]*$ ]] || continue
|
|
expected_identity="${pid}|${start_time}|${ppid}|${pgid}"
|
|
if _k3slr_process_identity current_identity "$pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$expected_identity" ]]; then
|
|
_k3slr_process_group_for_pid observed_group "$pid" 2>/dev/null || observed_group=''
|
|
_k3slr_process_group_for_pid caller_group "$BASHPID" 2>/dev/null || caller_group=''
|
|
if [[ "$observed_group" == "$pgid" && "$observed_group" != "$caller_group" ]]; then
|
|
_k3slr_signal_pid "$signal_name" "$pid" "$expected_identity" "$ppid" "$pgid" \
|
|
2>/dev/null || :
|
|
fi
|
|
fi
|
|
done <"${prefix}.roles"
|
|
[[ "$signal_name" == TERM ]] && /usr/bin/sleep 0.1
|
|
done
|
|
while read -r role pid ppid pgid start_time; do
|
|
[[ "$pid" =~ ^[1-9][0-9]*$ ]] || continue
|
|
expected_identity="${pid}|${start_time}|${ppid}|${pgid}"
|
|
if _k3slr_process_identity current_identity "$pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$expected_identity" ]]; then
|
|
cleanup_rc=1
|
|
fi
|
|
done <"${prefix}.roles"
|
|
fi
|
|
if [[ -n "$supervisor_pid" ]] &&
|
|
_k3slr_process_identity current_identity "$supervisor_pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$supervisor_identity" ]]; then
|
|
cleanup_rc=1
|
|
fi
|
|
return "$cleanup_rc"
|
|
}
|
|
|
|
task4_review5_phase_fixture_failure_cleanup_contract() (
|
|
local prefix="${fixture_root}/task4-review5-phase-fixture-cleanup"
|
|
local supervisor_pid supervisor_identity='' supervisor_pgid='' current_identity=''
|
|
/usr/bin/setsid /usr/bin/bash -c 'trap "" TERM; while :; do /usr/bin/sleep 1; done' &
|
|
supervisor_pid=$!
|
|
/usr/bin/sleep 0.02
|
|
_k3slr_process_identity supervisor_identity "$supervisor_pid" || return 1
|
|
_k3slr_process_group_for_pid supervisor_pgid "$supervisor_pid" || return 1
|
|
printf '%s\n' "$supervisor_pid" >"${prefix}.supervisor-pid"
|
|
printf '%s\n' "$supervisor_pgid" >"${prefix}.supervisor-pgid"
|
|
printf '%s\n' "$supervisor_identity" >"${prefix}.supervisor-identity"
|
|
: >"${prefix}.roles"
|
|
_task4_review5_phase_fixture_cleanup "$prefix" || {
|
|
if _k3slr_process_identity current_identity "$supervisor_pid" 2>/dev/null &&
|
|
[[ "$current_identity" == "$supervisor_identity" ]]; then
|
|
_k3slr_signal_pid KILL "$supervisor_pid" "$supervisor_identity" "$BASHPID" \
|
|
"$supervisor_pgid" 2>/dev/null || return 1
|
|
_k3slr_wait_child "$supervisor_pid" || :
|
|
fi
|
|
return 1
|
|
}
|
|
! kill -0 "$supervisor_pid" 2>/dev/null && ! _k3slr_group_exists "$supervisor_pgid"
|
|
)
|
|
|
|
task4_review5_default_supervisor_anchor() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}"
|
|
local prefix="${fixture_root}/task4-review5-anchor" killer_pid rc role pid ppid pgid start_time
|
|
local leaked=0 cleanup_rc=0 supervisor_pgid=''
|
|
export TASK4_REVIEW2_REQUIRE_REAL_TTY=1 TASK4_REVIEW3_SIGNAL_CASE=1
|
|
export TASK4_REVIEW1_PRODUCER_CASE=term_ignoring_descendant
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${prefix}.consumer"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${prefix}.producer"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${prefix}.commands"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${prefix}.db-stat"
|
|
export TASK4_REVIEW3_CALLER_PID_FILE="${prefix}.caller-pid"
|
|
export TASK4_REVIEW3_CALLER_PGID_FILE="${prefix}.caller-pgid"
|
|
export TASK4_REVIEW4_IDENTITY_ROLE_LOG="${prefix}.roles"
|
|
export TASK4_REVIEW5_TERM_IGNORE_READY="${prefix}.ready"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
: >"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
_k3slr_lifecycle_boundary() {
|
|
local phase="$1" boundary_pid="${2-}" identity=''
|
|
if [[ "$phase" == pid-published ]]; then
|
|
_k3slr_process_identity identity "$boundary_pid" || return 1
|
|
printf 'supervisor|%s\n' "$identity" >>"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
supervisor_pgid="$(/usr/bin/ps -o pgid= -p "$boundary_pid")" || return 1
|
|
supervisor_pgid="${supervisor_pgid//[[:space:]]/}"
|
|
printf '%s\n' "$supervisor_pgid" >"${prefix}.pgid"
|
|
fi
|
|
}
|
|
(
|
|
for ((probe=0; probe<300; probe++)); do
|
|
if [[ -s "$TASK4_REVIEW3_CALLER_PID_FILE" && -s "$TASK4_REVIEW5_TERM_IGNORE_READY" ]]; then break; fi
|
|
/usr/bin/sleep 0.01
|
|
done
|
|
[[ -s "$TASK4_REVIEW3_CALLER_PID_FILE" && -s "$TASK4_REVIEW5_TERM_IGNORE_READY" ]] || exit 1
|
|
builtin kill -TERM "$(<"$TASK4_REVIEW3_CALLER_PID_FILE")"
|
|
) &
|
|
killer_pid=$!
|
|
set +e
|
|
_k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx
|
|
rc=$?
|
|
wait "$killer_pid"
|
|
set -e
|
|
supervisor_pgid="$(<"${prefix}.pgid")"
|
|
while IFS='|' read -r role pid start_time ppid pgid; do
|
|
if kill -0 "$pid" 2>/dev/null; then leaked=$((leaked + 1)); fi
|
|
done <"$TASK4_REVIEW4_IDENTITY_ROLE_LOG"
|
|
if (( leaked > 0 )); then
|
|
task4_review5_cleanup_exact_recorded_roles "$TASK4_REVIEW4_IDENTITY_ROLE_LOG" || cleanup_rc=$?
|
|
fi
|
|
[[ "$cleanup_rc" -eq 0 ]] || return 1
|
|
if [[ "$rc" -ne 143 || "$leaked" -ne 0 || -s "$TASK4_REVIEW1_CONSUMER_LOG" ]] ||
|
|
_k3slr_group_exists "$supervisor_pgid"; then
|
|
printf 'task4 review5 anchor RED: rc=%s leaked=%s pgid_live=%s\n' \
|
|
"$rc" "$leaked" "$(_k3slr_group_exists "$supervisor_pgid" && printf yes || printf no)" >&2
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
task4_review5_feasibility_default_supervisor_anchor() (
|
|
local prefix="${fixture_root}/task4-review5-feas-anchor" requested_dir
|
|
local killer_pid rc supervisor_pgid='' role pid start_time ppid pgid leaked=0 cleanup_rc=0
|
|
source "$FEASIBILITY_PATH"
|
|
requested_dir="/tmp/k3slr-feasibility.anchor${BASHPID}${RANDOM}"
|
|
export TASK4_FEASIBILITY_CASE=anchor_TERM
|
|
export TASK4_FEASIBILITY_REQUESTED_DIR="$requested_dir"
|
|
export TASK4_FEASIBILITY_COMMAND_LOG="${prefix}.commands"
|
|
export TASK4_FEASIBILITY_MAIN_PID_FILE="${prefix}.main"
|
|
export TASK4_FEASIBILITY_CHILD_PID_FILE="${prefix}.child"
|
|
export TASK4_FEASIBILITY_ROLE_LOG="${prefix}.roles"
|
|
export TASK4_FEASIBILITY_TERM_IGNORE_READY="${prefix}.ready"
|
|
: >"$TASK4_FEASIBILITY_COMMAND_LOG"
|
|
: >"$TASK4_FEASIBILITY_ROLE_LOG"
|
|
_k3slr_command() { task4_feasibility_command_fake "$@"; }
|
|
_k3slr_lifecycle_boundary() {
|
|
local phase="$1" tracked_pid="${2-}" tracked_identity=''
|
|
if [[ "$phase" == feasibility-pid-published ]]; then
|
|
_k3slr_process_identity tracked_identity "$tracked_pid" || return 1
|
|
printf 'supervisor|%s\n' "$tracked_identity" >>"$TASK4_FEASIBILITY_ROLE_LOG"
|
|
supervisor_pgid="$(/usr/bin/ps -o pgid= -p "$tracked_pid")" || return 1
|
|
supervisor_pgid="${supervisor_pgid//[[:space:]]/}"
|
|
printf '%s\n' "$supervisor_pgid" >"${prefix}.pgid"
|
|
fi
|
|
}
|
|
(
|
|
local probe
|
|
for ((probe=0; probe<300; probe++)); do
|
|
if [[ -s "$TASK4_FEASIBILITY_MAIN_PID_FILE" &&
|
|
-s "$TASK4_FEASIBILITY_TERM_IGNORE_READY" ]]; then break; fi
|
|
/usr/bin/sleep 0.01
|
|
done
|
|
[[ -s "$TASK4_FEASIBILITY_MAIN_PID_FILE" &&
|
|
-s "$TASK4_FEASIBILITY_TERM_IGNORE_READY" ]] || exit 1
|
|
builtin kill -TERM "$(<"$TASK4_FEASIBILITY_MAIN_PID_FILE")"
|
|
) &
|
|
killer_pid=$!
|
|
set +e
|
|
k3slr_local_recovery_feasibility_main --execute </dev/ptmx >/dev/null 2>"${prefix}.stderr"
|
|
rc=$?
|
|
wait "$killer_pid"
|
|
set -e
|
|
supervisor_pgid="$(<"${prefix}.pgid")"
|
|
while IFS='|' read -r role pid start_time ppid pgid; do
|
|
if kill -0 "$pid" 2>/dev/null; then leaked=$((leaked + 1)); fi
|
|
done <"$TASK4_FEASIBILITY_ROLE_LOG"
|
|
if (( leaked > 0 )); then
|
|
task4_review5_cleanup_exact_recorded_roles "$TASK4_FEASIBILITY_ROLE_LOG" || cleanup_rc=$?
|
|
fi
|
|
[[ "$cleanup_rc" -eq 0 ]] || return 1
|
|
if [[ "$rc" -ne 143 || "$leaked" -ne 0 || -e "$requested_dir" ]] ||
|
|
_k3slr_group_exists "$supervisor_pgid"; then
|
|
printf 'task4 review5 feasibility anchor RED: rc=%s leaked=%s fixture=%s pgid_live=%s\n' \
|
|
"$rc" "$leaked" "$([[ -e "$requested_dir" ]] && printf yes || printf no)" \
|
|
"$(_k3slr_group_exists "$supervisor_pgid" && printf yes || printf no)" >&2
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
task4_review5_published_unpinned_cleanup() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}"
|
|
local mode prefix rc supervisor_pid supervisor_pgid recorded_identity current_identity caller_pgid
|
|
local definition leaked cleanup_rc raw='' state=''
|
|
definition="$(declare -f _k3slr_proc_stat_record)"
|
|
eval "${definition/_k3slr_proc_stat_record/_task4_review5_original_proc_stat_record}"
|
|
export TASK4_REVIEW2_REQUIRE_REAL_TTY=1 TASK4_REVIEW3_SIGNAL_CASE=1
|
|
export TASK4_REVIEW1_PRODUCER_CASE=blocking_signal
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
for mode in missing short malformed persistent; do
|
|
cleanup_rc=0
|
|
prefix="${fixture_root}/task4-review5-unpinned-${mode}"
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${prefix}.consumer"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${prefix}.producer"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${prefix}.commands"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${prefix}.db-stat"
|
|
export TASK4_REVIEW3_CALLER_PID_FILE="${prefix}.caller-pid"
|
|
export TASK4_REVIEW3_CALLER_PGID_FILE="${prefix}.caller-pgid"
|
|
export TASK4_REVIEW3_ROLE_LOG="${prefix}.roles"
|
|
export TASK4_REVIEW5_UNPINNED_MODE="$mode"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
: >"$TASK4_REVIEW3_ROLE_LOG"
|
|
: >"${prefix}.pid-signals"
|
|
: >"${prefix}.group-signals"
|
|
: >"${prefix}.waits"
|
|
TASK4_REVIEW5_UNPINNED_PID=''
|
|
_k3slr_lifecycle_boundary() {
|
|
local phase="$1" boundary_pid="${2-}" raw='' pinned='' state=''
|
|
if [[ "$phase" == pid-published ]]; then
|
|
_task4_review5_original_proc_stat_record raw "$boundary_pid" || return 1
|
|
_k3slr_parse_proc_stat_record pinned state "$boundary_pid" "$raw" || return 1
|
|
printf '%s\n' "$boundary_pid" >"${prefix}.pid"
|
|
printf '%s\n' "$pinned" >"${prefix}.identity"
|
|
/usr/bin/ps -o pgid= -p "$boundary_pid" | /usr/bin/tr -d '[:space:]' >"${prefix}.pgid"
|
|
TASK4_REVIEW5_UNPINNED_PID="$boundary_pid"
|
|
fi
|
|
}
|
|
_k3slr_proc_stat_record() {
|
|
local destination_name="$1" queried_pid="$2"
|
|
if [[ -n "$TASK4_REVIEW5_UNPINNED_PID" && "$queried_pid" == "$TASK4_REVIEW5_UNPINNED_PID" ]]; then
|
|
case "$TASK4_REVIEW5_UNPINNED_MODE" in
|
|
missing|persistent) return 1 ;;
|
|
short) printf -v "$destination_name" '%s' "${queried_pid} (short) T 1" ;;
|
|
malformed) printf -v "$destination_name" '%s' "${queried_pid} (bad) ? 01 02" ;;
|
|
esac
|
|
return 0
|
|
fi
|
|
_task4_review5_original_proc_stat_record "$destination_name" "$queried_pid"
|
|
}
|
|
_k3slr_signal_published_pid() {
|
|
printf '%s %s\n' "$1" "$2" >>"${prefix}.pid-signals"
|
|
builtin kill -KILL "$2"
|
|
}
|
|
_k3slr_signal_group() {
|
|
printf '%s %s\n' "$1" "$3" >>"${prefix}.group-signals"
|
|
return 99
|
|
}
|
|
_k3slr_wait_child() {
|
|
printf '%s\n' "$1" >>"${prefix}.waits"
|
|
wait "$1" >/dev/null 2>&1
|
|
}
|
|
set +e
|
|
_k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx
|
|
rc=$?
|
|
set -e
|
|
supervisor_pid="$(<"${prefix}.pid")"
|
|
supervisor_pgid="$(<"${prefix}.pgid")"
|
|
recorded_identity="$(<"${prefix}.identity")"
|
|
leaked=0
|
|
if kill -0 "$supervisor_pid" 2>/dev/null || _k3slr_group_exists "$supervisor_pgid"; then leaked=1; fi
|
|
if (( leaked )); then
|
|
caller_pgid="$(/usr/bin/ps -o pgid= -p "$BASHPID")"
|
|
caller_pgid="${caller_pgid//[[:space:]]/}"
|
|
if _task4_review5_original_proc_stat_record raw "$supervisor_pid" 2>/dev/null &&
|
|
_k3slr_parse_proc_stat_record current_identity state "$supervisor_pid" "$raw" &&
|
|
[[ "$current_identity" == "$recorded_identity" && "$supervisor_pgid" != "$caller_pgid" ]]; then
|
|
builtin kill -KILL "$supervisor_pid" 2>/dev/null || cleanup_rc=$?
|
|
else
|
|
cleanup_rc=1
|
|
fi
|
|
fi
|
|
[[ "${cleanup_rc:-0}" -eq 0 ]] || return 1
|
|
if [[ "$rc" -ne 1 || "$leaked" -ne 0 || -s "$TASK4_REVIEW1_CONSUMER_LOG" ||
|
|
-s "$TASK4_REVIEW3_ROLE_LOG" || -s "${prefix}.group-signals" ||
|
|
"$(/usr/bin/wc -l <"${prefix}.pid-signals")" -ne 1 ||
|
|
"$(/usr/bin/wc -l <"${prefix}.waits")" -ne 1 ]]; then
|
|
printf 'task4 review5 unpinned diagnostic: mode=%s rc=%s leaked=%s pid_signals=%s group_signals=%s waits=%s\n' \
|
|
"$mode" "$rc" "$leaked" "$(/usr/bin/wc -l <"${prefix}.pid-signals")" \
|
|
"$(/usr/bin/wc -l <"${prefix}.group-signals")" "$(/usr/bin/wc -l <"${prefix}.waits")" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
)
|
|
|
|
task4_review5_pending_priority() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}"
|
|
local prefix="${fixture_root}/task4-review5-priority-password" rc requested_dir
|
|
export TASK4_REVIEW2_REQUIRE_REAL_TTY=1 TASK4_REVIEW3_SIGNAL_CASE=1
|
|
export TASK4_REVIEW1_PRODUCER_CASE=blocking_signal
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${prefix}.consumer"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${prefix}.producer"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${prefix}.commands"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${prefix}.db-stat"
|
|
export TASK4_REVIEW3_CALLER_PID_FILE="${prefix}.caller-pid"
|
|
export TASK4_REVIEW3_CALLER_PGID_FILE="${prefix}.caller-pgid"
|
|
export TASK4_REVIEW3_ROLE_LOG="${prefix}.roles"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
: >"$TASK4_REVIEW3_ROLE_LOG"
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
_k3slr_lifecycle_boundary() {
|
|
if [[ "$1" == coproc-launch-before ]]; then
|
|
builtin kill -TERM "$(<"$TASK4_REVIEW3_CALLER_PID_FILE")"
|
|
builtin kill -INT "$(<"$TASK4_REVIEW3_CALLER_PID_FILE")"
|
|
fi
|
|
}
|
|
set +e
|
|
_k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -ne 143 || -s "$TASK4_REVIEW1_CONSUMER_LOG" || -s "$TASK4_REVIEW3_ROLE_LOG" ]]; then
|
|
printf 'task4 review5 password priority diagnostic: rc=%s\n' "$rc" >&2
|
|
return 1
|
|
fi
|
|
|
|
source "$FEASIBILITY_PATH"
|
|
requested_dir="/tmp/k3slr-feasibility.priority${BASHPID}${RANDOM}"
|
|
export TASK4_FEASIBILITY_CASE=signal_TERM
|
|
export TASK4_FEASIBILITY_REQUESTED_DIR="$requested_dir"
|
|
export TASK4_FEASIBILITY_COMMAND_LOG="${prefix}.feasibility-commands"
|
|
export TASK4_FEASIBILITY_MAIN_PID_FILE="${prefix}.feasibility-main"
|
|
: >"$TASK4_FEASIBILITY_COMMAND_LOG"
|
|
_k3slr_command() { task4_feasibility_command_fake "$@"; }
|
|
_k3slr_lifecycle_boundary() {
|
|
if [[ "$1" == feasibility-launch-before ]]; then
|
|
builtin kill -TERM "$(<"$TASK4_FEASIBILITY_MAIN_PID_FILE")"
|
|
builtin kill -INT "$(<"$TASK4_FEASIBILITY_MAIN_PID_FILE")"
|
|
fi
|
|
}
|
|
set +e
|
|
k3slr_local_recovery_feasibility_main --execute </dev/ptmx >/dev/null 2>"${prefix}.feasibility-stderr"
|
|
rc=$?
|
|
set -e
|
|
if [[ "$rc" -ne 143 || -e "$requested_dir" ]]; then
|
|
printf 'task4 review5 feasibility priority RED: rc=%s fixture=%s\n' "$rc" \
|
|
"$([[ -e "$requested_dir" ]] && printf yes || printf no)" >&2
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
task4_review5_feasibility_cleanup_failure_is_not_signal_success() (
|
|
local prefix="${fixture_root}/task4-review5-feas-cleanup-failure" requested_dir
|
|
local rc child_pid definition raw='' fail_pid=''
|
|
source "$FEASIBILITY_PATH"
|
|
requested_dir="/tmp/k3slr-feasibility.pendingfail${BASHPID}${RANDOM}"
|
|
export TASK4_FEASIBILITY_CASE=signal_TERM
|
|
export TASK4_FEASIBILITY_REQUESTED_DIR="$requested_dir"
|
|
export TASK4_FEASIBILITY_COMMAND_LOG="${prefix}.commands"
|
|
export TASK4_FEASIBILITY_MAIN_PID_FILE="${prefix}.main"
|
|
: >"$TASK4_FEASIBILITY_COMMAND_LOG"
|
|
: >"${prefix}.signal-count"
|
|
definition="$(declare -f _k3slr_proc_stat_record)"
|
|
eval "${definition/_k3slr_proc_stat_record/_task4_review5_feas_original_proc_stat_record}"
|
|
_k3slr_command() { task4_feasibility_command_fake "$@"; }
|
|
_k3slr_lifecycle_boundary() {
|
|
if [[ "$1" == feasibility-pid-published ]]; then
|
|
fail_pid="$2"
|
|
printf '%s\n' "$2" >"${prefix}.pid"
|
|
builtin kill -TERM "$(<"$TASK4_FEASIBILITY_MAIN_PID_FILE")"
|
|
fi
|
|
}
|
|
_k3slr_proc_stat_record() {
|
|
if [[ -n "$fail_pid" && "$2" == "$fail_pid" ]]; then return 1; fi
|
|
_task4_review5_feas_original_proc_stat_record "$@"
|
|
}
|
|
_k3slr_signal_published_pid() {
|
|
printf 'call\n' >>"${prefix}.signal-count"
|
|
if [[ "$(/usr/bin/wc -l <"${prefix}.signal-count")" -eq 1 ]]; then return 1; fi
|
|
builtin kill -KILL "$2"
|
|
}
|
|
set +e
|
|
k3slr_local_recovery_feasibility_main --execute </dev/ptmx >/dev/null 2>"${prefix}.stderr"
|
|
rc=$?
|
|
set -e
|
|
child_pid="$(<"${prefix}.pid")"
|
|
if [[ "$rc" -ne 1 || "$(/usr/bin/wc -l <"${prefix}.signal-count")" -ne 2 ||
|
|
-e "$requested_dir" ]] || kill -0 "$child_pid" 2>/dev/null; then
|
|
printf 'task4 review5 feasibility cleanup diagnostic: rc=%s calls=%s child_live=%s fixture=%s\n' \
|
|
"$rc" "$(/usr/bin/wc -l <"${prefix}.signal-count")" \
|
|
"$([[ -e /proc/$child_pid ]] && printf yes || printf no)" \
|
|
"$([[ -e "$requested_dir" ]] && printf yes || printf no)" >&2
|
|
return 1
|
|
fi
|
|
)
|
|
|
|
task4_review2_tty_and_bounded_validated_pipe() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}" producer_case
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${fixture_root}/task4-review2-consumer.log"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${fixture_root}/task4-review2-producer.log"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${fixture_root}/task4-review2-command.log"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${fixture_root}/task4-review2-db-stat.log"
|
|
export TASK4_REVIEW2_REQUIRE_REAL_TTY=1
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
|
|
TASK4_REVIEW1_PRODUCER_CASE=success
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
if ! _k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx; then
|
|
printf 'task4 review2 critical diagnostic\n' >&2
|
|
/usr/bin/tail -n 30 "$TASK4_REVIEW1_COMMAND_LOG" >&2
|
|
return 1
|
|
fi
|
|
[[ "$(/usr/bin/wc -l <"$TASK4_REVIEW1_CONSUMER_LOG")" -eq 1 ]] || return 1
|
|
/usr/bin/grep -q 'result=tty' "$TASK4_REVIEW1_COMMAND_LOG" || return 1
|
|
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
for producer_case in zero_success missing_lf double_lf invalid_class malformed_length oversized_success; do
|
|
TASK4_REVIEW1_PRODUCER_CASE="$producer_case"
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
assert_fails _k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME" </dev/ptmx
|
|
[[ ! -s "$TASK4_REVIEW1_CONSUMER_LOG" ]] || return 1
|
|
done
|
|
)
|
|
|
|
task4_review2_capture_one_line_exact_record() (
|
|
local captured=''
|
|
assert_succeeds _k3slr_capture_one_line captured /usr/bin/bash -c 'printf "abc\n"'
|
|
[[ "$captured" == abc ]] || return 1
|
|
assert_fails _k3slr_capture_one_line captured /usr/bin/bash -c 'printf abc'
|
|
assert_fails _k3slr_capture_one_line captured /usr/bin/bash -c 'printf "abc\n\n"'
|
|
assert_fails _k3slr_capture_one_line captured /usr/bin/bash -c 'printf "abc\r\n"'
|
|
assert_fails _k3slr_capture_one_line captured /usr/bin/bash -c 'printf "abc\ndef\n"'
|
|
assert_fails _k3slr_capture_one_line captured /usr/bin/bash -c 'printf "abc\n"; exit 7'
|
|
)
|
|
|
|
# The production breaks this catches are omission of --show-protected, adding
|
|
# --quiet back to the protected producer, or routing recovery through a
|
|
# read-write cryptsetup consumer. The only external boundary is closed below.
|
|
task4_review2_show_protected_and_readonly() (
|
|
local command_log="${fixture_root}/task1-show-protected.argv"
|
|
local stdout_file="${fixture_root}/task1-show-protected.stdout"
|
|
local stderr_file="${fixture_root}/task1-show-protected.stderr"
|
|
local secret_sentinel='Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N'
|
|
local rc=0
|
|
: >"$command_log"
|
|
: >"$stdout_file"
|
|
: >"$stderr_file"
|
|
# shellcheck source=/dev/null
|
|
source "$LIBRARY_PATH"
|
|
_k3slr_command() {
|
|
printf '%s\0' "$@" >>"$command_log"
|
|
case "$#:$1:${2-}:${3-}:${4-}:${5-}" in
|
|
'3:/usr/bin/test:-t:0::') return 0 ;;
|
|
'7:/usr/bin/keepassxc-cli:show:--show-protected:--attributes:Password')
|
|
[[ "${6-}" == /fixture/recovery.kdbx && "${7-}" == 'K3s Recovery LUKS' ]] || return 97
|
|
printf '%s\n' "$secret_sentinel"
|
|
;;
|
|
'11:/usr/bin/sudo:--non-interactive:--:/usr/sbin/cryptsetup:open')
|
|
[[ "${6-}" == --readonly && "${7-}" == --type && "${8-}" == luks2 &&
|
|
"${9-}" == --key-file=- && "${10-}" == /dev/loop23 &&
|
|
"${11-}" == k3s-recovery-proof ]] || return 97
|
|
IFS= read -r _ || return 1
|
|
;;
|
|
*) return 97 ;;
|
|
esac
|
|
}
|
|
_k3slr_keepass_password_stdout /fixture/recovery.kdbx 'K3s Recovery LUKS' \
|
|
>/dev/null 2>"$stderr_file" || rc=$?
|
|
if (( rc != 0 )); then
|
|
mapfile -d '' -t task1_red_argv <"$command_log"
|
|
[[ " ${task1_red_argv[*]} " == *' /usr/bin/keepassxc-cli show --quiet '* &&
|
|
" ${task1_red_argv[*]} " != *' --show-protected '* ]] ||
|
|
fail 'producer RED did not reach the legacy KeePass argv missing --show-protected'
|
|
fi
|
|
assert_eq 0 "$rc" 'protected KeePass producer uses the protected Password argv'
|
|
rc=0
|
|
_k3slr_cryptsetup_open_readonly_stdin /dev/loop23 k3s-recovery-proof \
|
|
< <(printf '%s\n' "$secret_sentinel") >"$stdout_file" 2>>"$stderr_file" || rc=$?
|
|
assert_eq 0 "$rc" 'read-only LUKS consumer succeeds'
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${fixture_root}/task1-readonly-pipe.consumer"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${fixture_root}/task1-readonly-pipe.producer"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${fixture_root}/task1-readonly-pipe.commands"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${fixture_root}/task1-readonly-pipe.db-stat"
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
TASK4_REVIEW1_PRODUCER_CASE=success
|
|
TASK4_REVIEW1_VALIDATION_CASE=''
|
|
export TASK4_REVIEW1_PRODUCER_CASE TASK4_REVIEW1_VALIDATION_CASE
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
assert_fails _k3slr_luks_open_readonly_from_keepass /fixture/recovery.kdbx \
|
|
'K3s Recovery LUKS' /dev/loop23
|
|
assert_fails _k3slr_luks_open_readonly_from_keepass /fixture/recovery.kdbx \
|
|
'K3s Recovery LUKS' /dev/loop23 k3s-recovery-proof extra
|
|
rc=0
|
|
_k3slr_luks_open_readonly_from_keepass \
|
|
"${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}" 'K3s Recovery LUKS' \
|
|
/dev/loop7 "$K3SLR_PROOF_MAPPING_NAME" </dev/ptmx || rc=$?
|
|
assert_eq 0 "$rc" 'four-argument read-only wrapper routes through open-readonly password pipe'
|
|
assert_eq 1 "$(/usr/bin/wc -l <"$TASK4_REVIEW1_CONSUMER_LOG")" \
|
|
'actual open-readonly password-pipe branch dispatches one consumer'
|
|
/usr/bin/grep -Fqx -- \
|
|
"/usr/bin/sudo --non-interactive -- /usr/sbin/cryptsetup open --readonly --type luks2 --key-file=- /dev/loop7 ${K3SLR_PROOF_MAPPING_NAME}" \
|
|
"$TASK4_REVIEW1_COMMAND_LOG" ||
|
|
fail 'actual open-readonly password-pipe branch dispatched a read-write or malformed consumer'
|
|
[[ ! -s "$stdout_file" && ! -s "$stderr_file" ]] ||
|
|
fail 'protected producer leaked a secret through public output'
|
|
! /usr/bin/grep -Fq -- "$secret_sentinel" "$command_log" ||
|
|
fail 'protected producer leaked a secret into the command log'
|
|
mapfile -d '' -t task1_argv <"$command_log"
|
|
assert_eq '/usr/bin/test' "${task1_argv[0]-}" 'producer checks the interactive stdin seam'
|
|
assert_eq '/usr/bin/keepassxc-cli' "${task1_argv[3]-}" 'producer uses KeePassXC'
|
|
assert_eq '--show-protected' "${task1_argv[5]-}" 'producer requests the protected Password field'
|
|
assert_eq '/usr/bin/sudo' "${task1_argv[10]-}" 'consumer uses the modeled sudo boundary'
|
|
assert_eq '--readonly' "${task1_argv[15]-}" 'consumer opens the LUKS mapping read-only'
|
|
)
|
|
|
|
# This uses the feasibility main's real synthetic flow, but replaces only its
|
|
# command seam and records the KDBX attachment state in a file so the model
|
|
# survives each tracked background subshell.
|
|
task4_review2_feasibility_show_protected() (
|
|
local case_root="${fixture_root}/task1-feas-show"
|
|
local command_log="${case_root}/commands.nul"
|
|
local trace_file="${case_root}/trace"
|
|
local stdout_file="${case_root}/stdout"
|
|
local stderr_file="${case_root}/stderr"
|
|
local secret_sentinel='Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N'
|
|
local rc=0
|
|
mkdir -p -- "$case_root"
|
|
: >"$command_log"
|
|
: >"$trace_file"
|
|
# shellcheck source=/dev/null
|
|
source "$FEASIBILITY_PATH"
|
|
_k3slrf_require_interactive_stdin() { return 0; }
|
|
_k3slrf_verify_prerequisites() { return 0; }
|
|
_k3slrf_require_cached_sudo() { return 0; }
|
|
_k3slrf_create_fixture_dir() {
|
|
printf -v "$1" '%s' "$case_root/database"
|
|
printf -v "$2" '%s' fixture-directory
|
|
printf -v "$3" '%s' true
|
|
mkdir -p -- "$case_root/database"
|
|
}
|
|
_k3slrf_cleanup_fixture_dir() { [[ "$1" == "$case_root/database" ]]; }
|
|
_k3slrf_directory_identity() { printf -v "$1" '%s' fixture-directory; }
|
|
_k3slrf_directory_identity_matches() { [[ "$2" == fixture-directory ]]; }
|
|
_k3slrf_file_identity() { [[ -f "$2" ]] && printf -v "$1" '%s' "file:$2"; }
|
|
_k3slrf_file_identity_matches() { [[ -f "$1" && "$2" == "file:$1" ]]; }
|
|
_k3slrf_run_tracked() {
|
|
( "$@" ) &
|
|
local child=$!
|
|
wait "$child"
|
|
}
|
|
_k3slrf_validate_generated_password_stdin() {
|
|
local received=''
|
|
IFS= read -r received
|
|
[[ "$received" == "$secret_sentinel" ]]
|
|
}
|
|
_k3slr_command() {
|
|
local database='' payload=''
|
|
printf '%s\0' "$@" >>"$command_log"
|
|
case "$1:${2-}" in
|
|
'/usr/bin/sha256sum:')
|
|
(( $# == 1 )) || return 97
|
|
printf 'master-hash\n' >>"$trace_file"
|
|
while IFS= read -r _; do :; done
|
|
printf '%064d -\n' 0
|
|
;;
|
|
'/usr/bin/mawk:'*)
|
|
(( $# == 2 )) && [[ "${2-}" == '{ print $1 }' ]] || return 97
|
|
printf 'master-field\n' >>"$trace_file"
|
|
while IFS= read -r _; do :; done
|
|
printf '%s\n' fixture-master
|
|
;;
|
|
'/usr/bin/stat:--format=%s')
|
|
(( $# == 4 )) && [[ "${3-}" == -- &&
|
|
"${4-}" == "${case_root}/database/keepass-listing."*.stdout ]] || return 97
|
|
/usr/bin/stat --format='%s' -- "${4}"
|
|
;;
|
|
'/usr/bin/rm:--')
|
|
(( $# == 4 )) && [[ "${3-}" == "${case_root}/database/keepass-listing."*.stdout &&
|
|
"${4-}" == "${case_root}/database/keepass-listing."*.stderr ]] || return 97
|
|
/usr/bin/rm -- "${3}" "${4}"
|
|
;;
|
|
'/usr/bin/keepassxc-cli:db-create')
|
|
(( $# == 5 )) && [[ "${3-}" == --quiet && "${4-}" == --set-password && "${5-}" == */synthetic.kdbx ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
: >"${5}"
|
|
printf 'db-create\n' >>"$trace_file"
|
|
;;
|
|
'/usr/bin/keepassxc-cli:add')
|
|
(( $# == 12 )) && [[ "${3-}" == --quiet && "${4-}" == --generate && "${5-}" == --length &&
|
|
"${6-}" == 40 && "${7-}" == --lower && "${8-}" == --upper &&
|
|
"${9-}" == --numeric && "${10-}" == --every-group &&
|
|
"${11-}" == */synthetic.kdbx && "${12-}" == 'K3s Recovery LUKS' ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
printf '%s\n' entry >"${11}"
|
|
printf 'entry-add\n' >>"$trace_file"
|
|
;;
|
|
'/usr/bin/keepassxc-cli:show')
|
|
if (( $# == 7 )); then
|
|
[[ "${4-}" == --attributes && "${5-}" == Password &&
|
|
"${6-}" == */synthetic.kdbx && "${7-}" == 'K3s Recovery LUKS' ]] || return 97
|
|
if [[ "${3-}" == --show-protected ]]; then
|
|
printf 'password-show-protected\n' >>"$trace_file"
|
|
while IFS= read -r _; do :; done
|
|
printf '%s\n' "$secret_sentinel"
|
|
elif [[ "${3-}" == --quiet ]]; then
|
|
printf 'password-show-legacy\n' >>"$trace_file"
|
|
return 97
|
|
else
|
|
return 97
|
|
fi
|
|
elif (( $# == 8 )); then
|
|
[[ "${3-}" == --quiet && "${4-}" == --attributes && "${5-}" == Title &&
|
|
"${6-}" == --show-attachments && "${7-}" == */synthetic.kdbx &&
|
|
"${8-}" == 'K3s Recovery LUKS' ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
if [[ -f "${7}.attachment-state" ]]; then
|
|
printf 'K3s Recovery LUKS\n\nAttachments:\n fixture.bin (30.0 B)\n'
|
|
else
|
|
printf 'K3s Recovery LUKS\n\nNo attachments present.\n'
|
|
fi
|
|
else
|
|
return 97
|
|
fi
|
|
;;
|
|
'/usr/bin/keepassxc-cli:attachment-import')
|
|
(( $# == 7 )) && [[ "${3-}" == --quiet && "${4-}" == */synthetic.kdbx &&
|
|
"${5-}" == 'K3s Recovery LUKS' && "${6-}" == fixture.bin && "${7-}" == */input.bin ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
printf '%s\n' attached >"${4}.attachment-state"
|
|
IFS= read -r payload <"${7}" || return 1
|
|
printf '%s\n' "$payload" >"${4}.attachment-data"
|
|
;;
|
|
'/usr/bin/keepassxc-cli:attachment-export')
|
|
(( $# == 7 )) && [[ "${3-}" == --quiet && "${4-}" == */synthetic.kdbx &&
|
|
"${5-}" == 'K3s Recovery LUKS' && "${6-}" == fixture.bin && "${7-}" == /proc/*/fd/* ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
IFS= read -r payload <"${4}.attachment-data" || return 1
|
|
printf '%s\n' "$payload" >"${7}"
|
|
;;
|
|
'/usr/bin/cmp:--silent') (( $# == 5 )) && [[ "${3-}" == -- && "${4-}" == */input.bin && "${5-}" == */output.bin ]] || return 97 ;;
|
|
*) return 97 ;;
|
|
esac
|
|
}
|
|
k3slr_local_recovery_feasibility_main --execute >"$stdout_file" 2>"$stderr_file" || rc=$?
|
|
if (( rc != 0 )); then
|
|
local task1_feas_index=0 task1_feas_legacy_seen=false task1_feas_protected_seen=false
|
|
mapfile -d '' -t task1_feas_red_argv <"$command_log"
|
|
for (( task1_feas_index=0; task1_feas_index+6<${#task1_feas_red_argv[@]}; task1_feas_index++ )); do
|
|
if [[ "${task1_feas_red_argv[task1_feas_index]}" == /usr/bin/keepassxc-cli &&
|
|
"${task1_feas_red_argv[task1_feas_index + 1]}" == show &&
|
|
"${task1_feas_red_argv[task1_feas_index + 2]}" == --show-protected &&
|
|
"${task1_feas_red_argv[task1_feas_index + 3]}" == --attributes &&
|
|
"${task1_feas_red_argv[task1_feas_index + 4]}" == Password &&
|
|
"${task1_feas_red_argv[task1_feas_index + 6]}" == 'K3s Recovery LUKS' ]]; then
|
|
task1_feas_protected_seen=true
|
|
break
|
|
elif [[ "${task1_feas_red_argv[task1_feas_index]}" == /usr/bin/keepassxc-cli &&
|
|
"${task1_feas_red_argv[task1_feas_index + 1]}" == show &&
|
|
"${task1_feas_red_argv[task1_feas_index + 2]}" == --quiet &&
|
|
"${task1_feas_red_argv[task1_feas_index + 3]}" == --attributes &&
|
|
"${task1_feas_red_argv[task1_feas_index + 4]}" == Password &&
|
|
"${task1_feas_red_argv[task1_feas_index + 6]}" == 'K3s Recovery LUKS' ]]; then
|
|
task1_feas_legacy_seen=true
|
|
break
|
|
fi
|
|
done
|
|
if "$task1_feas_protected_seen"; then
|
|
fail "feasibility failed after the protected password stage (stages: $(/usr/bin/tr '\n' ',' <"$trace_file"))"
|
|
fi
|
|
"$task1_feas_legacy_seen" || fail "feasibility RED did not reach the exact legacy password show argv (stages: $(/usr/bin/tr '\n' ',' <"$trace_file"))"
|
|
/usr/bin/grep -Fqx password-show-legacy "$trace_file" ||
|
|
fail 'feasibility RED did not persist the legacy password stage'
|
|
fi
|
|
assert_eq 0 "$rc" 'feasibility password check uses the protected Password argv'
|
|
! /usr/bin/grep -Fq -- "$secret_sentinel" "$stdout_file" "$stderr_file" "$command_log" ||
|
|
fail 'feasibility fixture leaked the password sentinel'
|
|
mapfile -d '' -t task1_feas_argv <"$command_log"
|
|
[[ " ${task1_feas_argv[*]} " == *' --show-protected '* ]] ||
|
|
fail 'feasibility did not request the protected Password field'
|
|
)
|
|
|
|
# The raw-NUL record is emitted only by the exact modeled show-attachments
|
|
# command. It is never placed in a Bash variable or command substitution by
|
|
# this fixture; feasibility must capture and reject it at its real boundary.
|
|
task4_review2_feasibility_attachment_listing() (
|
|
local case_root="${fixture_root}/task1-feas-listing"
|
|
local command_log="${case_root}/commands.nul"
|
|
local trace_file="${case_root}/trace"
|
|
local stdout_file="${case_root}/stdout"
|
|
local stderr_file="${case_root}/stderr"
|
|
local listing_case=raw_nul rc=0 state=''
|
|
mkdir -p -- "$case_root"
|
|
: >"$command_log"
|
|
: >"$trace_file"
|
|
# shellcheck source=/dev/null
|
|
source "$FEASIBILITY_PATH"
|
|
_k3slr_parse_keepass_attachment_listing() {
|
|
(( $# == 3 )) || return 97
|
|
case "$1" in
|
|
$'K3s Recovery LUKS\n\nNo attachments present.\n')
|
|
printf -v "$3" '%s' absent
|
|
printf 'parser:absent\n' >>"$trace_file"
|
|
;;
|
|
$'K3s Recovery LUKS\n\nAttachments:\n fixture.bin (30.0 B)\n')
|
|
printf -v "$3" '%s' present
|
|
printf 'parser:present\n' >>"$trace_file"
|
|
;;
|
|
*)
|
|
printf 'parser:rejected\n' >>"$trace_file"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
_k3slrf_require_interactive_stdin() { return 0; }
|
|
_k3slrf_verify_prerequisites() { return 0; }
|
|
_k3slrf_require_cached_sudo() { return 0; }
|
|
_k3slrf_create_fixture_dir() {
|
|
local new_dir="${case_root}/database-${RANDOM}"
|
|
mkdir -p -- "$new_dir"
|
|
printf -v "$1" '%s' "$new_dir"
|
|
printf -v "$2" '%s' fixture-directory
|
|
printf -v "$3" '%s' true
|
|
}
|
|
_k3slrf_cleanup_fixture_dir() { [[ "$1" == "${case_root}"/database-* ]]; }
|
|
_k3slrf_directory_identity() { printf -v "$1" '%s' fixture-directory; }
|
|
_k3slrf_directory_identity_matches() { [[ "$2" == fixture-directory ]]; }
|
|
_k3slrf_file_identity() { [[ -f "$2" ]] && printf -v "$1" '%s' "file:$2"; }
|
|
_k3slrf_file_identity_matches() { [[ -f "$1" && "$2" == "file:$1" ]]; }
|
|
_k3slrf_run_tracked() {
|
|
( "$@" ) &
|
|
local child=$!
|
|
wait "$child"
|
|
}
|
|
_k3slrf_validate_generated_password_stdin() { IFS= read -r _; }
|
|
_k3slr_command() {
|
|
local payload=''
|
|
printf '%s\0' "$@" >>"$command_log"
|
|
case "$1:${2-}" in
|
|
'/usr/bin/sha256sum:')
|
|
(( $# == 1 )) || return 97
|
|
while IFS= read -r _; do :; done
|
|
printf '%064d -\n' 0
|
|
;;
|
|
'/usr/bin/mawk:'*)
|
|
(( $# == 2 )) && [[ "${2-}" == '{ print $1 }' ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
printf '%s\n' fixture-master
|
|
;;
|
|
'/usr/bin/stat:--format=%s')
|
|
(( $# == 4 )) && [[ "${3-}" == -- &&
|
|
"${4-}" == "${case_root}/database-"*/keepass-listing.*.stdout ]] || return 97
|
|
/usr/bin/stat --format='%s' -- "${4}"
|
|
;;
|
|
'/usr/bin/rm:--')
|
|
(( $# == 4 )) && [[ "${3-}" == "${case_root}/database-"*/keepass-listing.*.stdout &&
|
|
"${4-}" == "${case_root}/database-"*/keepass-listing.*.stderr ]] || return 97
|
|
/usr/bin/rm -- "${3}" "${4}"
|
|
;;
|
|
'/usr/bin/keepassxc-cli:db-create')
|
|
(( $# == 5 )) && [[ "${3-}" == --quiet && "${4-}" == --set-password && "${5-}" == */synthetic.kdbx ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
: >"${5}"
|
|
;;
|
|
'/usr/bin/keepassxc-cli:add')
|
|
(( $# == 12 )) && [[ "${3-}" == --quiet && "${4-}" == --generate && "${5-}" == --length &&
|
|
"${6-}" == 40 && "${7-}" == --lower && "${8-}" == --upper &&
|
|
"${9-}" == --numeric && "${10-}" == --every-group && "${11-}" == */synthetic.kdbx &&
|
|
"${12-}" == 'K3s Recovery LUKS' ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
: >"${11}.entry"
|
|
;;
|
|
'/usr/bin/keepassxc-cli:show')
|
|
if (( $# == 7 )); then
|
|
[[ ( "${3-}" == --quiet || "${3-}" == --show-protected ) && "${4-}" == --attributes &&
|
|
"${5-}" == Password && "${6-}" == */synthetic.kdbx &&
|
|
"${7-}" == 'K3s Recovery LUKS' ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
printf '%s\n' 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N'
|
|
elif (( $# == 8 )); then
|
|
[[ "${3-}" == --quiet && "${4-}" == --attributes && "${5-}" == Title &&
|
|
"${6-}" == --show-attachments && "${7-}" == */synthetic.kdbx &&
|
|
"${8-}" == 'K3s Recovery LUKS' ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
case "$listing_case" in
|
|
raw_nul)
|
|
printf 'listing:raw-nul\n' >>"$trace_file"
|
|
printf 'K3s Recovery LUKS\n\nAttachments:\n fixture.bin\0 (30.0 B)\n'
|
|
;;
|
|
absent)
|
|
if [[ -f "${7}.attachment-state" ]]; then
|
|
printf 'listing:present\n' >>"$trace_file"
|
|
printf 'K3s Recovery LUKS\n\nAttachments:\n fixture.bin (30.0 B)\n'
|
|
else
|
|
printf 'listing:absent\n' >>"$trace_file"
|
|
printf 'K3s Recovery LUKS\n\nNo attachments present.\n'
|
|
fi
|
|
;;
|
|
present)
|
|
printf 'listing:present\n' >>"$trace_file"
|
|
printf 'K3s Recovery LUKS\n\nAttachments:\n fixture.bin (30.0 B)\n'
|
|
;;
|
|
size_8192)
|
|
printf 'listing:size-8192\n' >>"$trace_file"
|
|
printf 'K3s Recovery LUKS\n\n%08172d\n' 0
|
|
;;
|
|
size_8193)
|
|
printf 'listing:size-8193\n' >>"$trace_file"
|
|
printf 'K3s Recovery LUKS\n\n%08173d\n' 0
|
|
;;
|
|
*) return 97 ;;
|
|
esac
|
|
else
|
|
return 97
|
|
fi
|
|
;;
|
|
'/usr/bin/keepassxc-cli:attachment-import')
|
|
(( $# == 7 )) && [[ "${3-}" == --quiet && "${4-}" == */synthetic.kdbx &&
|
|
"${5-}" == 'K3s Recovery LUKS' && "${6-}" == fixture.bin && "${7-}" == */input.bin ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
IFS= read -r payload <"${7}" || return 1
|
|
printf '%s\n' "$payload" >"${4}.attachment-data"
|
|
: >"${4}.attachment-state"
|
|
printf 'import\n' >>"$trace_file"
|
|
;;
|
|
'/usr/bin/keepassxc-cli:attachment-export')
|
|
(( $# == 7 )) && [[ "${3-}" == --quiet && "${4-}" == */synthetic.kdbx &&
|
|
"${5-}" == 'K3s Recovery LUKS' && "${6-}" == fixture.bin && "${7-}" == /proc/*/fd/* ]] || return 97
|
|
while IFS= read -r _; do :; done
|
|
IFS= read -r payload <"${4}.attachment-data" || return 1
|
|
printf '%s\n' "$payload" >"${7}"
|
|
printf 'export\n' >>"$trace_file"
|
|
;;
|
|
'/usr/bin/cmp:--silent') (( $# == 5 )) && [[ "${3-}" == -- && "${4-}" == */input.bin && "${5-}" == */output.bin ]] || return 97 ;;
|
|
*) return 97 ;;
|
|
esac
|
|
}
|
|
set +e
|
|
k3slr_local_recovery_feasibility_main --execute >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
assert_eq 1 "$(/usr/bin/grep -Fxc 'listing:raw-nul' "$trace_file" || :)" \
|
|
'raw-NUL path dispatches exactly one attachment listing'
|
|
assert_eq 0 "$(/usr/bin/grep -Fc 'parser:' "$trace_file" || :)" \
|
|
'raw-NUL path reaches no parser'
|
|
assert_eq 0 "$(/usr/bin/grep -Fxc import "$trace_file" || :)" \
|
|
'raw-NUL path reaches no import'
|
|
assert_eq 0 "$(/usr/bin/grep -Fxc export "$trace_file" || :)" \
|
|
'raw-NUL path reaches no export'
|
|
! /usr/bin/grep -Fiq -- 'null byte' "$stderr_file" ||
|
|
fail 'raw-NUL capture emitted a Bash null-byte warning'
|
|
(( rc != 0 )) || fail 'raw-NUL attachment listing was accepted before bounded capture rejected it'
|
|
for listing_case in size_8192 size_8193; do
|
|
: >"$trace_file"
|
|
set +e
|
|
k3slr_local_recovery_feasibility_main --execute >>"$stdout_file" 2>>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
(( rc != 0 )) || fail "$listing_case attachment listing was accepted"
|
|
if [[ "$listing_case" == size_8192 ]]; then
|
|
assert_eq $'listing:size-8192\nparser:rejected' "$(<"$trace_file")" \
|
|
'8192-byte listing reaches parser and is rejected only by grammar'
|
|
else
|
|
assert_eq 'listing:size-8193' "$(<"$trace_file")" \
|
|
'8193-byte listing is rejected by the capture size gate before parser'
|
|
fi
|
|
done
|
|
: >"$trace_file"
|
|
listing_case=absent
|
|
assert_succeeds k3slr_local_recovery_feasibility_main --execute >>"$stdout_file" 2>>"$stderr_file"
|
|
assert_eq $'listing:absent\nparser:absent\nimport\nlisting:present\nparser:present\nexport' \
|
|
"$(<"$trace_file")" 'normal feasibility trace is absent to present with exact parser ordering'
|
|
! /usr/bin/grep -Fq -- 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N' \
|
|
"$stdout_file" "$stderr_file" "$command_log" || fail 'attachment fixture leaked password sentinel'
|
|
)
|
|
|
|
task4_review2_attachment_parser_contract() (
|
|
local state=unchanged record=''
|
|
# shellcheck source=/dev/null
|
|
source "$LIBRARY_PATH"
|
|
assert_succeeds _k3slr_parse_keepass_attachment_listing \
|
|
$'K3s Recovery LUKS\n\nNo attachments present.\n' fixture.bin state
|
|
assert_eq absent "$state" 'canonical KeePassXC absent record parses'
|
|
assert_succeeds _k3slr_parse_keepass_attachment_listing \
|
|
$'K3s Recovery LUKS\n\nAttachments:\n fixture.bin (30.0 B)\n' fixture.bin state
|
|
assert_eq present "$state" 'canonical KeePassXC present record parses'
|
|
for record in \
|
|
$'K3s Recovery LUKS\n\nAttachments:\n archive.bin (1.0 KiB)\n' \
|
|
$'K3s Recovery LUKS\n\nAttachments:\n archive.bin (2.5 MiB)\n' \
|
|
$'K3s Recovery LUKS\n\nAttachments:\n archive.bin (3.0 GiB)\n'; do
|
|
state=unchanged
|
|
assert_succeeds _k3slr_parse_keepass_attachment_listing "$record" archive.bin state
|
|
assert_eq present "$state" 'parser accepts exact non-fixture attachment and IEC size unit'
|
|
state=unchanged
|
|
assert_fails _k3slr_parse_keepass_attachment_listing "$record" fixture.bin state
|
|
assert_eq unchanged "$state" 'non-target attachment leaves output unchanged'
|
|
done
|
|
for record in '' $'K3s Recovery LUKS\n\nAttachments:\n fixture.bin (30.0 B)\n fixture.bin (30.0 B)\n' \
|
|
$'K3s Recovery LUKS\r\n\r\nNo attachments present.\r\n' \
|
|
$'Title: K3s Recovery LUKS\n\nAttachments:\n fixture.bin (30.0 B)\n' \
|
|
$'K3s Recovery LUKS\n\nAttachments:\n fixture.bin.old (30.0 B)\n' \
|
|
$'K3s Recovery LUKS\n\nNo attachments present.\nextra\n' \
|
|
$'K3s Recovery LUKS\n\nNo attachments present.'; do
|
|
state=unchanged
|
|
assert_fails _k3slr_parse_keepass_attachment_listing "$record" fixture.bin state
|
|
assert_eq unchanged "$state" 'malformed attachment record leaves output state unchanged'
|
|
done
|
|
printf -v record 'K3s Recovery LUKS\n\n%08172d\n' 0
|
|
assert_eq 8192 "${#record}" '8192-byte parser record is final-LF framed'
|
|
state=unchanged
|
|
assert_fails _k3slr_parse_keepass_attachment_listing "$record" fixture.bin state
|
|
assert_eq unchanged "$state" '8192-byte grammar failure leaves output unchanged'
|
|
printf -v record 'K3s Recovery LUKS\n\n%08173d\n' 0
|
|
assert_eq 8193 "${#record}" '8193-byte parser record is final-LF framed'
|
|
state=unchanged
|
|
assert_fails _k3slr_parse_keepass_attachment_listing "$record" fixture.bin state
|
|
assert_eq unchanged "$state" 'oversize parser record leaves output unchanged'
|
|
assert_fails _k3slr_parse_keepass_attachment_listing \
|
|
$'K3s Recovery LUKS\n\nNo attachments present.\n' fixture.bin unsafe-name
|
|
state=unchanged
|
|
assert_fails _k3slr_parse_keepass_attachment_listing \
|
|
$'K3s Recovery LUKS\n\nNo attachments present.\n' fixture.bin _k3slr_attachment_state
|
|
assert_eq unchanged "$state" 'reserved production-local output name cannot mutate caller state'
|
|
record=$'K3s Recovery LUKS\n\nNo attachments present.\n'
|
|
assert_fails _k3slr_parse_keepass_attachment_listing \
|
|
"$record" fixture.bin _k3slr_attachment_record
|
|
assert_eq $'K3s Recovery LUKS\n\nNo attachments present.\n' "$record" \
|
|
'reserved record-local output name leaves the caller record unchanged'
|
|
)
|
|
|
|
task4_review1_two_sentinel_leak_contract() (
|
|
local first='Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N'
|
|
local second='Zz9Yy8Xx7Ww6Vv5Uu4Tt3Ss2Rr1Qq0Pp8Oo7Nn6M'
|
|
local value uid sink
|
|
[[ "$first" != "$second" && "${#first}" -eq 40 && "${#second}" -eq 40 ]] || return 1
|
|
for value in "$first" "$second"; do
|
|
[[ "$value" =~ [a-z] && "$value" =~ [A-Z] && "$value" =~ [0-9] ]] || return 1
|
|
for sink in "${fixture_root}/task4-child.cmdline" "${fixture_root}/task4-child.environ" \
|
|
"${fixture_root}"/task4-*.stdout "${fixture_root}"/task4-*.stderr; do
|
|
[[ -e "$sink" ]] || continue
|
|
! /usr/bin/grep -Fq -- "$value" "$sink" || return 1
|
|
done
|
|
uid="$(/usr/bin/id -u)"
|
|
! /usr/bin/find /tmp /run -xdev -user "$uid" -name "*${value}*" -print -quit 2>/dev/null |
|
|
/usr/bin/grep -q . || return 1
|
|
! /usr/bin/find /tmp /run -xdev -user "$uid" -type f -readable \
|
|
! -path "${fixture_root}/*" -exec /usr/bin/grep -IlF -- "$value" '{}' + \
|
|
2>/dev/null | /usr/bin/grep -q . || return 1
|
|
done
|
|
)
|
|
|
|
task4_review1_preflight_and_lineage_gate() (
|
|
local database="${K3SLR_OUTER_MOUNT}/${K3SLR_DATABASE_RELATIVE}" validation_case
|
|
export TASK4_REVIEW1_CONSUMER_LOG="${fixture_root}/task4-review1-gate-consumer.log"
|
|
export TASK4_REVIEW1_PRODUCER_LOG="${fixture_root}/task4-review1-gate-producer.log"
|
|
export TASK4_REVIEW1_COMMAND_LOG="${fixture_root}/task4-review1-gate-command.log"
|
|
export TASK4_REVIEW1_DB_STAT_LOG="${fixture_root}/task4-review1-gate-db-stat.log"
|
|
_k3slr_command() { task4_review1_command_fake "$@"; }
|
|
TASK4_REVIEW1_PRODUCER_CASE=success
|
|
export TASK4_REVIEW1_PRODUCER_CASE
|
|
for validation_case in non_tty parent_symlink; do
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_COMMAND_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
TASK4_REVIEW1_VALIDATION_CASE="$validation_case"
|
|
export TASK4_REVIEW1_VALIDATION_CASE
|
|
assert_fails _k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME"
|
|
[[ ! -s "$TASK4_REVIEW1_PRODUCER_LOG" && ! -s "$TASK4_REVIEW1_CONSUMER_LOG" ]] || return 1
|
|
done
|
|
: >"$TASK4_REVIEW1_CONSUMER_LOG"
|
|
: >"$TASK4_REVIEW1_PRODUCER_LOG"
|
|
: >"$TASK4_REVIEW1_DB_STAT_LOG"
|
|
TASK4_REVIEW1_VALIDATION_CASE=post_drift
|
|
export TASK4_REVIEW1_VALIDATION_CASE
|
|
assert_fails _k3slr_luks_open_from_keepass "$database" "$K3SLR_KEEPASS_ENTRY" /dev/loop7 "$K3SLR_MAPPING_NAME"
|
|
[[ "$(/usr/bin/wc -l <"$TASK4_REVIEW1_PRODUCER_LOG")" -eq 1 && ! -s "$TASK4_REVIEW1_CONSUMER_LOG" ]]
|
|
)
|
|
|
|
task4_review1_exact_constants_become_readonly() (
|
|
local assignment
|
|
/usr/bin/bash -c '
|
|
K3SLR_KEEPASS_ENTRY="K3s Recovery LUKS"
|
|
K3SLR_KEEPASS_HEADER_ATTACHMENT="luks-header-backup.bin"
|
|
_K3SLR_INT64_MAX=9223372036854775807
|
|
_K3SLR_REQUIRED_MINIMUM_FREE_BYTES=10737418240
|
|
source "$1"
|
|
' task4-readonly "$LIBRARY_PATH"
|
|
for assignment in 'K3SLR_KEEPASS_ENTRY=changed' \
|
|
'K3SLR_KEEPASS_HEADER_ATTACHMENT=changed' '_K3SLR_INT64_MAX=1' \
|
|
'_K3SLR_REQUIRED_MINIMUM_FREE_BYTES=1'; do
|
|
! /usr/bin/bash -c '
|
|
K3SLR_KEEPASS_ENTRY="K3s Recovery LUKS"
|
|
K3SLR_KEEPASS_HEADER_ATTACHMENT="luks-header-backup.bin"
|
|
_K3SLR_INT64_MAX=9223372036854775807
|
|
_K3SLR_REQUIRED_MINIMUM_FREE_BYTES=10737418240
|
|
source "$1"
|
|
eval "$2"
|
|
' task4-readonly "$LIBRARY_PATH" "$assignment" 2>/dev/null || return 1
|
|
done
|
|
)
|
|
|
|
task4_review1_generated_password_exact_lf() (
|
|
# shellcheck source=/dev/null
|
|
source "$FEASIBILITY_PATH"
|
|
if ! printf '%s\n' 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N' |
|
|
_k3slrf_validate_generated_password_stdin; then printf 'exact-lf valid rejected\n' >&2; return 1; fi
|
|
if printf '%s' 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N' |
|
|
_k3slrf_validate_generated_password_stdin; then printf 'exact-lf missing accepted\n' >&2; return 1; fi
|
|
if printf '%s\n\n' 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9Kk0Ll1Mm2N' |
|
|
_k3slrf_validate_generated_password_stdin; then printf 'exact-lf double accepted\n' >&2; return 1; fi
|
|
)
|
|
|
|
task5a_wrapper_and_guard_contract() (
|
|
local wrapper main_name mode output status trace_output
|
|
|
|
for wrapper in "$PREPARE_WRAPPER_PATH" "$OPEN_WRAPPER_PATH" "$CLOSE_WRAPPER_PATH"; do
|
|
[[ -f "$wrapper" && ! -L "$wrapper" ]] || {
|
|
printf 'missing Task 5A lifecycle wrapper: %s\n' "$wrapper" >&2
|
|
return 1
|
|
}
|
|
assert_succeeds /usr/bin/bash -n "$wrapper"
|
|
done
|
|
|
|
for mode in prepare open close; do
|
|
case "$mode" in
|
|
prepare) wrapper="$PREPARE_WRAPPER_PATH" ;;
|
|
open) wrapper="$OPEN_WRAPPER_PATH" ;;
|
|
close) wrapper="$CLOSE_WRAPPER_PATH" ;;
|
|
esac
|
|
main_name="_k3slr_${mode}_main"
|
|
|
|
output="$({
|
|
PATH=/tmp/untrusted-path
|
|
LC_ALL=POSIX
|
|
umask 022
|
|
set -- --execute
|
|
# shellcheck source=/dev/null
|
|
source "$wrapper"
|
|
declare -F "$main_name" >/dev/null || return 1
|
|
declare -F _k3slr_wrapper_initial_guard >/dev/null || return 1
|
|
_k3slr_wrapper_initial_guard 0 '' >/dev/null 2>&1 && return 1
|
|
_k3slr_wrapper_initial_guard 1000 x >/dev/null 2>&1 && return 1
|
|
_k3slr_wrapper_initial_guard 1000 '' || return 1
|
|
printf '%s|%s|' "$PATH" "$LC_ALL"
|
|
umask
|
|
} 2>/dev/null)" || return 1
|
|
assert_eq '/usr/sbin:/usr/bin:/sbin:/bin|C|0077' "$output" \
|
|
"$mode wrapper establishes its early source-safe process guard"
|
|
|
|
trace_output="${fixture_root}/task5a-${mode}-xtrace.stderr"
|
|
if /usr/bin/bash -x "$wrapper" 2>"$trace_output"; then
|
|
printf '%s wrapper accepted xtrace\n' "$mode" >&2
|
|
return 1
|
|
fi
|
|
! /usr/bin/grep -Fq -- 'scripts/lib/k3s-local-recovery.sh' "$trace_output" || {
|
|
printf '%s wrapper discovered/sourced the repository before xtrace refusal\n' "$mode" >&2
|
|
return 1
|
|
}
|
|
|
|
output="$({
|
|
# shellcheck source=/dev/null
|
|
source "$wrapper"
|
|
_k3slr_lifecycle_main() {
|
|
(( $# == 2 )) || return 1
|
|
printf '%s|%s\n' "$1" "$2"
|
|
}
|
|
"$main_name"
|
|
} 2>/dev/null)" || return 1
|
|
assert_eq "${mode}|dry-run" "$output" "$mode no-argument parser selects dry-run"
|
|
|
|
for bad_args in '--help' '-h' '--execute --execute' '--unknown' 'positional'; do
|
|
# This deliberate word split enumerates fixed test arguments, never input.
|
|
# shellcheck disable=SC2086
|
|
if output="$({ source "$wrapper"; "$main_name" $bad_args; } 2>&1)"; then
|
|
printf '%s wrapper accepted invalid argv: %s\n' "$mode" "$bad_args" >&2
|
|
return 1
|
|
else
|
|
status=$?
|
|
fi
|
|
[[ "$status" == 2 && "$output" == *'Usage:'* ]] || {
|
|
printf '%s wrapper invalid argv did not produce usage exit 2: %s\n' "$mode" "$bad_args" >&2
|
|
return 1
|
|
}
|
|
done
|
|
done
|
|
|
|
assert_fails _k3slr_require_execute_tty
|
|
)
|
|
|
|
task5a_prepare_provider_capability_gate() (
|
|
local log="${fixture_root}/task5a-prepare-capability.log"
|
|
|
|
_k3slr_prove_header_restore() { :; }
|
|
export -f _k3slr_prove_header_restore
|
|
/usr/bin/bash -c '
|
|
source "$1"
|
|
! declare -F _k3slr_prove_header_restore >/dev/null
|
|
' task5a-provider-cleanup "$PREPARE_WRAPPER_PATH" || {
|
|
printf 'prepare wrapper retained an inherited/exported provider\n' >&2
|
|
return 1
|
|
}
|
|
unset -f _k3slr_prove_header_restore
|
|
|
|
# shellcheck source=/dev/null
|
|
source "$PREPARE_WRAPPER_PATH"
|
|
_k3slr_load_contract() {
|
|
(( $# == 1 )) || return 1
|
|
printf 'contract\n' >>"$log"
|
|
}
|
|
_k3slr_require_execute_tty() {
|
|
printf 'tty\n' >>"$log"
|
|
return 1
|
|
}
|
|
_k3slr_lifecycle_step() {
|
|
printf 'step:%s\n' "$1" >>"$log"
|
|
return 1
|
|
}
|
|
_k3slr_lifecycle_dispatch() {
|
|
printf 'dispatch\n' >>"$log"
|
|
return 1
|
|
}
|
|
_k3slr_command() {
|
|
printf 'unexpected-command:%s\n' "$*" >>"$log"
|
|
return 97
|
|
}
|
|
|
|
assert_fails _k3slr_prepare_main --execute
|
|
assert_eq contract "$(<"$log")" \
|
|
'prepare provider absence refuses after contract and before every later boundary'
|
|
)
|
|
|
|
task5a_open_close_capability_gate() (
|
|
local mode wrapper main_name state missing_binary log output expected line
|
|
local -a trusted_binaries=(
|
|
/usr/bin/keepassxc-cli
|
|
/usr/sbin/cryptsetup
|
|
/usr/sbin/losetup
|
|
/usr/sbin/blkid
|
|
/usr/sbin/smartctl
|
|
/usr/bin/mount
|
|
/usr/bin/umount
|
|
/usr/bin/findmnt
|
|
/usr/bin/lsblk
|
|
/usr/bin/readlink
|
|
/usr/bin/stat
|
|
/usr/bin/ntfsinfo
|
|
/usr/bin/ntfs-3g.probe
|
|
/usr/bin/fuser
|
|
/usr/bin/sync
|
|
/usr/bin/cmp
|
|
/usr/bin/cat
|
|
/usr/bin/sha256sum
|
|
/usr/bin/mawk
|
|
/usr/bin/od
|
|
/usr/bin/bash
|
|
/usr/bin/du
|
|
/usr/bin/test
|
|
/usr/bin/dd
|
|
/usr/bin/id
|
|
/usr/bin/ps
|
|
/usr/bin/sleep
|
|
/usr/bin/mkdir
|
|
/usr/bin/dpkg-query
|
|
/usr/bin/mktemp
|
|
/usr/bin/rm
|
|
/usr/bin/rmdir
|
|
/usr/bin/sudo
|
|
)
|
|
|
|
for mode in open close; do
|
|
case "$mode" in
|
|
open) wrapper="$OPEN_WRAPPER_PATH" ;;
|
|
close) wrapper="$CLOSE_WRAPPER_PATH" ;;
|
|
esac
|
|
main_name="_k3slr_${mode}_main"
|
|
log="${fixture_root}/task5a-${mode}-capability.log"
|
|
: >"$log"
|
|
|
|
# shellcheck source=/dev/null
|
|
source "$wrapper"
|
|
_k3slr_load_contract() {
|
|
(( $# == 1 )) || return 1
|
|
printf 'contract\n' >>"$log"
|
|
}
|
|
_k3slr_query_package_state() {
|
|
local destination_name="$1" package="$2" expected_version="$3" result
|
|
printf 'package:%s:%s\n' "$package" "$expected_version" >>"$log"
|
|
case "$package" in
|
|
keepassxc) result="${TASK5A_KEEPASS_STATE:-installed}" ;;
|
|
cryptsetup-bin) result="${TASK5A_CRYPTSETUP_STATE:-installed}" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
printf -v "$destination_name" '%s' "$result"
|
|
}
|
|
_k3slr_trusted_root_executable() {
|
|
printf 'binary:%s\n' "$1" >>"$log"
|
|
[[ -z "${TASK5A_MISSING_BINARY:-}" || "$1" != "$TASK5A_MISSING_BINARY" ]]
|
|
}
|
|
_k3slr_require_execute_tty() {
|
|
printf 'tty\n' >>"$log"
|
|
}
|
|
_k3slr_lifecycle_dispatch() {
|
|
printf 'dispatch:%s:%s\n' "$1" "$2" >>"$log"
|
|
}
|
|
_k3slr_command() {
|
|
printf 'unexpected-command:%s\n' "$*" >>"$log"
|
|
return 97
|
|
}
|
|
|
|
TASK5A_KEEPASS_STATE=installed
|
|
TASK5A_CRYPTSETUP_STATE=installed
|
|
TASK5A_MISSING_BINARY=''
|
|
assert_succeeds "$main_name" --execute
|
|
expected=$'contract\npackage:keepassxc:2.7.6+dfsg.1-1build3\npackage:cryptsetup-bin:2:2.7.0-1ubuntu4.2'
|
|
for line in "${trusted_binaries[@]}"; do
|
|
expected+=$'\n'binary:"$line"
|
|
done
|
|
expected+=$'\ntty\ndispatch:'"$mode"':_k3slr_prove_header_restore'
|
|
assert_eq "$expected" "$(<"$log")" \
|
|
"$mode gates packages and every trusted binary before TTY/dispatch"
|
|
|
|
for state in absent mixed wrong malformed; do
|
|
: >"$log"
|
|
case "$state" in
|
|
absent)
|
|
TASK5A_KEEPASS_STATE=absent
|
|
TASK5A_CRYPTSETUP_STATE=absent
|
|
;;
|
|
mixed)
|
|
TASK5A_KEEPASS_STATE=installed
|
|
TASK5A_CRYPTSETUP_STATE=absent
|
|
;;
|
|
wrong)
|
|
TASK5A_KEEPASS_STATE=wrong-version
|
|
TASK5A_CRYPTSETUP_STATE=installed
|
|
;;
|
|
malformed)
|
|
TASK5A_KEEPASS_STATE=malformed
|
|
TASK5A_CRYPTSETUP_STATE=installed
|
|
;;
|
|
esac
|
|
assert_fails "$main_name" --execute
|
|
output="$(<"$log")"
|
|
expected=$'contract\npackage:keepassxc:2.7.6+dfsg.1-1build3\npackage:cryptsetup-bin:2:2.7.0-1ubuntu4.2'
|
|
assert_eq "$expected" "$output" "$mode $state package state stops before binary/TTY/dispatch"
|
|
done
|
|
|
|
: >"$log"
|
|
TASK5A_KEEPASS_STATE=installed
|
|
TASK5A_CRYPTSETUP_STATE=installed
|
|
TASK5A_MISSING_BINARY=/usr/bin/stat
|
|
assert_fails "$main_name" --execute
|
|
output="$(<"$log")"
|
|
[[ "$output" == *$'binary:/usr/bin/stat' && "$output" != *$'\ntty' &&
|
|
"$output" != *$'\ndispatch:' && "$output" != *'unexpected-command:'* ]] || {
|
|
printf '%s missing binary did not stop at the capability boundary\n' "$mode" >&2
|
|
return 1
|
|
}
|
|
done
|
|
)
|
|
|
|
task5a_lifecycle_dispatch_contract() (
|
|
local mode step failure_step log expected actual
|
|
local -a prepare_steps=(
|
|
sudo
|
|
context
|
|
device-validator
|
|
estimate-phase
|
|
capacity-preflight
|
|
confirm-prepare
|
|
install-packages
|
|
feasibility
|
|
revalidate-device
|
|
mount-outer
|
|
revalidate-capacity
|
|
create-layout
|
|
create-kdbx
|
|
create-container
|
|
validate-allocation
|
|
create-loop
|
|
luks-format
|
|
luks-open
|
|
mkfs-ext4
|
|
mount-inner
|
|
write-marker-metadata
|
|
header-proof
|
|
close-inner
|
|
close-mapping
|
|
detach-loop
|
|
unmount-outer
|
|
closed-validator
|
|
)
|
|
local -a open_steps=(
|
|
sudo
|
|
context
|
|
confirm-open
|
|
'device-validator(--expect-device-ready)'
|
|
revalidate-context-device
|
|
mount-outer
|
|
validate-layout-kdbx-container
|
|
validate-allocation-and-ntfs-attributes
|
|
validate-outer-metadata
|
|
collision-check
|
|
create-loop
|
|
luks-open
|
|
mount-inner
|
|
validate-inner-metadata-marker-capacity
|
|
'open-validator(--expect-open)'
|
|
)
|
|
local -a close_steps=(
|
|
sudo
|
|
context
|
|
confirm-close
|
|
'open-validator(--expect-open)'
|
|
validate-metadata-and-full-lineage
|
|
busy-process-query-pass-1
|
|
busy-process-query-pass-2
|
|
sync-inner-filesystem
|
|
unmount-inner
|
|
close-main-mapping
|
|
detach-original-loop
|
|
unmount-outer
|
|
'closed-validator(--expect-closed)'
|
|
)
|
|
|
|
log="${fixture_root}/task5a-dispatch.log"
|
|
_k3slr_lifecycle_step() {
|
|
(( $# == 1 )) || return 1
|
|
printf 'hook:%s\n' "$1" >>"$log"
|
|
}
|
|
_k3slr_lifecycle_action() {
|
|
case "$1" in
|
|
prepare)
|
|
(( $# == 3 )) || return 1
|
|
[[ "$3" == _k3slr_prepare_invocation ]] || return 1
|
|
;;
|
|
open|close)
|
|
(( $# == 2 )) || return 1
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
printf 'action:%s\n' "$2" >>"$log"
|
|
[[ -z "${failure_step:-}" || "$2" != "$failure_step" ]]
|
|
}
|
|
task5a_fixture_header_provider() {
|
|
return 1
|
|
}
|
|
|
|
for mode in prepare open close; do
|
|
local -a steps=()
|
|
case "$mode" in
|
|
prepare) steps=("${prepare_steps[@]}") ;;
|
|
open) steps=("${open_steps[@]}") ;;
|
|
close) steps=("${close_steps[@]}") ;;
|
|
esac
|
|
: >"$log"
|
|
failure_step=''
|
|
assert_succeeds _k3slr_lifecycle_dispatch "$mode" task5a_fixture_header_provider
|
|
expected=''
|
|
for step in "${steps[@]}"; do
|
|
[[ -z "$expected" ]] || expected+=$'\n'
|
|
expected+="hook:${step}"$'\n'"action:${step}"
|
|
done
|
|
actual="$(<"$log")"
|
|
assert_eq "$expected" "$actual" "$mode emits the independent exact lifecycle order"
|
|
|
|
case "$mode" in
|
|
prepare) failure_step=create-loop ;;
|
|
open) failure_step=collision-check ;;
|
|
close) failure_step=unmount-inner ;;
|
|
esac
|
|
: >"$log"
|
|
assert_fails _k3slr_lifecycle_dispatch "$mode" task5a_fixture_header_provider
|
|
expected=''
|
|
for step in "${steps[@]}"; do
|
|
[[ -z "$expected" ]] || expected+=$'\n'
|
|
expected+="hook:${step}"$'\n'"action:${step}"
|
|
[[ "$step" == "$failure_step" ]] && break
|
|
done
|
|
actual="$(<"$log")"
|
|
assert_eq "$expected" "$actual" \
|
|
"$mode failure includes its own hook/action and dispatches no later action"
|
|
done
|
|
)
|
|
|
|
task5a_production_actions_fail_closed() (
|
|
local lifecycle step command_calls=0 action_rc=0 probe_rc=0 record=''
|
|
local -a prepare_unavailable=(
|
|
context estimate-phase capacity-preflight confirm-prepare install-packages
|
|
feasibility revalidate-device mount-outer revalidate-capacity create-layout
|
|
create-kdbx create-container validate-allocation create-loop luks-format luks-open
|
|
mkfs-ext4 mount-inner write-marker-metadata header-proof close-inner close-mapping
|
|
detach-loop unmount-outer closed-validator
|
|
)
|
|
local -a open_unavailable=(
|
|
sudo context confirm-open 'device-validator(--expect-device-ready)'
|
|
revalidate-context-device mount-outer validate-layout-kdbx-container
|
|
validate-allocation-and-ntfs-attributes validate-outer-metadata collision-check
|
|
create-loop luks-open mount-inner validate-inner-metadata-marker-capacity
|
|
'open-validator(--expect-open)'
|
|
)
|
|
local -a close_unavailable=(
|
|
sudo context confirm-close 'open-validator(--expect-open)'
|
|
validate-metadata-and-full-lineage busy-process-query-pass-1
|
|
busy-process-query-pass-2 sync-inner-filesystem unmount-inner close-main-mapping
|
|
detach-original-loop unmount-outer 'closed-validator(--expect-closed)'
|
|
)
|
|
_k3slr_command() {
|
|
(( command_calls += 1 ))
|
|
return 97
|
|
}
|
|
task5b_unavailable_probe_payload() {
|
|
local lifecycle="${1-}" step="${2-}" action_rc=0 command_calls=0
|
|
(( $# == 2 )) || return 1
|
|
if [[ "$lifecycle" == prepare ]]; then
|
|
_k3slr_lifecycle_action prepare "$step" _k3slr_prepare_invocation ||
|
|
action_rc=$?
|
|
else
|
|
_k3slr_lifecycle_action "$lifecycle" "$step" || action_rc=$?
|
|
fi
|
|
task5b_production_probe_emit_ok unavailable "$action_rc" "$command_calls"
|
|
}
|
|
task5b_production_probe_adversary_matrix || return 1
|
|
task5b_production_probe_static_graph_is_closed unavailable || return 1
|
|
for lifecycle in prepare open close; do
|
|
local -a actions=()
|
|
case "$lifecycle" in
|
|
prepare) actions=("${prepare_unavailable[@]}") ;;
|
|
open) actions=("${open_unavailable[@]}") ;;
|
|
close) actions=("${close_unavailable[@]}") ;;
|
|
esac
|
|
for step in "${actions[@]}"; do
|
|
record=''
|
|
probe_rc=0
|
|
record="$(
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm unavailable || return 1
|
|
task5b_unavailable_probe_payload "$lifecycle" "$step"
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" \
|
|
"historical unavailable probe completes safely: $lifecycle/$step"
|
|
assert_eq 'TASK5B_PROBE_OK|unavailable|1|0' "$record" \
|
|
"historical unavailable action is fail-closed before commands: $lifecycle/$step"
|
|
done
|
|
done
|
|
)
|
|
|
|
task5a_unowned_capture_cleanup_is_rejected() (
|
|
local description="$1" capture_directory capture_record
|
|
local rm_rc=0 rmdir_rc=0 records_preserved=0 directory_preserved=0
|
|
capture_directory="$(/usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
[[ "$capture_directory" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ &&
|
|
-d "$capture_directory" && ! -L "$capture_directory" ]] || return 1
|
|
: >"${capture_directory}/stdout"
|
|
: >"${capture_directory}/stderr"
|
|
|
|
_k3slr_command /usr/bin/rm -- "${capture_directory}/stdout" \
|
|
"${capture_directory}/stderr" >/dev/null 2>&1 || rm_rc=$?
|
|
if [[ -f "${capture_directory}/stdout" && ! -L "${capture_directory}/stdout" &&
|
|
-f "${capture_directory}/stderr" && ! -L "${capture_directory}/stderr" ]]; then
|
|
records_preserved=1
|
|
fi
|
|
for capture_record in "${capture_directory}/stdout" "${capture_directory}/stderr"; do
|
|
if [[ -f "$capture_record" && ! -L "$capture_record" ]]; then
|
|
/usr/bin/rm -- "$capture_record" || return 1
|
|
elif [[ -e "$capture_record" || -L "$capture_record" ]]; then
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
_k3slr_command /usr/bin/rmdir -- "$capture_directory" \
|
|
>/dev/null 2>&1 || rmdir_rc=$?
|
|
if [[ -d "$capture_directory" && ! -L "$capture_directory" ]]; then
|
|
directory_preserved=1
|
|
/usr/bin/rmdir -- "$capture_directory" || return 1
|
|
elif [[ -e "$capture_directory" || -L "$capture_directory" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
assert_eq 97 "$rm_rc" "$description rejects unowned record deletion before execution"
|
|
assert_eq 1 "$records_preserved" "$description preserves unowned capture records"
|
|
assert_eq 97 "$rmdir_rc" "$description rejects unowned directory deletion before execution"
|
|
assert_eq 1 "$directory_preserved" "$description preserves the unowned capture directory"
|
|
)
|
|
|
|
task5a_capture_path_is_safe() {
|
|
(( $# == 1 )) && [[ "$1" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ ]]
|
|
}
|
|
|
|
task5a_capture_directory_identity() {
|
|
local capture_directory="${1-}" identity
|
|
(( $# == 1 )) && task5a_capture_path_is_safe "$capture_directory" &&
|
|
[[ -d "$capture_directory" && ! -L "$capture_directory" ]] || return 97
|
|
identity="$(/usr/bin/stat --format='%d:%i' -- "$capture_directory")" || return 97
|
|
[[ "$identity" =~ ^[0-9]+:[0-9]+$ ]] || return 97
|
|
printf '%s\n' "$identity"
|
|
}
|
|
|
|
task5a_capture_log_read_exact() {
|
|
local log="${1-}" destination_name="${2-}" size line probe
|
|
local row_count=0
|
|
(( $# == 2 )) && [[ -f "$log" && ! -L "$log" ]] &&
|
|
[[ "$destination_name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || return 97
|
|
local -n destination="$destination_name"
|
|
destination=()
|
|
size="$(/usr/bin/stat --format='%s' -- "$log")" || return 97
|
|
[[ "$size" =~ ^[0-9]+$ ]] && (( size <= 1048576 )) || return 97
|
|
if IFS= read -r -d '' probe <"$log"; then
|
|
return 97
|
|
fi
|
|
while :; do
|
|
line=''
|
|
if IFS= read -r line; then
|
|
row_count=$((row_count + 1))
|
|
(( row_count <= 4096 && ${#line} > 0 && ${#line} <= 512 )) || return 97
|
|
[[ "$line" != *$'\r'* ]] || return 97
|
|
destination+=("$line")
|
|
else
|
|
[[ -z "$line" ]] || return 97
|
|
break
|
|
fi
|
|
done <"$log"
|
|
}
|
|
|
|
task5a_capture_ownership_log_require() {
|
|
local ownership_log="${1-}" capture_directory="${2-}" expected_phase="${3-}"
|
|
local row event path identity active_path='' active_identity='' active_phase=''
|
|
local target_phase=absent target_identity=''
|
|
local -a rows=()
|
|
local -A phase_by_path=() identity_by_path=()
|
|
(( $# == 3 )) || return 97
|
|
case "$expected_phase" in
|
|
complete) [[ -z "$capture_directory" ]] || return 97 ;;
|
|
absent|created|records-removed|removed)
|
|
task5a_capture_path_is_safe "$capture_directory" || return 97
|
|
;;
|
|
*) return 97 ;;
|
|
esac
|
|
task5a_capture_log_read_exact "$ownership_log" rows || return 97
|
|
for row in "${rows[@]}"; do
|
|
[[ "$row" =~ ^(created|records-removed|removed)\|(/tmp/k3slr-capture\.[A-Za-z0-9]{8})\|([0-9]+:[0-9]+)$ ]] || return 97
|
|
event="${BASH_REMATCH[1]}"
|
|
path="${BASH_REMATCH[2]}"
|
|
identity="${BASH_REMATCH[3]}"
|
|
case "$event" in
|
|
created)
|
|
[[ -z "$active_path" && -z "${phase_by_path[$path]+present}" ]] || return 97
|
|
active_path="$path"
|
|
active_identity="$identity"
|
|
active_phase=created
|
|
phase_by_path["$path"]=created
|
|
identity_by_path["$path"]="$identity"
|
|
;;
|
|
records-removed)
|
|
[[ "$active_path" == "$path" && "$active_phase" == created &&
|
|
"$active_identity" == "$identity" ]] || return 97
|
|
active_phase=records-removed
|
|
phase_by_path["$path"]=records-removed
|
|
;;
|
|
removed)
|
|
[[ "$active_path" == "$path" && "$active_phase" == records-removed &&
|
|
"$active_identity" == "$identity" ]] || return 97
|
|
phase_by_path["$path"]=removed
|
|
active_path=''
|
|
active_identity=''
|
|
active_phase=''
|
|
;;
|
|
esac
|
|
done
|
|
if [[ -n "$capture_directory" && -n "${phase_by_path[$capture_directory]+present}" ]]; then
|
|
target_phase="${phase_by_path[$capture_directory]}"
|
|
target_identity="${identity_by_path[$capture_directory]}"
|
|
fi
|
|
case "$expected_phase" in
|
|
complete) [[ -z "$active_path" ]] ;;
|
|
absent) [[ "$target_phase" == absent && -z "$active_path" ]] ;;
|
|
created|records-removed)
|
|
[[ "$target_phase" == "$expected_phase" && "$active_path" == "$capture_directory" ]]
|
|
;;
|
|
removed) [[ "$target_phase" == removed && -z "$active_path" ]] ;;
|
|
esac || return 97
|
|
[[ "$expected_phase" == complete || "$expected_phase" == absent ]] ||
|
|
printf '%s\n' "$target_identity"
|
|
}
|
|
|
|
task5a_capture_event_log_require() {
|
|
local event_log="${1-}" capture_directory="${2-}" expected_phase="${3-}"
|
|
local row event path command_id active_path='' active_phase='' target_phase=absent
|
|
local command_count=0
|
|
local -a rows=()
|
|
local -A phase_by_path=()
|
|
(( $# == 3 )) || return 97
|
|
case "$expected_phase" in
|
|
complete) [[ -z "$capture_directory" ]] || return 97 ;;
|
|
absent|created|records-removed|removed)
|
|
task5a_capture_path_is_safe "$capture_directory" || return 97
|
|
;;
|
|
*) return 97 ;;
|
|
esac
|
|
task5a_capture_log_read_exact "$event_log" rows || return 97
|
|
for row in "${rows[@]}"; do
|
|
if [[ "$row" =~ ^(mktemp|rm|rmdir)\|(/tmp/k3slr-capture\.[A-Za-z0-9]{8})$ ]]; then
|
|
event="${BASH_REMATCH[1]}"
|
|
path="${BASH_REMATCH[2]}"
|
|
case "$event" in
|
|
mktemp)
|
|
[[ -z "$active_path" && -z "${phase_by_path[$path]+present}" ]] || return 97
|
|
active_path="$path"
|
|
active_phase=created
|
|
command_count=0
|
|
phase_by_path["$path"]=created
|
|
;;
|
|
rm)
|
|
[[ "$active_path" == "$path" && "$active_phase" == created ]] || return 97
|
|
active_phase=records-removed
|
|
phase_by_path["$path"]=records-removed
|
|
;;
|
|
rmdir)
|
|
[[ "$active_path" == "$path" && "$active_phase" == records-removed ]] || return 97
|
|
phase_by_path["$path"]=removed
|
|
active_path=''
|
|
active_phase=''
|
|
command_count=0
|
|
;;
|
|
esac
|
|
elif [[ "$row" =~ ^command\|([A-Za-z0-9][A-Za-z0-9._:-]{0,63})$ ]]; then
|
|
command_id="${BASH_REMATCH[1]}"
|
|
[[ -n "$command_id" && -n "$active_path" && "$active_phase" == created &&
|
|
"$command_count" == 0 ]] || return 97
|
|
command_count=1
|
|
else
|
|
return 97
|
|
fi
|
|
done
|
|
if [[ -n "$capture_directory" && -n "${phase_by_path[$capture_directory]+present}" ]]; then
|
|
target_phase="${phase_by_path[$capture_directory]}"
|
|
fi
|
|
case "$expected_phase" in
|
|
complete) [[ -z "$active_path" ]] ;;
|
|
absent) [[ "$target_phase" == absent && -z "$active_path" ]] ;;
|
|
created|records-removed)
|
|
[[ "$target_phase" == "$expected_phase" && "$active_path" == "$capture_directory" ]]
|
|
;;
|
|
removed) [[ "$target_phase" == removed && -z "$active_path" ]] ;;
|
|
esac || return 97
|
|
}
|
|
|
|
task5a_capture_ledgers_require_coherent() {
|
|
local ownership_log="${1-}" event_log="${2-}" capture_directory="${3-}"
|
|
local expected_phase="${4-}" ownership_identity row phase path entry_index
|
|
local -a ownership_rows=() event_rows=()
|
|
local -a ownership_entries=() event_entries=()
|
|
(( $# == 4 )) || return 97
|
|
|
|
ownership_identity="$(task5a_capture_ownership_log_require \
|
|
"$ownership_log" "$capture_directory" "$expected_phase")" || return 97
|
|
task5a_capture_event_log_require \
|
|
"$event_log" "$capture_directory" "$expected_phase" || return 97
|
|
|
|
task5a_capture_log_read_exact "$ownership_log" ownership_rows || return 97
|
|
task5a_capture_log_read_exact "$event_log" event_rows || return 97
|
|
for row in "${ownership_rows[@]}"; do
|
|
[[ "$row" =~ ^(created|records-removed|removed)\|(/tmp/k3slr-capture\.[A-Za-z0-9]{8})\|[0-9]+:[0-9]+$ ]] || return 97
|
|
ownership_entries+=("${BASH_REMATCH[1]}|${BASH_REMATCH[2]}")
|
|
done
|
|
for row in "${event_rows[@]}"; do
|
|
if [[ "$row" =~ ^(mktemp|rm|rmdir)\|(/tmp/k3slr-capture\.[A-Za-z0-9]{8})$ ]]; then
|
|
case "${BASH_REMATCH[1]}" in
|
|
mktemp) phase=created ;;
|
|
rm) phase=records-removed ;;
|
|
rmdir) phase=removed ;;
|
|
*) return 97 ;;
|
|
esac
|
|
path="${BASH_REMATCH[2]}"
|
|
event_entries+=("${phase}|${path}")
|
|
elif [[ "$row" =~ ^command\|[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$ ]]; then
|
|
continue
|
|
else
|
|
return 97
|
|
fi
|
|
done
|
|
(( ${#ownership_entries[@]} == ${#event_entries[@]} )) || return 97
|
|
for (( entry_index=0; entry_index<${#ownership_entries[@]}; entry_index++ )); do
|
|
[[ "${ownership_entries[entry_index]}" == "${event_entries[entry_index]}" ]] ||
|
|
return 97
|
|
done
|
|
case "$expected_phase" in
|
|
created|records-removed|removed) printf '%s\n' "$ownership_identity" ;;
|
|
esac
|
|
}
|
|
|
|
task5a_capture_ledgers_allow_registration() {
|
|
local ownership_log="${1-}" event_log="${2-}"
|
|
(( $# == 2 )) || return 97
|
|
task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" '' complete
|
|
}
|
|
|
|
task5a_capture_ledger_register() {
|
|
local ownership_log="${1-}" event_log="${2-}" capture_directory="${3-}"
|
|
local current_identity
|
|
(( $# == 3 )) && task5a_capture_path_is_safe "$capture_directory" || return 97
|
|
task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" "$capture_directory" absent || return 97
|
|
current_identity="$(task5a_capture_directory_identity "$capture_directory")" || return 97
|
|
printf 'created|%s|%s\n' "$capture_directory" "$current_identity" >>"$ownership_log" ||
|
|
return 97
|
|
printf 'mktemp|%s\n' "$capture_directory" >>"$event_log" || return 97
|
|
task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" "$capture_directory" created >/dev/null || return 97
|
|
}
|
|
|
|
task5a_capture_ledger_require() {
|
|
task5a_capture_ownership_log_require "$@"
|
|
}
|
|
|
|
task5a_capture_event_phase_is() {
|
|
task5a_capture_event_log_require "$@"
|
|
}
|
|
|
|
task5a_remove_owned_capture_records() {
|
|
local ownership_log="${1-}" event_log="${2-}" capture_directory pinned_identity
|
|
local current_identity
|
|
shift 2 || return 97
|
|
(( $# == 4 )) && [[ "$1" == /usr/bin/rm && "$2" == -- &&
|
|
"$3" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}/stdout$ ]] || return 97
|
|
capture_directory="${3%/stdout}"
|
|
[[ "$4" == "${capture_directory}/stderr" && -d "$capture_directory" &&
|
|
! -L "$capture_directory" && -f "$3" && ! -L "$3" &&
|
|
-f "$4" && ! -L "$4" ]] || return 97
|
|
pinned_identity="$(task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" "$capture_directory" created)" || return 97
|
|
current_identity="$(task5a_capture_directory_identity "$capture_directory")" || return 97
|
|
[[ "$current_identity" == "$pinned_identity" ]] || return 97
|
|
"$@" || return $?
|
|
[[ ! -e "$3" && ! -L "$3" && ! -e "$4" && ! -L "$4" ]] || return 1
|
|
printf 'records-removed|%s|%s\n' "$capture_directory" "$pinned_identity" \
|
|
>>"$ownership_log" || return 97
|
|
printf 'rm|%s\n' "$capture_directory" >>"$event_log" || return 97
|
|
task5a_capture_ledgers_require_coherent "$ownership_log" "$event_log" \
|
|
"$capture_directory" records-removed >/dev/null || return 97
|
|
}
|
|
|
|
task5a_remove_owned_capture_directory() {
|
|
local ownership_log="${1-}" event_log="${2-}" capture_directory pinned_identity
|
|
local current_identity
|
|
shift 2 || return 97
|
|
(( $# == 3 )) && [[ "$1" == /usr/bin/rmdir && "$2" == -- ]] || return 97
|
|
capture_directory="$3"
|
|
task5a_capture_path_is_safe "$capture_directory" &&
|
|
[[ -d "$capture_directory" && ! -L "$capture_directory" ]] || return 97
|
|
pinned_identity="$(task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" "$capture_directory" records-removed)" || return 97
|
|
current_identity="$(task5a_capture_directory_identity "$capture_directory")" || return 97
|
|
[[ "$current_identity" == "$pinned_identity" ]] || return 97
|
|
"$@" || return $?
|
|
[[ ! -e "$capture_directory" && ! -L "$capture_directory" ]] || return 1
|
|
printf 'removed|%s|%s\n' "$capture_directory" "$pinned_identity" \
|
|
>>"$ownership_log" || return 97
|
|
printf 'rmdir|%s\n' "$capture_directory" >>"$event_log" || return 97
|
|
task5a_capture_ledgers_require_coherent "$ownership_log" "$event_log" \
|
|
"$capture_directory" removed >/dev/null || return 97
|
|
}
|
|
|
|
task5a_cleanup_capture_probe_path() {
|
|
local capture_directory="${1-}" capture_record
|
|
(( $# == 1 )) && task5a_capture_path_is_safe "$capture_directory" || return 1
|
|
if [[ ! -e "$capture_directory" && ! -L "$capture_directory" ]]; then
|
|
return 0
|
|
fi
|
|
[[ -d "$capture_directory" && ! -L "$capture_directory" ]] || return 1
|
|
for capture_record in "${capture_directory}/stdout" "${capture_directory}/stderr"; do
|
|
if [[ -f "$capture_record" && ! -L "$capture_record" ]]; then
|
|
/usr/bin/rm -- "$capture_record" || return 1
|
|
elif [[ -e "$capture_record" || -L "$capture_record" ]]; then
|
|
return 1
|
|
fi
|
|
done
|
|
/usr/bin/rmdir -- "$capture_directory"
|
|
}
|
|
|
|
task5a_capture_cleanup_phase_and_identity_is_enforced() (
|
|
local description="$1" early_path repeat_path drift_path replacement_path
|
|
local early_rmdir_rc=0 early_rm_rc=0 early_finish_rmdir_rc=0
|
|
local repeat_rm_rc=0 repeat_second_rm_rc=0 repeat_rmdir_rc=0
|
|
local reuse_rm_rc=0 reuse_rmdir_rc=0 drift_rm_rc=0 drift_rmdir_rc=0
|
|
local early_preserved=0 early_completed=0 repeat_records_preserved=0 repeat_removed=0
|
|
local reuse_records_preserved=0 reuse_directory_preserved=0
|
|
local drift_records_preserved=0 drift_directory_preserved=0
|
|
local drift_identity replacement_identity
|
|
|
|
early_path="$(_k3slr_command /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
task5a_capture_path_is_safe "$early_path" &&
|
|
[[ -d "$early_path" && ! -L "$early_path" ]] || return 1
|
|
_k3slr_command /usr/bin/rmdir -- "$early_path" \
|
|
>/dev/null 2>&1 || early_rmdir_rc=$?
|
|
[[ -d "$early_path" && ! -L "$early_path" ]] && early_preserved=1
|
|
: >"${early_path}/stdout"
|
|
: >"${early_path}/stderr"
|
|
_k3slr_command /usr/bin/rm -- "${early_path}/stdout" \
|
|
"${early_path}/stderr" >/dev/null 2>&1 || early_rm_rc=$?
|
|
_k3slr_command /usr/bin/rmdir -- "$early_path" \
|
|
>/dev/null 2>&1 || early_finish_rmdir_rc=$?
|
|
[[ ! -e "$early_path" && ! -L "$early_path" ]] && early_completed=1
|
|
task5a_cleanup_capture_probe_path "$early_path" || return 1
|
|
|
|
repeat_path="$(_k3slr_command /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
task5a_capture_path_is_safe "$repeat_path" &&
|
|
[[ -d "$repeat_path" && ! -L "$repeat_path" ]] || return 1
|
|
: >"${repeat_path}/stdout"
|
|
: >"${repeat_path}/stderr"
|
|
_k3slr_command /usr/bin/rm -- "${repeat_path}/stdout" \
|
|
"${repeat_path}/stderr" >/dev/null 2>&1 || repeat_rm_rc=$?
|
|
: >"${repeat_path}/stdout"
|
|
: >"${repeat_path}/stderr"
|
|
_k3slr_command /usr/bin/rm -- "${repeat_path}/stdout" \
|
|
"${repeat_path}/stderr" >/dev/null 2>&1 || repeat_second_rm_rc=$?
|
|
if [[ -f "${repeat_path}/stdout" && ! -L "${repeat_path}/stdout" &&
|
|
-f "${repeat_path}/stderr" && ! -L "${repeat_path}/stderr" ]]; then
|
|
repeat_records_preserved=1
|
|
fi
|
|
/usr/bin/rm -- "${repeat_path}/stdout" "${repeat_path}/stderr" || return 1
|
|
_k3slr_command /usr/bin/rmdir -- "$repeat_path" \
|
|
>/dev/null 2>&1 || repeat_rmdir_rc=$?
|
|
if [[ ! -e "$repeat_path" && ! -L "$repeat_path" ]]; then
|
|
repeat_removed=1
|
|
/usr/bin/mkdir -- "$repeat_path" || return 1
|
|
fi
|
|
[[ -d "$repeat_path" && ! -L "$repeat_path" ]] || return 1
|
|
: >"${repeat_path}/stdout"
|
|
: >"${repeat_path}/stderr"
|
|
_k3slr_command /usr/bin/rm -- "${repeat_path}/stdout" \
|
|
"${repeat_path}/stderr" >/dev/null 2>&1 || reuse_rm_rc=$?
|
|
if [[ -f "${repeat_path}/stdout" && ! -L "${repeat_path}/stdout" &&
|
|
-f "${repeat_path}/stderr" && ! -L "${repeat_path}/stderr" ]]; then
|
|
reuse_records_preserved=1
|
|
fi
|
|
/usr/bin/rm -- "${repeat_path}/stdout" "${repeat_path}/stderr" || return 1
|
|
_k3slr_command /usr/bin/rmdir -- "$repeat_path" \
|
|
>/dev/null 2>&1 || reuse_rmdir_rc=$?
|
|
[[ -d "$repeat_path" && ! -L "$repeat_path" ]] && reuse_directory_preserved=1
|
|
task5a_cleanup_capture_probe_path "$repeat_path" || return 1
|
|
|
|
drift_path="$(_k3slr_command /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
replacement_path="$(/usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
task5a_capture_path_is_safe "$drift_path" &&
|
|
task5a_capture_path_is_safe "$replacement_path" || return 1
|
|
drift_identity="$(task5a_capture_directory_identity "$drift_path")" || return 1
|
|
replacement_identity="$(task5a_capture_directory_identity "$replacement_path")" || return 1
|
|
[[ "$drift_identity" != "$replacement_identity" ]] || return 1
|
|
/usr/bin/rmdir -- "$drift_path" || return 1
|
|
/usr/bin/mv -- "$replacement_path" "$drift_path" || return 1
|
|
[[ -d "$drift_path" && ! -L "$drift_path" ]] || return 1
|
|
: >"${drift_path}/stdout"
|
|
: >"${drift_path}/stderr"
|
|
_k3slr_command /usr/bin/rm -- "${drift_path}/stdout" \
|
|
"${drift_path}/stderr" >/dev/null 2>&1 || drift_rm_rc=$?
|
|
if [[ -f "${drift_path}/stdout" && ! -L "${drift_path}/stdout" &&
|
|
-f "${drift_path}/stderr" && ! -L "${drift_path}/stderr" ]]; then
|
|
drift_records_preserved=1
|
|
fi
|
|
/usr/bin/rm -- "${drift_path}/stdout" "${drift_path}/stderr" || return 1
|
|
_k3slr_command /usr/bin/rmdir -- "$drift_path" \
|
|
>/dev/null 2>&1 || drift_rmdir_rc=$?
|
|
[[ -d "$drift_path" && ! -L "$drift_path" ]] && drift_directory_preserved=1
|
|
task5a_cleanup_capture_probe_path "$drift_path" || return 1
|
|
|
|
assert_eq 97 "$early_rmdir_rc" "$description rejects rmdir before record removal"
|
|
assert_eq 1 "$early_preserved" "$description preserves an early-rmdir owned directory"
|
|
assert_eq 0 "$early_rm_rc" "$description completes record removal after early rmdir rejection"
|
|
assert_eq 0 "$early_finish_rmdir_rc" \
|
|
"$description completes rmdir after early rmdir rejection"
|
|
assert_eq 1 "$early_completed" "$description completes the early-rmdir capture lifecycle"
|
|
assert_eq 0 "$repeat_rm_rc" "$description permits the first owned record removal"
|
|
assert_eq 97 "$repeat_second_rm_rc" "$description rejects repeated owned record removal"
|
|
assert_eq 1 "$repeat_records_preserved" "$description preserves records on repeated rm"
|
|
assert_eq 0 "$repeat_rmdir_rc" "$description permits rmdir after one successful rm"
|
|
assert_eq 1 "$repeat_removed" "$description removes a completed owned directory"
|
|
assert_eq 97 "$reuse_rm_rc" "$description rejects completed-path reuse for rm"
|
|
assert_eq 1 "$reuse_records_preserved" "$description preserves reused-path records"
|
|
assert_eq 97 "$reuse_rmdir_rc" "$description rejects completed-path reuse for rmdir"
|
|
assert_eq 1 "$reuse_directory_preserved" "$description preserves a reused directory"
|
|
assert_eq 97 "$drift_rm_rc" "$description rejects identity drift for rm"
|
|
assert_eq 1 "$drift_records_preserved" "$description preserves identity-drifted records"
|
|
assert_eq 97 "$drift_rmdir_rc" "$description rejects identity drift for rmdir"
|
|
assert_eq 1 "$drift_directory_preserved" "$description preserves an identity-drifted directory"
|
|
)
|
|
|
|
task5a_global_corrupt_capture_ledgers_are_rejected() (
|
|
local ownership_log="$1" event_log="$2" description="$3" target_path foreign_path
|
|
local rm_rc=0 records_preserved=0
|
|
local -a foreign_paths=(
|
|
/tmp/k3slr-capture.R5A00001
|
|
/tmp/k3slr-capture.R5A00002
|
|
/tmp/k3slr-capture.R5A00003
|
|
/tmp/k3slr-capture.R5A00004
|
|
)
|
|
target_path="$(_k3slr_command /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
task5a_capture_path_is_safe "$target_path" &&
|
|
[[ -d "$target_path" && ! -L "$target_path" ]] || return 1
|
|
for foreign_path in "${foreign_paths[@]}"; do
|
|
[[ "$target_path" != "$foreign_path" ]] || return 1
|
|
done
|
|
: >"${target_path}/stdout"
|
|
: >"${target_path}/stderr"
|
|
|
|
printf '%s\n' \
|
|
'removed|/tmp/k3slr-capture.R5A00001|101:101' \
|
|
'created|/tmp/k3slr-capture.R5A00002|102:102' \
|
|
'created|/tmp/k3slr-capture.R5A00002|102:102' \
|
|
'created|/tmp/k3slr-capture.R5A00003|103:103' \
|
|
'records-removed|/tmp/k3slr-capture.R5A00003|104:104' \
|
|
>>"$ownership_log"
|
|
printf '%s' 'created|/tmp/k3slr-capture.R5A00004|105:105' >>"$ownership_log"
|
|
printf '%s\n' \
|
|
'rm|/tmp/k3slr-capture.R5A00001' \
|
|
'mktemp|/tmp/k3slr-capture.R5A00002' \
|
|
'mktemp|/tmp/k3slr-capture.R5A00002' \
|
|
'malformed-event-row' \
|
|
>>"$event_log"
|
|
printf '%s' 'command|unterminated-probe' >>"$event_log"
|
|
|
|
_k3slr_command /usr/bin/rm -- "${target_path}/stdout" \
|
|
"${target_path}/stderr" >/dev/null 2>&1 || rm_rc=$?
|
|
if [[ -f "${target_path}/stdout" && ! -L "${target_path}/stdout" &&
|
|
-f "${target_path}/stderr" && ! -L "${target_path}/stderr" ]]; then
|
|
records_preserved=1
|
|
fi
|
|
task5a_cleanup_capture_probe_path "$target_path" || return 1
|
|
|
|
assert_eq 97 "$rm_rc" "$description rejects globally corrupt ledgers before deletion"
|
|
assert_eq 1 "$records_preserved" "$description preserves records for global ledger corruption"
|
|
)
|
|
|
|
task5a_cross_ledger_coherence_is_enforced() (
|
|
local ownership_log="$1" event_log="$2" path_log="$3" description="$4"
|
|
local registration_output='' registration_rc=0 registration_orphan=0
|
|
local rm_target rmdir_target target_identity rm_rc=0 rmdir_rc=0
|
|
local rm_preserved=0 rmdir_preserved=0 path_index
|
|
local -a registration_paths_before=() registration_paths_after=()
|
|
local -a path_log_before=() path_log_after=()
|
|
|
|
: >"$ownership_log"
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R6OWN001|601:701' \
|
|
'records-removed|/tmp/k3slr-capture.R6OWN001|601:701' \
|
|
'removed|/tmp/k3slr-capture.R6OWN001|601:701' >"$ownership_log"
|
|
printf '%s\n' \
|
|
'mktemp|/tmp/k3slr-capture.R6EVT001' \
|
|
'rm|/tmp/k3slr-capture.R6EVT001' \
|
|
'rmdir|/tmp/k3slr-capture.R6EVT001' >"$event_log"
|
|
shopt -s nullglob
|
|
registration_paths_before=(/tmp/k3slr-capture.????????)
|
|
mapfile -t path_log_before <"$path_log"
|
|
registration_output="$(_k3slr_command /usr/bin/mktemp --directory \
|
|
--tmpdir=/tmp k3slr-capture.XXXXXXXX)" || registration_rc=$?
|
|
registration_paths_after=(/tmp/k3slr-capture.????????)
|
|
mapfile -t path_log_after <"$path_log"
|
|
if (( ${#registration_paths_before[@]} != ${#registration_paths_after[@]} )); then
|
|
registration_orphan=1
|
|
else
|
|
for (( path_index=0; path_index<${#registration_paths_before[@]}; path_index++ )); do
|
|
if [[ "${registration_paths_before[path_index]}" != \
|
|
"${registration_paths_after[path_index]}" ]]; then
|
|
registration_orphan=1
|
|
fi
|
|
done
|
|
fi
|
|
if [[ -n "$registration_output" ]]; then
|
|
task5a_capture_path_is_safe "$registration_output" || return 1
|
|
task5a_cleanup_capture_probe_path "$registration_output" || return 1
|
|
fi
|
|
|
|
rm_target="$(/usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
target_identity="$(task5a_capture_directory_identity "$rm_target")" || return 1
|
|
: >"${rm_target}/stdout"
|
|
: >"${rm_target}/stderr"
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R6OWN002|602:702' \
|
|
'records-removed|/tmp/k3slr-capture.R6OWN002|602:702' \
|
|
'removed|/tmp/k3slr-capture.R6OWN002|602:702' \
|
|
"created|${rm_target}|${target_identity}" >"$ownership_log"
|
|
printf '%s\n' \
|
|
'mktemp|/tmp/k3slr-capture.R6EVT002' \
|
|
'rm|/tmp/k3slr-capture.R6EVT002' \
|
|
'rmdir|/tmp/k3slr-capture.R6EVT002' \
|
|
"mktemp|${rm_target}" >"$event_log"
|
|
_k3slr_command /usr/bin/rm -- "${rm_target}/stdout" \
|
|
"${rm_target}/stderr" >/dev/null 2>&1 || rm_rc=$?
|
|
if [[ -f "${rm_target}/stdout" && ! -L "${rm_target}/stdout" &&
|
|
-f "${rm_target}/stderr" && ! -L "${rm_target}/stderr" ]]; then
|
|
rm_preserved=1
|
|
fi
|
|
task5a_cleanup_capture_probe_path "$rm_target" || return 1
|
|
|
|
rmdir_target="$(/usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
target_identity="$(task5a_capture_directory_identity "$rmdir_target")" || return 1
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R6OWN003|603:703' \
|
|
'records-removed|/tmp/k3slr-capture.R6OWN003|603:703' \
|
|
'removed|/tmp/k3slr-capture.R6OWN003|603:703' \
|
|
"created|${rmdir_target}|${target_identity}" \
|
|
"records-removed|${rmdir_target}|${target_identity}" >"$ownership_log"
|
|
printf '%s\n' \
|
|
'mktemp|/tmp/k3slr-capture.R6EVT003' \
|
|
'rm|/tmp/k3slr-capture.R6EVT003' \
|
|
'rmdir|/tmp/k3slr-capture.R6EVT003' \
|
|
"mktemp|${rmdir_target}" \
|
|
"rm|${rmdir_target}" >"$event_log"
|
|
_k3slr_command /usr/bin/rmdir -- "$rmdir_target" \
|
|
>/dev/null 2>&1 || rmdir_rc=$?
|
|
[[ -d "$rmdir_target" && ! -L "$rmdir_target" ]] && rmdir_preserved=1
|
|
task5a_cleanup_capture_probe_path "$rmdir_target" || return 1
|
|
|
|
assert_eq 97 "$registration_rc" \
|
|
"$description rejects registration for individually complete mismatched ledgers"
|
|
assert_eq '' "$registration_output" \
|
|
"$description emits no mktemp output for mismatched completed histories"
|
|
assert_eq 0 "$registration_orphan" \
|
|
"$description creates no real mktemp directory for mismatched completed histories"
|
|
assert_eq "${#path_log_before[@]}" "${#path_log_after[@]}" \
|
|
"$description does not register a path for mismatched completed histories"
|
|
assert_eq 97 "$rm_rc" \
|
|
"$description rejects rm for mismatched history plus the same active target"
|
|
assert_eq 1 "$rm_preserved" \
|
|
"$description preserves records for mismatched history plus the same active target"
|
|
assert_eq 97 "$rmdir_rc" \
|
|
"$description rejects rmdir for mismatched history plus the same active target"
|
|
assert_eq 1 "$rmdir_preserved" \
|
|
"$description preserves the directory for mismatched history plus the same active target"
|
|
)
|
|
|
|
task5a_command_append_failure_is_rejected() (
|
|
local append_target="$1" ownership_log="$2" event_log="$3" path_log="$4"
|
|
local description="$5" capture_path original_mode output='' command_rc=0
|
|
local target_preserved=0 row_index
|
|
local -a append_before=() append_after=()
|
|
local -a ownership_before=() ownership_after=()
|
|
local -a event_before=() event_after=()
|
|
local -a paths_before=() paths_after=()
|
|
shift 5 || return 1
|
|
(( $# > 0 )) && [[ -f "$append_target" && ! -L "$append_target" ]] || return 1
|
|
original_mode="$(/usr/bin/stat --format='%a' -- "$append_target")" || return 1
|
|
[[ "$original_mode" =~ ^[0-7]{3,4}$ ]] || return 1
|
|
|
|
capture_path="$(_k3slr_command /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || return 1
|
|
task5a_capture_path_is_safe "$capture_path" &&
|
|
[[ -d "$capture_path" && ! -L "$capture_path" ]] || return 1
|
|
task5a_append_failure_cleanup() {
|
|
/usr/bin/chmod "$original_mode" -- "$append_target" >/dev/null 2>&1 || :
|
|
task5a_cleanup_capture_probe_path "$capture_path" >/dev/null 2>&1 || :
|
|
}
|
|
trap task5a_append_failure_cleanup EXIT
|
|
|
|
mapfile -t append_before <"$append_target"
|
|
mapfile -t ownership_before <"$ownership_log"
|
|
mapfile -t event_before <"$event_log"
|
|
mapfile -t paths_before <"$path_log"
|
|
/usr/bin/chmod 0400 -- "$append_target" || return 1
|
|
output="$(_k3slr_command "$@" 2>/dev/null)" || command_rc=$?
|
|
/usr/bin/chmod "$original_mode" -- "$append_target" || return 1
|
|
mapfile -t append_after <"$append_target"
|
|
mapfile -t ownership_after <"$ownership_log"
|
|
mapfile -t event_after <"$event_log"
|
|
mapfile -t paths_after <"$path_log"
|
|
[[ -d "$capture_path" && ! -L "$capture_path" ]] && target_preserved=1
|
|
task5a_cleanup_capture_probe_path "$capture_path" || return 1
|
|
trap - EXIT
|
|
|
|
assert_eq 97 "$command_rc" "$description returns rc 97 when its required audit append fails"
|
|
assert_eq '' "$output" "$description does not enter the recognized command branch"
|
|
assert_eq 1 "$target_preserved" "$description preserves the active capture target"
|
|
assert_eq "${#append_before[@]}" "${#append_after[@]}" \
|
|
"$description preserves the append target row count"
|
|
for (( row_index=0; row_index<${#append_before[@]}; row_index++ )); do
|
|
assert_eq "${append_before[row_index]}" "${append_after[row_index]}" \
|
|
"$description preserves every append target row"
|
|
done
|
|
assert_eq "${#ownership_before[@]}" "${#ownership_after[@]}" \
|
|
"$description preserves the ownership ledger row count"
|
|
for (( row_index=0; row_index<${#ownership_before[@]}; row_index++ )); do
|
|
assert_eq "${ownership_before[row_index]}" "${ownership_after[row_index]}" \
|
|
"$description preserves every ownership ledger row"
|
|
done
|
|
assert_eq "${#event_before[@]}" "${#event_after[@]}" \
|
|
"$description appends no command event"
|
|
for (( row_index=0; row_index<${#event_before[@]}; row_index++ )); do
|
|
assert_eq "${event_before[row_index]}" "${event_after[row_index]}" \
|
|
"$description preserves every event ledger row"
|
|
done
|
|
assert_eq "${#paths_before[@]}" "${#paths_after[@]}" \
|
|
"$description preserves the path ledger row count"
|
|
for (( row_index=0; row_index<${#paths_before[@]}; row_index++ )); do
|
|
assert_eq "${paths_before[row_index]}" "${paths_after[row_index]}" \
|
|
"$description preserves every path ledger row"
|
|
done
|
|
)
|
|
|
|
task5a_capture_global_parser_contract() (
|
|
local ownership_log="${fixture_root}/task5a-parser-ownership.log"
|
|
local event_log="${fixture_root}/task5a-parser-events.log"
|
|
local target=/tmp/k3slr-capture.R5P00001
|
|
declare -F task5a_capture_ownership_log_require >/dev/null || return 1
|
|
declare -F task5a_capture_event_log_require >/dev/null || return 1
|
|
declare -F task5a_capture_ledgers_require_coherent >/dev/null || return 1
|
|
|
|
printf '%s\n' "created|${target}|201:301" >"$ownership_log"
|
|
printf '%s\n' "mktemp|${target}" 'command|parser-probe' >"$event_log"
|
|
assert_succeeds task5a_capture_ownership_log_require \
|
|
"$ownership_log" "$target" created >/dev/null
|
|
assert_succeeds task5a_capture_event_log_require "$event_log" "$target" created
|
|
|
|
printf 'created|%s|201:301\0\n' "$target" >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf 'created|%s|201:301\r\n' "$target" >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf '\n' >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf 'created|%s|201:301|extra\n' "$target" >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf 'malformed-ownership-row\n' >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf 'created|%s|201:301' "$target" >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf '%s\n' \
|
|
'removed|/tmp/k3slr-capture.R5P00002|202:302' \
|
|
"created|${target}|201:301" >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R5P00002|202:302' \
|
|
'created|/tmp/k3slr-capture.R5P00002|202:302' \
|
|
"created|${target}|201:301" >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R5P00002|202:302' \
|
|
'records-removed|/tmp/k3slr-capture.R5P00002|203:303' \
|
|
"created|${target}|201:301" >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R5P00002|202:302' \
|
|
"created|${target}|201:301" >"$ownership_log"
|
|
assert_fails task5a_capture_ownership_log_require "$ownership_log" "$target" created
|
|
|
|
printf 'mktemp|%s\0\n' "$target" >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
printf 'mktemp|%s\r\n' "$target" >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
printf '\n' >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
printf 'mktemp|%s|extra\n' "$target" >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
printf 'malformed-event-row\n' >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
printf 'mktemp|%s' "$target" >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
printf '%s\n' \
|
|
'rm|/tmp/k3slr-capture.R5P00002' \
|
|
"mktemp|${target}" >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
printf '%s\n' \
|
|
'mktemp|/tmp/k3slr-capture.R5P00002' \
|
|
'mktemp|/tmp/k3slr-capture.R5P00002' \
|
|
"mktemp|${target}" >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
printf '%s\n' 'command|orphan-command' "mktemp|${target}" >"$event_log"
|
|
assert_fails task5a_capture_event_log_require "$event_log" "$target" created
|
|
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R6ORD001|611:711' \
|
|
'records-removed|/tmp/k3slr-capture.R6ORD001|611:711' \
|
|
'removed|/tmp/k3slr-capture.R6ORD001|611:711' \
|
|
'created|/tmp/k3slr-capture.R6ORD002|612:712' \
|
|
'records-removed|/tmp/k3slr-capture.R6ORD002|612:712' \
|
|
'removed|/tmp/k3slr-capture.R6ORD002|612:712' >"$ownership_log"
|
|
printf '%s\n' \
|
|
'mktemp|/tmp/k3slr-capture.R6ORD002' \
|
|
'rm|/tmp/k3slr-capture.R6ORD002' \
|
|
'rmdir|/tmp/k3slr-capture.R6ORD002' \
|
|
'mktemp|/tmp/k3slr-capture.R6ORD001' \
|
|
'rm|/tmp/k3slr-capture.R6ORD001' \
|
|
'rmdir|/tmp/k3slr-capture.R6ORD001' >"$event_log"
|
|
assert_fails task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" '' complete
|
|
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R6PHS001|613:713' \
|
|
'records-removed|/tmp/k3slr-capture.R6PHS001|613:713' \
|
|
'removed|/tmp/k3slr-capture.R6PHS001|613:713' \
|
|
"created|${target}|201:301" >"$ownership_log"
|
|
printf '%s\n' \
|
|
'mktemp|/tmp/k3slr-capture.R6PHS001' \
|
|
'rm|/tmp/k3slr-capture.R6PHS001' \
|
|
'rmdir|/tmp/k3slr-capture.R6PHS001' \
|
|
"mktemp|${target}" \
|
|
"rm|${target}" >"$event_log"
|
|
assert_fails task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" "$target" created
|
|
|
|
printf '%s\n' \
|
|
'created|/tmp/k3slr-capture.R6MAT001|614:714' \
|
|
'records-removed|/tmp/k3slr-capture.R6MAT001|614:714' \
|
|
'removed|/tmp/k3slr-capture.R6MAT001|614:714' >"$ownership_log"
|
|
printf '%s\n' \
|
|
'mktemp|/tmp/k3slr-capture.R6MAT001' \
|
|
'rm|/tmp/k3slr-capture.R6MAT001' \
|
|
'rmdir|/tmp/k3slr-capture.R6MAT001' >"$event_log"
|
|
assert_succeeds task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" '' complete
|
|
printf '%s\n' \
|
|
'mktemp|/tmp/k3slr-capture.R6MAT001' \
|
|
'command|already-validated' \
|
|
'rm|/tmp/k3slr-capture.R6MAT001' \
|
|
'rmdir|/tmp/k3slr-capture.R6MAT001' >"$event_log"
|
|
assert_succeeds task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" '' complete
|
|
)
|
|
|
|
task5a_corrupt_capture_registration_is_rejected() (
|
|
local ownership_log="$1" event_log="$2" description="$3"
|
|
local created_output='' registration_rc=0
|
|
: >"$ownership_log"
|
|
: >"$event_log"
|
|
printf '%s\n' 'created|/tmp/k3slr-capture.R5R00001|401:501' >"$ownership_log"
|
|
printf '%s\n' 'mktemp|/tmp/k3slr-capture.R5R00001' >"$event_log"
|
|
created_output="$(_k3slr_command /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX)" || registration_rc=$?
|
|
if [[ -n "$created_output" ]]; then
|
|
task5a_capture_path_is_safe "$created_output" &&
|
|
[[ -d "$created_output" && ! -L "$created_output" ]] || return 1
|
|
task5a_cleanup_capture_probe_path "$created_output" || return 1
|
|
fi
|
|
assert_eq 97 "$registration_rc" \
|
|
"$description rejects registration with a foreign incomplete capture"
|
|
assert_eq '' "$created_output" "$description creates no directory for rejected registration"
|
|
)
|
|
|
|
task5a_assert_capture_ownership_complete() {
|
|
local ownership_log="$1" event_log="$2" path_log="$3" description="$4"
|
|
local capture_path pinned_identity
|
|
local -a capture_paths=() ownership_rows=()
|
|
mapfile -t capture_paths <"$path_log"
|
|
mapfile -t ownership_rows <"$ownership_log"
|
|
assert_eq "$(( ${#capture_paths[@]} * 3 ))" "${#ownership_rows[@]}" \
|
|
"$description records exactly three ownership phases per capture"
|
|
for capture_path in "${capture_paths[@]}"; do
|
|
pinned_identity="$(task5a_capture_ledger_require \
|
|
"$ownership_log" "$capture_path" removed)" || return 1
|
|
[[ "$pinned_identity" =~ ^[0-9]+:[0-9]+$ ]] || return 1
|
|
task5a_capture_event_log_require "$event_log" "$capture_path" removed || return 1
|
|
done
|
|
task5a_capture_ownership_log_require "$ownership_log" '' complete || return 1
|
|
task5a_capture_event_log_require "$event_log" '' complete || return 1
|
|
}
|
|
|
|
task5a_exact_record_and_pure_parser_contract() (
|
|
local value state total free used phase marker_hash package_output package_error package_rc
|
|
local valid_hash=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
local valid_metadata valid_metadata_hex valid_marker valid_marker_hex invalid_hex expected
|
|
local context_case='' package_case='' invalid_rc=0
|
|
local event_log="${fixture_root}/task5a-record-events.log"
|
|
local path_log="${fixture_root}/task5a-record-paths.log"
|
|
local ownership_log="${fixture_root}/task5a-record-ownership.log"
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
|
|
task5a_record_argv_equals() {
|
|
local expected_count="${1-}" argument_index
|
|
local -a compared_arguments=()
|
|
shift || return 1
|
|
[[ "$expected_count" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
compared_arguments=("$@")
|
|
(( ${#compared_arguments[@]} == 2 * expected_count )) || return 1
|
|
for (( argument_index=0; argument_index<expected_count; argument_index++ )); do
|
|
[[ "${compared_arguments[argument_index]}" == \
|
|
"${compared_arguments[expected_count + argument_index]}" ]] || return 1
|
|
done
|
|
}
|
|
|
|
task5a_assert_record_capture_lineage() {
|
|
local expected_ids_record="$1" description="$2"
|
|
local event_index capture_path expected_count
|
|
local -a expected_ids=() events=() capture_paths=()
|
|
mapfile -t expected_ids <<<"$expected_ids_record"
|
|
mapfile -t events <"$event_log"
|
|
mapfile -t capture_paths <"$path_log"
|
|
expected_count="${#expected_ids[@]}"
|
|
assert_eq "$expected_count" "${#capture_paths[@]}" \
|
|
"$description creates one capture directory per query"
|
|
assert_eq "$((expected_count * 4))" "${#events[@]}" \
|
|
"$description records complete capture lifecycle groups"
|
|
for (( event_index=0; event_index<expected_count; event_index++ )); do
|
|
capture_path="${capture_paths[event_index]}"
|
|
[[ "$capture_path" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ ]] || return 1
|
|
assert_eq "mktemp|${capture_path}" "${events[event_index * 4]}" \
|
|
"$description starts capture with the exact created path"
|
|
assert_eq "command|${expected_ids[event_index]}" \
|
|
"${events[event_index * 4 + 1]}" \
|
|
"$description preserves the exact query order"
|
|
assert_eq "rm|${capture_path}" "${events[event_index * 4 + 2]}" \
|
|
"$description removes exactly the two capture records"
|
|
assert_eq "rmdir|${capture_path}" "${events[event_index * 4 + 3]}" \
|
|
"$description removes the same capture directory"
|
|
[[ ! -e "$capture_path" && ! -L "$capture_path" ]] || return 1
|
|
done
|
|
task5a_assert_capture_ownership_complete \
|
|
"$ownership_log" "$event_log" "$path_log" "$description"
|
|
}
|
|
|
|
_k3slr_command() {
|
|
local created capture_directory command_id=rejected-command
|
|
case "${1-}" in
|
|
/usr/bin/mktemp)
|
|
task5a_record_argv_equals 4 /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX "$@" || return 97
|
|
task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" '' complete || return 97
|
|
created="$("$@")" || return 1
|
|
[[ "$created" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ &&
|
|
-d "$created" && ! -L "$created" ]] || return 97
|
|
if ! task5a_capture_ledger_register \
|
|
"$ownership_log" "$event_log" "$created"; then
|
|
task5a_cleanup_capture_probe_path "$created" || return 1
|
|
return 97
|
|
fi
|
|
printf '%s\n' "$created" >>"$path_log"
|
|
printf '%s\n' "$created"
|
|
return 0
|
|
;;
|
|
/usr/bin/rm)
|
|
task5a_remove_owned_capture_records "$ownership_log" "$event_log" "$@"
|
|
return
|
|
;;
|
|
/usr/bin/rmdir)
|
|
task5a_remove_owned_capture_directory "$ownership_log" "$event_log" "$@"
|
|
return
|
|
;;
|
|
/usr/local/bin/k3s)
|
|
task5a_record_argv_equals 4 /usr/local/bin/k3s kubectl config \
|
|
current-context "$@" || return 97
|
|
command_id=current-context
|
|
;;
|
|
/usr/bin/dpkg-query)
|
|
if task5a_record_argv_equals 4 /usr/bin/dpkg-query --show \
|
|
'--showformat=${Status}|${Version}\n' keepassxc "$@"; then
|
|
command_id=package-keepassxc
|
|
elif task5a_record_argv_equals 4 /usr/bin/dpkg-query --show \
|
|
'--showformat=${Status}|${Version}\n' cryptsetup-bin "$@"; then
|
|
command_id=package-cryptsetup-bin
|
|
else
|
|
return 97
|
|
fi
|
|
;;
|
|
*) return 97 ;;
|
|
esac
|
|
|
|
printf 'command|%s\n' "$command_id" >>"$event_log" || return 97
|
|
case "$command_id" in
|
|
current-context)
|
|
case "$context_case" in
|
|
success) printf 'default\n' ;;
|
|
missing_lf) printf default ;;
|
|
stderr) printf 'default\n'; printf 'warning\n' >&2 ;;
|
|
status) printf 'default\n'; return 1 ;;
|
|
nul) printf 'default\0\n' ;;
|
|
*) return 97 ;;
|
|
esac
|
|
;;
|
|
package-keepassxc|package-cryptsetup-bin)
|
|
printf '%s' "$package_output"
|
|
printf '%s' "$package_error" >&2
|
|
return "$package_rc"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
task5a_fixture_ascii_hex() {
|
|
local input="$1" index character encoded
|
|
for (( index=0; index<${#input}; index++ )); do
|
|
character="${input:index:1}"
|
|
printf -v encoded '%02x' "'$character"
|
|
printf '%s' "$encoded"
|
|
done
|
|
}
|
|
valid_metadata=$'schema=k3slr-runtime-v1\nluks_uuid=12345678-1234-4abc-8def-1234567890ab\ncontainer_bytes=34359738368\ninner_label=K3S_RECOVERY\nmarker_sha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n'
|
|
valid_metadata_hex="$(task5a_fixture_ascii_hex "$valid_metadata")"$'\n'
|
|
valid_marker=$'12345678-1234-4abc-8def-1234567890ab\n'
|
|
valid_marker_hex="$(task5a_fixture_ascii_hex "$valid_marker")"$'\n'
|
|
|
|
assert_succeeds _k3slr_parse_current_context_record $'default\n' value
|
|
assert_eq default "$value" 'exact current context parses'
|
|
for value in default $'default\n\n' $'default\r\n' $'other\n' ''; do
|
|
assert_fails _k3slr_parse_current_context_record "$value" value
|
|
done
|
|
|
|
assert_succeeds task5a_unowned_capture_cleanup_is_rejected 'record capture fake'
|
|
assert_succeeds task5a_corrupt_capture_registration_is_rejected \
|
|
"$ownership_log" "$event_log" 'record capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
assert_succeeds task5a_cross_ledger_coherence_is_enforced \
|
|
"$ownership_log" "$event_log" "$path_log" 'record capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
assert_succeeds task5a_capture_global_parser_contract
|
|
assert_succeeds task5a_global_corrupt_capture_ledgers_are_rejected \
|
|
"$ownership_log" "$event_log" 'record capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
assert_succeeds task5a_capture_cleanup_phase_and_identity_is_enforced \
|
|
'record capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
context_case=success
|
|
assert_succeeds task5a_command_append_failure_is_rejected \
|
|
"$event_log" "$ownership_log" "$event_log" "$path_log" \
|
|
'record fake command-event append failure' \
|
|
/usr/local/bin/k3s kubectl config current-context
|
|
context_case=''
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
_k3slr_command /fixture/unknown >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'record fake rejects unknown argv with rc 97'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/local/bin/k3s 'kubectl config' current-context \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" \
|
|
'context fake rejects flattened text with wrong argv grouping'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/local/bin/k3s kubectl config current-context unexpected \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'context fake rejects an extra argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/local/bin/k3s kubectl config \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'context fake rejects a missing argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/local/bin/k3s kubectl config get-contexts \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'context fake rejects unknown same-executable argv'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/dpkg-query --show \
|
|
'--showformat=${Status}|${Version}\n keepassxc' \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" \
|
|
'package fake rejects flattened text with wrong argv grouping'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/dpkg-query --show \
|
|
'--showformat=${Status}|${Version}\n' keepassxc unexpected \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'package fake rejects an extra argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/dpkg-query --show \
|
|
'--showformat=${Status}|${Version}\n' \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'package fake rejects a missing argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/dpkg-query --list keepassxc \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'package fake rejects unknown same-executable argv'
|
|
[[ ! -s "$event_log" && ! -s "$path_log" ]] || return 1
|
|
|
|
context_case=success
|
|
assert_succeeds _k3slr_current_context value
|
|
assert_eq default "$value" 'current context query uses exact argv and exact record'
|
|
context_case=missing_lf
|
|
assert_fails _k3slr_current_context value
|
|
context_case=stderr
|
|
assert_fails _k3slr_current_context value
|
|
context_case=status
|
|
assert_fails _k3slr_current_context value
|
|
context_case=nul
|
|
assert_fails _k3slr_current_context value
|
|
|
|
assert_succeeds _k3slr_parse_package_query_record state keepassxc \
|
|
'2.7.6+dfsg.1-1build3' 0 $'install ok installed|2.7.6+dfsg.1-1build3\n' ''
|
|
assert_eq installed "$state" 'exact installed package record parses'
|
|
assert_succeeds _k3slr_parse_package_query_record state cryptsetup-bin \
|
|
'2:2.7.0-1ubuntu4.2' 1 '' $'dpkg-query: no packages found matching cryptsetup-bin\n'
|
|
assert_eq absent "$state" 'exact absent package record parses'
|
|
assert_succeeds _k3slr_parse_package_query_record state keepassxc \
|
|
'2.7.6+dfsg.1-1build3' 0 $'install ok installed|9.9.9\n' ''
|
|
assert_eq wrong-version "$state" 'wrong installed package version is classified'
|
|
assert_succeeds _k3slr_parse_package_query_record state keepassxc \
|
|
'2.7.6+dfsg.1-1build3' 0 $'deinstall ok config-files|2.7.6+dfsg.1-1build3\n' ''
|
|
assert_eq malformed "$state" 'wrong dpkg status is malformed'
|
|
assert_succeeds _k3slr_parse_package_query_record state keepassxc \
|
|
'2.7.6+dfsg.1-1build3' 1 'unexpected' $'dpkg-query: no packages found matching keepassxc\n'
|
|
assert_eq malformed "$state" 'absent record with stdout is malformed'
|
|
assert_succeeds _k3slr_parse_package_query_record state keepassxc \
|
|
'2.7.6+dfsg.1-1build3' 0 $'install ok installed|2.7.6+dfsg.1-1build3\n' $'warning\n'
|
|
assert_eq malformed "$state" 'installed record with stderr is malformed'
|
|
assert_succeeds _k3slr_parse_package_query_record state keepassxc \
|
|
'2.7.6+dfsg.1-1build3' 2 '' ''
|
|
assert_eq malformed "$state" 'unexpected dpkg exit is malformed'
|
|
assert_fails _k3slr_parse_package_query_record state other-package 1.0 0 $'install ok installed|1.0\n' ''
|
|
|
|
package_output=$'install ok installed|2.7.6+dfsg.1-1build3\n'
|
|
package_error=''
|
|
package_rc=0
|
|
assert_succeeds _k3slr_query_package_state state keepassxc '2.7.6+dfsg.1-1build3'
|
|
assert_eq installed "$state" 'package query uses exact argv and parses its record'
|
|
package_output=''
|
|
package_error=$'dpkg-query: no packages found matching cryptsetup-bin\n'
|
|
package_rc=1
|
|
assert_succeeds _k3slr_query_package_state state cryptsetup-bin '2:2.7.0-1ubuntu4.2'
|
|
assert_eq absent "$state" 'package query preserves exact absent stderr record'
|
|
package_output='install ok installed|2.7.6+dfsg.1-1build3'
|
|
package_error=''
|
|
package_rc=0
|
|
assert_succeeds _k3slr_query_package_state state keepassxc '2.7.6+dfsg.1-1build3'
|
|
assert_eq malformed "$state" 'package query preserves missing-LF framing for rejection'
|
|
package_output=$'install ok installed|2.7.6+dfsg.1-1build3\n'
|
|
package_error=$'warning\n'
|
|
package_rc=0
|
|
assert_succeeds _k3slr_query_package_state state keepassxc '2.7.6+dfsg.1-1build3'
|
|
assert_eq malformed "$state" 'package query preserves unexpected stderr for rejection'
|
|
|
|
expected="$(printf '%s\n' \
|
|
current-context \
|
|
current-context \
|
|
current-context \
|
|
current-context \
|
|
current-context \
|
|
package-keepassxc \
|
|
package-cryptsetup-bin \
|
|
package-keepassxc \
|
|
package-keepassxc)"
|
|
assert_succeeds task5a_assert_record_capture_lineage "$expected" \
|
|
'context and package composition'
|
|
|
|
assert_succeeds _k3slr_parse_phase_bytes_record $'phase_bytes=4294967296\n' phase
|
|
assert_eq 4294967296 "$phase" 'exact estimator record parses'
|
|
for value in 'phase_bytes=1' $'phase_bytes=01\n' $'phase_bytes=-1\n' \
|
|
$'phase_bytes=1\nextra=2\n' $'bytes=1\n' $'phase_bytes=9223372036854775808\n'; do
|
|
assert_fails _k3slr_parse_phase_bytes_record "$value" phase
|
|
done
|
|
assert_succeeds _k3slr_max_phase_bytes phase 7 11 9
|
|
assert_eq 11 "$phase" 'phase maximum never decreases after a lower observation'
|
|
assert_succeeds _k3slr_max_phase_bytes phase 11 11 15
|
|
assert_eq 15 "$phase" 'phase maximum advances for a larger third observation'
|
|
assert_fails _k3slr_max_phase_bytes phase 1 bad 3
|
|
|
|
assert_succeeds _k3slr_parse_statfs_record $'4096|1000|250\n' total free used
|
|
assert_eq 4096000 "$total" 'statfs total bytes use fundamental block size'
|
|
assert_eq 1024000 "$free" 'statfs free bytes use blocks available to user'
|
|
assert_eq 3072000 "$used" 'statfs conservative used bytes exclude unavailable blocks'
|
|
for value in '4096|1000|250' $'4096|1000|1001\n' $'0|1|1\n' \
|
|
$'4096|01|1\n' $'9223372036854775807|2|1\n' $'4096|1|1\nextra\n'; do
|
|
assert_fails _k3slr_parse_statfs_record "$value" total free used
|
|
done
|
|
assert_fails _k3slr_parse_statfs_record $'1|1|1\n' total total used
|
|
|
|
K3SLR_MINIMUM_FREE_BYTES=10737418240
|
|
assert_succeeds _k3slr_conservative_inner_capacity_fits \
|
|
34359738368 2147483648
|
|
assert_fails _k3slr_conservative_inner_capacity_fits \
|
|
34359738368 4294967296
|
|
assert_fails _k3slr_conservative_inner_capacity_fits 0 1
|
|
|
|
K3SLR_CONTAINER_SIZE_BYTES=34359738368
|
|
K3SLR_INNER_LABEL=K3S_RECOVERY
|
|
! declare -F _k3slr_parse_runtime_metadata >/dev/null || \
|
|
fail 'raw runtime metadata/UUID shell-variable parser API is forbidden'
|
|
! declare -F _k3slr_marker_record_is_valid >/dev/null || \
|
|
fail 'raw 37-byte marker shell-variable parser API is forbidden'
|
|
assert_succeeds _k3slr_parse_runtime_metadata_hex "$valid_metadata_hex" marker_hash
|
|
assert_eq "$valid_hash" "$marker_hash" 'runtime metadata returns marker hash'
|
|
assert_succeeds _k3slr_marker_hex_is_valid "$valid_marker_hex"
|
|
for value in \
|
|
"${valid_metadata%$'\n'}" \
|
|
"${valid_metadata/luks_uuid=/luks-uuid=}" \
|
|
"${valid_metadata/container_bytes=34359738368/container_bytes=34359738369}" \
|
|
"${valid_metadata/inner_label=K3S_RECOVERY/inner_label=OTHER}" \
|
|
"${valid_metadata/marker_sha256=/marker_sha256=A}" \
|
|
$'schema=k3slr-runtime-v1\r\nluks_uuid=12345678-1234-4abc-8def-1234567890ab\ncontainer_bytes=34359738368\ninner_label=K3S_RECOVERY\nmarker_sha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n'; do
|
|
invalid_hex="$(task5a_fixture_ascii_hex "$value")"$'\n'
|
|
assert_fails _k3slr_parse_runtime_metadata_hex "$invalid_hex" marker_hash
|
|
done
|
|
for value in $'12345678-1234-3abc-8def-1234567890ab\n' \
|
|
$'12345678-1234-4abc-7def-1234567890ab\n' \
|
|
'12345678-1234-4abc-8def-1234567890ab'; do
|
|
invalid_hex="$(task5a_fixture_ascii_hex "$value")"$'\n'
|
|
assert_fails _k3slr_marker_hex_is_valid "$invalid_hex"
|
|
done
|
|
)
|
|
|
|
task5a_ntfs_capacity_and_prepackage_seams() (
|
|
local value valid_text valid_hex invalid_hex od_record oversized snapshot snapshot_after
|
|
local total free identity output expected_identity calls=0
|
|
|
|
task5a_fixture_ascii_hex() {
|
|
local input="$1" index character encoded
|
|
for (( index=0; index<${#input}; index++ )); do
|
|
character="${input:index:1}"
|
|
printf -v encoded '%02x' "'$character"
|
|
printf '%s' "$encoded"
|
|
done
|
|
}
|
|
task5a_fixture_prepackage_snapshot() {
|
|
printf '%s\n' \
|
|
'canonical_partition=/dev/sda3' \
|
|
'partition_major_minor=8:3' \
|
|
'canonical_parent_disk=/dev/sda' \
|
|
'disk_major_minor=8:0' \
|
|
"filesystem_uuid=${K3SLR_RECOVERY_FS_UUID}" \
|
|
"partuuid=${K3SLR_RECOVERY_PARTUUID}" \
|
|
'filesystem_type=ntfs' \
|
|
"model=${K3SLR_RECOVERY_MODEL}" \
|
|
"serial=${K3SLR_RECOVERY_SERIAL}" \
|
|
"wwn=${K3SLR_RECOVERY_WWN}" \
|
|
'canonical_k3s_partition=/dev/sdb2' \
|
|
'k3s_partition_major_minor=8:34' \
|
|
'canonical_k3s_disk=/dev/sdb' \
|
|
'k3s_disk_major_minor=8:32' \
|
|
"k3s_filesystem_uuid=${K3SLR_K3S_FS_UUID}" \
|
|
"k3s_partuuid=${K3SLR_K3S_PARTUUID}" \
|
|
'k3s_filesystem_type=ext4' \
|
|
"k3s_model=${K3SLR_K3S_MODEL}" \
|
|
"k3s_serial=${K3SLR_K3S_SERIAL}" \
|
|
"k3s_wwn=${K3SLR_K3S_WWN}" \
|
|
'smart_health=PASSED' \
|
|
'smart_reallocated=0' \
|
|
'smart_pending=0' \
|
|
'smart_uncorrectable=0' \
|
|
'outer_mount=absent' \
|
|
'inner_mount=absent' \
|
|
'source_mounts=absent' \
|
|
'loops=absent' \
|
|
'mapper_directory=physical-root-safe' \
|
|
'mapping_path=absent' \
|
|
'proof_mapping_path=absent' \
|
|
'mapping_names=absent' \
|
|
'ntfs_probe=pass'
|
|
}
|
|
|
|
od_record=$' 20 30 61 0a\n'
|
|
assert_succeeds _k3slr_normalize_od_hex_record "$od_record" value
|
|
assert_eq $'2030610a\n' "$value" 'od tokens normalize to one bounded lowercase hex record'
|
|
for od_record in $' 20 0A 0a\n' $' 20 gg 0a\n' ' 20 0a' $'\n'; do
|
|
assert_fails _k3slr_normalize_od_hex_record "$od_record" value
|
|
done
|
|
oversized='00 '
|
|
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16; do
|
|
oversized+="$oversized"
|
|
done
|
|
oversized+='00 '
|
|
oversized="${oversized:0:196611}"$'\n'
|
|
assert_fails _k3slr_normalize_od_hex_record "$oversized" value
|
|
|
|
valid_text=$'Volume Information\n\tCluster Size: 4096\n Volume Size in Clusters: 1000000\nAllocated clusters 250000 (25.0%)\nFree Clusters: 750000 (75.0%)\n'
|
|
valid_hex="$(task5a_fixture_ascii_hex "$valid_text")"$'\n'
|
|
assert_succeeds _k3slr_parse_unmounted_ntfs_capacity_hex "$valid_hex" total free
|
|
assert_eq 4096000000 "$total" 'NTFS total bytes are cluster size times volume clusters'
|
|
assert_eq 3072000000 "$free" 'NTFS free bytes are cluster size times free clusters'
|
|
output="$(_k3slr_parse_unmounted_ntfs_capacity_hex "$valid_hex" total free)" || return 1
|
|
assert_eq '' "$output" 'NTFS parser never logs normalized or decoded evidence'
|
|
|
|
for value in \
|
|
"${valid_text%$'\n'}" \
|
|
"${valid_text/Cluster Size: 4096/Cluster Size: 04096}" \
|
|
$'Volume Information\n\tCluster\tSize: 4096\nVolume Size in Clusters: 1000000\nAllocated clusters 250000 (25.0%)\nFree Clusters: 750000 (75.0%)\n' \
|
|
$'Volume Information\n\tCluster Size:\t4096\nVolume Size in Clusters: 1000000\nAllocated clusters 250000 (25.0%)\nFree Clusters: 750000 (75.0%)\n' \
|
|
"${valid_text/Allocated clusters 250000/Allocated clusters 250001}" \
|
|
"${valid_text/25.0%/25.1%}" \
|
|
"${valid_text/Free Clusters:/Free clusters:}" \
|
|
"${valid_text/Free Clusters: 750000 (75.0%)/Free Clusters: 750000 (75.0%) suffix}" \
|
|
"${valid_text}Free Clusters: 750000 (75.0%)" \
|
|
"${valid_text}Cluster Size: 4096 suffix"$'\n' \
|
|
$'Cluster Size: 9223372036854775807\nVolume Size in Clusters: 2\nAllocated clusters 1 (50.0%)\nFree Clusters: 1 (50.0%)\n'; do
|
|
invalid_hex="$(task5a_fixture_ascii_hex "$value")"$'\n'
|
|
assert_fails _k3slr_parse_unmounted_ntfs_capacity_hex "$invalid_hex" total free
|
|
done
|
|
invalid_hex="${valid_hex%$'\n'}"
|
|
invalid_hex="${invalid_hex:0:12}00${invalid_hex:12}"$'\n'
|
|
assert_fails _k3slr_parse_unmounted_ntfs_capacity_hex "$invalid_hex" total free
|
|
invalid_hex="${valid_hex%$'\n'}"
|
|
invalid_hex="${invalid_hex:0:12}0d${invalid_hex:12}"$'\n'
|
|
assert_fails _k3slr_parse_unmounted_ntfs_capacity_hex "$invalid_hex" total free
|
|
invalid_hex="${valid_hex^^}"
|
|
assert_fails _k3slr_parse_unmounted_ntfs_capacity_hex "$invalid_hex" total free
|
|
assert_fails _k3slr_parse_unmounted_ntfs_capacity_hex "${valid_hex%?}f" total free
|
|
|
|
snapshot="$(task5a_fixture_prepackage_snapshot)"$'\n'
|
|
assert_succeeds _k3slr_validate_prepackage_snapshot "$snapshot"
|
|
assert_fails _k3slr_parse_prepackage_snapshot_fields "$snapshot" 'bad-name'
|
|
assert_fails _k3slr_parse_prepackage_snapshot_fields "$snapshot" _k3slr_snapshot_alias
|
|
for snapshot_after in \
|
|
"${snapshot/smart_pending=0/smart_pending=1}" \
|
|
"${snapshot/ntfs_probe=pass/ntfs_probe=fail}" \
|
|
"${snapshot/loops=absent/loops=present}" \
|
|
"${snapshot/disk_major_minor=8:0/disk_major_minor=8:32}" \
|
|
"${snapshot/filesystem_uuid=${K3SLR_RECOVERY_FS_UUID}/filesystem_uuid=AAAAAAAAAAAAAAAA}" \
|
|
"${snapshot}mapping_names=absent"; do
|
|
assert_fails _k3slr_validate_prepackage_snapshot "$snapshot_after"
|
|
done
|
|
|
|
_k3slr_collect_prepackage_snapshot() {
|
|
local destination_name="$1" selected="$snapshot"
|
|
calls=$((calls + 1))
|
|
if (( calls == 2 )) && [[ -n "${TASK5A_SECOND_SNAPSHOT:-}" ]]; then
|
|
selected="$TASK5A_SECOND_SNAPSHOT"
|
|
fi
|
|
printf -v "$destination_name" '%s' "$selected"
|
|
}
|
|
_k3slr_capture_ntfsinfo_hex() {
|
|
local destination_name="$1" partition="$2"
|
|
[[ "$partition" == /dev/sda3 ]] || return 1
|
|
printf -v "$destination_name" '%s' "$valid_hex"
|
|
}
|
|
calls=0
|
|
TASK5A_SECOND_SNAPSHOT=''
|
|
assert_succeeds _k3slr_prepackage_device_preflight
|
|
assert_eq 2 "$calls" 'pre-package device preflight pins byte-equal before/after snapshots'
|
|
|
|
calls=0
|
|
TASK5A_SECOND_SNAPSHOT="${snapshot/serial=${K3SLR_RECOVERY_SERIAL}/serial=DRIFTED}"
|
|
assert_fails _k3slr_prepackage_device_preflight
|
|
assert_eq 2 "$calls" 'pre-package drift is observed before refusal'
|
|
|
|
calls=0
|
|
TASK5A_SECOND_SNAPSHOT=''
|
|
assert_succeeds _k3slr_collect_unmounted_ntfs_capacity value identity
|
|
assert_eq "$valid_hex" "$value" 'capacity collector returns only normalized hex'
|
|
expected_identity="/dev/sda3|8:3|/dev/sda|8:0|${K3SLR_RECOVERY_FS_UUID}|${K3SLR_RECOVERY_PARTUUID}|ntfs|${K3SLR_RECOVERY_MODEL}|${K3SLR_RECOVERY_SERIAL}|${K3SLR_RECOVERY_WWN}|8:32"
|
|
assert_eq "$expected_identity" "$identity" 'capacity collector returns the strict pinned identity tuple'
|
|
assert_eq 2 "$calls" 'capacity collector repeats the full snapshot around ntfsinfo'
|
|
|
|
calls=0
|
|
TASK5A_SECOND_SNAPSHOT="${snapshot/partition_major_minor=8:3/partition_major_minor=8:4}"
|
|
value=unchanged
|
|
identity=unchanged
|
|
assert_fails _k3slr_collect_unmounted_ntfs_capacity value identity
|
|
assert_eq unchanged "$value" 'drift failure does not publish normalized hex'
|
|
assert_eq unchanged "$identity" 'drift failure does not publish identity'
|
|
)
|
|
|
|
task5a_exact_process_capture_contract() (
|
|
local capture_case=success stdout stderr status invalid_rc=0 expected
|
|
local event_log="${fixture_root}/task5a-capture-events.log"
|
|
local path_log="${fixture_root}/task5a-capture-paths.log"
|
|
local ownership_log="${fixture_root}/task5a-capture-ownership.log"
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
|
|
task5a_capture_argv_equals() {
|
|
local expected_count="${1-}" argument_index
|
|
local -a compared_arguments=()
|
|
shift || return 1
|
|
[[ "$expected_count" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
compared_arguments=("$@")
|
|
(( ${#compared_arguments[@]} == 2 * expected_count )) || return 1
|
|
for (( argument_index=0; argument_index<expected_count; argument_index++ )); do
|
|
[[ "${compared_arguments[argument_index]}" == \
|
|
"${compared_arguments[expected_count + argument_index]}" ]] || return 1
|
|
done
|
|
}
|
|
|
|
task5a_assert_process_capture_lineage() {
|
|
local expected_ids_record="$1" description="$2"
|
|
local event_index capture_path expected_count
|
|
local -a expected_ids=() events=() capture_paths=()
|
|
mapfile -t expected_ids <<<"$expected_ids_record"
|
|
mapfile -t events <"$event_log"
|
|
mapfile -t capture_paths <"$path_log"
|
|
expected_count="${#expected_ids[@]}"
|
|
assert_eq "$expected_count" "${#capture_paths[@]}" \
|
|
"$description creates one capture directory per execution"
|
|
assert_eq "$((expected_count * 4))" "${#events[@]}" \
|
|
"$description records complete capture lifecycle groups"
|
|
for (( event_index=0; event_index<expected_count; event_index++ )); do
|
|
capture_path="${capture_paths[event_index]}"
|
|
[[ "$capture_path" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ ]] || return 1
|
|
assert_eq "mktemp|${capture_path}" "${events[event_index * 4]}" \
|
|
"$description starts capture with its exact created path"
|
|
assert_eq "command|${expected_ids[event_index]}" \
|
|
"${events[event_index * 4 + 1]}" \
|
|
"$description runs the exact fixture argv"
|
|
assert_eq "rm|${capture_path}" "${events[event_index * 4 + 2]}" \
|
|
"$description removes exactly stdout and stderr"
|
|
assert_eq "rmdir|${capture_path}" "${events[event_index * 4 + 3]}" \
|
|
"$description removes the same capture directory"
|
|
[[ ! -e "$capture_path" && ! -L "$capture_path" ]] || return 1
|
|
done
|
|
task5a_assert_capture_ownership_complete \
|
|
"$ownership_log" "$event_log" "$path_log" "$description"
|
|
}
|
|
|
|
_k3slr_command() {
|
|
local created capture_directory
|
|
case "${1-}" in
|
|
/usr/bin/mktemp)
|
|
task5a_capture_argv_equals 4 /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX "$@" || return 97
|
|
task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" '' complete || return 97
|
|
created="$("$@")" || return 1
|
|
[[ "$created" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ &&
|
|
-d "$created" && ! -L "$created" ]] || return 97
|
|
if ! task5a_capture_ledger_register \
|
|
"$ownership_log" "$event_log" "$created"; then
|
|
task5a_cleanup_capture_probe_path "$created" || return 1
|
|
return 97
|
|
fi
|
|
printf '%s\n' "$created" >>"$path_log"
|
|
printf '%s\n' "$created"
|
|
return 0
|
|
;;
|
|
/usr/bin/rm)
|
|
task5a_remove_owned_capture_records "$ownership_log" "$event_log" "$@"
|
|
return
|
|
;;
|
|
/usr/bin/rmdir)
|
|
task5a_remove_owned_capture_directory "$ownership_log" "$event_log" "$@"
|
|
return
|
|
;;
|
|
/fixture/capture)
|
|
task5a_capture_argv_equals 3 /fixture/capture --mode emit "$@" || return 97
|
|
printf 'command|fixture-capture\n' >>"$event_log" || return 97
|
|
case "$capture_case" in
|
|
success)
|
|
printf 'stdout line\n'
|
|
printf 'stderr line\n' >&2
|
|
return 7
|
|
;;
|
|
nul) printf 'unsafe\0record\n' ;;
|
|
*) return 97 ;;
|
|
esac
|
|
;;
|
|
*) return 97 ;;
|
|
esac
|
|
}
|
|
|
|
assert_succeeds task5a_unowned_capture_cleanup_is_rejected \
|
|
'generic process capture fake'
|
|
assert_succeeds task5a_capture_cleanup_phase_and_identity_is_enforced \
|
|
'generic process capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
assert_succeeds task5a_cross_ledger_coherence_is_enforced \
|
|
"$ownership_log" "$event_log" "$path_log" 'generic process capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
capture_case=success
|
|
assert_succeeds task5a_command_append_failure_is_rejected \
|
|
"$event_log" "$ownership_log" "$event_log" "$path_log" \
|
|
'generic fake command-event append failure' \
|
|
/fixture/capture --mode emit
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
_k3slr_command /fixture/unknown >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'process-capture fake rejects unknown argv with rc 97'
|
|
invalid_rc=0
|
|
_k3slr_command /fixture/capture '--mode emit' \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" \
|
|
'process-capture fake rejects flattened text with wrong argv grouping'
|
|
invalid_rc=0
|
|
_k3slr_command /fixture/capture --mode emit unexpected \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" \
|
|
'process-capture fake rejects the known executable with an extra argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /fixture/capture --mode >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" \
|
|
'process-capture fake rejects the known executable with a missing argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /fixture/capture --mode discard \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" \
|
|
'process-capture fake rejects unknown same-executable argv'
|
|
[[ ! -s "$event_log" && ! -s "$path_log" ]] || return 1
|
|
|
|
assert_succeeds _k3slr_capture_process stdout stderr status \
|
|
/fixture/capture --mode emit
|
|
assert_eq $'stdout line\n' "$stdout" 'process capture preserves stdout terminal LF'
|
|
assert_eq $'stderr line\n' "$stderr" 'process capture preserves stderr terminal LF'
|
|
assert_eq 7 "$status" 'process capture preserves nonzero exit status'
|
|
|
|
capture_case=nul
|
|
assert_fails _k3slr_capture_process stdout stderr status \
|
|
/fixture/capture --mode emit
|
|
expected="$(printf '%s\n' fixture-capture fixture-capture)"
|
|
assert_succeeds task5a_assert_process_capture_lineage "$expected" \
|
|
'success and NUL-rejection process capture'
|
|
assert_fails _k3slr_capture_process stdout stdout status \
|
|
/fixture/capture --mode emit
|
|
assert_succeeds task5a_assert_process_capture_lineage "$expected" \
|
|
'duplicate-destination rejection leaves no capture residue'
|
|
)
|
|
|
|
task5a_prepackage_system_command_matrix() (
|
|
local log="${fixture_root}/task5a-prepackage-system.log"
|
|
local event_log="${fixture_root}/task5a-prepackage-system-events.log"
|
|
local path_log="${fixture_root}/task5a-prepackage-system-paths.log"
|
|
local ownership_log="${fixture_root}/task5a-prepackage-system-ownership.log"
|
|
local cryptsetup_log="${fixture_root}/task5a-prepackage-system-cryptsetup.log"
|
|
local snapshot system_case expected unknown_rc=0 wrong_group_rc=0 extra_arg_rc=0 missing_arg_rc=0
|
|
: >"$log"
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
: >"$cryptsetup_log"
|
|
|
|
task5a_argv_equals() {
|
|
local expected_count="${1-}" argument_index
|
|
local -a compared_arguments=()
|
|
shift || return 1
|
|
[[ "$expected_count" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
compared_arguments=("$@")
|
|
(( ${#compared_arguments[@]} == 2 * expected_count )) || return 1
|
|
for (( argument_index=0; argument_index<expected_count; argument_index++ )); do
|
|
[[ "${compared_arguments[argument_index]}" == \
|
|
"${compared_arguments[expected_count + argument_index]}" ]] || return 1
|
|
done
|
|
}
|
|
|
|
task5a_assert_capture_lineage_and_cleanup() {
|
|
local expected_ids_record="$1" description="$2"
|
|
local event_index capture_path expected_count
|
|
local -a expected_ids=() events=() capture_paths=()
|
|
mapfile -t expected_ids <<<"$expected_ids_record"
|
|
mapfile -t events <"$event_log"
|
|
mapfile -t capture_paths <"$path_log"
|
|
expected_count="${#expected_ids[@]}"
|
|
assert_eq "$expected_count" "${#capture_paths[@]}" \
|
|
"$description creates one capture directory per collector command"
|
|
assert_eq "$((expected_count * 4))" "${#events[@]}" \
|
|
"$description records complete capture lifecycle groups"
|
|
for (( event_index=0; event_index<expected_count; event_index++ )); do
|
|
capture_path="${capture_paths[event_index]}"
|
|
[[ "$capture_path" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ ]] || return 1
|
|
assert_eq "mktemp|${capture_path}" "${events[event_index * 4]}" \
|
|
"$description starts capture with the exact created path"
|
|
assert_eq "command|${expected_ids[event_index]}" "${events[event_index * 4 + 1]}" \
|
|
"$description preserves collector command order"
|
|
assert_eq "rm|${capture_path}" "${events[event_index * 4 + 2]}" \
|
|
"$description removes only the capture records"
|
|
assert_eq "rmdir|${capture_path}" "${events[event_index * 4 + 3]}" \
|
|
"$description removes the same capture directory"
|
|
[[ ! -e "$capture_path" && ! -L "$capture_path" ]] || return 1
|
|
done
|
|
task5a_assert_capture_ownership_complete \
|
|
"$ownership_log" "$event_log" "$path_log" "$description"
|
|
}
|
|
|
|
_k3slr_command() {
|
|
local created capture_directory command_id=rejected-command argument
|
|
for argument in "$@"; do
|
|
case "$argument" in
|
|
/usr/bin/cryptsetup|/usr/sbin/cryptsetup|cryptsetup)
|
|
printf 'cryptsetup-argument\n' >>"$cryptsetup_log"
|
|
;;
|
|
esac
|
|
done
|
|
case "${1-}" in
|
|
/usr/bin/mktemp)
|
|
(( $# == 4 )) && [[ "$2" == --directory && "$3" == --tmpdir=/tmp &&
|
|
"$4" == k3slr-capture.XXXXXXXX ]] || return 97
|
|
task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" '' complete || return 97
|
|
created="$("$@")" || return 1
|
|
[[ "$created" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ &&
|
|
-d "$created" && ! -L "$created" ]] || return 97
|
|
if ! task5a_capture_ledger_register \
|
|
"$ownership_log" "$event_log" "$created"; then
|
|
task5a_cleanup_capture_probe_path "$created" || return 1
|
|
return 97
|
|
fi
|
|
printf '%s\n' "$created" >>"$path_log"
|
|
printf '%s\n' "$created"
|
|
return 0
|
|
;;
|
|
/usr/bin/rm)
|
|
task5a_remove_owned_capture_records "$ownership_log" "$event_log" "$@"
|
|
return
|
|
;;
|
|
/usr/bin/rmdir)
|
|
task5a_remove_owned_capture_directory "$ownership_log" "$event_log" "$@"
|
|
return
|
|
;;
|
|
esac
|
|
|
|
case "${1-}" in
|
|
/usr/bin/readlink)
|
|
if task5a_argv_equals 4 \
|
|
/usr/bin/readlink -f -- "$K3SLR_RECOVERY_PARTITION_BY_ID" "$@"; then
|
|
command_id=readlink-recovery-partition
|
|
elif task5a_argv_equals 4 \
|
|
/usr/bin/readlink -f -- "$K3SLR_RECOVERY_DISK_BY_ID" "$@"; then
|
|
command_id=readlink-recovery-disk
|
|
elif task5a_argv_equals 4 \
|
|
/usr/bin/readlink -f -- "$K3SLR_K3S_PARTITION_BY_ID" "$@"; then
|
|
command_id=readlink-k3s-partition
|
|
elif task5a_argv_equals 4 \
|
|
/usr/bin/readlink -f -- "$K3SLR_K3S_DISK_BY_ID" "$@"; then
|
|
command_id=readlink-k3s-disk
|
|
else
|
|
:
|
|
fi
|
|
;;
|
|
/usr/bin/lsblk)
|
|
if task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output PKNAME -- /dev/sda3 "$@"; then
|
|
command_id=lsblk-recovery-partition-parent
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output MAJ:MIN -- /dev/sda3 "$@"; then
|
|
command_id=lsblk-recovery-partition-majmin
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output MODEL -- /dev/sda "$@"; then
|
|
command_id=lsblk-recovery-model
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output SERIAL -- /dev/sda "$@"; then
|
|
command_id=lsblk-recovery-serial
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output WWN -- /dev/sda "$@"; then
|
|
command_id=lsblk-recovery-wwn
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output MAJ:MIN -- /dev/sda "$@"; then
|
|
command_id=lsblk-recovery-disk-majmin
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output PKNAME -- /dev/sdb2 "$@"; then
|
|
command_id=lsblk-k3s-partition-parent
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output MAJ:MIN -- /dev/sdb2 "$@"; then
|
|
command_id=lsblk-k3s-partition-majmin
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output MODEL -- /dev/sdb "$@"; then
|
|
command_id=lsblk-k3s-model
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output SERIAL -- /dev/sdb "$@"; then
|
|
command_id=lsblk-k3s-serial
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output WWN -- /dev/sdb "$@"; then
|
|
command_id=lsblk-k3s-wwn
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/lsblk --noheadings --paths --output MAJ:MIN -- /dev/sdb "$@"; then
|
|
command_id=lsblk-k3s-disk-majmin
|
|
elif task5a_argv_equals 5 \
|
|
/usr/bin/lsblk --noheadings --raw --output NAME,TYPE "$@"; then
|
|
command_id=lsblk-all-names-types
|
|
else
|
|
:
|
|
fi
|
|
;;
|
|
/usr/bin/sudo)
|
|
if task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/sbin/blkid --output value \
|
|
--match-tag UUID -- /dev/sda3 "$@"; then
|
|
command_id=blkid-recovery-uuid
|
|
elif task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/sbin/blkid --output value \
|
|
--match-tag PARTUUID -- /dev/sda3 "$@"; then
|
|
command_id=blkid-recovery-partuuid
|
|
elif task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/sbin/blkid --output value \
|
|
--match-tag TYPE -- /dev/sda3 "$@"; then
|
|
command_id=blkid-recovery-type
|
|
elif task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/sbin/blkid --output value \
|
|
--match-tag UUID -- /dev/sdb2 "$@"; then
|
|
command_id=blkid-k3s-uuid
|
|
elif task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/sbin/blkid --output value \
|
|
--match-tag PARTUUID -- /dev/sdb2 "$@"; then
|
|
command_id=blkid-k3s-partuuid
|
|
elif task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/sbin/blkid --output value \
|
|
--match-tag TYPE -- /dev/sdb2 "$@"; then
|
|
command_id=blkid-k3s-type
|
|
elif task5a_argv_equals 7 \
|
|
/usr/bin/sudo --non-interactive -- /usr/sbin/smartctl -H -A /dev/sda "$@"; then
|
|
command_id=smartctl-recovery-health-attributes
|
|
elif task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/findmnt --noheadings --raw \
|
|
--output SOURCE,FSTYPE,OPTIONS,ID,MAJ:MIN --mountpoint \
|
|
/mnt/k3s-recovery-ssd "$@"; then
|
|
command_id=findmnt-outer-mountpoint
|
|
elif task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/findmnt --noheadings --raw \
|
|
--output SOURCE,FSTYPE,OPTIONS,ID,MAJ:MIN --mountpoint \
|
|
/srv/recovery/k3s "$@"; then
|
|
command_id=findmnt-inner-mountpoint
|
|
elif task5a_argv_equals 10 \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/findmnt --noheadings --raw \
|
|
--output TARGET --source /dev/sda3 "$@"; then
|
|
command_id=findmnt-recovery-source
|
|
elif task5a_argv_equals 9 \
|
|
/usr/bin/sudo --non-interactive -- /usr/sbin/losetup --list \
|
|
--noheadings --raw --output \
|
|
NAME,BACK-FILE,BACK-INO,BACK-MAJ:MIN,MAJ:MIN,OFFSET,SIZELIMIT "$@"; then
|
|
command_id=losetup-all
|
|
elif task5a_argv_equals 6 \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/ntfs-3g.probe \
|
|
--readwrite /dev/sda3 "$@"; then
|
|
command_id=ntfs-probe-readwrite
|
|
else
|
|
:
|
|
fi
|
|
;;
|
|
/usr/bin/stat)
|
|
if task5a_argv_equals 4 /usr/bin/stat '--format=%F|%u|%g|%a' -- \
|
|
/dev/mapper "$@"; then
|
|
command_id=stat-mapper-directory
|
|
fi
|
|
;;
|
|
/usr/bin/test)
|
|
if task5a_argv_equals 4 /usr/bin/test ! -L /dev/mapper "$@"; then
|
|
command_id=test-mapper-directory-not-symlink
|
|
elif task5a_argv_equals 4 \
|
|
/usr/bin/test ! -e /dev/mapper/k3s-recovery "$@"; then
|
|
command_id=test-main-mapping-absent
|
|
elif task5a_argv_equals 4 \
|
|
/usr/bin/test ! -L /dev/mapper/k3s-recovery "$@"; then
|
|
command_id=test-main-mapping-not-symlink
|
|
elif task5a_argv_equals 4 \
|
|
/usr/bin/test ! -e /dev/mapper/k3s-recovery-proof "$@"; then
|
|
command_id=test-proof-mapping-absent
|
|
elif task5a_argv_equals 4 \
|
|
/usr/bin/test ! -L /dev/mapper/k3s-recovery-proof "$@"; then
|
|
command_id=test-proof-mapping-not-symlink
|
|
else
|
|
:
|
|
fi
|
|
;;
|
|
*) : ;;
|
|
esac
|
|
|
|
printf '%s\n' "$command_id" >>"$log" || return 97
|
|
printf 'command|%s\n' "$command_id" >>"$event_log" || return 97
|
|
[[ "$command_id" != rejected-command ]] || return 97
|
|
case "$command_id" in
|
|
readlink-recovery-partition)
|
|
case "${system_case:-}" in
|
|
capture_stdout_no_lf) printf '/dev/sda3' ;;
|
|
capture_stderr) printf '/dev/sda3\n'; printf 'unexpected stderr\n' >&2 ;;
|
|
capture_status) printf '/dev/sda3\n'; return 7 ;;
|
|
*) printf '/dev/sda3\n' ;;
|
|
esac
|
|
;;
|
|
readlink-recovery-disk)
|
|
if [[ "${system_case:-}" == wrong_identity ]]; then printf '/dev/sdz\n'; else printf '/dev/sda\n'; fi
|
|
;;
|
|
readlink-k3s-partition) printf '/dev/sdb2\n' ;;
|
|
readlink-k3s-disk) printf '/dev/sdb\n' ;;
|
|
lsblk-recovery-partition-parent) printf '/dev/sda\n' ;;
|
|
lsblk-recovery-partition-majmin) printf '8:3\n' ;;
|
|
lsblk-recovery-model) printf '%s\n' "$K3SLR_RECOVERY_MODEL" ;;
|
|
lsblk-recovery-serial) printf '%s\n' "$K3SLR_RECOVERY_SERIAL" ;;
|
|
lsblk-recovery-wwn) printf '%s\n' "$K3SLR_RECOVERY_WWN" ;;
|
|
lsblk-recovery-disk-majmin) printf '8:0\n' ;;
|
|
lsblk-k3s-partition-parent) printf '/dev/sdb\n' ;;
|
|
lsblk-k3s-partition-majmin) printf '8:34\n' ;;
|
|
lsblk-k3s-model) printf '%s\n' "$K3SLR_K3S_MODEL" ;;
|
|
lsblk-k3s-serial) printf '%s\n' "$K3SLR_K3S_SERIAL" ;;
|
|
lsblk-k3s-wwn) printf '%s\n' "$K3SLR_K3S_WWN" ;;
|
|
lsblk-k3s-disk-majmin) printf '8:32\n' ;;
|
|
blkid-recovery-uuid) printf '%s\n' "$K3SLR_RECOVERY_FS_UUID" ;;
|
|
blkid-recovery-partuuid) printf '%s\n' "$K3SLR_RECOVERY_PARTUUID" ;;
|
|
blkid-recovery-type) printf 'ntfs\n' ;;
|
|
blkid-k3s-uuid) printf '%s\n' "$K3SLR_K3S_FS_UUID" ;;
|
|
blkid-k3s-partuuid) printf '%s\n' "$K3SLR_K3S_PARTUUID" ;;
|
|
blkid-k3s-type) printf 'ext4\n' ;;
|
|
smartctl-recovery-health-attributes)
|
|
if [[ "${system_case:-}" == smart ]]; then
|
|
printf 'SMART overall-health self-assessment test result: PASSED\n 5 Reallocated_Sector_Ct 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1\n197 Current_Pending_Sector 0\n198 Offline_Uncorrectable 0\n'
|
|
else
|
|
printf 'SMART overall-health self-assessment test result: PASSED\n 5 Reallocated_Sector_Ct 0\n197 Current_Pending_Sector 0\n198 Offline_Uncorrectable 0\n'
|
|
fi
|
|
;;
|
|
findmnt-outer-mountpoint)
|
|
if [[ "${system_case:-}" == mounted ]]; then printf '/dev/sda3 ntfs3 rw 1 8:3\n'; else return 1; fi
|
|
;;
|
|
findmnt-inner-mountpoint|findmnt-recovery-source) return 1 ;;
|
|
losetup-all)
|
|
if [[ "${system_case:-}" == loop ]]; then printf '/dev/loop7 /mnt/file 42 8:3 7:7 0 0\n'; fi
|
|
;;
|
|
stat-mapper-directory) printf 'directory|0|0|755\n' ;;
|
|
test-mapper-directory-not-symlink)
|
|
if [[ "${system_case:-}" == mapper_dir_symlink ]]; then return 1; fi
|
|
;;
|
|
test-main-mapping-absent|test-main-mapping-not-symlink|\
|
|
test-proof-mapping-absent|test-proof-mapping-not-symlink) ;;
|
|
lsblk-all-names-types)
|
|
if [[ "${system_case:-}" == mapping ]]; then printf 'sda disk\nk3s-recovery crypt\n'; else printf 'sda disk\nsda3 part\nsdb disk\nsdb2 part\n'; fi
|
|
;;
|
|
ntfs-probe-readwrite)
|
|
if [[ "${system_case:-}" == probe ]]; then return 1; fi
|
|
;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
assert_succeeds task5a_unowned_capture_cleanup_is_rejected \
|
|
'pre-package system capture fake'
|
|
assert_succeeds task5a_capture_cleanup_phase_and_identity_is_enforced \
|
|
'pre-package system capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
assert_succeeds task5a_cross_ledger_coherence_is_enforced \
|
|
"$ownership_log" "$event_log" "$path_log" 'pre-package system capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
: >"$log"
|
|
system_case=''
|
|
assert_succeeds task5a_command_append_failure_is_rejected \
|
|
"$log" "$ownership_log" "$event_log" "$path_log" \
|
|
'pre-package fake command-log append failure' \
|
|
/usr/bin/readlink -f -- "$K3SLR_RECOVERY_PARTITION_BY_ID"
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
: >"$log"
|
|
assert_succeeds task5a_command_append_failure_is_rejected \
|
|
"$event_log" "$ownership_log" "$event_log" "$path_log" \
|
|
'pre-package fake command-event append failure' \
|
|
/usr/bin/readlink -f -- "$K3SLR_RECOVERY_PARTITION_BY_ID"
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
: >"$log"
|
|
_k3slr_command /fixture/unknown >/dev/null 2>&1 || unknown_rc=$?
|
|
assert_eq 97 "$unknown_rc" 'pre-package fake rejects unknown argv with rc 97'
|
|
_k3slr_command /usr/bin/readlink \
|
|
"-f -- ${K3SLR_RECOVERY_PARTITION_BY_ID}" >/dev/null 2>&1 || wrong_group_rc=$?
|
|
assert_eq 97 "$wrong_group_rc" \
|
|
'pre-package fake rejects same executable and flattened text with wrong argv grouping'
|
|
_k3slr_command /usr/bin/readlink -f -- \
|
|
"$K3SLR_RECOVERY_PARTITION_BY_ID" unexpected >/dev/null 2>&1 || extra_arg_rc=$?
|
|
assert_eq 97 "$extra_arg_rc" \
|
|
'pre-package fake rejects same executable with an extra argv element'
|
|
_k3slr_command /usr/bin/readlink -f -- >/dev/null 2>&1 || missing_arg_rc=$?
|
|
assert_eq 97 "$missing_arg_rc" \
|
|
'pre-package fake rejects same executable with a missing argv element'
|
|
: >"$log"
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
: >"$cryptsetup_log"
|
|
|
|
system_case=''
|
|
assert_succeeds _k3slr_collect_prepackage_snapshot snapshot
|
|
assert_succeeds _k3slr_validate_prepackage_snapshot "$snapshot"
|
|
expected="$(printf '%s\n' \
|
|
readlink-recovery-partition \
|
|
readlink-recovery-disk \
|
|
readlink-k3s-partition \
|
|
readlink-k3s-disk \
|
|
lsblk-recovery-partition-parent \
|
|
lsblk-recovery-partition-majmin \
|
|
lsblk-recovery-model \
|
|
lsblk-recovery-serial \
|
|
lsblk-recovery-wwn \
|
|
lsblk-recovery-disk-majmin \
|
|
lsblk-k3s-partition-parent \
|
|
lsblk-k3s-partition-majmin \
|
|
lsblk-k3s-model \
|
|
lsblk-k3s-serial \
|
|
lsblk-k3s-wwn \
|
|
lsblk-k3s-disk-majmin \
|
|
blkid-recovery-uuid \
|
|
blkid-recovery-partuuid \
|
|
blkid-recovery-type \
|
|
blkid-k3s-uuid \
|
|
blkid-k3s-partuuid \
|
|
blkid-k3s-type \
|
|
smartctl-recovery-health-attributes \
|
|
findmnt-outer-mountpoint \
|
|
findmnt-inner-mountpoint \
|
|
findmnt-recovery-source \
|
|
losetup-all \
|
|
stat-mapper-directory \
|
|
test-mapper-directory-not-symlink \
|
|
test-main-mapping-absent \
|
|
test-main-mapping-not-symlink \
|
|
test-proof-mapping-absent \
|
|
test-proof-mapping-not-symlink \
|
|
lsblk-all-names-types \
|
|
ntfs-probe-readwrite)"
|
|
assert_eq "$expected" "$(<"$log")" \
|
|
'pre-package collector uses the independently hard-coded canonical command order'
|
|
assert_succeeds task5a_assert_capture_lineage_and_cleanup "$expected" \
|
|
'successful pre-package composition'
|
|
[[ ! -s "$cryptsetup_log" ]] || return 1
|
|
|
|
for system_case in capture_stdout_no_lf capture_stderr capture_status \
|
|
wrong_identity smart mounted loop mapping mapper_dir_symlink probe; do
|
|
: >"$log"
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
: >"$cryptsetup_log"
|
|
assert_fails _k3slr_collect_prepackage_snapshot snapshot
|
|
assert_succeeds task5a_assert_capture_lineage_and_cleanup "$(<"$log")" \
|
|
"failing pre-package composition (${system_case})"
|
|
[[ ! -s "$cryptsetup_log" ]] || return 1
|
|
done
|
|
)
|
|
|
|
task5a_ntfsinfo_pipeline_contract() (
|
|
local hex expected_hex pipeline_case='' invalid_rc=0 expected_mawk
|
|
local event_log="${fixture_root}/task5a-ntfs-pipeline-events.log"
|
|
local path_log="${fixture_root}/task5a-ntfs-pipeline-paths.log"
|
|
local ownership_log="${fixture_root}/task5a-ntfs-pipeline-ownership.log"
|
|
local stage_log="${fixture_root}/task5a-ntfs-pipeline-stages.log"
|
|
local sudo_status_log="${fixture_root}/task5a-ntfs-pipeline-sudo-status.log"
|
|
local od_status_log="${fixture_root}/task5a-ntfs-pipeline-od-status.log"
|
|
local mawk_status_log="${fixture_root}/task5a-ntfs-pipeline-mawk-status.log"
|
|
|
|
task5a_ntfs_argv_equals() {
|
|
local expected_count="${1-}" argument_index
|
|
local -a compared_arguments=()
|
|
shift || return 1
|
|
[[ "$expected_count" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
compared_arguments=("$@")
|
|
(( ${#compared_arguments[@]} == 2 * expected_count )) || return 1
|
|
for (( argument_index=0; argument_index<expected_count; argument_index++ )); do
|
|
[[ "${compared_arguments[argument_index]}" == \
|
|
"${compared_arguments[expected_count + argument_index]}" ]] || return 1
|
|
done
|
|
}
|
|
|
|
task5a_reset_ntfs_stage_logs() {
|
|
: >"$stage_log"
|
|
: >"$sudo_status_log"
|
|
: >"$od_status_log"
|
|
: >"$mawk_status_log"
|
|
}
|
|
|
|
task5a_assert_ntfs_stage_run() {
|
|
local description="$1" expected_sudo="$2" expected_od="$3" expected_mawk_status="$4"
|
|
local stage sudo_count=0 od_count=0 mawk_count=0
|
|
local -a stages=() sudo_statuses=() od_statuses=() mawk_statuses=()
|
|
mapfile -t stages <"$stage_log"
|
|
mapfile -t sudo_statuses <"$sudo_status_log"
|
|
mapfile -t od_statuses <"$od_status_log"
|
|
mapfile -t mawk_statuses <"$mawk_status_log"
|
|
for stage in "${stages[@]}"; do
|
|
case "$stage" in
|
|
sudo) sudo_count=$((sudo_count + 1)) ;;
|
|
od) od_count=$((od_count + 1)) ;;
|
|
mawk) mawk_count=$((mawk_count + 1)) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
done
|
|
assert_eq 3 "${#stages[@]}" "$description runs exactly three pipeline stages"
|
|
assert_eq 1 "$sudo_count" "$description runs the producer exactly once"
|
|
assert_eq 1 "$od_count" "$description runs od exactly once"
|
|
assert_eq 1 "$mawk_count" "$description runs mawk exactly once"
|
|
assert_eq 1 "${#sudo_statuses[@]}" "$description records one producer status"
|
|
assert_eq 1 "${#od_statuses[@]}" "$description records one od status"
|
|
assert_eq 1 "${#mawk_statuses[@]}" "$description records one mawk status"
|
|
assert_eq "$expected_sudo" "${sudo_statuses[0]}" "$description preserves producer status"
|
|
assert_eq "$expected_od" "${od_statuses[0]}" "$description preserves od status"
|
|
assert_eq "$expected_mawk_status" "${mawk_statuses[0]}" \
|
|
"$description preserves mawk status"
|
|
}
|
|
|
|
task5a_assert_ntfs_capture_lineage() {
|
|
local expected_count="$1" description="$2"
|
|
local event_index capture_path
|
|
local -a events=() capture_paths=()
|
|
mapfile -t events <"$event_log"
|
|
mapfile -t capture_paths <"$path_log"
|
|
assert_eq "$expected_count" "${#capture_paths[@]}" \
|
|
"$description creates one capture directory per pipeline"
|
|
assert_eq "$((expected_count * 3))" "${#events[@]}" \
|
|
"$description records complete capture cleanup groups"
|
|
for (( event_index=0; event_index<expected_count; event_index++ )); do
|
|
capture_path="${capture_paths[event_index]}"
|
|
[[ "$capture_path" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ ]] || return 1
|
|
assert_eq "mktemp|${capture_path}" "${events[event_index * 3]}" \
|
|
"$description starts capture with its exact created path"
|
|
assert_eq "rm|${capture_path}" "${events[event_index * 3 + 1]}" \
|
|
"$description removes exactly stdout and stderr"
|
|
assert_eq "rmdir|${capture_path}" "${events[event_index * 3 + 2]}" \
|
|
"$description removes the same capture directory"
|
|
[[ ! -e "$capture_path" && ! -L "$capture_path" ]] || return 1
|
|
done
|
|
task5a_assert_capture_ownership_complete \
|
|
"$ownership_log" "$event_log" "$path_log" "$description"
|
|
}
|
|
|
|
task5a_fixture_ascii_hex() {
|
|
local input="$1" index character encoded
|
|
for (( index=0; index<${#input}; index++ )); do
|
|
character="${input:index:1}"
|
|
printf -v encoded '%02x' "'$character"
|
|
printf '%s' "$encoded"
|
|
done
|
|
}
|
|
expected_mawk=$'{\n for (i = 1; i <= NF; i++) {\n if ($i !~ /^[0-9a-f]{2}$/ || ++bytes > 65536) {\n bad = 1\n exit\n }\n hex = hex $i\n }\n}\nEND {\n if (bad || bytes == 0) exit 1\n print hex\n}'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
task5a_reset_ntfs_stage_logs
|
|
|
|
_k3slr_command() {
|
|
local created capture_directory command_rc=0
|
|
case "${1-}" in
|
|
/usr/bin/mktemp)
|
|
task5a_ntfs_argv_equals 4 /usr/bin/mktemp --directory --tmpdir=/tmp \
|
|
k3slr-capture.XXXXXXXX "$@" || return 97
|
|
task5a_capture_ledgers_require_coherent \
|
|
"$ownership_log" "$event_log" '' complete || return 97
|
|
created="$("$@")" || return 1
|
|
[[ "$created" =~ ^/tmp/k3slr-capture\.[A-Za-z0-9]{8}$ &&
|
|
-d "$created" && ! -L "$created" ]] || return 97
|
|
if ! task5a_capture_ledger_register \
|
|
"$ownership_log" "$event_log" "$created"; then
|
|
task5a_cleanup_capture_probe_path "$created" || return 1
|
|
return 97
|
|
fi
|
|
printf '%s\n' "$created" >>"$path_log"
|
|
printf '%s\n' "$created"
|
|
return 0
|
|
;;
|
|
/usr/bin/rm)
|
|
task5a_remove_owned_capture_records "$ownership_log" "$event_log" "$@"
|
|
return
|
|
;;
|
|
/usr/bin/rmdir)
|
|
task5a_remove_owned_capture_directory "$ownership_log" "$event_log" "$@"
|
|
return
|
|
;;
|
|
/usr/bin/sudo)
|
|
task5a_ntfs_argv_equals 7 /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/ntfsinfo --mft --notime /dev/sda3 "$@" || return 97
|
|
printf 'sudo\n' >>"$stage_log"
|
|
case "$pipeline_case" in
|
|
boundary_65536)
|
|
if /usr/bin/head --bytes=65536 /dev/zero; then command_rc=0; else command_rc=$?; fi
|
|
;;
|
|
boundary_65537)
|
|
if /usr/bin/head --bytes=65537 /dev/zero; then command_rc=0; else command_rc=$?; fi
|
|
;;
|
|
*)
|
|
printf 'Cluster Size: 4096\nVolume Size in Clusters: 1000000\nAllocated clusters 250000 (25.0%%)\nFree Clusters: 750000 (75.0%%)\n'
|
|
command_rc=0
|
|
;;
|
|
esac
|
|
if [[ "$pipeline_case" == producer_stderr ]]; then
|
|
printf 'unexpected stderr\n' >&2
|
|
elif [[ "$pipeline_case" == producer_failure ]]; then
|
|
command_rc=7
|
|
fi
|
|
printf '%s\n' "$command_rc" >>"$sudo_status_log"
|
|
return "$command_rc"
|
|
;;
|
|
/usr/bin/od)
|
|
task5a_ntfs_argv_equals 4 /usr/bin/od -An -v -tx1 "$@" || return 97
|
|
printf 'od\n' >>"$stage_log"
|
|
if "$@"; then command_rc=0; else command_rc=$?; fi
|
|
[[ "$pipeline_case" != od_failure || "$command_rc" != 0 ]] || command_rc=8
|
|
printf '%s\n' "$command_rc" >>"$od_status_log"
|
|
return "$command_rc"
|
|
;;
|
|
/usr/bin/mawk)
|
|
task5a_ntfs_argv_equals 2 /usr/bin/mawk "$expected_mawk" "$@" || return 97
|
|
printf 'mawk\n' >>"$stage_log"
|
|
if "$@"; then command_rc=0; else command_rc=$?; fi
|
|
[[ "$pipeline_case" != mawk_failure || "$command_rc" != 0 ]] || command_rc=9
|
|
printf '%s\n' "$command_rc" >>"$mawk_status_log"
|
|
return "$command_rc"
|
|
;;
|
|
*) return 97 ;;
|
|
esac
|
|
}
|
|
|
|
assert_succeeds task5a_unowned_capture_cleanup_is_rejected \
|
|
'ntfsinfo pipeline capture fake'
|
|
assert_succeeds task5a_capture_cleanup_phase_and_identity_is_enforced \
|
|
'ntfsinfo pipeline capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
assert_succeeds task5a_cross_ledger_coherence_is_enforced \
|
|
"$ownership_log" "$event_log" "$path_log" 'ntfsinfo pipeline capture fake'
|
|
: >"$event_log"
|
|
: >"$path_log"
|
|
: >"$ownership_log"
|
|
_k3slr_command /fixture/unknown >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects unknown argv with rc 97'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/sudo \
|
|
'--non-interactive -- /usr/bin/ntfsinfo --mft --notime /dev/sda3' \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" \
|
|
'ntfsinfo fake rejects grouped sudo arguments that flatten to the expected text'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/sudo --non-interactive -- /usr/bin/ntfsinfo --mft \
|
|
--notime /dev/sda3 unexpected >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects an extra sudo argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/sudo --non-interactive -- /usr/bin/ntfsinfo --mft \
|
|
--notime >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects a missing sudo argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/sudo --version >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects unknown same-sudo argv'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/od '-An -v -tx1' >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects grouped od argv'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/od -An -v -tx1 unexpected \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects an extra od argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/od -An -v >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects a missing od argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/od --version >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects unknown same-od argv'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/mawk "$expected_mawk" unexpected \
|
|
>/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects an extra mawk argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/mawk >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects a missing mawk argv element'
|
|
invalid_rc=0
|
|
_k3slr_command /usr/bin/mawk '{ print }' >/dev/null 2>&1 || invalid_rc=$?
|
|
assert_eq 97 "$invalid_rc" 'ntfsinfo fake rejects unknown same-mawk argv'
|
|
[[ ! -s "$event_log" && ! -s "$path_log" && ! -s "$stage_log" ]] || return 1
|
|
|
|
pipeline_case=''
|
|
assert_succeeds _k3slr_capture_ntfsinfo_hex hex /dev/sda3
|
|
expected_hex="$(task5a_fixture_ascii_hex $'Cluster Size: 4096\nVolume Size in Clusters: 1000000\nAllocated clusters 250000 (25.0%)\nFree Clusters: 750000 (75.0%)\n')"$'\n'
|
|
assert_eq "$expected_hex" "$hex" 'ntfsinfo pipeline returns only normalized bounded hex'
|
|
assert_succeeds task5a_assert_ntfs_stage_run 'successful ntfsinfo pipeline' 0 0 0
|
|
|
|
for pipeline_case in producer_failure od_failure mawk_failure producer_stderr; do
|
|
task5a_reset_ntfs_stage_logs
|
|
assert_fails _k3slr_capture_ntfsinfo_hex hex /dev/sda3
|
|
case "$pipeline_case" in
|
|
producer_failure)
|
|
assert_succeeds task5a_assert_ntfs_stage_run \
|
|
'producer-only failure pipeline' 7 0 0
|
|
;;
|
|
od_failure)
|
|
assert_succeeds task5a_assert_ntfs_stage_run \
|
|
'od-only failure pipeline' 0 8 0
|
|
;;
|
|
mawk_failure)
|
|
assert_succeeds task5a_assert_ntfs_stage_run \
|
|
'mawk-only failure pipeline' 0 0 9
|
|
;;
|
|
producer_stderr)
|
|
assert_succeeds task5a_assert_ntfs_stage_run \
|
|
'stderr-rejection pipeline' 0 0 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
pipeline_case=boundary_65536
|
|
task5a_reset_ntfs_stage_logs
|
|
assert_succeeds _k3slr_capture_ntfsinfo_hex hex /dev/sda3
|
|
assert_eq 131073 "${#hex}" 'ntfsinfo pipeline accepts exactly 65,536 bytes plus output LF'
|
|
[[ "$hex" =~ ^0+$'\n'$ ]] || return 1
|
|
assert_succeeds task5a_assert_ntfs_stage_run '65,536-byte pipeline boundary' 0 0 0
|
|
|
|
pipeline_case=boundary_65537
|
|
task5a_reset_ntfs_stage_logs
|
|
assert_fails _k3slr_capture_ntfsinfo_hex hex /dev/sda3
|
|
assert_succeeds task5a_assert_ntfs_stage_run '65,537-byte pipeline rejection' 0 0 1
|
|
|
|
assert_succeeds task5a_assert_ntfs_capture_lineage 7 \
|
|
'success, status, stderr, and byte-boundary pipelines'
|
|
)
|
|
|
|
task5a_dry_run_and_prepackage_action_binding() (
|
|
local mode wrapper main_name log output action_rc=0 probe_rc=0 record=''
|
|
local command_calls=0
|
|
for mode in prepare open close; do
|
|
case "$mode" in
|
|
prepare) wrapper="$PREPARE_WRAPPER_PATH" ;;
|
|
open) wrapper="$OPEN_WRAPPER_PATH" ;;
|
|
close) wrapper="$CLOSE_WRAPPER_PATH" ;;
|
|
esac
|
|
main_name="_k3slr_${mode}_main"
|
|
log="${fixture_root}/task5a-${mode}-dry-run.log"
|
|
: >"$log"
|
|
# shellcheck source=/dev/null
|
|
source "$wrapper"
|
|
_k3slr_load_contract() {
|
|
printf 'contract\n' >>"$log"
|
|
}
|
|
_k3slr_prepackage_device_preflight() {
|
|
printf 'prepackage\n' >>"$log"
|
|
}
|
|
_k3slr_require_execute_tty() {
|
|
printf 'unexpected-tty\n' >>"$log"
|
|
return 97
|
|
}
|
|
_k3slr_command() {
|
|
(( command_calls += 1 ))
|
|
printf 'unexpected-command:%s\n' "$*" >>"$log"
|
|
return 97
|
|
}
|
|
output="$("$main_name")" || return 1
|
|
[[ "$output" == "DRY RUN: ${mode} lifecycle; no changes made." ]] || return 1
|
|
assert_eq $'contract\nprepackage' "$(<"$log")" \
|
|
"$mode dry-run uses only the non-interactive pre-package evidence seam"
|
|
|
|
: >"$log"
|
|
_k3slr_prepackage_device_preflight() {
|
|
printf 'prepackage\n' >>"$log"
|
|
return 1
|
|
}
|
|
assert_fails "$main_name"
|
|
assert_eq $'contract\nprepackage' "$(<"$log")" \
|
|
"$mode dry-run fails closed when cached read-only evidence is unavailable"
|
|
done
|
|
|
|
log="${fixture_root}/task5a-device-validator-binding.log"
|
|
: >"$log"
|
|
_k3slr_prepackage_device_preflight() {
|
|
printf 'prepackage\n' >>"$log"
|
|
}
|
|
_k3slr_command() {
|
|
(( command_calls += 1 ))
|
|
return 97
|
|
}
|
|
task5b_legacy_binding_probe_payload() {
|
|
local binding_case="${1-}" action_rc=0 command_calls=0
|
|
(( $# == 1 )) || return 1
|
|
case "$binding_case" in
|
|
prepare-device)
|
|
_k3slr_lifecycle_action prepare device-validator \
|
|
_k3slr_prepare_invocation || action_rc=$?
|
|
;;
|
|
prepare-sudo)
|
|
_k3slr_lifecycle_action prepare sudo _k3slr_prepare_invocation ||
|
|
action_rc=$?
|
|
;;
|
|
open-device)
|
|
_k3slr_lifecycle_action open 'device-validator(--expect-device-ready)' ||
|
|
action_rc=$?
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
task5b_production_probe_emit_ok legacy_binding "$action_rc" "$command_calls"
|
|
}
|
|
task5b_production_probe_adversary_matrix || return 1
|
|
task5b_production_probe_static_graph_is_closed legacy_binding || return 1
|
|
record="$(
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm legacy_binding || return 1
|
|
task5b_legacy_binding_probe_payload prepare-device
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" 'prepare device-validator binding probe completes safely'
|
|
assert_eq 'TASK5B_PROBE_OK|legacy_binding|1|0' "$record" \
|
|
'prepare device-validator binding is fail-closed before commands'
|
|
assert_eq '' "$(<"$log")" \
|
|
'prepare action integration remains fail-closed until its later checkpoint'
|
|
record=''
|
|
probe_rc=0
|
|
record="$(
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm legacy_binding || return 1
|
|
task5b_legacy_binding_probe_payload prepare-sudo
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" 'prepare sudo binding probe completes safely'
|
|
assert_eq 'TASK5B_PROBE_OK|legacy_binding|97|1' "$record" \
|
|
'prepare sudo binding reaches only the modeled command failure'
|
|
record=''
|
|
probe_rc=0
|
|
record="$(
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm legacy_binding || return 1
|
|
task5b_legacy_binding_probe_payload open-device
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" 'open device-validator binding probe completes safely'
|
|
assert_eq 'TASK5B_PROBE_OK|legacy_binding|1|0' "$record" \
|
|
'open device-validator binding is fail-closed before commands'
|
|
)
|
|
|
|
# The production break this catches is a Task 5B fake command boundary that
|
|
# accepts a hazardous near-match or reaches a real executable. Expected argv
|
|
# below are independent test literals; none are derived from production arrays.
|
|
task5b_gate0_route_equals() {
|
|
local route_id="${1-}" argument_index
|
|
local -a actual_arguments=() expected_arguments=()
|
|
shift || return 1
|
|
actual_arguments=("$@")
|
|
case "$route_id" in
|
|
sudo-validate)
|
|
expected_arguments=(/usr/bin/sudo --validate)
|
|
;;
|
|
sudo-cached-validate)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive --validate)
|
|
;;
|
|
apt-update)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/apt-get update)
|
|
;;
|
|
apt-install)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/apt-get install --yes
|
|
keepassxc=2.7.6+dfsg.1-1build3 cryptsetup-bin=2:2.7.0-1ubuntu4.2)
|
|
;;
|
|
feasibility-execute)
|
|
expected_arguments=(/usr/bin/bash
|
|
/home/donghyeon/workspace/platform/scripts/validate/k3s-local-recovery-feasibility.sh
|
|
--execute)
|
|
;;
|
|
outer-scaffold-mkdir)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/mkdir
|
|
--mode=0700 -- /mnt/k3s-recovery-ssd)
|
|
;;
|
|
outer-mount)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/mount -t ntfs3
|
|
-o rw,uid=1000,gid=1000,dmask=0077,fmask=0177,nodev,nosuid,noexec
|
|
/dev/disk/by-id/wwn-0x500a0751e6aa6254-part3 /mnt/k3s-recovery-ssd)
|
|
;;
|
|
layout-root-mkdir)
|
|
expected_arguments=(/usr/bin/mkdir --mode=0700 --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery)
|
|
;;
|
|
layout-vault-mkdir)
|
|
expected_arguments=(/usr/bin/mkdir --mode=0700 --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault)
|
|
;;
|
|
layout-backups-mkdir)
|
|
expected_arguments=(/usr/bin/mkdir --mode=0700 --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault/backups)
|
|
;;
|
|
layout-containers-mkdir)
|
|
expected_arguments=(/usr/bin/mkdir --mode=0700 --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers)
|
|
;;
|
|
probe-mktemp)
|
|
expected_arguments=(/usr/bin/mktemp
|
|
--tmpdir=/mnt/k3s-recovery-ssd/HyeonworksRecovery
|
|
.k3slr-hardlink-probe.XXXXXXXX)
|
|
;;
|
|
probe-link)
|
|
expected_arguments=(/usr/bin/ln --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-hardlink-probe.FIXTURE1
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-hardlink-probe.FIXTURE1.link)
|
|
;;
|
|
probe-unlink-link)
|
|
expected_arguments=(/usr/bin/unlink --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-hardlink-probe.FIXTURE1.link)
|
|
;;
|
|
probe-unlink-source)
|
|
expected_arguments=(/usr/bin/unlink --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-hardlink-probe.FIXTURE1)
|
|
;;
|
|
probe-sync)
|
|
expected_arguments=(/usr/bin/sync --file-system
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery)
|
|
;;
|
|
keepass-db-create)
|
|
expected_arguments=(/usr/bin/keepassxc-cli db-create --quiet --set-password
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault/hyeonworks-recovery.kdbx)
|
|
;;
|
|
keepass-add)
|
|
expected_arguments=(/usr/bin/keepassxc-cli add --quiet --generate --length 40
|
|
--lower --upper --numeric --every-group
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault/hyeonworks-recovery.kdbx
|
|
'K3s Recovery LUKS')
|
|
;;
|
|
container-dd)
|
|
expected_arguments=(/usr/bin/dd if=/dev/zero
|
|
of=/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks
|
|
bs=16M count=2048 conv=excl,fsync status=progress)
|
|
;;
|
|
outer-umount)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/umount --
|
|
/mnt/k3s-recovery-ssd)
|
|
;;
|
|
loop-create)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/sbin/losetup
|
|
--find --show --nooverlap --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks)
|
|
;;
|
|
luks-format)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/sbin/cryptsetup
|
|
luksFormat --batch-mode --type luks2 --key-file=- /dev/loop37)
|
|
;;
|
|
luks-open)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/sbin/cryptsetup
|
|
open --type luks2 --key-file=- /dev/loop37 k3s-recovery)
|
|
;;
|
|
mkfs-ext4)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/sbin/mkfs.ext4
|
|
-F -L K3S_RECOVERY /dev/mapper/k3s-recovery)
|
|
;;
|
|
inner-parent-mkdir)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/mkdir
|
|
--mode=0700 -- /srv/recovery)
|
|
;;
|
|
inner-mountpoint-mkdir)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/mkdir
|
|
--mode=0700 -- /srv/recovery/k3s)
|
|
;;
|
|
inner-mount)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/mount -t ext4
|
|
-o rw,nodev,nosuid,noexec /dev/mapper/k3s-recovery /srv/recovery/k3s)
|
|
;;
|
|
inner-chown)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/chown
|
|
root:root /srv/recovery/k3s)
|
|
;;
|
|
inner-chmod)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/chmod
|
|
0700 /srv/recovery/k3s)
|
|
;;
|
|
marker-mktemp)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/mktemp
|
|
--tmpdir=/srv/recovery/k3s .k3slr-marker.tmp.XXXXXXXX)
|
|
;;
|
|
marker-dd)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/dd
|
|
of=/srv/recovery/k3s/.k3slr-marker.tmp.FIXTURE1 bs=37 count=1
|
|
iflag=fullblock conv=notrunc,fsync status=none)
|
|
;;
|
|
outer-metadata-mktemp)
|
|
expected_arguments=(/usr/bin/mktemp
|
|
--tmpdir=/mnt/k3s-recovery-ssd/HyeonworksRecovery
|
|
.k3slr-volume.tmp.XXXXXXXX)
|
|
;;
|
|
inner-metadata-mktemp)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/mktemp
|
|
--tmpdir=/srv/recovery/k3s .k3slr-volume.tmp.XXXXXXXX)
|
|
;;
|
|
outer-metadata-dd)
|
|
expected_arguments=(/usr/bin/dd
|
|
of=/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1
|
|
bs=4096 count=1 iflag=fullblock conv=notrunc,fsync status=none)
|
|
;;
|
|
inner-metadata-dd)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/dd
|
|
if=/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1
|
|
of=/srv/recovery/k3s/.k3slr-volume.tmp.FIXTURE1 bs=4096 count=1
|
|
iflag=fullblock conv=notrunc,fsync status=none)
|
|
;;
|
|
marker-stage-sync)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/sync
|
|
--file-system /srv/recovery/k3s/.k3slr-marker.tmp.FIXTURE1)
|
|
;;
|
|
marker-publish)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/ln --
|
|
/srv/recovery/k3s/.k3slr-marker.tmp.FIXTURE1
|
|
/srv/recovery/k3s/.recovery-marker)
|
|
;;
|
|
outer-stage-sync)
|
|
expected_arguments=(/usr/bin/sync --file-system
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1)
|
|
;;
|
|
inner-stage-sync)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/sync
|
|
--file-system /srv/recovery/k3s/.k3slr-volume.tmp.FIXTURE1)
|
|
;;
|
|
outer-metadata-publish)
|
|
expected_arguments=(/usr/bin/ln --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/volume.env)
|
|
;;
|
|
inner-metadata-publish)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/ln --
|
|
/srv/recovery/k3s/.k3slr-volume.tmp.FIXTURE1
|
|
/srv/recovery/k3s/.recovery-volume.env)
|
|
;;
|
|
marker-stage-unlink)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/unlink --
|
|
/srv/recovery/k3s/.k3slr-marker.tmp.FIXTURE1)
|
|
;;
|
|
outer-stage-unlink)
|
|
expected_arguments=(/usr/bin/unlink --
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1)
|
|
;;
|
|
inner-stage-unlink)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/unlink --
|
|
/srv/recovery/k3s/.k3slr-volume.tmp.FIXTURE1)
|
|
;;
|
|
inner-parent-sync)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/sync
|
|
--file-system /srv/recovery/k3s)
|
|
;;
|
|
inner-umount)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/bin/umount --
|
|
/srv/recovery/k3s)
|
|
;;
|
|
mapping-close)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/sbin/cryptsetup
|
|
close k3s-recovery)
|
|
;;
|
|
loop-detach)
|
|
expected_arguments=(/usr/bin/sudo --non-interactive -- /usr/sbin/losetup
|
|
--detach /dev/loop37)
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
(( ${#actual_arguments[@]} == ${#expected_arguments[@]} )) || return 1
|
|
for (( argument_index=0; argument_index<${#expected_arguments[@]}; argument_index++ )); do
|
|
[[ "${actual_arguments[argument_index]}" == "${expected_arguments[argument_index]}" ]] ||
|
|
return 1
|
|
done
|
|
}
|
|
|
|
task5b_gate0_fake_command() {
|
|
local route_id
|
|
local -a route_ids=(
|
|
sudo-validate sudo-cached-validate apt-update apt-install feasibility-execute
|
|
outer-scaffold-mkdir outer-mount layout-root-mkdir layout-vault-mkdir
|
|
layout-backups-mkdir layout-containers-mkdir probe-mktemp probe-link
|
|
probe-unlink-link probe-unlink-source probe-sync keepass-db-create keepass-add
|
|
container-dd outer-umount loop-create luks-format luks-open mkfs-ext4
|
|
inner-parent-mkdir inner-mountpoint-mkdir inner-mount inner-chown inner-chmod
|
|
marker-mktemp marker-dd outer-metadata-mktemp inner-metadata-mktemp
|
|
outer-metadata-dd inner-metadata-dd marker-stage-sync marker-publish
|
|
outer-stage-sync inner-stage-sync outer-metadata-publish inner-metadata-publish
|
|
marker-stage-unlink outer-stage-unlink inner-stage-unlink inner-parent-sync
|
|
inner-umount mapping-close loop-detach
|
|
)
|
|
for route_id in "${route_ids[@]}"; do
|
|
if task5b_gate0_route_equals "$route_id" "$@"; then
|
|
(( TASK5B_GATE0_MODEL_DISPATCH_COUNT += 1 ))
|
|
(( TASK5B_GATE0_MODEL_EFFECT_COUNT += 1 ))
|
|
TASK5B_GATE0_LAST_ROUTE="$route_id"
|
|
TASK5B_GATE0_LAST_OUTPUT="modeled:${route_id}"
|
|
return 0
|
|
fi
|
|
done
|
|
return 97
|
|
}
|
|
|
|
readonly -a TASK5B_GATE0_ACTIVE_GRAPH_FUNCTIONS=(
|
|
_k3slr_command assert_eq fail task5b_gate0_active_matrix
|
|
task5b_gate0_assert_rejected task5b_gate0_assert_route
|
|
task5b_gate0_fake_command task5b_gate0_reset_route_record
|
|
task5b_gate0_route_equals task5b_gate0_assignment_only_status
|
|
task5b_gate0_real_exec_fuse
|
|
)
|
|
|
|
task5b_gate0_assignment_only_status() {
|
|
local candidate="${1-}" lhs rhs character next_character quote='' mode=scalar
|
|
local index=0 length depth=0 escaped=0 word_ended=0 append_assignment=0
|
|
(( $# == 1 )) || return 1
|
|
candidate="${candidate#"${candidate%%[![:space:]]*}"}"
|
|
candidate="${candidate%"${candidate##*[![:space:]]}"}"
|
|
[[ "$candidate" == *=* ]] || return 1
|
|
lhs="${candidate%%=*}"
|
|
rhs="${candidate#*=}"
|
|
if [[ "$lhs" == *+ ]]; then
|
|
lhs="${lhs%+}"
|
|
append_assignment=1
|
|
fi
|
|
[[ "$lhs" =~ ^[a-zA-Z_][a-zA-Z0-9_]*(\[([a-zA-Z0-9_]+|[a-zA-Z0-9_]+[[:space:]]*[-+][[:space:]]*[0-9]+)\])?$ ]] ||
|
|
return 1
|
|
length="${#rhs}"
|
|
(( length > 0 )) || return 0
|
|
(( append_assignment == 0 )) || [[ "${rhs:0:1}" == '(' ]] || return 1
|
|
if [[ "${rhs:0:1}" == '(' ]]; then
|
|
mode=compound
|
|
depth=1
|
|
index=1
|
|
fi
|
|
while (( index < length )); do
|
|
character="${rhs:index:1}"
|
|
next_character=''
|
|
(( index + 1 >= length )) || next_character="${rhs:index + 1:1}"
|
|
if (( escaped == 1 )); then
|
|
escaped=0
|
|
(( index += 1 ))
|
|
continue
|
|
fi
|
|
case "$quote" in
|
|
single)
|
|
[[ "$character" != "'" ]] || quote=''
|
|
(( index += 1 ))
|
|
continue
|
|
;;
|
|
double)
|
|
case "$character" in
|
|
'\\') escaped=1 ;;
|
|
'"') quote='' ;;
|
|
'`') return 1 ;;
|
|
'$')
|
|
[[ "$next_character" != '(' ]] || return 1
|
|
;;
|
|
esac
|
|
(( index += 1 ))
|
|
continue
|
|
;;
|
|
esac
|
|
if (( depth > 0 )) && [[ "$mode" == arithmetic ]]; then
|
|
case "$character" in
|
|
'`') return 1 ;;
|
|
'$')
|
|
[[ "$next_character" != '(' ]] || return 1
|
|
;;
|
|
'(') (( depth += 1 )) ;;
|
|
')') (( depth -= 1 )) ;;
|
|
esac
|
|
(( index += 1 ))
|
|
continue
|
|
fi
|
|
case "$character" in
|
|
"'") quote=single ;;
|
|
'"') quote=double ;;
|
|
'\\') escaped=1 ;;
|
|
'`'|';'|'&'|'|'|'<'|'>') return 1 ;;
|
|
'$')
|
|
if [[ "${rhs:index:3}" == '$((' ]]; then
|
|
mode=arithmetic
|
|
depth=2
|
|
(( index += 3 ))
|
|
continue
|
|
fi
|
|
[[ "$next_character" != '(' ]] || return 1
|
|
;;
|
|
'(')
|
|
[[ "$mode" == compound ]] || return 1
|
|
(( depth += 1 ))
|
|
;;
|
|
')')
|
|
[[ "$mode" == compound ]] || return 1
|
|
(( depth -= 1 ))
|
|
(( depth >= 0 )) || return 1
|
|
if (( depth == 0 )); then
|
|
(( index += 1 ))
|
|
while (( index < length )); do
|
|
[[ "${rhs:index:1}" =~ [[:space:]] ]] || return 1
|
|
(( index += 1 ))
|
|
done
|
|
return 0
|
|
fi
|
|
;;
|
|
$'\n'|$'\r')
|
|
[[ "$mode" == compound ]] || return 1
|
|
;;
|
|
[[:space:]])
|
|
if [[ "$mode" != compound ]]; then
|
|
word_ended=1
|
|
fi
|
|
;;
|
|
*)
|
|
(( word_ended == 0 )) || return 1
|
|
;;
|
|
esac
|
|
(( index += 1 ))
|
|
done
|
|
[[ -z "$quote" && "$escaped" == 0 ]] || return 1
|
|
if [[ "$mode" == compound && "$depth" -gt 0 ]]; then
|
|
return 2
|
|
fi
|
|
(( depth == 0 ))
|
|
}
|
|
|
|
task5b_gate0_real_exec_fuse() {
|
|
local candidate="${BASH_COMMAND-}" execution_word='' assignment_rc=0
|
|
candidate="${candidate#"${candidate%%[![:space:]]*}"}"
|
|
while [[ "$candidate" =~ ^(if|elif|while|until|then|\!|time)[[:space:]]+(.+)$ ]]; do
|
|
candidate="${BASH_REMATCH[2]}"
|
|
done
|
|
case "$candidate" in
|
|
''|'{'|'}'|'[['*|'(('*|for\ *|case\ *|do|done|esac) return 0 ;;
|
|
esac
|
|
if [[ "$candidate" =~ ^[a-zA-Z_][a-zA-Z0-9_]*(\[[^]]+\])?\+?= ]]; then
|
|
task5b_gate0_assignment_only_status "$candidate" || assignment_rc=$?
|
|
(( assignment_rc == 0 )) && return 0
|
|
(( TASK5B_GATE0_ACTUAL_EXEC_COUNT += 1 ))
|
|
if [[ "${TASK5B_GATE0_FUSE_PROBE_SILENT:-0}" != 1 ]]; then
|
|
printf 'Task 5B Gate 0 blocked assignment-prefixed execution\n' >&9
|
|
fi
|
|
exit 96
|
|
fi
|
|
read -r execution_word _ <<<"$candidate"
|
|
execution_word="${execution_word%;}"
|
|
case "$execution_word" in
|
|
\$*|\"\$*|\'\$*|/*|\"/*|\'/*|command|builtin|exec|eval|source|.)
|
|
(( TASK5B_GATE0_ACTUAL_EXEC_COUNT += 1 ))
|
|
if [[ "${TASK5B_GATE0_FUSE_PROBE_SILENT:-0}" != 1 ]]; then
|
|
printf 'Task 5B Gate 0 blocked dynamic/external execution word: %s\n' \
|
|
"$execution_word" >&9
|
|
fi
|
|
exit 96
|
|
;;
|
|
esac
|
|
case "$execution_word" in
|
|
_k3slr_command|assert_eq|fail|task5b_gate0_active_matrix|\
|
|
task5b_gate0_assert_rejected|task5b_gate0_assert_route|\
|
|
task5b_gate0_fake_command|task5b_gate0_reset_route_record|\
|
|
task5b_gate0_route_equals|exit|local|shift|return|printf|read|:)
|
|
return 0
|
|
;;
|
|
esac
|
|
(( TASK5B_GATE0_ACTUAL_EXEC_COUNT += 1 ))
|
|
if [[ "${TASK5B_GATE0_FUSE_PROBE_SILENT:-0}" != 1 ]]; then
|
|
printf 'Task 5B Gate 0 blocked non-builtin execution word: %s\n' \
|
|
"$execution_word" >&9
|
|
fi
|
|
exit 96
|
|
}
|
|
|
|
task5b_gate0_adversary_direct() {
|
|
/fixture/task5b-unlisted-direct
|
|
}
|
|
|
|
task5b_gate0_adversary_indented() {
|
|
"$@"
|
|
}
|
|
|
|
task5b_gate0_adversary_control_word() {
|
|
if command "$@"; then
|
|
:
|
|
fi
|
|
}
|
|
|
|
task5b_gate0_adversary_array() {
|
|
local -a task5b_runner=(/fixture/task5b-unlisted-array)
|
|
"${task5b_runner[@]}"
|
|
}
|
|
|
|
task5b_gate0_adversary_variable() {
|
|
local task5b_runner=/fixture/task5b-unlisted-variable
|
|
"$task5b_runner"
|
|
}
|
|
|
|
task5b_gate0_adversary_shell_c() {
|
|
/fixture/unlisted/bash -c ':'
|
|
}
|
|
|
|
task5b_gate0_adversary_helper_root() {
|
|
task5b_gate0_adversary_helper_leaf
|
|
}
|
|
|
|
task5b_gate0_adversary_helper_leaf() {
|
|
/fixture/task5b-unlisted-helper
|
|
}
|
|
|
|
task5b_gate0_adversary_trap_disable() {
|
|
trap - DEBUG
|
|
set +T
|
|
shopt -u extdebug
|
|
}
|
|
|
|
task5b_gate0_adversary_assignment_bare() {
|
|
TASK5B_ASSIGNMENT_PREFIX= trap - DEBUG
|
|
}
|
|
|
|
task5b_gate0_adversary_assignment_simple() {
|
|
TASK5B_ASSIGNMENT_PREFIX=1 /usr/bin/printf TASK5B_ASSIGNMENT_PREFIX_CANARY
|
|
}
|
|
|
|
task5b_gate0_adversary_assignment_quoted() {
|
|
TASK5B_ASSIGNMENT_PREFIX="a b" trap - DEBUG
|
|
}
|
|
|
|
task5b_gate0_adversary_assignment_indexed() {
|
|
TASK5B_ASSIGNMENT_PREFIX[0]=1 trap - DEBUG
|
|
}
|
|
|
|
task5b_gate0_static_safe_sample() {
|
|
local value=''
|
|
task5b_gate0_route_equals sudo-validate /usr/bin/sudo --validate || return 1
|
|
value=modeled
|
|
[[ "$value" == modeled ]]
|
|
}
|
|
|
|
task5b_gate0_static_assignment_prefixes_are_closed() {
|
|
local body="${1-}" line candidate='' compound_candidate=''
|
|
local assignment_rc=0
|
|
(( $# == 1 )) || return 1
|
|
while IFS= read -r line; do
|
|
candidate="${line#"${line%%[![:space:]]*}"}"
|
|
candidate="${candidate%"${candidate##*[![:space:]]}"}"
|
|
while [[ "$candidate" =~ ^(if|elif|while|until|then|\!|time)[[:space:]]+(.+)$ ]]; do
|
|
candidate="${BASH_REMATCH[2]}"
|
|
done
|
|
[[ "$candidate" != *';' ]] || candidate="${candidate%;}"
|
|
if [[ -n "$compound_candidate" ]]; then
|
|
compound_candidate+=$'\n'"$candidate"
|
|
assignment_rc=0
|
|
task5b_gate0_assignment_only_status "$compound_candidate" ||
|
|
assignment_rc=$?
|
|
case "$assignment_rc" in
|
|
0) compound_candidate='' ;;
|
|
2) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
continue
|
|
fi
|
|
[[ "$candidate" =~ ^[a-zA-Z_][a-zA-Z0-9_]*(\[[^]]+\])?\+?= ]] ||
|
|
continue
|
|
assignment_rc=0
|
|
task5b_gate0_assignment_only_status "$candidate" || assignment_rc=$?
|
|
case "$assignment_rc" in
|
|
0) ;;
|
|
2) compound_candidate="$candidate" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
done <<<"$body"
|
|
if [[ -n "$compound_candidate" ]]; then
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
task5b_gate0_static_execution_positions_are_closed() {
|
|
local body="${1-}"
|
|
(( $# == 1 )) || return 1
|
|
[[ -n "$body" ]] || return 1
|
|
if /usr/bin/grep -En \
|
|
'^[[:space:]]*((if|elif|while|until|then|!|time)[[:space:]]+)*("?/?[^[:space:]]*/(ba|z|k|da)?sh|bash|sh)[[:space:]].*(-c|--command)([[:space:]]|$)|^[[:space:]]*((if|elif|while|until|then|!|time)[[:space:]]+)*("?\$|\$\{|/|"/|command([[:space:]]|$)|builtin[[:space:]]+command([[:space:]]|$)|exec([[:space:]]|$)|eval([[:space:]]|$)|source([[:space:]]|$)|\.[[:space:]])|\$\*|\$\{[^}]+\[\*\][^}]*\}|`|\$\(|^[[:space:]]*(trap([[:space:]]|$)|set[[:space:]]+\+T([[:space:]]|$)|shopt[[:space:]].*extdebug)' \
|
|
<<<"$body" >/dev/null; then
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
task5b_gate0_static_body_is_closed() {
|
|
local body="${1-}"
|
|
(( $# == 1 )) || return 1
|
|
task5b_gate0_static_assignment_prefixes_are_closed "$body" || return 1
|
|
task5b_gate0_static_execution_positions_are_closed "$body"
|
|
}
|
|
|
|
task5b_gate0_definition_hash() {
|
|
local destination_name="${1-}" function_name="${2-}" body='' hash_record=''
|
|
(( $# == 2 )) || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
body="$(declare -f "$function_name")" || return 1
|
|
hash_record="$(printf '%s\n' "$body" | /usr/bin/sha256sum)" || return 1
|
|
[[ "$hash_record" =~ ^([0-9a-f]{64})[[:space:]]+-$ ]] || return 1
|
|
printf -v "$destination_name" '%s' "${BASH_REMATCH[1]}"
|
|
}
|
|
|
|
task5b_gate0_normalize_static_body_data_lines() {
|
|
local body="${1-}" destination_name="${2-}" function_name="${3-}"
|
|
local line normalized='' previous_continues=0
|
|
(( $# == 3 )) || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
while IFS= read -r line; do
|
|
if (( previous_continues == 1 )); then
|
|
normalized+=' task5b_static_argument '
|
|
elif [[ "$function_name" == task5b_gate0_route_equals &&
|
|
"$line" =~ ^[[:space:]]+/ ]]; then
|
|
normalized+=' task5b_static_array_value '
|
|
fi
|
|
normalized+="$line"$'\n'
|
|
previous_continues=0
|
|
[[ "$line" != *'\' ]] || previous_continues=1
|
|
done <<<"$body"
|
|
if [[ "$function_name" == fail ]]; then
|
|
normalized="${normalized//\"\$*\"/task5b_static_message}"
|
|
fi
|
|
normalized="${normalized//\$\(\(/task5b_static_arithmetic_(}"
|
|
printf -v "$destination_name" '%s' "$normalized"
|
|
}
|
|
|
|
task5b_gate0_fuse_body_is_closed() {
|
|
local body="${1-}" exact_allowlist
|
|
(( $# == 1 )) || return 1
|
|
[[ -n "$body" ]] || return 1
|
|
[[ "$body" != *'trap - DEBUG'* && "$body" != *'set +T'* &&
|
|
"$body" != *'shopt -u extdebug'* ]] || return 1
|
|
exact_allowlist='_k3slr_command | assert_eq | fail | task5b_gate0_active_matrix | task5b_gate0_assert_rejected | task5b_gate0_assert_route | task5b_gate0_fake_command | task5b_gate0_reset_route_record | task5b_gate0_route_equals | exit | local | shift | return | printf | read | :)'
|
|
[[ "$body" == *"$exact_allowlist"* ]] || return 1
|
|
[[ "$body" != *'task5b_gate0_dynamic_external_helper'* ]] || return 1
|
|
[[ "$body" != *'*=*)'* ]] || return 1
|
|
}
|
|
|
|
task5b_gate0_assignment_parser_body_is_closed() {
|
|
local body="${1-}"
|
|
(( $# == 1 )) || return 1
|
|
[[ -n "$body" ]] || return 1
|
|
if /usr/bin/grep -En \
|
|
'^[[:space:]]*(trap([[:space:]]|$)|set[[:space:]]+\+T([[:space:]]|$)|shopt[[:space:]].*extdebug|/|"/|command([[:space:]]|$)|builtin[[:space:]]+command([[:space:]]|$)|exec([[:space:]]|$)|eval([[:space:]]|$)|source([[:space:]]|$)|\.[[:space:]])' \
|
|
<<<"$body" >/dev/null; then
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
task5b_gate0_active_graph_is_closed() {
|
|
local function_name body='' normalized_body=''
|
|
for function_name in "${TASK5B_GATE0_ACTIVE_GRAPH_FUNCTIONS[@]}"; do
|
|
body="$(declare -f "$function_name")" || return 1
|
|
if [[ "$function_name" == task5b_gate0_real_exec_fuse ]]; then
|
|
task5b_gate0_fuse_body_is_closed "$body" || {
|
|
printf 'Task 5B Gate 0 static fuse scan rejected: %s\n' \
|
|
"$function_name" >&2
|
|
return 1
|
|
}
|
|
continue
|
|
fi
|
|
if [[ "$function_name" == task5b_gate0_assignment_only_status ]]; then
|
|
task5b_gate0_assignment_parser_body_is_closed "$body" || {
|
|
printf 'Task 5B Gate 0 assignment parser scan rejected: %s\n' \
|
|
"$function_name" >&2
|
|
return 1
|
|
}
|
|
continue
|
|
fi
|
|
task5b_gate0_normalize_static_body_data_lines "$body" normalized_body \
|
|
"$function_name" || return 1
|
|
task5b_gate0_static_assignment_prefixes_are_closed "$body" || {
|
|
printf 'Task 5B Gate 0 assignment-prefix scan rejected: %s\n' \
|
|
"$function_name" >&2
|
|
return 1
|
|
}
|
|
task5b_gate0_static_execution_positions_are_closed "$normalized_body" || {
|
|
printf 'Task 5B Gate 0 active graph scan rejected: %s\n' \
|
|
"$function_name" >&2
|
|
return 1
|
|
}
|
|
done
|
|
}
|
|
|
|
task5b_gate0_active_graph_hash() {
|
|
local destination_name="${1-}" function_name body='' graph_record=''
|
|
(( $# == 1 )) || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
for function_name in "${TASK5B_GATE0_ACTIVE_GRAPH_FUNCTIONS[@]}"; do
|
|
body="$(declare -f "$function_name")" || return 1
|
|
graph_record+="${function_name}"$'\n'"${body}"$'\n'
|
|
done
|
|
body="$(printf '%s' "$graph_record" | /usr/bin/sha256sum)" || return 1
|
|
[[ "$body" =~ ^([0-9a-f]{64})[[:space:]]+-$ ]] || return 1
|
|
printf -v "$destination_name" '%s' "${BASH_REMATCH[1]}"
|
|
}
|
|
|
|
task5b_gate0_static_adversarial_contract() {
|
|
local body=''
|
|
body="$(declare -f task5b_gate0_static_safe_sample)" || return 1
|
|
assert_succeeds task5b_gate0_static_body_is_closed "$body"
|
|
for adversary in task5b_gate0_adversary_direct task5b_gate0_adversary_indented \
|
|
task5b_gate0_adversary_control_word task5b_gate0_adversary_array \
|
|
task5b_gate0_adversary_variable task5b_gate0_adversary_shell_c \
|
|
task5b_gate0_adversary_trap_disable \
|
|
task5b_gate0_adversary_assignment_bare \
|
|
task5b_gate0_adversary_assignment_simple \
|
|
task5b_gate0_adversary_assignment_quoted \
|
|
task5b_gate0_adversary_assignment_indexed; do
|
|
body="$(declare -f "$adversary")" || return 1
|
|
assert_fails task5b_gate0_static_body_is_closed "$body"
|
|
done
|
|
body="$(declare -f task5b_gate0_adversary_helper_root)"$'\n'\
|
|
"$(declare -f task5b_gate0_adversary_helper_leaf)" || return 1
|
|
assert_fails task5b_gate0_static_body_is_closed "$body"
|
|
}
|
|
|
|
task5b_gate0_full_graph_static_adversarial_contract() (
|
|
local scan_rc=0
|
|
task5b_gate0_assert_rejected() {
|
|
TASK5B_ASSIGNMENT_PREFIX=1 trap - DEBUG
|
|
/usr/bin/printf 'TASK5B_FULL_GRAPH_SCAN_CANARY'
|
|
}
|
|
task5b_gate0_active_graph_is_closed 2>/dev/null || scan_rc=$?
|
|
assert_eq 1 "$scan_rc" \
|
|
'actual full-graph scanner rejects a mutated allowlisted helper'
|
|
)
|
|
|
|
task5b_gate0_dynamic_external_helper() {
|
|
/usr/bin/printf 'TASK5B_REAL_EXEC_CANARY'
|
|
}
|
|
|
|
task5b_gate0_dynamic_fuse_contract() {
|
|
local probe_rc=0 probe_output=''
|
|
probe_output="$(
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_FUSE_PROBE_SILENT=1
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_gate0_real_exec_fuse' DEBUG
|
|
/usr/bin/true
|
|
)" || probe_rc=$?
|
|
assert_eq 96 "$probe_rc" 'generic fuse blocks an unlisted absolute executable'
|
|
assert_eq '' "$probe_output" 'unlisted absolute executable produces no output'
|
|
|
|
probe_rc=0
|
|
probe_output="$(
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_FUSE_PROBE_SILENT=1
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_gate0_real_exec_fuse' DEBUG
|
|
task5b_gate0_dynamic_external_helper
|
|
)" || probe_rc=$?
|
|
assert_eq 96 "$probe_rc" 'generic fuse blocks helper-mediated external execution'
|
|
assert_eq '' "$probe_output" 'helper-mediated external canary never executes'
|
|
|
|
probe_rc=0
|
|
probe_output="$(
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_FUSE_PROBE_SILENT=1
|
|
task5b_gate0_assert_rejected() {
|
|
trap - DEBUG
|
|
/usr/bin/printf 'TASK5B_TRAP_DISABLE_CANARY'
|
|
}
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_gate0_real_exec_fuse' DEBUG
|
|
task5b_gate0_assert_rejected
|
|
)" || probe_rc=$?
|
|
assert_eq 96 "$probe_rc" 'generic fuse blocks trap removal in an allowlisted helper'
|
|
assert_eq '' "$probe_output" 'trap-removal helper canary never executes'
|
|
|
|
probe_rc=0
|
|
probe_output="$(
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_FUSE_PROBE_SILENT=1
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_gate0_real_exec_fuse' DEBUG
|
|
TASK5B_ASSIGNMENT_PREFIX= trap - DEBUG
|
|
/usr/bin/printf 'TASK5B_BARE_ASSIGNMENT_CANARY'
|
|
)" || probe_rc=$?
|
|
assert_eq 96 "$probe_rc" 'generic fuse blocks a bare assignment prefix'
|
|
assert_eq '' "$probe_output" 'bare assignment prefix leaves trap and canary closed'
|
|
|
|
probe_rc=0
|
|
probe_output="$(
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_FUSE_PROBE_SILENT=1
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_gate0_real_exec_fuse' DEBUG
|
|
TASK5B_ASSIGNMENT_PREFIX=1 /usr/bin/printf 'TASK5B_SIMPLE_ASSIGNMENT_CANARY'
|
|
)" || probe_rc=$?
|
|
assert_eq 96 "$probe_rc" 'generic fuse blocks a simple assignment prefix'
|
|
assert_eq '' "$probe_output" 'simple assignment prefix external effect stays zero'
|
|
|
|
probe_rc=0
|
|
probe_output="$(
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_FUSE_PROBE_SILENT=1
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_gate0_real_exec_fuse' DEBUG
|
|
TASK5B_ASSIGNMENT_PREFIX="a b" trap - DEBUG
|
|
/usr/bin/printf 'TASK5B_QUOTED_ASSIGNMENT_CANARY'
|
|
)" || probe_rc=$?
|
|
assert_eq 96 "$probe_rc" 'generic fuse blocks a quoted assignment prefix'
|
|
assert_eq '' "$probe_output" 'quoted assignment prefix leaves trap and canary closed'
|
|
|
|
probe_rc=0
|
|
probe_output="$(
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_FUSE_PROBE_SILENT=1
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_gate0_real_exec_fuse' DEBUG
|
|
TASK5B_ASSIGNMENT_PREFIX[0]=1 trap - DEBUG
|
|
/usr/bin/printf 'TASK5B_INDEXED_ASSIGNMENT_CANARY'
|
|
)" || probe_rc=$?
|
|
assert_eq 96 "$probe_rc" 'generic fuse blocks an indexed assignment prefix'
|
|
assert_eq '' "$probe_output" 'indexed assignment prefix leaves trap and canary closed'
|
|
}
|
|
|
|
task5b_gate0_reset_route_record() {
|
|
TASK5B_GATE0_MODEL_DISPATCH_COUNT=0
|
|
TASK5B_GATE0_MODEL_EFFECT_COUNT=0
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_LAST_ROUTE=''
|
|
TASK5B_GATE0_LAST_OUTPUT=''
|
|
: >"$TASK5B_GATE0_STDOUT_PATH"
|
|
: >"$TASK5B_GATE0_STDERR_PATH"
|
|
}
|
|
|
|
task5b_gate0_assert_rejected() {
|
|
local description="$1" rejected_rc=0
|
|
shift
|
|
task5b_gate0_reset_route_record
|
|
_k3slr_command "$@" >"$TASK5B_GATE0_STDOUT_PATH" \
|
|
2>"$TASK5B_GATE0_STDERR_PATH" || rejected_rc=$?
|
|
assert_eq 97 "$rejected_rc" "$description returns exact rc 97"
|
|
assert_eq 0 "$TASK5B_GATE0_MODEL_DISPATCH_COUNT" "$description has model dispatch 0"
|
|
assert_eq 0 "$TASK5B_GATE0_MODEL_EFFECT_COUNT" "$description has model effect 0"
|
|
assert_eq 0 "$TASK5B_GATE0_ACTUAL_EXEC_COUNT" "$description has actual exec 0"
|
|
[[ ! -s "$TASK5B_GATE0_STDOUT_PATH" && ! -s "$TASK5B_GATE0_STDERR_PATH" ]] ||
|
|
fail "$description emitted stdout or stderr"
|
|
}
|
|
|
|
task5b_gate0_assert_route() {
|
|
local route_id="$1" route_rc=0 last_index swap
|
|
local -a exact_arguments=() near_arguments=()
|
|
shift
|
|
exact_arguments=("$@")
|
|
|
|
task5b_gate0_reset_route_record
|
|
_k3slr_command "${exact_arguments[@]}" >"$TASK5B_GATE0_STDOUT_PATH" \
|
|
2>"$TASK5B_GATE0_STDERR_PATH" || route_rc=$?
|
|
assert_eq 0 "$route_rc" "$route_id accepts its exact argv"
|
|
assert_eq 1 "$TASK5B_GATE0_MODEL_DISPATCH_COUNT" "$route_id dispatches exactly once"
|
|
assert_eq 1 "$TASK5B_GATE0_MODEL_EFFECT_COUNT" "$route_id records exactly one model effect"
|
|
assert_eq 0 "$TASK5B_GATE0_ACTUAL_EXEC_COUNT" "$route_id has actual exec 0"
|
|
assert_eq "$route_id" "$TASK5B_GATE0_LAST_ROUTE" "$route_id records its route"
|
|
assert_eq "modeled:${route_id}" "$TASK5B_GATE0_LAST_OUTPUT" "$route_id records output"
|
|
[[ ! -s "$TASK5B_GATE0_STDOUT_PATH" && ! -s "$TASK5B_GATE0_STDERR_PATH" ]] ||
|
|
fail "$route_id exact route emitted stdout or stderr"
|
|
|
|
near_arguments=("${exact_arguments[@]}" task5b-extra)
|
|
task5b_gate0_assert_rejected "$route_id extra-argument near-match" \
|
|
"${near_arguments[@]}"
|
|
|
|
last_index=$(( ${#exact_arguments[@]} - 1 ))
|
|
near_arguments=("${exact_arguments[@]:0:last_index}")
|
|
task5b_gate0_assert_rejected "$route_id missing-argument near-match" \
|
|
"${near_arguments[@]}"
|
|
|
|
near_arguments=("${exact_arguments[@]}")
|
|
near_arguments[last_index]=task5b-wrong-value
|
|
task5b_gate0_assert_rejected "$route_id wrong-value near-match" \
|
|
"${near_arguments[@]}"
|
|
|
|
if (( ${#exact_arguments[@]} >= 3 )); then
|
|
near_arguments=("${exact_arguments[@]}")
|
|
swap="${near_arguments[last_index - 1]}"
|
|
near_arguments[last_index - 1]="${near_arguments[last_index]}"
|
|
near_arguments[last_index]="$swap"
|
|
task5b_gate0_assert_rejected "$route_id wrong-order near-match" \
|
|
"${near_arguments[@]}"
|
|
|
|
near_arguments=("${exact_arguments[@]:0:last_index - 1}")
|
|
near_arguments+=("${exact_arguments[last_index - 1]} ${exact_arguments[last_index]}")
|
|
task5b_gate0_assert_rejected "$route_id wrong-grouping near-match" \
|
|
"${near_arguments[@]}"
|
|
fi
|
|
}
|
|
|
|
task5b_gate0_static_fake_scan() {
|
|
local fake_body route_body catchall_body fake_hash='' route_hash='' catchall_hash=''
|
|
local active_graph_hash=''
|
|
fake_body="$(declare -f task5b_gate0_fake_command)" || return 1
|
|
route_body="$(declare -f task5b_gate0_route_equals)" || return 1
|
|
catchall_body="$(declare -f _k3slr_command)" || return 1
|
|
task5b_gate0_static_body_is_closed "$fake_body" || return 1
|
|
task5b_gate0_static_body_is_closed "$catchall_body" || return 1
|
|
task5b_gate0_definition_hash fake_hash task5b_gate0_fake_command || return 1
|
|
task5b_gate0_definition_hash route_hash task5b_gate0_route_equals || return 1
|
|
task5b_gate0_definition_hash catchall_hash _k3slr_command || return 1
|
|
task5b_gate0_active_graph_is_closed || return 1
|
|
task5b_gate0_full_graph_static_adversarial_contract || return 1
|
|
task5b_gate0_active_graph_hash active_graph_hash || return 1
|
|
[[ "$fake_hash" == ef2cfc5af6fff12713b20f8734fd28114312d695a7df7d478f9bddd155398d09 ]] ||
|
|
return 1
|
|
[[ "$route_hash" == 53f6a8cdb6726626a1db2136d7c38e4f2c52c4eaaeb3671506fb504475586a19 ]] ||
|
|
return 1
|
|
[[ "$catchall_hash" == 76448cb3a82bf60c7904b7d6217762b2769c65069a5e54f086b960d5a63ca3ce ]] ||
|
|
return 1
|
|
[[ "$active_graph_hash" == 5b3cbe49958bfabe8556d2ec60cdfaf2fdcf586e41953e2681b1720399332293 ]] ||
|
|
return 1
|
|
[[ "$fake_body" == *'return 0'* && "$fake_body" == *'return 97'* ]] || return 1
|
|
[[ "$fake_body" != *'task5b_gate0_real_exec_fuse'* ]] || return 1
|
|
task5b_gate0_static_adversarial_contract
|
|
}
|
|
|
|
task5b_gate0_contract() (
|
|
local gate0_stdout="${fixture_root}/task5b-gate0.stdout"
|
|
local gate0_stderr="${fixture_root}/task5b-gate0.stderr"
|
|
local matrix_rc=0
|
|
|
|
# Foreground source-loaded fixture: production code is loaded, then only its
|
|
# lowest command seam is replaced. No child/background fixture is started.
|
|
# shellcheck source=/dev/null
|
|
source "$LIBRARY_PATH"
|
|
_k3slr_command() { task5b_gate0_fake_command "$@"; }
|
|
exec 9>&2
|
|
|
|
TASK5B_GATE0_STDOUT_PATH="$gate0_stdout"
|
|
TASK5B_GATE0_STDERR_PATH="$gate0_stderr"
|
|
TASK5B_GATE0_MODEL_DISPATCH_COUNT=0
|
|
TASK5B_GATE0_MODEL_EFFECT_COUNT=0
|
|
TASK5B_GATE0_ACTUAL_EXEC_COUNT=0
|
|
TASK5B_GATE0_LAST_ROUTE=''
|
|
TASK5B_GATE0_LAST_OUTPUT=''
|
|
: >"$TASK5B_GATE0_STDOUT_PATH"
|
|
: >"$TASK5B_GATE0_STDERR_PATH"
|
|
|
|
task5b_gate0_active_matrix() {
|
|
task5b_gate0_assert_route sudo-validate /usr/bin/sudo --validate
|
|
task5b_gate0_assert_route sudo-cached-validate \
|
|
/usr/bin/sudo --non-interactive --validate
|
|
task5b_gate0_assert_route apt-update \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/apt-get update
|
|
task5b_gate0_assert_route apt-install \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/apt-get install --yes \
|
|
keepassxc=2.7.6+dfsg.1-1build3 cryptsetup-bin=2:2.7.0-1ubuntu4.2
|
|
task5b_gate0_assert_route feasibility-execute /usr/bin/bash \
|
|
/home/donghyeon/workspace/platform/scripts/validate/k3s-local-recovery-feasibility.sh \
|
|
--execute
|
|
task5b_gate0_assert_route outer-scaffold-mkdir \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/mkdir --mode=0700 -- \
|
|
/mnt/k3s-recovery-ssd
|
|
task5b_gate0_assert_route outer-mount \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/mount -t ntfs3 -o \
|
|
rw,uid=1000,gid=1000,dmask=0077,fmask=0177,nodev,nosuid,noexec \
|
|
/dev/disk/by-id/wwn-0x500a0751e6aa6254-part3 /mnt/k3s-recovery-ssd
|
|
task5b_gate0_assert_route layout-root-mkdir /usr/bin/mkdir --mode=0700 -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery
|
|
task5b_gate0_assert_route layout-vault-mkdir /usr/bin/mkdir --mode=0700 -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault
|
|
task5b_gate0_assert_route layout-backups-mkdir /usr/bin/mkdir --mode=0700 -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault/backups
|
|
task5b_gate0_assert_route layout-containers-mkdir /usr/bin/mkdir --mode=0700 -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers
|
|
task5b_gate0_assert_route probe-mktemp /usr/bin/mktemp \
|
|
--tmpdir=/mnt/k3s-recovery-ssd/HyeonworksRecovery \
|
|
.k3slr-hardlink-probe.XXXXXXXX
|
|
task5b_gate0_assert_route probe-link /usr/bin/ln -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-hardlink-probe.FIXTURE1 \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-hardlink-probe.FIXTURE1.link
|
|
task5b_gate0_assert_route probe-unlink-link /usr/bin/unlink -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-hardlink-probe.FIXTURE1.link
|
|
task5b_gate0_assert_route probe-unlink-source /usr/bin/unlink -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-hardlink-probe.FIXTURE1
|
|
task5b_gate0_assert_route probe-sync /usr/bin/sync --file-system \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery
|
|
task5b_gate0_assert_route keepass-db-create /usr/bin/keepassxc-cli db-create \
|
|
--quiet --set-password \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault/hyeonworks-recovery.kdbx
|
|
task5b_gate0_assert_route keepass-add /usr/bin/keepassxc-cli add --quiet \
|
|
--generate --length 40 --lower --upper --numeric --every-group \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/vault/hyeonworks-recovery.kdbx \
|
|
'K3s Recovery LUKS'
|
|
task5b_gate0_assert_route container-dd /usr/bin/dd if=/dev/zero \
|
|
of=/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks \
|
|
bs=16M count=2048 conv=excl,fsync status=progress
|
|
task5b_gate0_assert_route outer-umount /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/umount -- /mnt/k3s-recovery-ssd
|
|
task5b_gate0_assert_route loop-create /usr/bin/sudo --non-interactive -- \
|
|
/usr/sbin/losetup --find --show --nooverlap -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks
|
|
task5b_gate0_assert_route luks-format /usr/bin/sudo --non-interactive -- \
|
|
/usr/sbin/cryptsetup luksFormat --batch-mode --type luks2 --key-file=- \
|
|
/dev/loop37
|
|
task5b_gate0_assert_route luks-open /usr/bin/sudo --non-interactive -- \
|
|
/usr/sbin/cryptsetup open --type luks2 --key-file=- /dev/loop37 k3s-recovery
|
|
task5b_gate0_assert_route mkfs-ext4 /usr/bin/sudo --non-interactive -- \
|
|
/usr/sbin/mkfs.ext4 -F -L K3S_RECOVERY /dev/mapper/k3s-recovery
|
|
task5b_gate0_assert_route inner-parent-mkdir /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/mkdir --mode=0700 -- /srv/recovery
|
|
task5b_gate0_assert_route inner-mountpoint-mkdir \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/mkdir --mode=0700 -- \
|
|
/srv/recovery/k3s
|
|
task5b_gate0_assert_route inner-mount /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/mount -t ext4 -o rw,nodev,nosuid,noexec \
|
|
/dev/mapper/k3s-recovery /srv/recovery/k3s
|
|
task5b_gate0_assert_route inner-chown /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/chown root:root /srv/recovery/k3s
|
|
task5b_gate0_assert_route inner-chmod /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/chmod 0700 /srv/recovery/k3s
|
|
task5b_gate0_assert_route marker-mktemp /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/mktemp --tmpdir=/srv/recovery/k3s .k3slr-marker.tmp.XXXXXXXX
|
|
task5b_gate0_assert_route marker-dd /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/dd of=/srv/recovery/k3s/.k3slr-marker.tmp.FIXTURE1 bs=37 count=1 \
|
|
iflag=fullblock conv=notrunc,fsync status=none
|
|
task5b_gate0_assert_route outer-metadata-mktemp /usr/bin/mktemp \
|
|
--tmpdir=/mnt/k3s-recovery-ssd/HyeonworksRecovery \
|
|
.k3slr-volume.tmp.XXXXXXXX
|
|
task5b_gate0_assert_route inner-metadata-mktemp \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/mktemp \
|
|
--tmpdir=/srv/recovery/k3s .k3slr-volume.tmp.XXXXXXXX
|
|
task5b_gate0_assert_route outer-metadata-dd /usr/bin/dd \
|
|
of=/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1 \
|
|
bs=4096 count=1 iflag=fullblock conv=notrunc,fsync status=none
|
|
task5b_gate0_assert_route inner-metadata-dd \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/dd \
|
|
if=/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1 \
|
|
of=/srv/recovery/k3s/.k3slr-volume.tmp.FIXTURE1 bs=4096 count=1 \
|
|
iflag=fullblock conv=notrunc,fsync status=none
|
|
task5b_gate0_assert_route marker-stage-sync /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/sync --file-system /srv/recovery/k3s/.k3slr-marker.tmp.FIXTURE1
|
|
task5b_gate0_assert_route marker-publish /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/ln -- /srv/recovery/k3s/.k3slr-marker.tmp.FIXTURE1 \
|
|
/srv/recovery/k3s/.recovery-marker
|
|
task5b_gate0_assert_route outer-stage-sync /usr/bin/sync --file-system \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1
|
|
task5b_gate0_assert_route inner-stage-sync /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/sync --file-system /srv/recovery/k3s/.k3slr-volume.tmp.FIXTURE1
|
|
task5b_gate0_assert_route outer-metadata-publish /usr/bin/ln -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1 \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/volume.env
|
|
task5b_gate0_assert_route inner-metadata-publish \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/ln -- \
|
|
/srv/recovery/k3s/.k3slr-volume.tmp.FIXTURE1 \
|
|
/srv/recovery/k3s/.recovery-volume.env
|
|
task5b_gate0_assert_route marker-stage-unlink \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/unlink -- \
|
|
/srv/recovery/k3s/.k3slr-marker.tmp.FIXTURE1
|
|
task5b_gate0_assert_route outer-stage-unlink /usr/bin/unlink -- \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/.k3slr-volume.tmp.FIXTURE1
|
|
task5b_gate0_assert_route inner-stage-unlink \
|
|
/usr/bin/sudo --non-interactive -- /usr/bin/unlink -- \
|
|
/srv/recovery/k3s/.k3slr-volume.tmp.FIXTURE1
|
|
task5b_gate0_assert_route inner-parent-sync /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/sync --file-system /srv/recovery/k3s
|
|
task5b_gate0_assert_route inner-umount /usr/bin/sudo --non-interactive -- \
|
|
/usr/bin/umount -- /srv/recovery/k3s
|
|
task5b_gate0_assert_route mapping-close /usr/bin/sudo --non-interactive -- \
|
|
/usr/sbin/cryptsetup close k3s-recovery
|
|
task5b_gate0_assert_route loop-detach /usr/bin/sudo --non-interactive -- \
|
|
/usr/sbin/losetup --detach /dev/loop37
|
|
|
|
task5b_gate0_assert_rejected 'unknown executable' \
|
|
/fixture/task5b-unknown --execute
|
|
}
|
|
|
|
# This scan must pass before the first exact hazardous argv is presented to
|
|
# the fake. It is independent of every dynamic route assertion below.
|
|
task5b_gate0_static_fake_scan || return 1
|
|
task5b_gate0_dynamic_fuse_contract || return 1
|
|
|
|
# The active DEBUG trap exists only inside this foreground nested subshell.
|
|
# Returning from the subshell removes it without an allowlisted teardown word.
|
|
(
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_gate0_real_exec_fuse' DEBUG
|
|
task5b_gate0_active_matrix
|
|
) || matrix_rc=$?
|
|
(( matrix_rc == 0 )) || return "$matrix_rc"
|
|
|
|
exec 9>&-
|
|
/usr/bin/rm -- "$TASK5B_GATE0_STDOUT_PATH" "$TASK5B_GATE0_STDERR_PATH"
|
|
)
|
|
|
|
# Production lifecycle probes need a second boundary beyond their command
|
|
# seam doubles: if production regresses to a direct executable, the DEBUG trap
|
|
# must reject that simple command before Bash executes it. Runtime membership
|
|
# and the pre-trap static scan share this exact, profile-scoped function set.
|
|
readonly -A TASK5B_PRODUCTION_PROBE_ALLOWED_FUNCTIONS=(
|
|
[unavailable/_k3slr_lifecycle_action]=1
|
|
[unavailable/_k3slr_command]=1
|
|
[unavailable/task5b_unavailable_probe_payload]=1
|
|
[unavailable/task5b_production_probe_emit_ok]=1
|
|
[legacy_binding/_k3slr_lifecycle_action]=1
|
|
[legacy_binding/_k3slr_command]=1
|
|
[legacy_binding/task5b_legacy_binding_probe_payload]=1
|
|
[legacy_binding/task5b_production_probe_emit_ok]=1
|
|
[api_dispatch/_k3slr_lifecycle_dispatch]=1
|
|
[api_dispatch/_k3slr_lifecycle_step]=1
|
|
[api_dispatch/_k3slr_lifecycle_action]=1
|
|
[api_dispatch/_k3slr_command]=1
|
|
[api_dispatch/task5b_api_dispatch_probe_payload]=1
|
|
[api_dispatch/task5b_production_probe_emit_ok]=1
|
|
[api_direct/_k3slr_lifecycle_action]=1
|
|
[api_direct/_k3slr_command]=1
|
|
[api_direct/task5b_api_direct_probe_payload]=1
|
|
[api_direct/task5b_production_probe_emit_ok]=1
|
|
[api_main_arity/_k3slr_lifecycle_main]=1
|
|
[api_main_arity/task5b_api_main_arity_probe_payload]=1
|
|
[api_main_arity/task5b_production_probe_emit_ok]=1
|
|
[action1_main/_k3slr_lifecycle_main]=1
|
|
[action1_main/_k3slr_load_contract]=1
|
|
[action1_main/_k3slr_parse_contract]=1
|
|
[action1_main/_k3slr_contract_value_is_valid]=1
|
|
[action1_main/_k3slr_uint]=1
|
|
[action1_main/_k3slr_require_execute_tty]=1
|
|
[action1_main/_k3slr_tty_capability]=1
|
|
[action1_main/_k3slr_lifecycle_dispatch]=1
|
|
[action1_main/_k3slr_lifecycle_step]=1
|
|
[action1_main/_k3slr_lifecycle_action]=1
|
|
[action1_main/_k3slr_command]=1
|
|
[action1_main/task5b_gate0_route_equals]=1
|
|
[action1_main/task5b_action1_main_probe_payload]=1
|
|
[action1_main/task5b_production_probe_emit_ok]=1
|
|
[action1_alternate/_k3slr_lifecycle_action]=1
|
|
[action1_alternate/_k3slr_command]=1
|
|
[action1_alternate/task5b_gate0_route_equals]=1
|
|
[action1_alternate/task5b_action1_alternate_probe_payload]=1
|
|
[action1_alternate/task5b_production_probe_emit_ok]=1
|
|
[canary/task5b_production_probe_canary_helper_root]=1
|
|
[canary/task5b_production_probe_canary_helper_leaf]=1
|
|
)
|
|
readonly -A TASK5B_PRODUCTION_PROBE_EXPECTED_HASHES=(
|
|
[core/task5b_production_probe_profile_is_known]=ed04cce41523a02acbebea479607e696dc5ab7ec1486a4914230b74572cd85af
|
|
[core/task5b_production_probe_function_is_allowed]=559b5481974916559ae305975aaab82f88a542dc53547dee4575d599d62903ea
|
|
[core/task5b_production_probe_builtin_is_allowed]=58cb03fd47554c9cf9ef4b52372e7125b86c86cd88b23364ff31c41e41e1c84b
|
|
[core/task5b_production_probe_debug]=662c89fd5213726938630c16b475d894f5a19ec922a8c09dc7563312c55e38a7
|
|
[core/task5b_production_probe_emit_ok]=278abe2c9fb6b744caa3a5517ebc8b504bee1409df4819e6fb9be1129b22fbd0
|
|
[core/task5b_production_probe_canary_graph_is_pinned]=c3c0724de2a4d7da8d20739d5f66d46dc54bde69c2e791d3171d2e4c72392ac5
|
|
[core/task5b_production_probe_body_is_closed]=bc1d27a01debe57be58252f402ddd958fbd9c4fe73da5a4a2c7cfef7851e067d
|
|
[core/task5b_production_probe_arm]=e391120fe268bbfb2b67c8d6cb23591fa9f5635dc1549a9f066c4aba711d5c34
|
|
[core/task5b_gate0_assignment_only_status]=075118e7edcd50ef5722f01874d2b138b4ab6cc483a6c17be517bc9a45df7af8
|
|
[unavailable/_k3slr_command]=8634e9ebe9c34a48eff4381729ce6883b95a69a69c6c7218498b6db8a7806248
|
|
[unavailable/task5b_unavailable_probe_payload]=e83567872c19a73a08baa434eb80e449f493505d6945c4d4f4cae2ffedaa2435
|
|
[unavailable/task5b_production_probe_emit_ok]=278abe2c9fb6b744caa3a5517ebc8b504bee1409df4819e6fb9be1129b22fbd0
|
|
[unavailable/_k3slr_lifecycle_action]=a499b7694c449633f3ce538e8296b1d543d04565c046508587d18b677aa12384
|
|
[legacy_binding/task5b_legacy_binding_probe_payload]=46d73155080f35fdd4c9ec1d531ed50f6ac77447b6389549d934f862e4430316
|
|
[legacy_binding/_k3slr_lifecycle_action]=a499b7694c449633f3ce538e8296b1d543d04565c046508587d18b677aa12384
|
|
[legacy_binding/task5b_production_probe_emit_ok]=278abe2c9fb6b744caa3a5517ebc8b504bee1409df4819e6fb9be1129b22fbd0
|
|
[legacy_binding/_k3slr_command]=8634e9ebe9c34a48eff4381729ce6883b95a69a69c6c7218498b6db8a7806248
|
|
[api_dispatch/_k3slr_command]=8634e9ebe9c34a48eff4381729ce6883b95a69a69c6c7218498b6db8a7806248
|
|
[api_dispatch/_k3slr_lifecycle_step]=1cf91eba1179fbb35f58ca20dbf0e62f2ff3b754cc64f6e4c6547c1a1193b212
|
|
[api_dispatch/task5b_api_dispatch_probe_payload]=341b9a6990134bc9f95fb881e4596a4db94fbfbc4303e8afe61720f1684958e4
|
|
[api_dispatch/_k3slr_lifecycle_action]=a499b7694c449633f3ce538e8296b1d543d04565c046508587d18b677aa12384
|
|
[api_dispatch/_k3slr_lifecycle_dispatch]=e407e698b9a69c47c9969db009211e47614c9753f701fa99f82a6d9e84c82477
|
|
[api_dispatch/task5b_production_probe_emit_ok]=278abe2c9fb6b744caa3a5517ebc8b504bee1409df4819e6fb9be1129b22fbd0
|
|
[api_dispatch/task5b_api_state_debug_observer]=c9bec0cfb5204ee77ac32fab9eb31b00cf033d6fd3ac897a98878991ca13897f
|
|
[api_direct/_k3slr_lifecycle_action]=a499b7694c449633f3ce538e8296b1d543d04565c046508587d18b677aa12384
|
|
[api_direct/_k3slr_command]=8634e9ebe9c34a48eff4381729ce6883b95a69a69c6c7218498b6db8a7806248
|
|
[api_direct/task5b_api_direct_probe_payload]=c4723fd1f9a04b2095a1935c21c5cebca01f2e13c75f9ceda59d58ad1a909332
|
|
[api_direct/task5b_production_probe_emit_ok]=278abe2c9fb6b744caa3a5517ebc8b504bee1409df4819e6fb9be1129b22fbd0
|
|
[api_main_arity/task5b_api_main_arity_probe_payload]=5c23d4c2810c534318d210de7c35a6ac7f0fbf52ab88edbd421c30d2c534ff8a
|
|
[api_main_arity/_k3slr_lifecycle_main]=677f7f482840af5c5a7dc37c4f7abe6a470a24ed108030cb14fe1f3b1c8ae53f
|
|
[api_main_arity/task5b_production_probe_emit_ok]=278abe2c9fb6b744caa3a5517ebc8b504bee1409df4819e6fb9be1129b22fbd0
|
|
[action1_main/task5b_production_probe_emit_ok]=278abe2c9fb6b744caa3a5517ebc8b504bee1409df4819e6fb9be1129b22fbd0
|
|
[action1_main/_k3slr_contract_value_is_valid]=b8eab6a6688c3aac2456bf9a4987eeeefec673a1dccf5e80e785cc9dcbddc7dd
|
|
[action1_main/_k3slr_lifecycle_action]=a499b7694c449633f3ce538e8296b1d543d04565c046508587d18b677aa12384
|
|
[action1_main/task5b_action1_main_probe_payload]=ac0431e91d90989ae9eb49dd7ebcecbc12a87f99f50771db0fce62ce9960aa7c
|
|
[action1_main/_k3slr_parse_contract]=a27e7c60983ecb4792349c69bc6020ee51dfdd6f0adefbf4359b54b20cf64b0d
|
|
[action1_main/_k3slr_uint]=e60445f9cc78399afebda8d2010fbd64ceae23da4cfa15373c60122127df6945
|
|
[action1_main/task5b_gate0_route_equals]=53f6a8cdb6726626a1db2136d7c38e4f2c52c4eaaeb3671506fb504475586a19
|
|
[action1_main/_k3slr_tty_capability]=c59e220dbcc6f3d9af8707cc365c456ee404d20deed8150171a8fb37d87a7497
|
|
[action1_main/_k3slr_load_contract]=71855162c947b01b145e79aa4e30a448c21690efa183c56ccafe97b776912cce
|
|
[action1_main/_k3slr_require_execute_tty]=9d194fe3ba36a7bd25aaa5b53eefeafa4d23fe8f307995be7a9039472b42a838
|
|
[action1_main/_k3slr_command]=6e84784f49cc5e8072cdd4c8c0cccbd20ef15e5886228946d1c7c670967a3ec2
|
|
[action1_main/_k3slr_lifecycle_main]=677f7f482840af5c5a7dc37c4f7abe6a470a24ed108030cb14fe1f3b1c8ae53f
|
|
[action1_main/_k3slr_lifecycle_dispatch]=e407e698b9a69c47c9969db009211e47614c9753f701fa99f82a6d9e84c82477
|
|
[action1_main/_k3slr_lifecycle_step]=1cf91eba1179fbb35f58ca20dbf0e62f2ff3b754cc64f6e4c6547c1a1193b212
|
|
[action1_main/task5b_action1_debug_observer]=ecaf3a7e1ee93fd5f330301b8fc21c0300834bd5ec376c766af704de83271cee
|
|
[action1_alternate/task5b_gate0_route_equals]=53f6a8cdb6726626a1db2136d7c38e4f2c52c4eaaeb3671506fb504475586a19
|
|
[action1_alternate/_k3slr_lifecycle_action]=a499b7694c449633f3ce538e8296b1d543d04565c046508587d18b677aa12384
|
|
[action1_alternate/_k3slr_command]=6e84784f49cc5e8072cdd4c8c0cccbd20ef15e5886228946d1c7c670967a3ec2
|
|
[action1_alternate/task5b_action1_alternate_probe_payload]=aa5693a60539d54eda18fdefef47c7ad20891d94d0478df41a0afa46860565cc
|
|
[action1_alternate/task5b_production_probe_emit_ok]=278abe2c9fb6b744caa3a5517ebc8b504bee1409df4819e6fb9be1129b22fbd0
|
|
[canary/task5b_production_probe_canary_helper_root]=d14b146f01b0a6ef5a36be1d613994901683a503661348195b5c2eeca7b9aa9b
|
|
[canary/task5b_production_probe_canary_helper_leaf]=a94ebf016f987960b669e5a7c9b776e95b7d305675dc74868b46bc25460810a9
|
|
)
|
|
|
|
task5b_production_probe_profile_is_known() {
|
|
(( $# == 1 )) || return 1
|
|
case "$1" in
|
|
unavailable|legacy_binding|api_dispatch|api_direct|api_main_arity|\
|
|
action1_main|action1_alternate|canary) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
task5b_production_probe_function_is_allowed() {
|
|
local profile="${1-}" function_name="${2-}"
|
|
(( $# == 2 )) || return 1
|
|
task5b_production_probe_profile_is_known "$profile" || return 1
|
|
[[ "${TASK5B_PRODUCTION_PROBE_ALLOWED_FUNCTIONS["$profile/$function_name"]+present}" == present ]]
|
|
}
|
|
|
|
task5b_production_probe_builtin_is_allowed() {
|
|
local profile="${1-}" builtin_name="${2-}"
|
|
(( $# == 2 )) || return 1
|
|
task5b_production_probe_profile_is_known "$profile" || return 1
|
|
case "$profile" in
|
|
canary)
|
|
[[ "$builtin_name" == printf ]]
|
|
;;
|
|
action1_main)
|
|
case "$builtin_name" in
|
|
declare|local|printf|return|shift) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
*)
|
|
case "$builtin_name" in
|
|
local|printf|return|shift) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
esac
|
|
}
|
|
|
|
task5b_production_probe_debug() {
|
|
local -a saved_bash_rematch=("${BASH_REMATCH[@]}")
|
|
local candidate="${1-}" observed_step="${2-}" execution_word=''
|
|
local assignment_rc=0
|
|
(( $# == 2 )) || exit 96
|
|
task5b_production_probe_profile_is_known "$task5b_probe_profile" || exit 96
|
|
case "$task5b_probe_profile" in
|
|
api_dispatch)
|
|
task5b_api_state_debug_observer "$candidate" || {
|
|
printf 'TASK5B_PROBE_BLOCK|%s|1\n' "$task5b_probe_profile" >&9
|
|
exit 96
|
|
}
|
|
;;
|
|
action1_main)
|
|
task5b_action1_debug_observer "$candidate" "$observed_step" || {
|
|
printf 'TASK5B_PROBE_BLOCK|%s|1\n' "$task5b_probe_profile" >&9
|
|
exit 96
|
|
}
|
|
;;
|
|
esac
|
|
candidate="${candidate#"${candidate%%[![:space:]]*}"}"
|
|
while [[ "$candidate" =~ ^(if|elif|while|until|then|\!|time)[[:space:]]+(.+)$ ]]; do
|
|
candidate="${BASH_REMATCH[2]}"
|
|
done
|
|
case "$candidate" in
|
|
''|'{'|'}'|'[['*|'(('*|for\ *|case\ *|do|done|esac)
|
|
BASH_REMATCH=("${saved_bash_rematch[@]}")
|
|
return 0
|
|
;;
|
|
esac
|
|
if [[ "$task5b_probe_profile" == action1_main ]]; then
|
|
case "$candidate" in
|
|
'IFS= read -r line'|'seen["$key"]=1'|'result["$key"]="$value"')
|
|
BASH_REMATCH=("${saved_bash_rematch[@]}")
|
|
return 0
|
|
;;
|
|
esac
|
|
fi
|
|
if [[ "$candidate" =~ ^[a-zA-Z_][a-zA-Z0-9_]*(\[[^]]+\])?\+?= ]]; then
|
|
task5b_gate0_assignment_only_status "$candidate" || assignment_rc=$?
|
|
if (( assignment_rc == 0 )); then
|
|
BASH_REMATCH=("${saved_bash_rematch[@]}")
|
|
return 0
|
|
fi
|
|
printf 'TASK5B_PROBE_BLOCK|%s|1\n' "$task5b_probe_profile" >&9
|
|
exit 96
|
|
fi
|
|
read -r execution_word _ <<<"$candidate"
|
|
execution_word="${execution_word%;}"
|
|
case "$execution_word" in
|
|
\$*|\"\$*|\'\$*|/*|\"/*|\'/*|sudo|command|builtin|exec|eval|source|.|\
|
|
trap|set|shopt|unset|enable|function)
|
|
printf 'TASK5B_PROBE_BLOCK|%s|1\n' "$task5b_probe_profile" >&9
|
|
exit 96
|
|
;;
|
|
esac
|
|
if task5b_production_probe_builtin_is_allowed "$task5b_probe_profile" \
|
|
"$execution_word" ||
|
|
task5b_production_probe_function_is_allowed "$task5b_probe_profile" \
|
|
"$execution_word"; then
|
|
BASH_REMATCH=("${saved_bash_rematch[@]}")
|
|
return 0
|
|
fi
|
|
printf 'TASK5B_PROBE_BLOCK|%s|1\n' "$task5b_probe_profile" >&9
|
|
exit 96
|
|
}
|
|
|
|
task5b_production_probe_emit_ok() {
|
|
(( $# >= 1 )) || return 1
|
|
printf 'TASK5B_PROBE_OK|%s' "$1" >&9
|
|
shift
|
|
printf '|%s' "$@" >&9
|
|
printf '\n' >&9
|
|
}
|
|
|
|
task5b_production_probe_canary_graph_is_pinned() {
|
|
local entry function_name actual_hash='' expected_hash='' function_count=0
|
|
(( $# == 0 )) || return 1
|
|
for entry in "${!TASK5B_PRODUCTION_PROBE_ALLOWED_FUNCTIONS[@]}"; do
|
|
[[ "$entry" == canary/* ]] || continue
|
|
function_name="${entry#*/}"
|
|
(( function_count += 1 ))
|
|
expected_hash="${TASK5B_PRODUCTION_PROBE_EXPECTED_HASHES[$entry]-}"
|
|
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
|
|
task5b_gate0_definition_hash actual_hash "$function_name" || return 1
|
|
[[ "$actual_hash" == "$expected_hash" ]] || return 1
|
|
done
|
|
(( function_count == 2 ))
|
|
}
|
|
|
|
task5b_production_probe_body_is_closed() {
|
|
local profile="${1-}" function_name="${2-}" body="${3-}"
|
|
local line='' scan_body='' character='' next_character='' quote=''
|
|
local mode=normal escaped=0 index=0 length=0 line_number=0
|
|
local allowed_redirection_count=0 expected_redirection_count=0
|
|
(( $# == 3 )) || return 1
|
|
task5b_production_probe_profile_is_known "$profile" || return 1
|
|
[[ "$function_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ && -n "$body" ]] ||
|
|
return 1
|
|
case "$profile/$function_name" in
|
|
*/task5b_production_probe_emit_ok) expected_redirection_count=3 ;;
|
|
action1_main/_k3slr_parse_contract|*/_k3slr_lifecycle_main)
|
|
expected_redirection_count=1
|
|
;;
|
|
esac
|
|
while IFS= read -r line; do
|
|
if [[ "$function_name" == task5b_production_probe_emit_ok ]]; then
|
|
case "$line" in
|
|
" printf 'TASK5B_PROBE_OK|%s' \"\$1\" 1>&9;"|\
|
|
" printf '|%s' \"\$@\" 1>&9;"|" printf '\\n' 1>&9")
|
|
(( allowed_redirection_count += 1 ))
|
|
continue
|
|
;;
|
|
esac
|
|
elif [[ "$profile/$function_name" == action1_main/_k3slr_parse_contract &&
|
|
"$line" == " done < \"\$file\";" ]]; then
|
|
(( allowed_redirection_count += 1 ))
|
|
continue
|
|
elif [[ "$function_name" == _k3slr_lifecycle_main &&
|
|
"$line" == ' declare -F _k3slr_prove_header_restore > /dev/null || return 1;' ]]; then
|
|
(( allowed_redirection_count += 1 ))
|
|
continue
|
|
fi
|
|
scan_body+="$line"$'\n'
|
|
done <<<"$body"
|
|
(( allowed_redirection_count == expected_redirection_count )) || return 1
|
|
[[ "$scan_body" != *'<('* && "$scan_body" != *'>('* &&
|
|
"$scan_body" != *'trap - DEBUG'* && "$scan_body" != *'set +T'* &&
|
|
"$scan_body" != *'shopt -u extdebug'* && "$scan_body" != *'unset '* &&
|
|
"$scan_body" != *'enable '* ]] || return 1
|
|
while IFS= read -r line; do
|
|
(( line_number += 1 ))
|
|
if (( line_number > 1 )) &&
|
|
[[ "$line" =~ ^[[:space:]]*(function[[:space:]]+|[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(\)) ]]; then
|
|
return 1
|
|
fi
|
|
quote=''
|
|
mode=normal
|
|
escaped=0
|
|
index=0
|
|
length="${#line}"
|
|
while (( index < length )); do
|
|
character="${line:index:1}"
|
|
next_character=''
|
|
(( index + 1 >= length )) || next_character="${line:index + 1:1}"
|
|
if (( escaped == 1 )); then
|
|
escaped=0
|
|
(( index += 1 ))
|
|
continue
|
|
fi
|
|
case "$quote" in
|
|
single)
|
|
[[ "$character" != "'" ]] || quote=''
|
|
(( index += 1 ))
|
|
continue
|
|
;;
|
|
double)
|
|
case "$character" in
|
|
'\\') escaped=1 ;;
|
|
'"') quote='' ;;
|
|
esac
|
|
(( index += 1 ))
|
|
continue
|
|
;;
|
|
esac
|
|
case "$mode" in
|
|
conditional)
|
|
if [[ "$character$next_character" == ']]' ]]; then
|
|
mode=normal
|
|
(( index += 2 ))
|
|
else
|
|
(( index += 1 ))
|
|
fi
|
|
continue
|
|
;;
|
|
arithmetic)
|
|
if [[ "$character$next_character" == '))' ]]; then
|
|
mode=normal
|
|
(( index += 2 ))
|
|
else
|
|
(( index += 1 ))
|
|
fi
|
|
continue
|
|
;;
|
|
esac
|
|
case "$character" in
|
|
"'") quote=single ;;
|
|
'"') quote=double ;;
|
|
'\\') escaped=1 ;;
|
|
'[')
|
|
if [[ "$next_character" == '[' ]]; then
|
|
mode=conditional
|
|
(( index += 2 ))
|
|
continue
|
|
fi
|
|
;;
|
|
'(')
|
|
if [[ "$next_character" == '(' ]]; then
|
|
mode=arithmetic
|
|
(( index += 2 ))
|
|
continue
|
|
fi
|
|
;;
|
|
'<'|'>') return 1 ;;
|
|
esac
|
|
(( index += 1 ))
|
|
done
|
|
done <<<"$scan_body"
|
|
}
|
|
|
|
task5b_production_probe_core_is_closed() {
|
|
local function_name actual_hash='' expected_hash=''
|
|
local -a core_functions=(
|
|
task5b_production_probe_profile_is_known
|
|
task5b_production_probe_function_is_allowed
|
|
task5b_production_probe_builtin_is_allowed
|
|
task5b_production_probe_debug
|
|
task5b_production_probe_emit_ok
|
|
task5b_production_probe_canary_graph_is_pinned
|
|
task5b_production_probe_body_is_closed
|
|
task5b_production_probe_arm
|
|
task5b_gate0_assignment_only_status
|
|
)
|
|
(( $# == 0 )) || return 1
|
|
for function_name in "${core_functions[@]}"; do
|
|
expected_hash="${TASK5B_PRODUCTION_PROBE_EXPECTED_HASHES["core/$function_name"]-}"
|
|
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
|
|
task5b_gate0_definition_hash actual_hash "$function_name" || return 1
|
|
[[ "$actual_hash" == "$expected_hash" ]] || {
|
|
printf 'Task 5B production probe core hash mismatch: %s expected=%s actual=%s\n' \
|
|
"$function_name" "$expected_hash" "$actual_hash" >&2
|
|
return 1
|
|
}
|
|
done
|
|
}
|
|
|
|
task5b_production_probe_static_graph_is_closed() {
|
|
local profile="${1-}" entry function_name body='' normalized_body=''
|
|
local actual_hash='' expected_hash='' function_count=0 line='' line_number=0
|
|
(( $# == 1 )) || return 1
|
|
task5b_production_probe_profile_is_known "$profile" || return 1
|
|
[[ "$profile" != canary ]] || return 1
|
|
task5b_production_probe_core_is_closed || return 1
|
|
if [[ "$profile" == api_dispatch ]]; then
|
|
function_name=task5b_api_state_debug_observer
|
|
entry="$profile/$function_name"
|
|
expected_hash="${TASK5B_PRODUCTION_PROBE_EXPECTED_HASHES[$entry]-}"
|
|
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
|
|
task5b_gate0_definition_hash actual_hash "$function_name" || return 1
|
|
[[ "$actual_hash" == "$expected_hash" ]] || {
|
|
printf 'Task 5B production probe observer hash mismatch: %s expected=%s actual=%s\n' \
|
|
"$function_name" "$expected_hash" "$actual_hash" >&2
|
|
return 1
|
|
}
|
|
elif [[ "$profile" == action1_main ]]; then
|
|
function_name=task5b_action1_debug_observer
|
|
entry="$profile/$function_name"
|
|
expected_hash="${TASK5B_PRODUCTION_PROBE_EXPECTED_HASHES[$entry]-}"
|
|
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
|
|
task5b_gate0_definition_hash actual_hash "$function_name" || return 1
|
|
[[ "$actual_hash" == "$expected_hash" ]] || {
|
|
printf 'Task 5B production probe observer hash mismatch: %s expected=%s actual=%s\n' \
|
|
"$function_name" "$expected_hash" "$actual_hash" >&2
|
|
return 1
|
|
}
|
|
fi
|
|
for entry in "${!TASK5B_PRODUCTION_PROBE_ALLOWED_FUNCTIONS[@]}"; do
|
|
[[ "$entry" == "$profile/"* ]] || continue
|
|
function_name="${entry#*/}"
|
|
(( function_count += 1 ))
|
|
expected_hash="${TASK5B_PRODUCTION_PROBE_EXPECTED_HASHES[$entry]-}"
|
|
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
|
|
task5b_gate0_definition_hash actual_hash "$function_name" || return 1
|
|
[[ "$actual_hash" == "$expected_hash" ]] || {
|
|
printf 'Task 5B production probe profile hash mismatch: %s expected=%s actual=%s\n' \
|
|
"$entry" "$expected_hash" "$actual_hash" >&2
|
|
return 1
|
|
}
|
|
body="$(declare -f "$function_name")" || return 1
|
|
task5b_production_probe_body_is_closed "$profile" "$function_name" \
|
|
"$body" || {
|
|
printf 'Task 5B production probe body scan failed: %s\n' "$entry" >&2
|
|
return 1
|
|
}
|
|
[[ "$body" != *'<('* && "$body" != *'>('* &&
|
|
"$body" != *'trap - DEBUG'* && "$body" != *'set +T'* &&
|
|
"$body" != *'shopt -u extdebug'* && "$body" != *'unset '* &&
|
|
"$body" != *'enable '* ]] || {
|
|
printf 'Task 5B production probe forbidden token: %s\n' "$entry" >&2
|
|
return 1
|
|
}
|
|
line_number=0
|
|
while IFS= read -r line; do
|
|
(( line_number += 1 ))
|
|
if (( line_number > 1 )) &&
|
|
[[ "$line" =~ ^[[:space:]]*(function[[:space:]]+|[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(\)) ]]; then
|
|
printf 'Task 5B production probe nested definition: %s\n' "$entry" >&2
|
|
return 1
|
|
fi
|
|
done <<<"$body"
|
|
task5b_gate0_normalize_static_body_data_lines "$body" normalized_body \
|
|
"$function_name" || {
|
|
printf 'Task 5B production probe normalization failed: %s\n' "$entry" >&2
|
|
return 1
|
|
}
|
|
if [[ "$function_name" != _k3slr_parse_contract ]]; then
|
|
task5b_gate0_static_assignment_prefixes_are_closed "$body" || {
|
|
printf 'Task 5B production probe assignment scan failed: %s\n' "$entry" >&2
|
|
return 1
|
|
}
|
|
fi
|
|
task5b_gate0_static_execution_positions_are_closed "$normalized_body" ||
|
|
{
|
|
printf 'Task 5B production probe execution scan failed: %s\n' "$entry" >&2
|
|
return 1
|
|
}
|
|
done
|
|
(( function_count > 1 )) || return 1
|
|
}
|
|
|
|
task5b_production_probe_arm() {
|
|
(( $# == 1 )) || return 1
|
|
task5b_production_probe_profile_is_known "$1" || return 1
|
|
task5b_probe_profile="$1"
|
|
shopt -s extdebug
|
|
set -T
|
|
trap 'task5b_production_probe_debug "${BASH_COMMAND-}" "${step-}"' DEBUG
|
|
}
|
|
|
|
task5b_production_probe_canary_helper_root() {
|
|
task5b_production_probe_canary_helper_leaf
|
|
}
|
|
|
|
task5b_production_probe_canary_helper_leaf() {
|
|
/usr/bin/printf 'TASK5B_ACTION1_HELPER_EXEC_CANARY'
|
|
}
|
|
|
|
task5b_production_probe_canary_unexpected_helper() {
|
|
/usr/bin/printf 'TASK5B_UNEXPECTED_HELPER_EXEC_CANARY'
|
|
}
|
|
|
|
task5b_production_probe_static_redirection_adversary() {
|
|
printf 'TASK5B_REDIRECTION_CANARY' >"$fixture_root/task5b-redirection-canary"
|
|
}
|
|
|
|
task5b_production_probe_static_process_substitution_adversary() {
|
|
: < <(/usr/bin/printf 'TASK5B_PROCESS_SUBSTITUTION_CANARY')
|
|
}
|
|
|
|
task5b_production_probe_static_redefinition_adversary() {
|
|
task5b_nested_redefinition() { :; }
|
|
}
|
|
|
|
task5b_production_probe_adversary_matrix() (
|
|
local adversary='' record='' probe_rc=0 body='' graph_body=''
|
|
local -a adversaries=(
|
|
direct helper variable array command-wrapper builtin-wrapper exec-wrapper
|
|
eval-wrapper source-wrapper unexpected-helper trap-disable functrace-disable
|
|
extdebug-disable unset-wrapper enable-wrapper
|
|
)
|
|
task5b_production_probe_core_is_closed || return 1
|
|
task5b_production_probe_canary_graph_is_pinned || return 1
|
|
graph_body="$(declare -f task5b_production_probe_static_graph_is_closed)" ||
|
|
return 1
|
|
[[ "$graph_body" == *'task5b_production_probe_body_is_closed "$profile" "$function_name" "$body"'* ]] ||
|
|
fail 'production graph closure uses the same body scanner as static adversaries'
|
|
for adversary in "${adversaries[@]}"; do
|
|
record=''
|
|
probe_rc=0
|
|
record="$(
|
|
local runner=''
|
|
local -a runners=()
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm canary || return 1
|
|
case "$adversary" in
|
|
direct) /usr/bin/printf 'TASK5B_DIRECT_EXEC_CANARY' ;;
|
|
helper) task5b_production_probe_canary_helper_root ;;
|
|
variable)
|
|
runner=/usr/bin/printf
|
|
"$runner" 'TASK5B_VARIABLE_EXEC_CANARY'
|
|
;;
|
|
array)
|
|
runners=(/usr/bin/printf TASK5B_ARRAY_EXEC_CANARY)
|
|
"${runners[@]}"
|
|
;;
|
|
command-wrapper) command /usr/bin/printf 'TASK5B_COMMAND_EXEC_CANARY' ;;
|
|
builtin-wrapper)
|
|
builtin command /usr/bin/printf 'TASK5B_BUILTIN_EXEC_CANARY'
|
|
;;
|
|
exec-wrapper) exec /usr/bin/printf 'TASK5B_EXEC_CANARY' ;;
|
|
eval-wrapper) eval '/usr/bin/printf TASK5B_EVAL_CANARY' ;;
|
|
source-wrapper) source /fixture/task5b-source-canary ;;
|
|
unexpected-helper) task5b_production_probe_canary_unexpected_helper ;;
|
|
trap-disable) trap - DEBUG ;;
|
|
functrace-disable) set +T ;;
|
|
extdebug-disable) shopt -u extdebug ;;
|
|
unset-wrapper) unset task5b_probe_profile ;;
|
|
enable-wrapper) enable -n printf ;;
|
|
*) return 1 ;;
|
|
esac
|
|
printf 'TASK5B_CANARY_ESCAPED'
|
|
)" || probe_rc=$?
|
|
assert_eq 96 "$probe_rc" "production probe fuse blocks adversary: $adversary"
|
|
assert_eq 'TASK5B_PROBE_BLOCK|canary|1' "$record" \
|
|
"production probe adversary has one block record and no effect: $adversary"
|
|
done
|
|
body="$(declare -f task5b_production_probe_static_redirection_adversary)" ||
|
|
return 1
|
|
assert_fails task5b_production_probe_body_is_closed canary \
|
|
task5b_production_probe_static_redirection_adversary "$body"
|
|
body="$(declare -f task5b_production_probe_static_process_substitution_adversary)" ||
|
|
return 1
|
|
assert_fails task5b_production_probe_body_is_closed canary \
|
|
task5b_production_probe_static_process_substitution_adversary "$body"
|
|
body="$(declare -f task5b_production_probe_static_redefinition_adversary)" ||
|
|
return 1
|
|
assert_fails task5b_production_probe_body_is_closed canary \
|
|
task5b_production_probe_static_redefinition_adversary "$body"
|
|
)
|
|
|
|
task5b_mountinfo_counts_record() {
|
|
local record="${1-}" source_destination="${2-}" outer_destination="${3-}"
|
|
local inner_destination="${4-}" name line target source field_index
|
|
local source_count=0 outer_count=0 inner_count=0
|
|
local -a fields=()
|
|
(( $# == 4 )) || return 1
|
|
for name in "$source_destination" "$outer_destination" "$inner_destination"; do
|
|
[[ "$name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
done
|
|
while IFS= read -r line; do
|
|
[[ -n "$line" ]] || continue
|
|
read -r -a fields <<<"$line"
|
|
(( ${#fields[@]} >= 10 )) || return 1
|
|
target="${fields[4]}"
|
|
source=''
|
|
for (( field_index=0; field_index<${#fields[@]}; field_index++ )); do
|
|
if [[ "${fields[field_index]}" == - ]]; then
|
|
(( field_index + 2 < ${#fields[@]} )) || return 1
|
|
source="${fields[field_index + 2]}"
|
|
break
|
|
fi
|
|
done
|
|
[[ -n "$source" ]] || return 1
|
|
[[ "$source" != /dev/sda3 ]] || (( source_count += 1 ))
|
|
[[ "$target" != /mnt/k3s-recovery-ssd ]] || (( outer_count += 1 ))
|
|
[[ "$target" != /srv/recovery/k3s ]] || (( inner_count += 1 ))
|
|
done <<<"$record"
|
|
printf -v "$source_destination" '%s' "$source_count"
|
|
printf -v "$outer_destination" '%s' "$outer_count"
|
|
printf -v "$inner_destination" '%s' "$inner_count"
|
|
}
|
|
|
|
task5b_loop_backing_is_recovery() {
|
|
(( $# == 1 )) || return 1
|
|
case "$1" in
|
|
/fixture/recovery-partition|\
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks)
|
|
return 0
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
task5b_read_exact_single_line() {
|
|
local destination_name="${1-}" source_path="${2-}"
|
|
local -a source_lines=()
|
|
(( $# == 2 )) || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
[[ -r "$source_path" ]] || return 1
|
|
mapfile -t source_lines <"$source_path" || return 1
|
|
(( ${#source_lines[@]} == 1 )) || return 1
|
|
[[ -n "${source_lines[0]}" ]] || return 1
|
|
printf -v "$destination_name" '%s' "${source_lines[0]}"
|
|
}
|
|
|
|
task5b_loop_identity_count_from_record() {
|
|
local record="${1-}" recovery_major_minor="${2-}" destination_name="${3-}"
|
|
local line loop_name loop_minor back_inode back_major_minor loop_major_minor
|
|
local offset sizelimit diskseq_before cached_diskseq diskseq_after
|
|
local encoded_filename extra identity_match_count=0
|
|
local -A seen_loop_names=()
|
|
(( $# == 3 )) || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
[[ "$recovery_major_minor" =~ ^[0-9]+:[0-9]+$ ]] || return 1
|
|
while IFS= read -r line; do
|
|
[[ -n "$line" ]] || continue
|
|
IFS='|' read -r loop_name back_inode back_major_minor loop_major_minor \
|
|
offset sizelimit diskseq_before cached_diskseq diskseq_after \
|
|
encoded_filename extra <<<"$line"
|
|
[[ -z "${extra:-}" ]] || return 1
|
|
[[ "$loop_name" =~ ^/dev/loop([0-9]+)$ ]] || return 1
|
|
loop_minor="${BASH_REMATCH[1]}"
|
|
[[ -z "${seen_loop_names[$loop_name]+present}" ]] || return 1
|
|
seen_loop_names["$loop_name"]=1
|
|
[[ "$back_inode" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
[[ "$back_major_minor" =~ ^[0-9]+:[0-9]+$ ]] || return 1
|
|
[[ "$loop_major_minor" == "7:${loop_minor}" ]] || return 1
|
|
[[ "$offset" =~ ^[0-9]+$ && "$sizelimit" =~ ^[0-9]+$ ]] || return 1
|
|
[[ "$diskseq_before" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
[[ "$cached_diskseq" == "$diskseq_before" ]] || return 1
|
|
[[ "$diskseq_after" == "$diskseq_before" ]] || return 1
|
|
[[ "$encoded_filename" =~ ^([a-zA-Z0-9._/:+-]|\\x[0-9a-fA-F]{2})+$ ]] || return 1
|
|
[[ "$back_major_minor" != "$recovery_major_minor" ]] ||
|
|
(( identity_match_count += 1 ))
|
|
done <<<"$record"
|
|
printf -v "$destination_name" '%s' "$identity_match_count"
|
|
}
|
|
|
|
task5b_loop_live_identity_line() {
|
|
local loop_name="${1-}" recovery_major_minor="${2-}" destination_name="${3-}"
|
|
local loop_minor
|
|
local sysfs_root loop_major_minor_before='' loop_major_minor_after=''
|
|
local diskseq_before='' diskseq_after='' backing_before='' backing_after=''
|
|
local offset_before='' offset_after='' sizelimit_before='' sizelimit_after=''
|
|
local udev_path line value back_major_minor='' back_inode=''
|
|
local encoded_filename='' cached_diskseq='' field_name
|
|
local back_major_minor_count=0 back_inode_count=0 encoded_filename_count=0
|
|
local cached_diskseq_count=0
|
|
(( $# == 3 )) || return 1
|
|
[[ "$loop_name" =~ ^loop([0-9]+)$ ]] || return 1
|
|
loop_minor="${BASH_REMATCH[1]}"
|
|
[[ "$recovery_major_minor" =~ ^[0-9]+:[0-9]+$ ]] || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
sysfs_root="/sys/class/block/${loop_name}"
|
|
task5b_read_exact_single_line loop_major_minor_before "${sysfs_root}/dev" || return 1
|
|
task5b_read_exact_single_line diskseq_before "${sysfs_root}/diskseq" || return 1
|
|
task5b_read_exact_single_line backing_before \
|
|
"${sysfs_root}/loop/backing_file" || return 1
|
|
task5b_read_exact_single_line offset_before "${sysfs_root}/loop/offset" || return 1
|
|
task5b_read_exact_single_line sizelimit_before \
|
|
"${sysfs_root}/loop/sizelimit" || return 1
|
|
[[ "$loop_major_minor_before" == "7:${loop_minor}" ]] || return 1
|
|
[[ "$diskseq_before" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
[[ "$offset_before" =~ ^[0-9]+$ && "$sizelimit_before" =~ ^[0-9]+$ ]] ||
|
|
return 1
|
|
udev_path="/run/udev/data/b${loop_major_minor_before}"
|
|
[[ -r "$udev_path" ]] || return 1
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
E:ID_LOOP_BACKING_DEVICE=*)
|
|
value="${line#E:ID_LOOP_BACKING_DEVICE=}"
|
|
(( back_major_minor_count += 1 ))
|
|
back_major_minor="$value"
|
|
;;
|
|
E:ID_LOOP_BACKING_INODE=*)
|
|
value="${line#E:ID_LOOP_BACKING_INODE=}"
|
|
(( back_inode_count += 1 ))
|
|
back_inode="$value"
|
|
;;
|
|
E:ID_LOOP_BACKING_FILENAME_ENC=*)
|
|
value="${line#E:ID_LOOP_BACKING_FILENAME_ENC=}"
|
|
(( encoded_filename_count += 1 ))
|
|
encoded_filename="$value"
|
|
;;
|
|
S:disk/by-diskseq/*)
|
|
value="${line#S:disk/by-diskseq/}"
|
|
(( cached_diskseq_count += 1 ))
|
|
cached_diskseq="$value"
|
|
;;
|
|
esac
|
|
done <"$udev_path" || return 1
|
|
for field_name in back_major_minor back_inode encoded_filename cached_diskseq; do
|
|
[[ -n "${!field_name}" ]] || return 1
|
|
done
|
|
(( back_major_minor_count == 1 && back_inode_count == 1 &&
|
|
encoded_filename_count == 1 && cached_diskseq_count == 1 )) || return 1
|
|
[[ "$back_major_minor" =~ ^[0-9]+:[0-9]+$ ]] || return 1
|
|
[[ "$back_inode" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
[[ "$encoded_filename" =~ ^([a-zA-Z0-9._/:+-]|\\x[0-9a-fA-F]{2})+$ ]] || return 1
|
|
[[ "$cached_diskseq" == "$diskseq_before" ]] || return 1
|
|
task5b_read_exact_single_line loop_major_minor_after "${sysfs_root}/dev" || return 1
|
|
task5b_read_exact_single_line diskseq_after "${sysfs_root}/diskseq" || return 1
|
|
task5b_read_exact_single_line backing_after \
|
|
"${sysfs_root}/loop/backing_file" || return 1
|
|
task5b_read_exact_single_line offset_after "${sysfs_root}/loop/offset" || return 1
|
|
task5b_read_exact_single_line sizelimit_after \
|
|
"${sysfs_root}/loop/sizelimit" || return 1
|
|
[[ "$loop_major_minor_after" == "$loop_major_minor_before" ]] || return 1
|
|
[[ "$diskseq_after" == "$diskseq_before" ]] || return 1
|
|
[[ "$backing_after" == "$backing_before" ]] || return 1
|
|
[[ "$offset_after" == "$offset_before" ]] || return 1
|
|
[[ "$sizelimit_after" == "$sizelimit_before" ]] || return 1
|
|
if task5b_loop_backing_is_recovery "$backing_before"; then
|
|
[[ "$back_major_minor" == "$recovery_major_minor" ]] || return 1
|
|
fi
|
|
printf -v "$destination_name" '%s' \
|
|
"/dev/${loop_name}|${back_inode}|${back_major_minor}|${loop_major_minor_before}|${offset_before}|${sizelimit_before}|${diskseq_before}|${cached_diskseq}|${diskseq_after}|${encoded_filename}"
|
|
}
|
|
|
|
task5b_recovery_loop_count() {
|
|
local destination_name="${1-}" recovery_major_minor=''
|
|
local loop_path loop_name identity_line identity_record='' loop_index
|
|
local -a before_paths=() after_paths=()
|
|
(( $# == 1 )) || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
task5b_read_exact_single_line recovery_major_minor /sys/class/block/sda3/dev ||
|
|
return 1
|
|
[[ "$recovery_major_minor" =~ ^[0-9]+:[0-9]+$ ]] || return 1
|
|
shopt -s nullglob
|
|
before_paths=(/sys/class/block/loop*/loop/backing_file)
|
|
for loop_path in "${before_paths[@]}"; do
|
|
loop_name="${loop_path#/sys/class/block/}"
|
|
loop_name="${loop_name%%/*}"
|
|
task5b_loop_live_identity_line "$loop_name" "$recovery_major_minor" \
|
|
identity_line || return 1
|
|
identity_record+="$identity_line"$'\n'
|
|
done
|
|
after_paths=(/sys/class/block/loop*/loop/backing_file)
|
|
shopt -u nullglob
|
|
(( ${#after_paths[@]} == ${#before_paths[@]} )) || return 1
|
|
for (( loop_index=0; loop_index<${#before_paths[@]}; loop_index++ )); do
|
|
[[ "${after_paths[loop_index]}" == "${before_paths[loop_index]}" ]] || return 1
|
|
done
|
|
task5b_loop_identity_count_from_record "$identity_record" \
|
|
"$recovery_major_minor" "$destination_name"
|
|
}
|
|
|
|
task5b_path_state_from_observation() {
|
|
local destination_name="${1-}" exists="${2-}" symlink="${3-}" state=absent
|
|
(( $# == 3 )) || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
[[ "$exists" == 0 || "$exists" == 1 ]] || return 1
|
|
[[ "$symlink" == 0 || "$symlink" == 1 ]] || return 1
|
|
(( exists == 0 && symlink == 0 )) || state=present
|
|
printf -v "$destination_name" '%s' "$state"
|
|
}
|
|
|
|
task5b_path_state() {
|
|
local destination_name="${1-}" path="${2-}" exists=0 symlink=0
|
|
(( $# == 2 )) || return 1
|
|
[[ -e "$path" ]] && exists=1
|
|
[[ -L "$path" ]] && symlink=1
|
|
task5b_path_state_from_observation "$destination_name" "$exists" "$symlink"
|
|
}
|
|
|
|
task5b_fixture_process_argv_matches() {
|
|
local current_pid="${1-}" candidate_pid="${2-}" executable='' argument
|
|
local test_script_match=0 fixture_marker_match=0
|
|
shift 2 || return 1
|
|
[[ "$current_pid" =~ ^[1-9][0-9]*$ && "$candidate_pid" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
[[ "$candidate_pid" != "$current_pid" ]] || return 1
|
|
(( $# >= 1 )) || return 1
|
|
executable="$1"
|
|
[[ "$executable" == bash || "$executable" == /bin/bash ||
|
|
"$executable" == /usr/bin/bash ]] || return 1
|
|
for argument in "$@"; do
|
|
case "$argument" in
|
|
"$REPOSITORY_ROOT/scripts/validate/test-k3s-local-recovery.sh"|\
|
|
scripts/validate/test-k3s-local-recovery.sh)
|
|
test_script_match=1
|
|
;;
|
|
task5b-fixture|task5b-fixture:*) fixture_marker_match=1 ;;
|
|
esac
|
|
done
|
|
(( test_script_match == 1 || fixture_marker_match == 1 ))
|
|
}
|
|
|
|
task5b_expected_safe_live_state_record() {
|
|
local destination_name="${1-}"
|
|
(( $# == 1 )) || return 1
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
printf -v "$destination_name" '%s' \
|
|
$'mounts_sda3=0\nmounts_outer=0\nmounts_inner=0\nouter_mount_path=absent\ninner_mount_path=absent\nrecovery_loops=0\nmapper_main=absent\nmapper_proof=absent\nreserved_root=absent\ntask5b_temp_residue=0\ntask5b_fixture_processes=0'
|
|
}
|
|
|
|
task5b_live_state_record() {
|
|
local destination_name="${1-}" line mountinfo_record=''
|
|
local mounts_sda3=0 mounts_outer=0 mounts_inner=0 recovery_loops=0
|
|
local outer_mount_path=absent inner_mount_path=absent
|
|
local mapper_main=absent mapper_proof=absent reserved_root=absent
|
|
local task5b_temp_residue=0 task5b_fixture_processes=0 pid_path candidate_pid
|
|
local -a residue_paths=() process_arguments=()
|
|
[[ "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
|
|
while IFS= read -r line; do
|
|
mountinfo_record+="$line"$'\n'
|
|
done </proc/self/mountinfo
|
|
task5b_mountinfo_counts_record "$mountinfo_record" mounts_sda3 mounts_outer \
|
|
mounts_inner || return 1
|
|
task5b_recovery_loop_count recovery_loops || return 1
|
|
task5b_path_state outer_mount_path /mnt/k3s-recovery-ssd || return 1
|
|
task5b_path_state inner_mount_path /srv/recovery/k3s || return 1
|
|
task5b_path_state mapper_main /dev/mapper/k3s-recovery || return 1
|
|
task5b_path_state mapper_proof /dev/mapper/k3s-recovery-proof || return 1
|
|
task5b_path_state reserved_root \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery || return 1
|
|
shopt -s nullglob
|
|
residue_paths=("${fixture_root}"/task5b-* "${fixture_root}"/k3slr-capture.*
|
|
/tmp/k3slr-task5b.* /tmp/k3slr-capture.* /tmp/task5b-fixture.*)
|
|
task5b_temp_residue="${#residue_paths[@]}"
|
|
shopt -u nullglob
|
|
for pid_path in /proc/[0-9]*/cmdline; do
|
|
candidate_pid="${pid_path#/proc/}"
|
|
candidate_pid="${candidate_pid%/cmdline}"
|
|
process_arguments=()
|
|
while IFS= read -r -d '' line; do
|
|
process_arguments+=("$line")
|
|
done <"$pid_path" 2>/dev/null || :
|
|
task5b_fixture_process_argv_matches "$BASHPID" "$candidate_pid" \
|
|
"${process_arguments[@]}" || continue
|
|
(( task5b_fixture_processes += 1 ))
|
|
done
|
|
printf -v "$destination_name" \
|
|
'mounts_sda3=%s\nmounts_outer=%s\nmounts_inner=%s\nouter_mount_path=%s\ninner_mount_path=%s\nrecovery_loops=%s\nmapper_main=%s\nmapper_proof=%s\nreserved_root=%s\ntask5b_temp_residue=%s\ntask5b_fixture_processes=%s' \
|
|
"$mounts_sda3" "$mounts_outer" "$mounts_inner" "$outer_mount_path" \
|
|
"$inner_mount_path" "$recovery_loops" "$mapper_main" "$mapper_proof" \
|
|
"$reserved_root" "$task5b_temp_residue" "$task5b_fixture_processes"
|
|
}
|
|
|
|
task5b_gate0_review_round1_adversarial_assertions() {
|
|
local mount_record mounts_sda3=0 mounts_outer=0 mounts_inner=0 path_state=''
|
|
local selector_output='' selector_rc=0
|
|
mount_record=$'36 25 8:3 / /mnt/k3s-recovery-ssd rw - ntfs3 /dev/sda3 rw\n37 25 253:0 / /srv/recovery/k3s rw - ext4 /dev/mapper/k3s-recovery rw\n'
|
|
assert_succeeds task5b_mountinfo_counts_record "$mount_record" mounts_sda3 \
|
|
mounts_outer mounts_inner
|
|
assert_eq 1 "$mounts_sda3" 'collector parses exact /dev/sda3 source'
|
|
assert_eq 1 "$mounts_outer" 'collector parses exact outer mountpoint'
|
|
assert_eq 1 "$mounts_inner" 'collector parses exact inner mountpoint'
|
|
|
|
assert_succeeds task5b_loop_backing_is_recovery /fixture/recovery-partition
|
|
assert_succeeds task5b_loop_backing_is_recovery \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks
|
|
assert_fails task5b_loop_backing_is_recovery /dev/sda3
|
|
assert_fails task5b_loop_backing_is_recovery /other/recovery-partition
|
|
assert_fails task5b_loop_backing_is_recovery \
|
|
/mnt/k3s-recovery-ssd/HyeonworksRecovery/containers/k3s-recovery.luks.bak
|
|
|
|
assert_succeeds task5b_path_state_from_observation path_state 0 0
|
|
assert_eq absent "$path_state" 'collector treats only nonexistence and nonsymlink as absent'
|
|
assert_succeeds task5b_path_state_from_observation path_state 0 1
|
|
assert_eq present "$path_state" 'collector treats a dangling symlink as present'
|
|
assert_succeeds task5b_path_state_from_observation path_state 1 0
|
|
assert_eq present "$path_state" 'collector treats an existing path as present'
|
|
|
|
assert_fails task5b_fixture_process_argv_matches 100 100 /usr/bin/bash \
|
|
scripts/validate/test-k3s-local-recovery.sh
|
|
assert_succeeds task5b_fixture_process_argv_matches 100 101 /usr/bin/bash \
|
|
scripts/validate/test-k3s-local-recovery.sh
|
|
assert_succeeds task5b_fixture_process_argv_matches 100 102 /bin/bash -c : \
|
|
task5b-fixture:gate0
|
|
assert_fails task5b_fixture_process_argv_matches 100 103 /usr/bin/bash -c :
|
|
assert_fails task5b_fixture_process_argv_matches 100 104 /usr/bin/sleep 10 \
|
|
task5b-fixture:gate0
|
|
|
|
selector_output="$(
|
|
TASK5B_FOCUS=not-a-task5b-focus TASK5A_FOCUS=dispatcher \
|
|
/usr/bin/timeout 10s /usr/bin/bash "${BASH_SOURCE[0]}" 2>&1
|
|
)" || selector_rc=$?
|
|
assert_eq 2 "$selector_rc" 'unknown Task 5B focus is rejected before legacy selection'
|
|
assert_eq 'TEST FAILURE: unsupported TASK5B_FOCUS: not-a-task5b-focus' \
|
|
"$selector_output" 'unknown Task 5B focus emits only the early selector diagnostic'
|
|
}
|
|
|
|
task5b_gate0_review_round2_adversarial_assertions() {
|
|
local record='' match_count=0 mutated_helper_body='' fuse_body=''
|
|
|
|
record='/dev/loop37|4242|8:3|7:37|0|0|99|99|99|\x2ffixture\x2frecovery.luks'
|
|
assert_succeeds task5b_loop_identity_count_from_record "$record" 8:3 \
|
|
match_count
|
|
assert_eq 1 "$match_count" 'loop identity parser counts exact backing MAJ:MIN'
|
|
|
|
record='/dev/loop37|4242|8:33|7:37|0|0|99|99|99|\x2froot\x2fsnap'
|
|
assert_succeeds task5b_loop_identity_count_from_record "$record" 8:3 \
|
|
match_count
|
|
assert_eq 0 "$match_count" 'loop identity parser rejects a nonmatching device'
|
|
|
|
record='/dev/loop37|4242|8:3|7:37|0|0|99|99|99|\x2fdeleted\x20backing'
|
|
assert_succeeds task5b_loop_identity_count_from_record "$record" 8:3 \
|
|
match_count
|
|
assert_eq 1 "$match_count" \
|
|
'deleted or missing backing pathname cannot hide matching device identity'
|
|
|
|
assert_fails task5b_loop_identity_count_from_record \
|
|
'/dev/loop37|4242|8:3|7:37|0|0|99|99' 8:3 match_count
|
|
assert_fails task5b_loop_identity_count_from_record \
|
|
'/dev/loop37|4242|8:3|7:37|0|0|99|98|99|\x2fstale' 8:3 match_count
|
|
record=$'/dev/loop37|4242|8:3|7:37|0|0|99|99|99|\\x2fone\n/dev/loop37|4243|8:33|7:37|0|0|99|99|99|\\x2ftwo'
|
|
assert_fails task5b_loop_identity_count_from_record "$record" 8:3 match_count
|
|
|
|
mutated_helper_body="$(declare -f task5b_gate0_assert_rejected)"$'\n'\
|
|
'trap - DEBUG
|
|
/usr/bin/printf TASK5B_TRAP_DISABLE_CANARY' || return 1
|
|
assert_fails task5b_gate0_static_body_is_closed "$mutated_helper_body"
|
|
fuse_body="$(declare -f task5b_gate0_real_exec_fuse)" || return 1
|
|
[[ "$fuse_body" != *'|trap)'* ]] ||
|
|
fail 'active fuse must not allowlist DEBUG-trap teardown'
|
|
}
|
|
|
|
task5b_api_state_trimmed_line_count() {
|
|
local body="${1-}" expected_line="${2-}" line count=0
|
|
(( $# == 2 )) || return 1
|
|
while IFS= read -r line; do
|
|
line="${line#"${line%%[![:space:]]*}"}"
|
|
line="${line%"${line##*[![:space:]]}"}"
|
|
[[ "$line" != "$expected_line" ]] || (( count += 1 ))
|
|
done <<<"$body"
|
|
printf '%s\n' "$count"
|
|
}
|
|
|
|
task5b_api_state_dispatcher_has_obsolete_provider_flow() {
|
|
local body="${1-}" line
|
|
local provider_token_re='("\$header_provider"|\$header_provider|"\$\{header_provider\}"|\$\{header_provider\})'
|
|
local direct_start_re='' control_provider_re='' separator_provider_re=''
|
|
(( $# == 1 )) || return 1
|
|
direct_start_re="^[[:space:]]*${provider_token_re}([[:space:];|&]|$)"
|
|
control_provider_re="(^|[[:space:]])(if|then|elif|while|until|!)[[:space:]]+${provider_token_re}([[:space:];|&]|$)"
|
|
separator_provider_re="(&&|\|\||;)[[:space:]]*${provider_token_re}([[:space:];|&]|$)"
|
|
while IFS= read -r line; do
|
|
line="${line#"${line%%[![:space:]]*}"}"
|
|
line="${line%"${line##*[![:space:]]}"}"
|
|
[[ "$line" != *'${@:3}'* && "$line" != *provider_arguments* ]] ||
|
|
return 0
|
|
[[ ! "$line" =~ $direct_start_re &&
|
|
! "$line" =~ $control_provider_re &&
|
|
! "$line" =~ $separator_provider_re ]] || return 0
|
|
if [[ "$line" == *'$header_provider'* ||
|
|
"$line" == *'${header_provider}'* ]]; then
|
|
case "$line" in
|
|
action_arguments=*|action_arguments+=*|_k3slr_lifecycle_action[[:space:]]*)
|
|
return 0
|
|
;;
|
|
esac
|
|
[[ "$line" =~ ^[[:alnum:]_]*arguments(\+)?= ]] && return 0
|
|
fi
|
|
done <<<"$body"
|
|
return 1
|
|
}
|
|
|
|
# The production breaks this catches are the obsolete caller-provider argv
|
|
# forwarding API, a prepare action that still accepts two arguments, or state
|
|
# created anywhere except one empty dispatcher-owned associative declaration.
|
|
# The production dispatcher/action are observed directly and are never
|
|
# replaced by a fixture success stub.
|
|
task5b_api_state_contract() (
|
|
local dispatch_body='' action_body='' main_body='' library_source=''
|
|
local state_declaration_count=0 state_append_count=0 action_call_count=0
|
|
local main_dispatch_count=0 state_reference_count=0
|
|
local hook_calls=0 action_calls=0 hook_calls_exact=0 action_calls_exact=0
|
|
local command_calls=0 command_calls_exact=0 command_calls_direct=0
|
|
local tty_calls=0 provider_calls=0 action_rc=0
|
|
local dispatch_exact_actual_exec=0 command_direct_actual_exec=0
|
|
local main_arity_actual_exec=0 wrong_action_actual_exec=0
|
|
local dispatch_exact_rc=0 dispatch_invalid_rc=0 dispatch_case='' index=0
|
|
local main_wrong_arity_successes=0 main_contract_sentinel=not-loaded
|
|
local probe_rc=0 record='' record_tag='' record_profile='' record_extra=''
|
|
local caller_value=''
|
|
local -a invalid_dispatch_cases=(zero one prepare-third open-third close-four)
|
|
local -a invalid_dispatch_rcs=() invalid_dispatch_hook_calls=()
|
|
local -a invalid_dispatch_action_calls=() invalid_dispatch_command_calls=()
|
|
local -a invalid_dispatch_actual_exec=()
|
|
local -a wrong_action_rcs=() record_fields=()
|
|
local -A caller_seed=([caller_provider_argument]=must-not-be-read)
|
|
|
|
# shellcheck source=/dev/null
|
|
source "$LIBRARY_PATH"
|
|
_k3slr_command() { (( command_calls += 1 )); return 97; }
|
|
_k3slr_tty_capability() { (( tty_calls += 1 )); return 1; }
|
|
_k3slr_read_tty_line() { (( tty_calls += 1 )); return 1; }
|
|
task5b_api_state_header_provider() { (( provider_calls += 1 )); return 97; }
|
|
task5b_api_state_debug_observer() {
|
|
local observed_command="${1-}"
|
|
(( $# == 1 )) || return 1
|
|
case "$observed_command" in
|
|
_k3slr_lifecycle_step\ *) (( hook_calls += 1 )) ;;
|
|
_k3slr_lifecycle_action\ *) (( action_calls += 1 )) ;;
|
|
esac
|
|
}
|
|
task5b_api_dispatch_probe_payload() {
|
|
local dispatch_case="${1-}" dispatch_rc=0 hook_calls=0 action_calls=0
|
|
local command_calls=0 provider_calls=0
|
|
local -A caller_seed=([caller_provider_argument]=must-not-be-read)
|
|
(( $# == 1 )) || return 1
|
|
case "$dispatch_case" in
|
|
exact)
|
|
_k3slr_lifecycle_dispatch prepare task5b_api_state_header_provider ||
|
|
dispatch_rc=$?
|
|
;;
|
|
zero) _k3slr_lifecycle_dispatch || dispatch_rc=$? ;;
|
|
one) _k3slr_lifecycle_dispatch prepare || dispatch_rc=$? ;;
|
|
prepare-third)
|
|
_k3slr_lifecycle_dispatch prepare task5b_api_state_header_provider \
|
|
caller_seed || dispatch_rc=$?
|
|
;;
|
|
open-third)
|
|
_k3slr_lifecycle_dispatch open task5b_api_state_header_provider \
|
|
caller_seed || dispatch_rc=$?
|
|
;;
|
|
close-four)
|
|
_k3slr_lifecycle_dispatch close task5b_api_state_header_provider \
|
|
caller_seed extra || dispatch_rc=$?
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
task5b_production_probe_emit_ok api_dispatch "$dispatch_rc" "$hook_calls" \
|
|
"$action_calls" "$command_calls" "$provider_calls" \
|
|
"${caller_seed[caller_provider_argument]}"
|
|
}
|
|
task5b_api_direct_probe_payload() {
|
|
local direct_case="${1-}" action_rc=0 command_calls=0
|
|
local -a wrong_action_rcs=()
|
|
(( $# == 1 )) || return 1
|
|
case "$direct_case" in
|
|
exact)
|
|
_k3slr_lifecycle_action prepare sudo _k3slr_prepare_invocation ||
|
|
action_rc=$?
|
|
task5b_production_probe_emit_ok api_direct "$action_rc" "$command_calls"
|
|
;;
|
|
wrong-arities)
|
|
_k3slr_lifecycle_action || action_rc=$?
|
|
wrong_action_rcs+=("$action_rc")
|
|
action_rc=0
|
|
_k3slr_lifecycle_action prepare || action_rc=$?
|
|
wrong_action_rcs+=("$action_rc")
|
|
action_rc=0
|
|
_k3slr_lifecycle_action prepare sudo || action_rc=$?
|
|
wrong_action_rcs+=("$action_rc")
|
|
action_rc=0
|
|
_k3slr_lifecycle_action prepare sudo _k3slr_prepare_invocation extra ||
|
|
action_rc=$?
|
|
wrong_action_rcs+=("$action_rc")
|
|
action_rc=0
|
|
_k3slr_lifecycle_action open || action_rc=$?
|
|
wrong_action_rcs+=("$action_rc")
|
|
action_rc=0
|
|
_k3slr_lifecycle_action open sudo extra || action_rc=$?
|
|
wrong_action_rcs+=("$action_rc")
|
|
action_rc=0
|
|
_k3slr_lifecycle_action close || action_rc=$?
|
|
wrong_action_rcs+=("$action_rc")
|
|
action_rc=0
|
|
_k3slr_lifecycle_action close sudo extra || action_rc=$?
|
|
wrong_action_rcs+=("$action_rc")
|
|
task5b_production_probe_emit_ok api_direct "$command_calls" \
|
|
"${wrong_action_rcs[@]}"
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
task5b_api_main_arity_probe_payload() {
|
|
local main_wrong_arity_successes=0
|
|
(( $# == 0 )) || return 1
|
|
_k3slr_lifecycle_main && (( main_wrong_arity_successes += 1 ))
|
|
_k3slr_lifecycle_main prepare && (( main_wrong_arity_successes += 1 ))
|
|
_k3slr_lifecycle_main prepare execute caller_seed &&
|
|
(( main_wrong_arity_successes += 1 ))
|
|
task5b_production_probe_emit_ok api_main_arity \
|
|
"$main_wrong_arity_successes" "$K3SLR_SCHEMA_VERSION"
|
|
}
|
|
task5b_production_probe_adversary_matrix || return 1
|
|
task5b_production_probe_static_graph_is_closed api_dispatch || return 1
|
|
task5b_production_probe_static_graph_is_closed api_direct || return 1
|
|
task5b_production_probe_static_graph_is_closed api_main_arity || return 1
|
|
|
|
record="$(
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm api_dispatch || return 1
|
|
task5b_api_dispatch_probe_payload exact
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" 'exact dispatcher probe completes without a fuse block'
|
|
IFS='|' read -r record_tag record_profile dispatch_exact_rc hook_calls_exact \
|
|
action_calls_exact command_calls_exact provider_calls caller_value \
|
|
record_extra <<<"$record" || return 1
|
|
[[ "$record_tag" == TASK5B_PROBE_OK && "$record_profile" == api_dispatch &&
|
|
-z "$record_extra" ]] || return 1
|
|
assert_eq 0 "$provider_calls" 'exact dispatcher does not call its provider'
|
|
assert_eq must-not-be-read "$caller_value" \
|
|
'exact dispatcher leaves caller-owned state unchanged'
|
|
|
|
for dispatch_case in "${invalid_dispatch_cases[@]}"; do
|
|
record=''
|
|
probe_rc=0
|
|
record="$(
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm api_dispatch || return 1
|
|
task5b_api_dispatch_probe_payload "$dispatch_case"
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" \
|
|
"wrong dispatcher arity probe completes without a fuse block: $dispatch_case"
|
|
IFS='|' read -r record_tag record_profile dispatch_invalid_rc hook_calls \
|
|
action_calls command_calls provider_calls caller_value record_extra \
|
|
<<<"$record" || return 1
|
|
[[ "$record_tag" == TASK5B_PROBE_OK && "$record_profile" == api_dispatch &&
|
|
-z "$record_extra" ]] || return 1
|
|
invalid_dispatch_rcs+=("$dispatch_invalid_rc")
|
|
invalid_dispatch_hook_calls+=("$hook_calls")
|
|
invalid_dispatch_action_calls+=("$action_calls")
|
|
invalid_dispatch_command_calls+=("$command_calls")
|
|
invalid_dispatch_actual_exec+=(0)
|
|
assert_eq 0 "$provider_calls" \
|
|
"wrong dispatcher arity calls no provider: $dispatch_case"
|
|
assert_eq must-not-be-read "$caller_value" \
|
|
"wrong dispatcher arity leaves caller state unchanged: $dispatch_case"
|
|
done
|
|
|
|
record=''
|
|
probe_rc=0
|
|
record="$(
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm api_direct || return 1
|
|
task5b_api_direct_probe_payload exact
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" 'exact direct Action 1 probe completes without a fuse block'
|
|
IFS='|' read -r record_tag record_profile action_rc command_calls_direct \
|
|
record_extra <<<"$record" || return 1
|
|
[[ "$record_tag" == TASK5B_PROBE_OK && "$record_profile" == api_direct &&
|
|
-z "$record_extra" ]] || return 1
|
|
|
|
K3SLR_WRAPPER_CONTRACT="$CONTRACT_PATH"
|
|
K3SLR_SCHEMA_VERSION="$main_contract_sentinel"
|
|
record=''
|
|
probe_rc=0
|
|
record="$(
|
|
K3SLR_WRAPPER_CONTRACT="$CONTRACT_PATH"
|
|
K3SLR_SCHEMA_VERSION="$main_contract_sentinel"
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm api_main_arity || return 1
|
|
task5b_api_main_arity_probe_payload
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" 'wrong main arity probe completes without a fuse block'
|
|
IFS='|' read -r record_tag record_profile main_wrong_arity_successes \
|
|
K3SLR_SCHEMA_VERSION record_extra <<<"$record" || return 1
|
|
[[ "$record_tag" == TASK5B_PROBE_OK &&
|
|
"$record_profile" == api_main_arity && -z "$record_extra" ]] || return 1
|
|
|
|
dispatch_body="$(declare -f _k3slr_lifecycle_dispatch)" || return 1
|
|
action_body="$(declare -f _k3slr_lifecycle_action)" || return 1
|
|
main_body="$(declare -f _k3slr_lifecycle_main)" || return 1
|
|
library_source="$(<"$LIBRARY_PATH")" || return 1
|
|
state_declaration_count="$(task5b_api_state_trimmed_line_count "$dispatch_body" \
|
|
'local -A _k3slr_prepare_invocation=();')" || return 1
|
|
state_append_count="$(task5b_api_state_trimmed_line_count "$dispatch_body" \
|
|
'action_arguments+=(_k3slr_prepare_invocation);')" || return 1
|
|
action_call_count="$(task5b_api_state_trimmed_line_count "$dispatch_body" \
|
|
'_k3slr_lifecycle_action "${action_arguments[@]}" || return 1;')" || return 1
|
|
main_dispatch_count="$(task5b_api_state_trimmed_line_count "$main_body" \
|
|
'_k3slr_lifecycle_dispatch "$lifecycle" _k3slr_prove_header_restore')" || return 1
|
|
while IFS= read -r line; do
|
|
[[ "$line" != *'_k3slr_prepare_invocation'* ]] ||
|
|
(( state_reference_count += 1 ))
|
|
done <<<"$library_source"
|
|
|
|
(( hook_calls_exact > 0 )) ||
|
|
fail 'exact two-argument prepare dispatcher did not reach its production hook'
|
|
(( action_calls_exact > 0 )) ||
|
|
fail 'exact two-argument prepare dispatcher did not reach its production action'
|
|
(( dispatch_exact_rc != 0 )) ||
|
|
fail 'API/state checkpoint production dispatcher is not fail-closed'
|
|
for index in "${!invalid_dispatch_cases[@]}"; do
|
|
dispatch_case="${invalid_dispatch_cases[$index]}"
|
|
(( invalid_dispatch_rcs[index] != 0 )) ||
|
|
fail "wrong dispatcher arity succeeded: $dispatch_case"
|
|
assert_eq 0 "${invalid_dispatch_hook_calls[$index]}" \
|
|
"wrong dispatcher arity reached production hook: $dispatch_case"
|
|
assert_eq 0 "${invalid_dispatch_action_calls[$index]}" \
|
|
"wrong dispatcher arity reached production action: $dispatch_case"
|
|
assert_eq 0 "${invalid_dispatch_command_calls[$index]}" \
|
|
"wrong dispatcher arity reached the command seam: $dispatch_case"
|
|
assert_eq 0 "${invalid_dispatch_actual_exec[$index]}" \
|
|
"wrong dispatcher arity executes no real command: $dispatch_case"
|
|
done
|
|
assert_eq must-not-be-read "${caller_seed[caller_provider_argument]}" \
|
|
'rejected caller state/provider argument remains unchanged'
|
|
assert_eq 1 "$command_calls_exact" \
|
|
'exact dispatcher reaches the safe Action 1 command fuse once'
|
|
assert_eq 1 "$command_calls_direct" \
|
|
'exact direct Action 1 reaches the safe command fuse once'
|
|
assert_eq 0 "$dispatch_exact_actual_exec" \
|
|
'exact dispatcher executes no real command'
|
|
assert_eq 0 "$command_direct_actual_exec" \
|
|
'exact direct Action 1 executes no real command'
|
|
assert_eq 0 "$main_arity_actual_exec" \
|
|
'wrong main arity executes no real command'
|
|
assert_eq 0 "$tty_calls" 'API/state arity matrix has TTY effect 0'
|
|
assert_eq 0 "$provider_calls" 'API/state arity matrix has provider effect 0'
|
|
assert_eq 97 "$action_rc" \
|
|
'exact three-argument Action 1 preserves the safe command-fuse failure'
|
|
assert_eq 0 "$main_wrong_arity_successes" \
|
|
'main rejects zero, one, and three arguments'
|
|
assert_eq not-loaded "$K3SLR_SCHEMA_VERSION" \
|
|
'wrong main arity fails before contract load effects'
|
|
|
|
assert_eq 1 "$state_declaration_count" \
|
|
'dispatcher has one direct empty prepare associative-state declaration'
|
|
assert_eq 1 "$state_append_count" \
|
|
'dispatcher appends the fixed prepare-state name once to action argv'
|
|
assert_eq 1 "$action_call_count" \
|
|
'dispatcher has one production action invocation site'
|
|
assert_eq 1 "$main_dispatch_count" \
|
|
'main has one exact two-argument dispatcher invocation site'
|
|
assert_eq 3 "$state_reference_count" \
|
|
'production has only the prepare-state declaration, fixed action carrier, and Action 1 literal guard'
|
|
assert_fails task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"
|
|
assert_succeeds task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n''${@:3}'
|
|
assert_succeeds task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n''provider_arguments=("$header_provider")'
|
|
assert_succeeds task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n''"$header_provider" restore'
|
|
assert_succeeds task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n''if "$header_provider"; then :; fi'
|
|
assert_succeeds task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n'': && "$header_provider" restore'
|
|
assert_succeeds task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n''! "$header_provider" restore'
|
|
assert_succeeds task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n'': || "$header_provider" restore'
|
|
assert_succeeds task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n''action_arguments+=("$header_provider")'
|
|
assert_fails task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n''[[ "$header_provider" == _k3slr_prove_header_restore ]] || return 1'
|
|
assert_fails task5b_api_state_dispatcher_has_obsolete_provider_flow \
|
|
"$dispatch_body"$'\n''declare -F "$header_provider" >/dev/null || return 1'
|
|
[[ "$dispatch_body" == *'header_provider="${2-}"'* ]] ||
|
|
fail 'dispatcher invocation-local provider identity binding is absent'
|
|
[[ "$dispatch_body" != *'${@:3}'* && "$dispatch_body" != *provider_arguments* ]] ||
|
|
fail 'dispatcher retains obsolete caller provider argument compatibility'
|
|
[[ "$dispatch_body" == *'(( $# == 2 )) || return 1'* ]] ||
|
|
fail 'dispatcher exact two-argument arity guard is absent'
|
|
[[ "$action_body" == *'prepare)'*'(( $# == 3 )) || return 1'* &&
|
|
"$action_body" == *'open | close)'*'(( $# == 2 )) || return 1'* ]] ||
|
|
fail 'production action handler retains obsolete lifecycle arity'
|
|
[[ "$main_body" != *'_k3slr_prepare_invocation'* &&
|
|
"$main_body" != *'${@:3}'* ]] ||
|
|
fail 'main creates or forwards prepare invocation state'
|
|
[[ "$main_body" == *'(( $# == 2 )) || return 1'* ]] ||
|
|
fail 'main exact two-argument arity guard is absent'
|
|
[[ "${main_body%%_k3slr_load_contract*}" == *'(( $# == 2 )) || return 1'* ]] ||
|
|
fail 'main arity guard does not precede contract effects'
|
|
|
|
record=''
|
|
probe_rc=0
|
|
record="$(
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm api_direct || return 1
|
|
task5b_api_direct_probe_payload wrong-arities
|
|
)" || probe_rc=$?
|
|
assert_eq 0 "$probe_rc" 'wrong action arity probe completes without a fuse block'
|
|
IFS='|' read -r -a record_fields <<<"$record" || return 1
|
|
record_tag="${record_fields[0]-}"
|
|
record_profile="${record_fields[1]-}"
|
|
command_calls="${record_fields[2]-}"
|
|
wrong_action_rcs=("${record_fields[@]:3}")
|
|
[[ "$record_tag" == TASK5B_PROBE_OK && "$record_profile" == api_direct &&
|
|
${#wrong_action_rcs[@]} == 8 ]] || return 1
|
|
for action_rc in "${wrong_action_rcs[@]}"; do
|
|
(( action_rc != 0 )) || fail 'wrong action arity unexpectedly succeeded'
|
|
done
|
|
assert_eq 0 "$command_calls" 'wrong action/dispatcher arity has command effect 0'
|
|
assert_eq 0 "$wrong_action_actual_exec" \
|
|
'wrong action/dispatcher arity has no fuse block'
|
|
assert_eq 0 "$tty_calls" 'wrong action/dispatcher arity has TTY effect 0'
|
|
assert_eq 0 "$provider_calls" 'wrong action/dispatcher arity has provider effect 0'
|
|
)
|
|
|
|
# The production breaks this catches are a high-level TTY gate that bypasses
|
|
# the low capability seam, fails to preserve its status, or reaches it after a
|
|
# wrong-arity call. Only the lowest seam is replaced in this source-loaded
|
|
# foreground subshell.
|
|
task5b_tty_seam_contract() (
|
|
local tty_capability_calls=0 tty_capability_rc=0 high_rc=0
|
|
|
|
# shellcheck source=/dev/null
|
|
source "$LIBRARY_PATH"
|
|
_k3slr_tty_capability() {
|
|
(( tty_capability_calls += 1 ))
|
|
return "$tty_capability_rc"
|
|
}
|
|
|
|
high_rc=0
|
|
_k3slr_require_execute_tty || high_rc=$?
|
|
assert_eq 1 "$tty_capability_calls" \
|
|
'TTY high helper delegates exactly once on low-capability success'
|
|
assert_eq 0 "$high_rc" \
|
|
'TTY high helper preserves low-capability success'
|
|
|
|
tty_capability_calls=0
|
|
tty_capability_rc=73
|
|
high_rc=0
|
|
_k3slr_require_execute_tty || high_rc=$?
|
|
assert_eq 1 "$tty_capability_calls" \
|
|
'TTY high helper delegates exactly once on low-capability failure'
|
|
assert_eq 73 "$high_rc" \
|
|
'TTY high helper preserves low-capability failure'
|
|
|
|
tty_capability_calls=0
|
|
tty_capability_rc=0
|
|
high_rc=0
|
|
_k3slr_require_execute_tty unexpected || high_rc=$?
|
|
assert_eq 1 "$high_rc" 'TTY high helper rejects wrong arity'
|
|
assert_eq 0 "$tty_capability_calls" \
|
|
'TTY high helper rejects wrong arity before the low seam'
|
|
)
|
|
|
|
task5b_action1_literal_line_count() {
|
|
local body="${1-}" needle="${2-}" destination_name="${3-}" line count=0
|
|
(( $# == 3 )) || return 1
|
|
[[ -n "$needle" && "$destination_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] ||
|
|
return 1
|
|
while IFS= read -r line; do
|
|
[[ "$line" != *"$needle"* ]] || (( count += 1 ))
|
|
done <<<"$body"
|
|
printf -v "$destination_name" '%s' "$count"
|
|
}
|
|
|
|
# Every literal direct prepare/sudo test call must live in a fixture whose
|
|
# command seam fails closed or in the Action 1 exact matcher. The historical
|
|
# unavailable-action loop is indirect, so it has its own command fuse and must
|
|
# no longer enumerate the now-available prepare/sudo action.
|
|
task5b_action1_nonfocused_direct_sudo_paths_are_fused() {
|
|
local direct_needle='' test_source='' safe_bodies=''
|
|
local dryrun_body='' api_state_body='' action1_body='' unavailable_body=''
|
|
local source_count=0 safe_count=0
|
|
direct_needle='_k3slr_lifecycle_action prepare '"sudo"
|
|
test_source="$(<"${BASH_SOURCE[0]}")" || return 1
|
|
dryrun_body="$(declare -f task5a_dry_run_and_prepackage_action_binding)" ||
|
|
return 1
|
|
api_state_body="$(declare -f task5b_api_state_contract)" || return 1
|
|
action1_body="$(declare -f task5b_action1_command_binding_contract)" ||
|
|
return 1
|
|
unavailable_body="$(declare -f task5a_production_actions_fail_closed)" ||
|
|
return 1
|
|
safe_bodies="$dryrun_body"$'\n'"$api_state_body"$'\n'"$action1_body"
|
|
task5b_action1_literal_line_count "$test_source" "$direct_needle" \
|
|
source_count || return 1
|
|
task5b_action1_literal_line_count "$safe_bodies" "$direct_needle" \
|
|
safe_count || return 1
|
|
(( source_count > 0 && source_count == safe_count )) || return 1
|
|
[[ "$dryrun_body" == *'_k3slr_command ()'* &&
|
|
"$dryrun_body" == *'return 97'* &&
|
|
"$dryrun_body" == *'task5b_production_probe_arm legacy_binding'* ]] ||
|
|
return 1
|
|
[[ "$api_state_body" == *'_k3slr_command ()'* &&
|
|
"$api_state_body" == *'return 97'* &&
|
|
"$api_state_body" == *'task5b_production_probe_arm api_direct'* ]] ||
|
|
return 1
|
|
[[ "$action1_body" == *'task5b_gate0_route_equals sudo-validate "$@"'* &&
|
|
"$action1_body" == *'return 97'* &&
|
|
"$action1_body" == *'task5b_production_probe_arm action1_main'* &&
|
|
"$action1_body" == *'task5b_production_probe_arm action1_alternate'* ]] ||
|
|
return 1
|
|
[[ "$unavailable_body" == *'_k3slr_command ()'* &&
|
|
"$unavailable_body" == *'return 97'* &&
|
|
"$unavailable_body" == *'task5b_production_probe_arm unavailable'* &&
|
|
"$unavailable_body" != *'prepare_unavailable=(sudo'* ]] || return 1
|
|
}
|
|
|
|
# The production breaks this catches are a prepare/sudo handler that omits or
|
|
# misframes the exact sudo validation argv, reaches a command after sudo, calls
|
|
# the header provider early, or accepts any state name except the dispatcher-
|
|
# owned literal. The real main, dispatcher, hook, action, and invocation state
|
|
# remain untouched; only the lowest TTY and command seams are replaced.
|
|
task5b_action1_command_binding_contract() (
|
|
local tty_calls=0 tty_rc=0 sudo_dispatches=0 sudo_rc=0
|
|
local unexpected_commands=0 actual_exec=0 provider_calls=0 main_rc=0
|
|
local hook_sudo_entries=0 hook_context_entries=0 hook_suffix_entries=0
|
|
local action_sudo_entries=0 action_context_entries=0 action_suffix_entries=0
|
|
local action_rc=0 command_body=''
|
|
local probe_record='' record_tag='' record_profile='' record_extra=''
|
|
|
|
# shellcheck source=/dev/null
|
|
source "$LIBRARY_PATH"
|
|
K3SLR_WRAPPER_CONTRACT="$CONTRACT_PATH"
|
|
|
|
_k3slr_prove_header_restore() {
|
|
(( provider_calls += 1 ))
|
|
return 97
|
|
}
|
|
_k3slr_tty_capability() {
|
|
(( tty_calls += 1 ))
|
|
return "$tty_rc"
|
|
}
|
|
_k3slr_command() {
|
|
if task5b_gate0_route_equals sudo-validate "$@"; then
|
|
(( sudo_dispatches += 1 ))
|
|
return "$sudo_rc"
|
|
fi
|
|
(( unexpected_commands += 1 ))
|
|
return 97
|
|
}
|
|
task5b_action1_debug_observer() {
|
|
local observed_command="${1-}" observed_step="${2-}"
|
|
(( $# == 2 )) || return 1
|
|
case "$observed_command" in
|
|
_k3slr_lifecycle_step\ *)
|
|
case "$observed_step" in
|
|
sudo) (( hook_sudo_entries += 1 )) ;;
|
|
context) (( hook_context_entries += 1 )) ;;
|
|
*) (( hook_suffix_entries += 1 )) ;;
|
|
esac
|
|
;;
|
|
_k3slr_lifecycle_action\ *)
|
|
case "$observed_step" in
|
|
sudo) (( action_sudo_entries += 1 )) ;;
|
|
context) (( action_context_entries += 1 )) ;;
|
|
*) (( action_suffix_entries += 1 )) ;;
|
|
esac
|
|
;;
|
|
esac
|
|
}
|
|
task5b_action1_main_probe_payload() {
|
|
local probe_rc=0 tty_calls=0 sudo_dispatches=0 unexpected_commands=0
|
|
local provider_calls=0 hook_sudo_entries=0 hook_context_entries=0
|
|
local hook_suffix_entries=0 action_sudo_entries=0
|
|
local action_context_entries=0 action_suffix_entries=0
|
|
(( $# == 0 )) || return 1
|
|
_k3slr_lifecycle_main prepare execute || probe_rc=$?
|
|
task5b_production_probe_emit_ok action1_main "$probe_rc" "$tty_calls" \
|
|
"$sudo_dispatches" "$unexpected_commands" "$provider_calls" \
|
|
"$hook_sudo_entries" "$hook_context_entries" "$hook_suffix_entries" \
|
|
"$action_sudo_entries" "$action_context_entries" \
|
|
"$action_suffix_entries"
|
|
}
|
|
task5b_action1_alternate_probe_payload() {
|
|
local probe_rc=0 tty_calls=0 sudo_dispatches=0 unexpected_commands=0
|
|
local provider_calls=0 hook_sudo_entries=0 hook_context_entries=0
|
|
local hook_suffix_entries=0 action_sudo_entries=0
|
|
local action_context_entries=0 action_suffix_entries=0
|
|
(( $# == 0 )) || return 1
|
|
_k3slr_lifecycle_action prepare sudo task5b_alternate_state || probe_rc=$?
|
|
task5b_production_probe_emit_ok action1_alternate "$probe_rc" \
|
|
"$tty_calls" "$sudo_dispatches" "$unexpected_commands" \
|
|
"$provider_calls" "$hook_sudo_entries" "$hook_context_entries" \
|
|
"$hook_suffix_entries" "$action_sudo_entries" \
|
|
"$action_context_entries" "$action_suffix_entries"
|
|
}
|
|
task5b_action1_run_probe() {
|
|
local probe_kind="${1-}" probe_profile=''
|
|
local guarded_rc=0
|
|
(( $# == 1 )) || return 1
|
|
case "$probe_kind" in
|
|
main) probe_profile=action1_main ;;
|
|
alternate) probe_profile=action1_alternate ;;
|
|
*) return 1 ;;
|
|
esac
|
|
case "$probe_kind" in
|
|
main)
|
|
probe_record="$(
|
|
local task5b_probe_profile=''
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm action1_main || return 1
|
|
task5b_action1_main_probe_payload
|
|
)" || guarded_rc=$?
|
|
;;
|
|
alternate)
|
|
probe_record="$(
|
|
local task5b_probe_profile=''
|
|
exec 9>&1
|
|
exec 2>&1
|
|
task5b_production_probe_arm action1_alternate || return 1
|
|
task5b_action1_alternate_probe_payload
|
|
)" || guarded_rc=$?
|
|
;;
|
|
esac
|
|
if (( guarded_rc != 0 )); then
|
|
return "$guarded_rc"
|
|
fi
|
|
record_tag=''
|
|
record_extra=''
|
|
IFS='|' read -r record_tag record_profile action_rc tty_calls \
|
|
sudo_dispatches unexpected_commands provider_calls hook_sudo_entries \
|
|
hook_context_entries hook_suffix_entries action_sudo_entries \
|
|
action_context_entries action_suffix_entries record_extra \
|
|
<<<"$probe_record" || return 1
|
|
[[ "$record_tag" == TASK5B_PROBE_OK &&
|
|
"$record_profile" == "$probe_profile" && -z "$record_extra" ]] ||
|
|
fail "Action 1 $probe_kind model keeps combined stdout and stderr empty"
|
|
actual_exec=0
|
|
}
|
|
task5b_action1_run_main() {
|
|
task5b_action1_run_probe main || return 1
|
|
main_rc="$action_rc"
|
|
}
|
|
|
|
task5b_production_probe_adversary_matrix || {
|
|
printf 'TEST FAILURE: Action 1 production-probe adversary matrix failed\n' >&2
|
|
return 1
|
|
}
|
|
task5b_production_probe_static_graph_is_closed action1_main || {
|
|
printf 'TEST FAILURE: Action 1 main exact graph scan failed\n' >&2
|
|
return 1
|
|
}
|
|
task5b_production_probe_static_graph_is_closed action1_alternate || {
|
|
printf 'TEST FAILURE: Action 1 alternate exact graph scan failed\n' >&2
|
|
return 1
|
|
}
|
|
|
|
task5b_action1_run_main || {
|
|
printf 'TEST FAILURE: Action 1 main guarded probe did not emit an OK record\n' >&2
|
|
return 1
|
|
}
|
|
(( main_rc != 0 )) || fail 'Action 1 success model stops at pending context'
|
|
assert_eq 1 "$tty_calls" 'Action 1 full main reaches the low TTY seam once'
|
|
(( action_sudo_entries > 0 )) ||
|
|
fail 'Action 1 full main reaches the production prepare/sudo handler'
|
|
assert_eq 1 "$sudo_dispatches" \
|
|
'Action 1 production handler dispatches exact sudo validation once'
|
|
(( hook_sudo_entries > 0 )) ||
|
|
fail 'Action 1 success model records the sudo lifecycle hook entry'
|
|
(( hook_context_entries > 0 )) ||
|
|
fail 'Action 1 success model reaches the pending context hook entry'
|
|
(( action_context_entries > 0 )) ||
|
|
fail 'Action 1 success model reaches the pending context action entry'
|
|
assert_eq 0 "$hook_suffix_entries" \
|
|
'Action 1 success model reaches no hook after pending context'
|
|
assert_eq 0 "$action_suffix_entries" \
|
|
'Action 1 success model reaches no action after pending context'
|
|
assert_eq 0 "$unexpected_commands" \
|
|
'Action 1 success model dispatches no command after sudo validation'
|
|
assert_eq 0 "$actual_exec" 'Action 1 success model executes no real command'
|
|
assert_eq 0 "$provider_calls" 'Action 1 success model does not call the provider'
|
|
|
|
command_body="$(declare -f _k3slr_command)" || return 1
|
|
assert_succeeds task5b_gate0_static_body_is_closed "$command_body"
|
|
assert_succeeds task5b_action1_nonfocused_direct_sudo_paths_are_fused
|
|
|
|
tty_calls=0
|
|
tty_rc=71
|
|
sudo_dispatches=0
|
|
sudo_rc=0
|
|
unexpected_commands=0
|
|
provider_calls=0
|
|
hook_sudo_entries=0
|
|
hook_context_entries=0
|
|
hook_suffix_entries=0
|
|
action_sudo_entries=0
|
|
action_context_entries=0
|
|
action_suffix_entries=0
|
|
task5b_action1_run_main
|
|
(( main_rc != 0 )) || fail 'Action 1 rejects a failed low TTY capability'
|
|
assert_eq 1 "$tty_calls" 'Action 1 TTY failure calls the low seam once'
|
|
assert_eq 0 "$hook_sudo_entries" 'Action 1 TTY failure reaches no dispatcher hook'
|
|
assert_eq 0 "$action_sudo_entries" 'Action 1 TTY failure reaches no action'
|
|
assert_eq 0 "$sudo_dispatches" 'Action 1 TTY failure dispatches no sudo command'
|
|
assert_eq 0 "$unexpected_commands" 'Action 1 TTY failure dispatches no command'
|
|
assert_eq 0 "$actual_exec" 'Action 1 TTY failure executes no real command'
|
|
assert_eq 0 "$provider_calls" 'Action 1 TTY failure calls no provider'
|
|
|
|
tty_calls=0
|
|
tty_rc=0
|
|
sudo_dispatches=0
|
|
sudo_rc=97
|
|
unexpected_commands=0
|
|
provider_calls=0
|
|
hook_sudo_entries=0
|
|
hook_context_entries=0
|
|
hook_suffix_entries=0
|
|
action_sudo_entries=0
|
|
action_context_entries=0
|
|
action_suffix_entries=0
|
|
task5b_action1_run_main
|
|
(( main_rc != 0 )) || fail 'Action 1 propagates modeled sudo failure'
|
|
assert_eq 1 "$tty_calls" 'Action 1 sudo failure calls the low TTY seam once'
|
|
(( hook_sudo_entries > 0 )) ||
|
|
fail 'Action 1 sudo failure reaches the sudo hook entry'
|
|
(( action_sudo_entries > 0 )) ||
|
|
fail 'Action 1 sudo failure reaches the production sudo action entry'
|
|
assert_eq 1 "$sudo_dispatches" \
|
|
'Action 1 sudo failure reaches the exact sudo model once'
|
|
assert_eq 0 "$hook_context_entries" \
|
|
'Action 1 sudo failure reaches no context hook'
|
|
assert_eq 0 "$action_context_entries" \
|
|
'Action 1 sudo failure reaches no context action'
|
|
assert_eq 0 "$hook_suffix_entries" 'Action 1 sudo failure reaches no suffix hook'
|
|
assert_eq 0 "$action_suffix_entries" \
|
|
'Action 1 sudo failure reaches no suffix action'
|
|
assert_eq 0 "$unexpected_commands" \
|
|
'Action 1 sudo failure dispatches no unexpected command'
|
|
assert_eq 0 "$actual_exec" 'Action 1 sudo failure executes no real command'
|
|
assert_eq 0 "$provider_calls" 'Action 1 sudo failure calls no provider'
|
|
|
|
task5b_action1_run_probe alternate
|
|
assert_eq 1 "$action_rc" 'Action 1 rejects an alternate exact-three state name'
|
|
assert_eq 0 "$sudo_dispatches" \
|
|
'Action 1 alternate state name dispatches no sudo command'
|
|
assert_eq 0 "$unexpected_commands" \
|
|
'Action 1 alternate state name dispatches no unexpected command'
|
|
assert_eq 0 "$actual_exec" 'Action 1 alternate state executes no real command'
|
|
assert_eq 0 "$provider_calls" 'Action 1 alternate state calls no provider'
|
|
)
|
|
|
|
task5b_run_focus() {
|
|
local focus="${1-}" expected_record before_record after_record focus_rc=0
|
|
(( $# == 1 )) || return 1
|
|
task5b_expected_safe_live_state_record expected_record || return 1
|
|
task5b_live_state_record before_record || return 1
|
|
assert_eq "$expected_record" "$before_record" \
|
|
"Task 5B $focus pre-snapshot is independently all-zero/all-absent" || return 1
|
|
task5b_gate0_contract || return 1
|
|
case "$focus" in
|
|
gate0)
|
|
task5b_gate0_review_round1_adversarial_assertions || focus_rc=$?
|
|
if (( focus_rc == 0 )); then
|
|
task5b_gate0_review_round2_adversarial_assertions || focus_rc=$?
|
|
fi
|
|
;;
|
|
api_state)
|
|
task5b_api_state_contract || focus_rc=$?
|
|
;;
|
|
tty_seam)
|
|
task5b_tty_seam_contract || focus_rc=$?
|
|
;;
|
|
action1)
|
|
task5b_action1_command_binding_contract || focus_rc=$?
|
|
;;
|
|
*) focus_rc=1 ;;
|
|
esac
|
|
task5b_gate0_contract || return 1
|
|
task5b_live_state_record after_record || return 1
|
|
assert_eq "$expected_record" "$after_record" \
|
|
"Task 5B $focus post-snapshot is independently all-zero/all-absent" || return 1
|
|
assert_eq "$before_record" "$after_record" \
|
|
"Task 5B $focus focus preserves promptless live state/process/temp record" || return 1
|
|
(( focus_rc == 0 )) || return "$focus_rc"
|
|
}
|
|
|
|
if [[ "${TASK5B_FOCUS:-}" == gate0 || "${TASK5B_FOCUS:-}" == api_state ||
|
|
"${TASK5B_FOCUS:-}" == tty_seam || "${TASK5B_FOCUS:-}" == action1 ]]; then
|
|
assert_succeeds task5b_run_focus "$TASK5B_FOCUS"
|
|
elif [[ "${TASK5A_FOCUS:-}" == wrappers ]]; then
|
|
assert_succeeds task5a_wrapper_and_guard_contract
|
|
elif [[ "${TASK5A_FOCUS:-}" == capabilities ]]; then
|
|
assert_succeeds task5a_prepare_provider_capability_gate
|
|
assert_succeeds task5a_open_close_capability_gate
|
|
elif [[ "${TASK5A_FOCUS:-}" == dispatcher ]]; then
|
|
assert_succeeds task5a_lifecycle_dispatch_contract
|
|
assert_succeeds task5a_production_actions_fail_closed
|
|
elif [[ "${TASK5A_FOCUS:-}" == records ]]; then
|
|
assert_succeeds task5a_exact_record_and_pure_parser_contract
|
|
elif [[ "${TASK5A_FOCUS:-}" == prepackage ]]; then
|
|
assert_succeeds task5a_ntfs_capacity_and_prepackage_seams
|
|
elif [[ "${TASK5A_FOCUS:-}" == capture ]]; then
|
|
assert_succeeds task5a_exact_process_capture_contract
|
|
elif [[ "${TASK5A_FOCUS:-}" == prepackage_system ]]; then
|
|
assert_succeeds task5a_prepackage_system_command_matrix
|
|
assert_succeeds task5a_ntfsinfo_pipeline_contract
|
|
elif [[ "${TASK5A_FOCUS:-}" == dryrun ]]; then
|
|
assert_succeeds task5a_dry_run_and_prepackage_action_binding
|
|
elif [[ "${TASK4_REVIEW5_FOCUS:-}" == priority ]]; then
|
|
assert_succeeds task4_review5_pending_priority
|
|
elif [[ "${TASK4_REVIEW5_FOCUS:-}" == feas_cleanup ]]; then
|
|
assert_succeeds task4_review5_feasibility_cleanup_failure_is_not_signal_success
|
|
elif [[ "${TASK4_REVIEW5_FOCUS:-}" == unpinned ]]; then
|
|
assert_succeeds task4_review5_published_unpinned_cleanup
|
|
elif [[ "${TASK4_REVIEW5_FOCUS:-}" == parser ]]; then
|
|
assert_succeeds task4_review5_exact_proc_record_contract
|
|
elif [[ "${TASK4_REVIEW5_FOCUS:-}" == anchor ]]; then
|
|
assert_succeeds task4_review5_default_supervisor_anchor
|
|
assert_succeeds task4_review5_feasibility_default_supervisor_anchor
|
|
elif [[ "${TASK4_REVIEW5_FOCUS:-}" == phase_fixture_cleanup ]]; then
|
|
assert_succeeds task4_review5_phase_fixture_failure_cleanup_contract
|
|
elif [[ "${TASK4_REVIEW4_FOCUS:-}" == phases ]]; then
|
|
assert_succeeds task4_review4_signal_phase_matrix
|
|
elif [[ "${TASK4_REVIEW4_FOCUS:-}" == boundaries ]]; then
|
|
assert_succeeds task4_review4_stale_cached_group_boundary
|
|
assert_succeeds task4_review4_direct_fallback_boundaries
|
|
assert_succeeds task4_review4_term_ignoring_group_escalates
|
|
assert_succeeds task4_review4_wait_clear_is_atomic
|
|
elif [[ "${TASK4_REVIEW4_FOCUS:-}" == stale_group ]]; then
|
|
assert_succeeds task4_review4_stale_cached_group_boundary
|
|
elif [[ "${TASK4_REVIEW3_FOCUS:-}" == signal ]]; then
|
|
assert_succeeds task4_review3_nested_pipeline_signal_cleanup
|
|
elif [[ "${TASK4_REVIEW3_FOCUS:-}" == early_signal ]]; then
|
|
assert_succeeds task4_review3_launch_window_signal_cleanup
|
|
elif [[ "${TASK4_REVIEW2_FOCUS:-}" == critical ]]; then
|
|
assert_succeeds task4_review2_tty_and_bounded_validated_pipe
|
|
elif [[ "${TASK4_REVIEW2_FOCUS:-}" == show_protected ]]; then
|
|
assert_succeeds task4_review2_show_protected_and_readonly
|
|
elif [[ "${TASK4_REVIEW2_FOCUS:-}" == feas_show_protected ]]; then
|
|
assert_succeeds task4_review2_feasibility_show_protected
|
|
elif [[ "${TASK4_REVIEW2_FOCUS:-}" == feas_attachment_listing ]]; then
|
|
assert_succeeds task4_review2_feasibility_attachment_listing
|
|
assert_succeeds task4_review2_attachment_parser_contract
|
|
elif [[ "${TASK4_REVIEW2_FOCUS:-}" == capture ]]; then
|
|
assert_succeeds task4_review2_capture_one_line_exact_record
|
|
elif [[ "${TASK4_REVIEW2_FOCUS:-}" == atomic ]]; then
|
|
assert_succeeds task4_review2_atomic_save_transition
|
|
elif [[ "${TASK4_REVIEW1_FOCUS:-}" == critical ]]; then
|
|
assert_succeeds task4_review1_producer_must_finish_before_consumer
|
|
elif [[ "${TASK4_REVIEW1_FOCUS:-}" == lineage ]]; then
|
|
assert_succeeds task4_review1_preflight_and_lineage_gate
|
|
elif [[ "${TASK4_REVIEW1_FOCUS:-}" == constants_lf ]]; then
|
|
assert_succeeds task4_review1_exact_constants_become_readonly
|
|
assert_succeeds task4_review1_generated_password_exact_lf
|
|
elif [[ "${TASK4_REVIEW1_FOCUS:-}" == signal ]]; then
|
|
assert_succeeds task4_review1_blocking_child_is_reaped
|
|
elif [[ "${TASK4_REVIEW1_FOCUS:-}" == noclobber ]]; then
|
|
assert_succeeds task4_review1_noclobber_lifecycle
|
|
elif [[ "${TASK4_REVIEW1_FOCUS:-}" == parser ]]; then
|
|
assert_succeeds task4_review1_raw_package_record
|
|
elif [[ "${TASK4_REVIEW1_FOCUS:-}" == leak ]]; then
|
|
assert_succeeds task4_review1_two_sentinel_leak_contract
|
|
else
|
|
assert_succeeds task5a_wrapper_and_guard_contract
|
|
assert_succeeds task5a_prepare_provider_capability_gate
|
|
assert_succeeds task5a_open_close_capability_gate
|
|
assert_succeeds task5a_lifecycle_dispatch_contract
|
|
assert_succeeds task5a_production_actions_fail_closed
|
|
assert_succeeds task5a_exact_record_and_pure_parser_contract
|
|
assert_succeeds task5a_ntfs_capacity_and_prepackage_seams
|
|
assert_succeeds task5a_exact_process_capture_contract
|
|
assert_succeeds task5a_prepackage_system_command_matrix
|
|
assert_succeeds task5a_ntfsinfo_pipeline_contract
|
|
assert_succeeds task5a_dry_run_and_prepackage_action_binding
|
|
assert_succeeds task4_pipeline_contract
|
|
assert_succeeds task4_tty_and_secret_api_contract
|
|
assert_succeeds task4_feasibility_cleanup_and_leak_contract
|
|
assert_succeeds task4_review1_producer_must_finish_before_consumer
|
|
assert_succeeds task4_review1_preflight_and_lineage_gate
|
|
assert_succeeds task4_review1_exact_constants_become_readonly
|
|
assert_succeeds task4_review1_generated_password_exact_lf
|
|
assert_succeeds task4_review1_blocking_child_is_reaped
|
|
assert_succeeds task4_review1_noclobber_lifecycle
|
|
assert_succeeds task4_review1_raw_package_record
|
|
assert_succeeds task4_review1_two_sentinel_leak_contract
|
|
assert_succeeds task4_review2_tty_and_bounded_validated_pipe
|
|
assert_succeeds task4_review2_show_protected_and_readonly
|
|
assert_succeeds task4_review2_feasibility_show_protected
|
|
assert_succeeds task4_review2_feasibility_attachment_listing
|
|
assert_succeeds task4_review2_attachment_parser_contract
|
|
assert_succeeds task4_review2_capture_one_line_exact_record
|
|
assert_succeeds task4_review2_atomic_save_transition
|
|
assert_succeeds task4_review3_nested_pipeline_signal_cleanup
|
|
assert_succeeds task4_review3_launch_window_signal_cleanup
|
|
assert_succeeds task4_review4_stale_cached_group_boundary
|
|
assert_succeeds task4_review4_direct_fallback_boundaries
|
|
assert_succeeds task4_review4_term_ignoring_group_escalates
|
|
assert_succeeds task4_review4_wait_clear_is_atomic
|
|
assert_succeeds task4_review4_signal_phase_matrix
|
|
assert_succeeds task4_review5_exact_proc_record_contract
|
|
assert_succeeds task4_review5_default_supervisor_anchor
|
|
assert_succeeds task4_review5_feasibility_default_supervisor_anchor
|
|
assert_succeeds task4_review5_phase_fixture_failure_cleanup_contract
|
|
assert_succeeds task4_review5_published_unpinned_cleanup
|
|
assert_succeeds task4_review5_pending_priority
|
|
assert_succeeds task4_review5_feasibility_cleanup_failure_is_not_signal_success
|
|
fi
|
|
|
|
printf 'PASS: local recovery contract, capacity, and validator fixtures\n'
|