forked from Manuel/meeting-assistant
389 lines
17 KiB
Bash
389 lines
17 KiB
Bash
#!/bin/bash
|
|
# Existing macOS Recovery/launchd runtime hook; never installs or erases anything.
|
|
set -u
|
|
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
|
|
export PATH
|
|
PROOF_TOKEN="@@PROOF_TOKEN@@"
|
|
STATE_DIR="/Volumes/installstate"
|
|
PROOF_LOG="$STATE_DIR/proof.log"
|
|
RESULT="$STATE_DIR/result.json"
|
|
EXPECTED_BYTES=68719476736
|
|
MAX_LOG_BYTES=4194304
|
|
MAX_OUTPUT_BYTES=524288
|
|
TIMER_FIFO="/tmp/native-diagnostic-$PROOF_TOKEN-$$.fifo"
|
|
PENDING_OUTPUTS=()
|
|
ACTIVE_COMMAND=""
|
|
ACTIVE_TIMER=""
|
|
# BEGIN disk IPC diagnostic
|
|
ACTIVE_OBSERVER=""
|
|
# END disk IPC diagnostic
|
|
os_version=""
|
|
architecture=""
|
|
uid=-1
|
|
system_exit=-1
|
|
arbitration_exit=-1
|
|
recovery_exit=-1
|
|
disk_list_exit=-1
|
|
selected_disk=""
|
|
disk_bytes=0
|
|
|
|
count=0
|
|
while [ ! -d "$STATE_DIR" ] && (( count < 120 )); do
|
|
/sbin/mount_9p installstate >/dev/null 2>&1 || :
|
|
count=$((count + 1))
|
|
sleep 1
|
|
done
|
|
[ -d "$STATE_DIR" ] || exit 1
|
|
: > "$PROOF_LOG" || exit 1
|
|
exec 3>> "$PROOF_LOG" || exit 1
|
|
rm -f "$RESULT" "$RESULT.tmp"
|
|
printf '[proof-token] %s\n' "$PROOF_TOKEN" >&3
|
|
|
|
finish() {
|
|
local success="$1" reason="$2"
|
|
flush_outputs || { success=false; reason=diagnostic_log_budget_exceeded; }
|
|
printf '[proof-result] %s: %s\n' "$success" "$reason" >&3
|
|
printf '{"token":"%s","success":%s,"reason":"%s","osVersion":"%s","architecture":"%s","uid":%s,"systemExit":%s,"diskArbitrationExit":%s,"recoveryExit":%s,"diskListExit":%s,"disk":"%s","diskBytes":%s,"readOnly":false}\n' \
|
|
"$PROOF_TOKEN" "$success" "$reason" "$os_version" "$architecture" "$uid" \
|
|
"$system_exit" "$arbitration_exit" "$recovery_exit" "$disk_list_exit" \
|
|
"$selected_disk" "$disk_bytes" > "$RESULT.tmp"
|
|
/bin/mv -f "$RESULT.tmp" "$RESULT" || exit 1
|
|
exec 9>&-
|
|
[ ! -p "$TIMER_FIFO" ] || /bin/rm -f "$TIMER_FIFO"
|
|
# Keep the service alive for the bounded host diagnostic to capture evidence.
|
|
while :; do sleep 60; done
|
|
}
|
|
|
|
init_timer_fifo() {
|
|
# Recovery has Bash 3.2 before any SDK is installed. Its read timeout uses
|
|
# alarm(), avoiding a separate sleep process for every command and grace period.
|
|
[ ! -e "$TIMER_FIFO" ] || exit 1
|
|
/usr/bin/mkfifo -m 600 "$TIMER_FIFO" || exit 1
|
|
exec 9<> "$TIMER_FIFO" || exit 1
|
|
}
|
|
|
|
flush_outputs() {
|
|
(( ${#PENDING_OUTPUTS[@]} > 0 )) || return 0
|
|
local started=$SECONDS sizes="/tmp/native-diagnostic-$$.sizes" proof_size output_size raw_size
|
|
local raw_count=0 raw_valid=1 pending_count=${#PENDING_OUTPUTS[@]}
|
|
local bounded="/tmp/native-diagnostic-$$.flush"
|
|
# One bounded native copy per group, rather than tail/stat startup per command.
|
|
# Keep native byte-oriented copying: Bash 3.2 read -n would read large outputs
|
|
# one byte per system call. Small scalar reads below have a separate tight bound.
|
|
/usr/bin/tail -c "$MAX_OUTPUT_BYTES" "${PENDING_OUTPUTS[@]}" > "$bounded" || return 1
|
|
/usr/bin/stat -f '%z' "$PROOF_LOG" "$bounded" "${PENDING_OUTPUTS[@]}" > "$sizes" || return 1
|
|
{
|
|
IFS= read -r proof_size; IFS= read -r output_size
|
|
while IFS= read -r raw_size; do
|
|
raw_count=$((raw_count + 1))
|
|
[[ "$raw_size" =~ ^[0-9]+$ ]] && (( raw_size <= MAX_OUTPUT_BYTES )) || raw_valid=0
|
|
done
|
|
} < "$sizes"
|
|
PENDING_OUTPUTS=()
|
|
[[ "$proof_size" =~ ^[0-9]+$ && "$output_size" =~ ^[0-9]+$ ]] || return 1
|
|
(( raw_valid == 1 && raw_count == pending_count )) || return 1
|
|
(( proof_size + output_size + 1024 <= MAX_LOG_BYTES )) || return 1
|
|
/bin/cat "$bounded" >&3 || return 1
|
|
printf '\n[proof-flush] outputs-bytes=%s elapsed=%ss\n' "$output_size" "$((SECONDS - started))" >&3
|
|
}
|
|
|
|
read_scalar() {
|
|
local value status
|
|
# All three values are short native machine/uid/version scalars. Reject excess
|
|
# content instead of accepting a truncated first line as a successful gate.
|
|
IFS= read -r -n 65 -d '' value < "$LAST_OUTPUT"; status=$?
|
|
# EOF is mandatory: the byte bound or a NUL delimiter must never hide a suffix.
|
|
(( status == 1 && ${#value} < 65 )) || return 1
|
|
value=${value%$'\n'}
|
|
[[ "$value" != *$'\n'* ]] || return 1
|
|
SCALAR="$value"
|
|
}
|
|
|
|
# BEGIN native SystemVersion plist request helpers
|
|
read_native_system_version() {
|
|
# Recovery has Bash 3.2 before .NET. The numerical candidate is provisional;
|
|
# only the owned host's complete XML/evidence binding can qualify readiness.
|
|
local LC_ALL=C source="/System/Library/CoreServices/SystemVersion.plist"
|
|
local value status owner candidate remainder started=$SECONDS invalid_bytes
|
|
local raw="$STATE_DIR/native-system-version.plist"
|
|
local ready="$STATE_DIR/native-system-version.request"
|
|
NATIVE_VERSION_ERROR=native_system_version_read_failed
|
|
printf '[native-version-start] method=guest-file/host-xml source=%s seconds=%s\n' "$source" "$started" >&3
|
|
IFS= read -r -n 81 -d '' owner < "$STATE_DIR/run.owner"; status=$?
|
|
(( status == 1 && ${#owner} <= 80 )) &&
|
|
[ "$owner" = "$PROOF_TOKEN"$'\n' ] || { NATIVE_VERSION_ERROR=native_system_version_foreign_owner; return 1; }
|
|
[ ! -e "$ready" ] && [ ! -L "$ready" ] &&
|
|
[ ! -e "$raw" ] && [ ! -L "$raw" ] || { NATIVE_VERSION_ERROR=native_system_version_stale_exchange; return 1; }
|
|
[ -f "$source" ] || return 1
|
|
IFS= read -r -n 4097 -d '' value < "$source"; status=$?
|
|
# EOF is mandatory. NUL stops read with status 0; 4097 is the overbound sentinel.
|
|
(( status == 1 && ${#value} > 0 && ${#value} <= 4096 )) || { NATIVE_VERSION_ERROR=native_system_version_invalid_bytes; return 1; }
|
|
invalid_bytes=${value//$'\t'/}
|
|
invalid_bytes=${invalid_bytes//$'\r'/}
|
|
invalid_bytes=${invalid_bytes//$'\n'/}
|
|
[[ "$invalid_bytes" =~ [[:cntrl:]] ]] && { NATIVE_VERSION_ERROR=native_system_version_binary; return 1; }
|
|
printf '%s' "$value" > "$raw" || return 1
|
|
# Publish this final marker only after the complete raw write has closed.
|
|
printf '%s\n%s\n%s\nready:%s\n' "$PROOF_TOKEN" "$source" "${#value}" "$PROOF_TOKEN" > "$ready" || return 1
|
|
printf '[native-version-request] method=guest-file/host-xml source=%s bytes=%s seconds=%s\n' "$source" "${#value}" "$SECONDS" >&3
|
|
# This is a candidate from exactly these bytes, not an XML validity check.
|
|
NATIVE_VERSION_ERROR=native_system_version_candidate_invalid
|
|
[[ "$value" == *'<key>ProductVersion</key>'* ]] || return 1
|
|
remainder=${value#*'<key>ProductVersion</key>'}
|
|
remainder=${remainder#"${remainder%%[![:space:]]*}"}
|
|
[[ "$remainder" == '<string>'* ]] || return 1
|
|
remainder=${remainder#'<string>'}
|
|
candidate=${remainder%%'</string>'*}
|
|
[ "$candidate" != "$remainder" ] && [[ "$candidate" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || return 1
|
|
SCALAR="$candidate"
|
|
NATIVE_VERSION_ERROR=""
|
|
printf '[native-version-candidate] method=guest-file/host-xml source=%s candidate=%s qualified=false elapsed=%ss\n' "$source" "$SCALAR" "$((SECONDS - started))" >&3
|
|
return 0
|
|
}
|
|
# END native SystemVersion plist request helpers
|
|
# BEGIN disk IPC diagnostic
|
|
# Optional observations have their own child/timer ownership. Stack/thread
|
|
# observations target only this probe's live diskutil; none qualifies readiness.
|
|
observe_disk_query() {
|
|
local disk_process="$1" output="$2" observation_child="" observation_timer=""
|
|
local stack_output="$STATE_DIR/diskutil-stack.txt"
|
|
cancel_observation() {
|
|
trap '' TERM INT
|
|
if [ -n "$observation_child" ]; then
|
|
kill -TERM "$observation_child" 2>/dev/null || :
|
|
IFS= read -r -t 2 -u 9 unused || :
|
|
kill -KILL "$observation_child" 2>/dev/null || :
|
|
wait "$observation_child" 2>/dev/null || :
|
|
fi
|
|
[ -z "$observation_timer" ] || { kill -TERM "$observation_timer" 2>/dev/null || :; wait "$observation_timer" 2>/dev/null || :; }
|
|
printf '[disk-observation] stopped after the owned disk query\n' >> "$output"
|
|
exit 143
|
|
}
|
|
observe_command() {
|
|
local name="$1" status started=$SECONDS
|
|
shift
|
|
printf '\n[disk-observation-command] %s:' "$name" >> "$output"
|
|
printf ' %s' "$@" >> "$output"
|
|
printf '\n' >> "$output"
|
|
"$@" >> "$output" 2>&1 &
|
|
observation_child=$!
|
|
(
|
|
trap 'exit 0' TERM INT
|
|
IFS= read -r -t 60 -u 9 unused || :
|
|
printf '[disk-observation-timeout] %s child=%s limit=60s\n' "$name" "$observation_child" >> "$output"
|
|
kill -TERM "$observation_child" 2>/dev/null || :
|
|
IFS= read -r -t 2 -u 9 unused || :
|
|
kill -KILL "$observation_child" 2>/dev/null || :
|
|
) &
|
|
observation_timer=$!
|
|
wait "$observation_child"; status=$?
|
|
kill -TERM "$observation_timer" 2>/dev/null || :
|
|
wait "$observation_timer" 2>/dev/null || :
|
|
printf '[disk-observation-exit] %s status=%s elapsed=%ss\n' "$name" "$status" "$((SECONDS - started))" >> "$output"
|
|
OBSERVATION_EXIT="$status"
|
|
observation_child=""; observation_timer=""
|
|
}
|
|
observe_live_command() {
|
|
local name="$1"
|
|
shift
|
|
if ! kill -0 "$disk_process" 2>/dev/null; then
|
|
printf '[disk-observation-unavailable] %s: owned diskutil already exited\n' "$name" >> "$output"
|
|
elif [ ! -x "$1" ]; then
|
|
printf '[disk-observation-unavailable] %s: %s is unavailable\n' "$name" "$1" >> "$output"
|
|
else
|
|
observe_command "$name" "$@"
|
|
fi
|
|
}
|
|
trap cancel_observation TERM INT
|
|
printf '[disk-observation] owned-diskutil-child=%s parent-shell=%s\n' "$disk_process" "$$" >> "$output"
|
|
observe_live_command diskutil-threads-before /bin/ps -M -p "$disk_process"
|
|
if [ ! -x /usr/bin/sample ]; then
|
|
printf '[disk-observation-unavailable] diskutil-stack: /usr/bin/sample is unavailable\n' >> "$output"
|
|
elif ! kill -0 "$disk_process" 2>/dev/null; then
|
|
printf '[disk-observation-unavailable] diskutil-stack: owned diskutil already exited\n' >> "$output"
|
|
elif [ -e "$stack_output" ] || [ -L "$stack_output" ]; then
|
|
printf '[disk-observation-unavailable] diskutil-stack: output already exists\n' >> "$output"
|
|
elif : > "$stack_output"; then
|
|
printf '[disk-stack-attempt] owned-diskutil-child=%s duration=1s interval=100ms limit=60s output=%s observation-only=true\n' "$disk_process" "$stack_output" >> "$output"
|
|
observe_command diskutil-stack /usr/bin/sample "$disk_process" 1 100 -mayDie -file "$stack_output"
|
|
printf '[disk-stack-result] status=%s observation-only=true; raw report is captured by the Linux host\n' "$OBSERVATION_EXIT" >> "$output"
|
|
else
|
|
printf '[disk-observation-unavailable] diskutil-stack: output cannot be created\n' >> "$output"
|
|
fi
|
|
observe_live_command storagekit-live /bin/launchctl print system/com.apple.storagekitd
|
|
observe_live_command diskutil-threads-after /bin/ps -M -p "$disk_process"
|
|
}
|
|
|
|
stop_disk_observation() {
|
|
[ -n "$ACTIVE_OBSERVER" ] || return 0
|
|
kill -TERM "$ACTIVE_OBSERVER" 2>/dev/null || :
|
|
wait "$ACTIVE_OBSERVER" 2>/dev/null || :
|
|
ACTIVE_OBSERVER=""
|
|
}
|
|
|
|
# END disk IPC diagnostic
|
|
cancel_probe() {
|
|
trap '' TERM INT
|
|
# BEGIN disk IPC diagnostic
|
|
stop_disk_observation
|
|
# END disk IPC diagnostic
|
|
if [ -n "$ACTIVE_COMMAND" ]; then
|
|
kill -TERM "$ACTIVE_COMMAND" 2>/dev/null || :
|
|
IFS= read -r -t 2 -u 9 unused || :
|
|
kill -KILL "$ACTIVE_COMMAND" 2>/dev/null || :
|
|
wait "$ACTIVE_COMMAND" 2>/dev/null || :
|
|
fi
|
|
[ -z "$ACTIVE_TIMER" ] || { kill -TERM "$ACTIVE_TIMER" 2>/dev/null || :; wait "$ACTIVE_TIMER" 2>/dev/null || :; }
|
|
ACTIVE_COMMAND=""; ACTIVE_TIMER=""
|
|
finish false probe_cancelled
|
|
}
|
|
|
|
run_command() {
|
|
local name="$1"
|
|
shift
|
|
local process timer exit_code started waited command_limit=45
|
|
# Run 4161: even native uname/ps startup took 34-42s under TCG.
|
|
# Isolate only the failed UID gate; every other watchdog remains unchanged.
|
|
[[ "$name" != uid ]] || command_limit=180
|
|
# BEGIN disk IPC diagnostic
|
|
if [[ "$name" == disks && "${attempt:-0}" == 1 ]]; then command_limit=600; fi
|
|
# END disk IPC diagnostic
|
|
LAST_OUTPUT="/tmp/native-diagnostic-$name.out"
|
|
printf '\n[proof-command] %s:' "$name" >&3
|
|
printf ' %s' "$@" >&3
|
|
printf '\n' >&3
|
|
started=$SECONDS
|
|
"$@" > "$LAST_OUTPUT" 2>&1 &
|
|
process=$!
|
|
ACTIVE_COMMAND="$process"
|
|
printf '[proof-start] %s child=%s shell=%s parent=%s seconds=%s\n' "$name" "$process" "$$" "$PPID" "$started" >&3
|
|
printf '[proof-limit] %s %ss\n' "$name" "$command_limit" >&3
|
|
(
|
|
trap 'exit 0' TERM INT
|
|
IFS= read -r -t "$command_limit" -u 9 unused || :
|
|
printf '[proof-timeout] %s child=%s elapsed=%ss signal=TERM\n' "$name" "$process" "$((SECONDS - started))" >&3
|
|
kill -TERM "$process" 2>/dev/null || :
|
|
IFS= read -r -t 2 -u 9 unused || :
|
|
kill -KILL "$process" 2>/dev/null || :
|
|
) &
|
|
timer=$!
|
|
ACTIVE_TIMER="$timer"
|
|
# BEGIN disk IPC diagnostic
|
|
if [[ "$name" == disks && "$attempt" == 1 ]]; then
|
|
local observation_output="/tmp/native-diagnostic-disk-observation.out"
|
|
: > "$observation_output"
|
|
observe_disk_query "$process" "$observation_output" &
|
|
ACTIVE_OBSERVER=$!
|
|
printf '[disk-observation-start] observer=%s owned-diskutil-child=%s\n' "$ACTIVE_OBSERVER" "$process" >&3
|
|
PENDING_OUTPUTS+=("$observation_output")
|
|
fi
|
|
# END disk IPC diagnostic
|
|
wait "$process"
|
|
exit_code=$?
|
|
waited=$SECONDS
|
|
# Includes fork/exec/wait, but excludes timer cleanup and evidence copying.
|
|
printf '[proof-native-wait] %s child=%s elapsed=%ss exit=%s\n' "$name" "$process" "$((waited - started))" "$exit_code" >&3
|
|
kill -TERM "$timer" 2>/dev/null || :
|
|
wait "$timer" 2>/dev/null || :
|
|
# BEGIN disk IPC diagnostic
|
|
stop_disk_observation
|
|
# END disk IPC diagnostic
|
|
ACTIVE_COMMAND=""; ACTIVE_TIMER=""
|
|
printf '[proof-cleanup] %s child=%s elapsed=%ss total=%ss\n' "$name" "$process" "$((SECONDS - waited))" "$((SECONDS - started))" >&3
|
|
printf '[proof-exit] %s\n' "$exit_code" >&3
|
|
LAST_EXIT="$exit_code"
|
|
PENDING_OUTPUTS+=("$LAST_OUTPUT")
|
|
return 0
|
|
}
|
|
|
|
diagnose_failure() {
|
|
run_command kernel /usr/bin/uname -a
|
|
run_command account /usr/bin/id
|
|
run_command context /usr/sbin/sysctl kern.bootargs machdep.cpu.brand_string machdep.cpu.features machdep.cpu.leaf7_features
|
|
run_command parent /bin/ps -p "$$" -p "$PPID" -o pid=,ppid=,comm=
|
|
run_command processes /bin/ps -axo pid,ppid,comm
|
|
}
|
|
|
|
fail_probe() {
|
|
local reason="$1"
|
|
flush_outputs || finish false diagnostic_log_budget_exceeded
|
|
diagnose_failure
|
|
finish false "$reason"
|
|
}
|
|
|
|
init_timer_fifo
|
|
trap cancel_probe TERM INT
|
|
|
|
# Test the required native gates before optional process/CPU diagnostics.
|
|
run_command architecture /usr/bin/uname -m
|
|
(( LAST_EXIT == 0 )) || fail_probe architecture_probe_failed
|
|
read_scalar || fail_probe architecture_output_invalid
|
|
architecture="$SCALAR"
|
|
[ "$architecture" = x86_64 ] || fail_probe unexpected_guest_architecture
|
|
run_command uid /usr/bin/id -u
|
|
(( LAST_EXIT == 0 )) || fail_probe uid_probe_failed
|
|
read_scalar || fail_probe uid_output_invalid
|
|
uid="$SCALAR"
|
|
[ "$uid" = 0 ] || fail_probe recovery_account_not_root
|
|
# BEGIN native SystemVersion plist getter
|
|
read_native_system_version || fail_probe "$NATIVE_VERSION_ERROR"
|
|
# END native SystemVersion plist getter
|
|
os_version="$SCALAR"
|
|
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
|
|
(( ${os_version%%.*} >= 14 )) || fail_probe unsupported_macos_version
|
|
flush_outputs || finish false diagnostic_log_budget_exceeded
|
|
|
|
# BEGIN disk IPC diagnostic
|
|
printf '[disk-diagnostic-runtime] bash=%s stack-observation-only=true\n' "$BASH_VERSION" >&3
|
|
run_command arbitration_before /bin/launchctl print system/com.apple.diskarbitrationd
|
|
run_command management_before /bin/launchctl print system/com.apple.storagekitd
|
|
run_command media_before /usr/sbin/ioreg -r -c IOMedia -l -w 0
|
|
flush_outputs || finish false diagnostic_log_budget_exceeded
|
|
|
|
# END disk IPC diagnostic
|
|
# Bound readiness independently of the host's 40-minute overall deadline.
|
|
readiness_start=$SECONDS
|
|
attempt=0
|
|
while (( attempt < 1 && SECONDS - readiness_start < 600 )); do
|
|
attempt=$((attempt + 1))
|
|
printf '\n[readiness-attempt] %s\n' "$attempt" >&3
|
|
run_command disks /usr/sbin/diskutil list physical
|
|
disk_list_exit="$LAST_EXIT"
|
|
if (( disk_list_exit == 0 )); then
|
|
disk_list=$(cat "$LAST_OUTPUT")
|
|
candidates=0
|
|
while IFS= read -r disk; do
|
|
[ -n "$disk" ] || continue
|
|
run_command "info-$disk" /usr/sbin/diskutil info "/dev/$disk"
|
|
(( LAST_EXIT == 0 )) || continue
|
|
info=$(cat "$LAST_OUTPUT")
|
|
if printf '%s\n' "$info" | grep -Eq '^[[:space:]]*(Read-Only (Media|Device)|(Media|Device) Read-Only):[[:space:]]*Yes'; then
|
|
continue
|
|
fi
|
|
printf '%s\n' "$info" | grep -Eq '^[[:space:]]*(Read-Only (Media|Device)|(Media|Device) Read-Only):[[:space:]]*No' || continue
|
|
size=$(printf '%s\n' "$info" | sed -nE 's/^[[:space:]]*Disk Size:.*\(([0-9]+) Bytes\).*/\1/p' | head -n 1)
|
|
[[ "$size" =~ ^[0-9]+$ ]] || continue
|
|
(( size == EXPECTED_BYTES )) || continue
|
|
candidates=$((candidates + 1))
|
|
selected_disk="/dev/$disk"
|
|
disk_bytes="$size"
|
|
printf '[writable-target] %s %s bytes\n' "$selected_disk" "$disk_bytes" >&3
|
|
done < <(printf '%s\n' "$disk_list" | sed -nE 's#^/dev/(disk[0-9]+).*#\1#p')
|
|
(( candidates <= 1 )) || fail_probe ambiguous_writable_64g_disks
|
|
if (( candidates == 1 )); then
|
|
# Re-probe live launchd domains after disk readiness, preserving native exits.
|
|
run_command system_ready /bin/launchctl print system
|
|
system_exit="$LAST_EXIT"
|
|
run_command arbitration_ready /bin/launchctl print system/com.apple.diskarbitrationd
|
|
arbitration_exit="$LAST_EXIT"
|
|
run_command recovery_ready /bin/launchctl print system/com.apple.recoveryosd
|
|
recovery_exit="$LAST_EXIT"
|
|
(( system_exit == 0 && arbitration_exit == 0 && recovery_exit == 0 )) || fail_probe service_domain_not_ready
|
|
finish true native_recovery_and_writable_64g_disk_ready
|
|
fi
|
|
fi
|
|
flush_outputs || finish false diagnostic_log_budget_exceeded
|
|
IFS= read -r -t 5 -u 9 unused || :
|
|
done
|
|
fail_probe disk_management_or_writable_target_not_ready
|