#!/bin/zsh # boot_host_preflight.sh — Diagnose whether the host can launch the signed # vphone-cli binary required for PV=3 virtualization boot/DFU flows. set -euo pipefail SCRIPT_DIR="${0:A:h}" PROJECT_ROOT="${SCRIPT_DIR:h}" ASSERT_BOOTABLE=0 QUIET=0 LESS=0 while [[ $# -gt 0 ]]; do case "$1" in --assert-bootable) ASSERT_BOOTABLE=1 shift ;; --quiet) QUIET=1 shift ;; --less) LESS=1 shift ;; *) echo "Unknown option: $1" >&2 exit 2 ;; esac done cd "$PROJECT_ROOT" # The caller (vphone-cli) passes VPHONE_CLI_BIN = the running binary, so we # check THAT — the bundled .app's Contents/MacOS/vphone-cli, not a dev # .build/release path that doesn't exist inside the bundle. Falls back to the # dev layout for standalone/`make` invocation. RELEASE_BIN="${VPHONE_CLI_BIN:-${PROJECT_ROOT}/.build/release/vphone-cli}" DEBUG_BIN="${PROJECT_ROOT}/.build/debug/vphone-cli" ENTITLEMENTS="${PROJECT_ROOT}/sources/vphone.entitlements" TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/vphone-preflight.XXXXXX")" TMP_SIGNED_DEBUG="${TMP_DIR}/vphone-cli.debug.signed" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT print_section() { (( QUIET == 0 )) || return 0 echo "" echo "=== $1 ===" } run_capture() { local label="$1" shift local log_file="${TMP_DIR}/${label}.log" local rc=0 "$@" >"$log_file" 2>&1 || rc=$? (( QUIET == 0 )) && echo "[${label}] exit=${rc}" if (( QUIET == 0 )) && [[ -s "$log_file" ]]; then sed -n '1,40p' "$log_file" fi return "$rc" } MODEL_NAME="$(system_profiler SPHardwareDataType 2>/dev/null | awk -F': ' '/Model Name/ {print $2; exit}')" HV_VMM_PRESENT="$(sysctl -n kern.hv_vmm_present 2>/dev/null || true)" SIP_STATUS="$(csrutil status)" RESEARCH_GUEST_STATUS="$( # First pass with EOF stdin — captures the menu without blocking _out=$(csrutil allow-research-guests status /dev/null || true) if ! echo "$_out" | grep -q "Pick a macOS installation"; then # Single install or already got a direct answer echo "$_out" else # Multiple installs: try to auto-select "Macintosh HD" _num=$(echo "$_out" | awk '/Macintosh HD/ { match($0, /[0-9]+/); if (RSTART > 0) { print substr($0, RSTART, RLENGTH); exit } }') if [[ -n "$_num" ]]; then _result=$(printf '%s\n' "$_num" | csrutil allow-research-guests status 2>/dev/null \ | grep -o 'Allow Research Guests status:.*' || true) if [[ -n "$_result" ]]; then echo "(auto-selected: Macintosh HD) $_result" exit 0 fi fi # No Macintosh HD found, or auto-select failed — prompt interactively # only when a controlling TTY exists. Non-interactive runs should degrade # cleanly instead of failing on /dev/tty access under set -e. if { : >/dev/tty; } 2>/dev/null; then printf '%s\n' "$_out" >/dev/tty printf 'Pick a macOS installation: ' >/dev/tty read _choice /dev/null \ | grep -o 'Allow Research Guests status:.*' || echo 'unavailable' else echo 'unavailable' fi else echo 'unavailable (multiple installs; no interactive tty)' fi fi )" CURRENT_BOOT_ARGS="$(sysctl -n kern.bootargs 2>/dev/null || true)" NEXT_BOOT_ARGS="$({ nvram boot-args 2>/dev/null || true; } | sed 's/^boot-args[[:space:]]*//')" ASSESSMENT_STATUS="$(spctl --status 2>/dev/null || true)" print_section "Host" sw_vers echo "model: $MODEL_NAME" echo "kern.hv_vmm_present: $HV_VMM_PRESENT" echo "SIP: $SIP_STATUS" echo "allow-research-guests: $RESEARCH_GUEST_STATUS" echo "current kern.bootargs: $CURRENT_BOOT_ARGS" echo "next-boot nvram boot-args: $NEXT_BOOT_ARGS" echo "assessment: $ASSESSMENT_STATUS" if (( ASSERT_BOOTABLE == 1 )); then if [[ "$HV_VMM_PRESENT" == "1" ]] || [[ "$MODEL_NAME" == "Apple Virtual Machine 1" ]]; then (( QUIET == 0 )) && { echo "" echo "Error: nested Apple VM host detected; Virtualization.framework guest boot is unavailable here." >&2 } exit 3 fi fi print_section "Entitlements" if [[ -f "$RELEASE_BIN" ]]; then codesign -d --entitlements :- "$RELEASE_BIN" 2>/dev/null || true else echo "missing release binary: $RELEASE_BIN" fi print_section "Policy" if [[ -f "$RELEASE_BIN" ]]; then spctl --assess --type execute --verbose=4 "$RELEASE_BIN" 2>&1 || true fi print_section "Unsigned Debug Binary" DEBUG_HELP_RC=0 if [[ ! -f "$DEBUG_BIN" ]]; then echo "(skipped — debug binary not built; run 'make patcher_build' to include)" else set +e run_capture "debug_help" "$DEBUG_BIN" --help DEBUG_HELP_RC=$? set -e fi print_section "Signed Release Binary" if [[ ! -f "$RELEASE_BIN" ]]; then echo "missing release binary: $RELEASE_BIN" exit 1 fi set +e run_capture "release_help" "$RELEASE_BIN" --help RELEASE_HELP_RC=$? set -e print_section "Signed Debug Control" SIGNED_DEBUG_HELP_RC=0 if [[ ! -f "$DEBUG_BIN" ]]; then echo "(skipped — debug binary not built)" else cp "$DEBUG_BIN" "$TMP_SIGNED_DEBUG" codesign --force --sign - --entitlements "$ENTITLEMENTS" "$TMP_SIGNED_DEBUG" >/dev/null set +e run_capture "signed_debug_help" "$TMP_SIGNED_DEBUG" --help SIGNED_DEBUG_HELP_RC=$? set -e fi print_section "Result" echo "If unsigned debug runs but either signed binary exits 137 / signal 9," echo "the host is not currently permitting the required private virtualization entitlements." echo "If the signed release binary exits 0 but the signed debug control still exits 137," echo "a path/CDHash-scoped amfidont bypass may already be active for this repo." echo "Typical requirements for this project are:" echo " 1. macOS 15+ with PV=3 support" echo " 2. Host hardware must expose Virtualization.framework VM support (not a nested VM without virtualization availability)" echo " 3. SIP disabled" echo " 4. allow-research-guests enabled in Recovery OS" echo " 5. AMFI / execution policy state that permits the private entitlements" echo " 6. Gatekeeper / assessment configured so the signed binary is launchable" if (( ASSERT_BOOTABLE == 1 )); then if (( RELEASE_HELP_RC != 0 )); then (( QUIET == 0 )) && { echo "" echo "Error: signed release vphone-cli is not launchable on this host (exit $RELEASE_HELP_RC)." >&2 } exit "$RELEASE_HELP_RC" fi fi