Add kernel patch testing scripts and targets

Introduce a set of tools for fast and batch testing of kernel JB patches: add testing_batch.sh, testing_do_patch.sh, testing_do_save.sh and testing_kernel_patch.py (plus a results _summary). Update the Makefile with new targets (testing_do_save, testing_kernel_patch, testing_do_patch, testing_batch) and PHONY entries to wire them into the build/test workflow. Modify kernel_jb.py to enable the amfi cdhash-in-trustcache patch (A1) and mark the amfi execve kill-path patch as causing PANIC (A2). These changes allow restoring a base kernel backup, applying single patches quickly, running per-patch DFU boots (with timeout/panic detection), and collecting logs for batch runs.
This commit is contained in:
Lakr
2026-03-04 19:51:48 +08:00
parent 9bb53b7368
commit ae599d5b3b
7 changed files with 470 additions and 6 deletions
+1
View File
@@ -321,3 +321,4 @@ TODO.md
scripts/vphoned/vphoned
sources/vphone-cli/VPhoneBuildInfo.swift
setup_logs/
/testing_results
+16 -1
View File
@@ -73,6 +73,9 @@ help:
@echo " make ramdisk_send Send ramdisk to device"
@echo " make testing_ramdisk_build Build boot chain only (no SSH, no CFW)"
@echo " make testing_ramdisk_send Send testing boot chain to device"
@echo " make testing_do_save Full pipeline + save base kernel backup"
@echo " make testing_do_patch PATCH=name Test single JB kernel patch (fast)"
@echo " make testing_kernel_patch PATCH=name Restore+patch kernel only (no boot)"
@echo ""
@echo "CFW:"
@echo " make cfw_install Install CFW mods via SSH"
@@ -219,7 +222,7 @@ restore:
# Ramdisk
# ═══════════════════════════════════════════════════════════════════
.PHONY: ramdisk_build ramdisk_send testing_ramdisk_build testing_ramdisk_send
.PHONY: ramdisk_build ramdisk_send testing_ramdisk_build testing_ramdisk_send testing_do testing_do_save testing_kernel_patch testing_do_patch
ramdisk_build:
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/ramdisk_build.py" .
@@ -236,6 +239,18 @@ testing_ramdisk_send:
testing_do:
zsh "$(CURDIR)/$(SCRIPTS)/testing_do.sh"
testing_do_save:
zsh "$(CURDIR)/$(SCRIPTS)/testing_do_save.sh"
testing_kernel_patch:
cd $(VM_DIR) && $(PYTHON) "$(CURDIR)/$(SCRIPTS)/testing_kernel_patch.py" . $(PATCH)
testing_do_patch:
zsh "$(CURDIR)/$(SCRIPTS)/testing_do_patch.sh" $(PATCH)
testing_batch:
zsh "$(CURDIR)/$(SCRIPTS)/testing_batch.sh" $(PATCHES)
# ═══════════════════════════════════════════════════════════════════
# CFW
# ═══════════════════════════════════════════════════════════════════
+6 -4
View File
@@ -57,11 +57,13 @@ class KernelJBPatcher(
def find_all(self):
self.patches = []
# Commented patches are broken to boot into panic.
# Group A: Existing patches
# self.patch_amfi_cdhash_in_trustcache() # A1
# self.patch_amfi_execve_kill_path() # A2
# self.patch_task_conversion_eval_internal() # A3
# self.patch_sandbox_hooks_extended() # A4
self.patch_amfi_cdhash_in_trustcache() # A1
# self.patch_amfi_execve_kill_path() # A2 (PANIC)
self.patch_task_conversion_eval_internal() # A3
self.patch_sandbox_hooks_extended() # A4
# Group B: Simple patches (string-anchored / pattern-matched)
# self.patch_post_validation_additional() # B5
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env zsh
set -euo pipefail
# ═══════════════════════════════════════════════════════════════════
# testing_batch.sh — Batch-test kernel JB patches one at a time.
#
# Prerequisite: run `make testing_do_save` first to create base backup.
#
# For each patch:
# 1. Restore base kernel + apply single patch
# 2. Build ramdisk
# 3. Boot DFU (vphone-cli --no-graphics --dfu)
# 4. Send boot chain via irecovery
# 5. Wait up to 2 minutes, watching for panic
# 6. Save output → testing_results/<patch_name>.log
# 7. Kill everything, move to next patch
#
# Usage:
# ./testing_batch.sh # test all uncommented B/C patches
# ./testing_batch.sh patch_mac_mount patch_dounmount # test specific patches
# ═══════════════════════════════════════════════════════════════════
PROJECT_DIR="$(cd "$(dirname "${0:a:h}")" && pwd)"
cd "$PROJECT_DIR"
VM_DIR="$PROJECT_DIR/${VM_DIR:-vm}"
RESULTS_DIR="$PROJECT_DIR/testing_results"
TIMEOUT_SECS=120 # 2 minutes
BINARY="$PROJECT_DIR/.build/release/vphone-cli"
IRECOVERY="$PROJECT_DIR/.limd/bin/irecovery"
PYTHON="$PROJECT_DIR/.venv/bin/python3"
mkdir -p "$RESULTS_DIR"
# ─── Default patch list (all B + C patches) ────────────────────────
ALL_PATCHES=(
patch_post_validation_additional # B5
patch_proc_security_policy # B6
patch_proc_pidinfo # B7
patch_convert_port_to_map # B8
patch_vm_fault_enter_prepare # B9
patch_vm_map_protect # B10
patch_mac_mount # B11
patch_dounmount # B12
patch_bsd_init_auth # B13
patch_spawn_validate_persona # B14
patch_task_for_pid # B15
patch_load_dylinker # B16
patch_shared_region_map # B17
patch_nvram_verify_permission # B18
patch_io_secure_bsd_root # B19
patch_thid_should_crash # B20
patch_cred_label_update_execve # C21
patch_syscallmask_apply_to_proc # C22
patch_hook_cred_label_update_execve # C23
patch_kcall10 # C24
)
# Use args if provided, otherwise test all
if (( $# > 0 )); then
PATCHES=("$@")
else
PATCHES=("${ALL_PATCHES[@]}")
fi
echo "═══════════════════════════════════════════════════════════════"
echo " Batch kernel JB patch tester"
echo " Testing ${#PATCHES[@]} patch(es), ${TIMEOUT_SECS}s timeout each"
echo " Results → $RESULTS_DIR/"
echo "═══════════════════════════════════════════════════════════════"
echo ""
# ─── Verify backup exists ──────────────────────────────────────────
RESTORE_DIR=$(find "$VM_DIR" -maxdepth 1 -type d -name '*Restore*' | head -1)
KERNEL_BACKUP=$(find "$RESTORE_DIR" -name 'kernelcache.research.vphone600.base_backup' 2>/dev/null | head -1)
if [[ -z "$KERNEL_BACKUP" ]]; then
echo "[-] No kernel backup found. Run 'make testing_do_save' first."
exit 1
fi
echo "[*] Kernel backup: $KERNEL_BACKUP"
echo ""
# ─── Summary file ──────────────────────────────────────────────────
SUMMARY="$RESULTS_DIR/_summary.txt"
echo "Batch test run: $(date)" > "$SUMMARY"
echo "Timeout: ${TIMEOUT_SECS}s" >> "$SUMMARY"
echo "─────────────────────────────────────────" >> "$SUMMARY"
# ─── Test each patch ───────────────────────────────────────────────
PASS_COUNT=0
FAIL_COUNT=0
ERROR_COUNT=0
TOTAL=${#PATCHES[@]}
for i in {1..$TOTAL}; do
PATCH="${PATCHES[$i]}"
LOG="$RESULTS_DIR/${PATCH}.log"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " [$i/$TOTAL] Testing: $PATCH"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Clean up any prior vphone-cli
pkill -9 vphone-cli 2>/dev/null || true
sleep 1
# Header in log
{
echo "Patch: $PATCH"
echo "Date: $(date)"
echo "Timeout: ${TIMEOUT_SECS}s"
echo "══════════════════════════════════════════"
} > "$LOG"
# Step 1: Restore kernel + apply patch
echo " [1/4] Patching kernel..."
{
echo ""
echo "── kernel patch ──"
"$PYTHON" "$PROJECT_DIR/scripts/testing_kernel_patch.py" "$VM_DIR" "$PATCH" 2>&1
} >> "$LOG" 2>&1
PATCH_EXIT=$?
if (( PATCH_EXIT != 0 )); then
echo " [-] Patch failed (exit $PATCH_EXIT) — skipping"
echo "$PATCH ERROR patch_failed" >> "$SUMMARY"
ERROR_COUNT=$(( ERROR_COUNT + 1 ))
continue
fi
# Step 2: Build ramdisk
echo " [2/4] Building ramdisk..."
{
echo ""
echo "── ramdisk build ──"
make -C "$PROJECT_DIR" testing_ramdisk_build VM_DIR="$VM_DIR" 2>&1
} >> "$LOG" 2>&1
# Step 3: Boot DFU (capture output)
echo " [3/4] Booting DFU..."
{
echo ""
echo "── boot output ──"
} >> "$LOG"
# Start vphone-cli in background, capturing output
"$BINARY" \
--rom "$VM_DIR/AVPBooter.vresearch1.bin" \
--disk "$VM_DIR/Disk.img" \
--nvram "$VM_DIR/nvram.bin" \
--machine-id "$VM_DIR/machineIdentifier.bin" \
--cpu 8 --memory 8192 \
--sep-rom "$VM_DIR/AVPSEPBooter.vresearch1.bin" \
--sep-storage "$VM_DIR/SEPStorage" \
--no-graphics --dfu \
>> "$LOG" 2>&1 &
VM_PID=$!
# Step 4: Send boot chain
echo " [4/4] Sending boot chain..."
{
echo ""
echo "── ramdisk send ──"
make -C "$PROJECT_DIR" testing_ramdisk_send VM_DIR="$VM_DIR" 2>&1
} >> "$LOG" 2>&1 || true
# ─── Monitor for panic or timeout ──────────────────────────────
echo " [*] Monitoring for ${TIMEOUT_SECS}s (panic or timeout)..."
RESULT="TIMEOUT"
START_TIME=$SECONDS
while (( SECONDS - START_TIME < TIMEOUT_SECS )); do
# Check if VM died
if ! kill -0 "$VM_PID" 2>/dev/null; then
RESULT="VM_DIED"
break
fi
# Check log for panic
if grep -qi 'panic' "$LOG" 2>/dev/null; then
# Give a few more seconds for full panic log to flush
sleep 5
RESULT="PANIC"
break
fi
sleep 3
done
ELAPSED=$(( SECONDS - START_TIME ))
# Kill VM
kill -9 "$VM_PID" 2>/dev/null || true
wait "$VM_PID" 2>/dev/null || true
# Also kill any orphaned vphone-cli
pkill -9 vphone-cli 2>/dev/null || true
# Append result to log
{
echo ""
echo "══════════════════════════════════════════"
echo "RESULT: $RESULT"
echo "ELAPSED: ${ELAPSED}s"
} >> "$LOG"
# Record
case "$RESULT" in
PANIC)
echo " [X] PANIC after ${ELAPSED}s → $LOG"
echo "$PATCH PANIC ${ELAPSED}s" >> "$SUMMARY"
FAIL_COUNT=$(( FAIL_COUNT + 1 ))
;;
VM_DIED)
echo " [X] VM died after ${ELAPSED}s → $LOG"
echo "$PATCH VM_DIED ${ELAPSED}s" >> "$SUMMARY"
FAIL_COUNT=$(( FAIL_COUNT + 1 ))
;;
TIMEOUT)
echo " [+] No panic in ${TIMEOUT_SECS}s (likely OK) → $LOG"
echo "$PATCH OK ${TIMEOUT_SECS}s" >> "$SUMMARY"
PASS_COUNT=$(( PASS_COUNT + 1 ))
;;
esac
echo ""
sleep 2
done
# ─── Final summary ─────────────────────────────────────────────────
{
echo ""
echo "═══════════════════════════════════════════"
echo "TOTAL: $TOTAL PASS: $PASS_COUNT FAIL: $FAIL_COUNT ERROR: $ERROR_COUNT"
} >> "$SUMMARY"
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo " DONE — $TOTAL tested: $PASS_COUNT pass, $FAIL_COUNT fail, $ERROR_COUNT error"
echo " Summary: $SUMMARY"
echo " Logs: $RESULTS_DIR/<patch_name>.log"
echo "═══════════════════════════════════════════════════════════════"
cat "$SUMMARY"
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env zsh
set -euo pipefail
# Fast test a single kernel JB patch.
# Restores base kernel backup, applies one patch, rebuilds ramdisk, boots.
#
# Usage: ./testing_do_patch.sh <patch_name>
# or: make testing_do_patch PATCH=patch_vm_fault_enter_prepare
# ─── Track child PIDs for cleanup ───────────────────────────────────
typeset -a CHILD_PIDS=()
cleanup() {
echo "\n[patch] cleaning up..."
for pid in "${CHILD_PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
echo "[patch] killing PID $pid"
kill -9 "$pid" 2>/dev/null || true
fi
done
exit 0
}
trap cleanup EXIT INT TERM
PROJECT_DIR="$(cd "$(dirname "${0:a:h}")" && pwd)"
cd "$PROJECT_DIR"
PATCH_NAME="${1:-}"
if [[ -z "$PATCH_NAME" ]]; then
echo "Usage: $0 <patch_name>"
echo " e.g. $0 patch_vm_fault_enter_prepare"
exit 1
fi
VM_DIR="${VM_DIR:-vm}"
# ─── Kill existing vphone-cli ──────────────────────────────────────
echo "[patch] killing existing vphone-cli..."
pkill -9 vphone-cli 2>/dev/null || true
sleep 1
# ─── Restore kernel + apply single patch ───────────────────────────
echo "[patch] restoring base kernel + applying: $PATCH_NAME"
make testing_kernel_patch PATCH="$PATCH_NAME"
# ─── Rebuild ramdisk ────────────────────────────────────────────────
echo "[patch] testing_ramdisk_build..."
make testing_ramdisk_build
# ─── Send ramdisk in background ────────────────────────────────────
echo "[patch] testing_ramdisk_send (background)..."
make testing_ramdisk_send &
CHILD_PIDS+=($!)
# ─── Boot DFU ──────────────────────────────────────────────────────
echo "[patch] boot_dfu..."
make boot_dfu &
CHILD_PIDS+=($!)
echo "[patch] waiting for boot_dfu (PID ${CHILD_PIDS[-1]})..."
wait "${CHILD_PIDS[-1]}" 2>/dev/null || true
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env zsh
set -euo pipefail
# Save a base-patched kernel for fast per-patch testing.
# Run once, then use testing_do_patch.sh <patch_name> to test individual patches.
PROJECT_DIR="$(cd "$(dirname "${0:a:h}")" && pwd)"
cd "$PROJECT_DIR"
VM_DIR="${VM_DIR:-vm}"
# ─── Kill existing vphone-cli ──────────────────────────────────────
echo "[save] killing existing vphone-cli..."
pkill -9 vphone-cli 2>/dev/null || true
sleep 1
# ─── Full pipeline with base patches only ──────────────────────────
echo "[save] fw_prepare..."
make fw_prepare
echo "[save] fw_patch_jb..."
make fw_patch_jb
# ─── Find and save kernelcache backup ──────────────────────────────
RESTORE_DIR=$(find "$VM_DIR" -maxdepth 1 -type d -name '*Restore*' | head -1)
KERNEL_PATH=$(find "$RESTORE_DIR" -name 'kernelcache.research.vphone600' | head -1)
if [[ -z "$KERNEL_PATH" ]]; then
echo "[-] kernelcache not found in $RESTORE_DIR"
exit 1
fi
BACKUP_PATH="${KERNEL_PATH}.base_backup"
cp "$KERNEL_PATH" "$BACKUP_PATH"
echo "[save] kernel backup saved: $BACKUP_PATH ($(wc -c < "$BACKUP_PATH") bytes)"
echo "[save] done. Now use: make testing_do_patch PATCH=<name>"
echo ""
echo "Available patch names:"
echo " patch_post_validation_additional (B5)"
echo " patch_proc_security_policy (B6)"
echo " patch_proc_pidinfo (B7)"
echo " patch_convert_port_to_map (B8)"
echo " patch_vm_fault_enter_prepare (B9)"
echo " patch_vm_map_protect (B10)"
echo " patch_mac_mount (B11)"
echo " patch_dounmount (B12)"
echo " patch_bsd_init_auth (B13)"
echo " patch_spawn_validate_persona (B14)"
echo " patch_task_for_pid (B15)"
echo " patch_load_dylinker (B16)"
echo " patch_shared_region_map (B17)"
echo " patch_nvram_verify_permission (B18)"
echo " patch_io_secure_bsd_root (B19)"
echo " patch_thid_should_crash (B20)"
echo " patch_cred_label_update_execve (C21)"
echo " patch_syscallmask_apply_to_proc (C22)"
echo " patch_hook_cred_label_update_execve (C23)"
echo " patch_kcall10 (C24)"
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
testing_kernel_patch.py — Restore base kernel backup and apply a single JB patch.
Usage:
python3 testing_kernel_patch.py <vm_directory> <patch_name> [patch_name2 ...]
Example:
python3 testing_kernel_patch.py vm patch_vm_fault_enter_prepare
python3 testing_kernel_patch.py vm patch_mac_mount patch_dounmount
"""
import os
import shutil
import sys
from fw_patch import find_file, find_restore_dir, load_firmware, save_firmware
from patchers.kernel_jb import KernelJBPatcher
def main():
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <vm_dir> <patch_name> [patch_name2 ...]")
sys.exit(1)
vm_dir = os.path.abspath(sys.argv[1])
patch_names = sys.argv[2:]
restore_dir = find_restore_dir(vm_dir)
if not restore_dir:
print(f"[-] No *Restore* directory found in {vm_dir}")
sys.exit(1)
kernel_path = find_file(restore_dir, ["kernelcache.research.vphone600"], "kernelcache")
backup_path = kernel_path + ".base_backup"
if not os.path.exists(backup_path):
print(f"[-] No backup found: {backup_path}")
print(f" Run 'make testing_do_save' first.")
sys.exit(1)
# Restore from backup
shutil.copy2(backup_path, kernel_path)
print(f"[*] Restored kernel from backup ({os.path.getsize(backup_path)} bytes)")
# Load the kernel
im4p, data, was_im4p, original_raw = load_firmware(kernel_path)
fmt = "IM4P" if was_im4p else "raw"
print(f"[*] Loaded: {fmt}, {len(data)} bytes")
# Create patcher (inherits from KernelJBPatcherBase which inherits from KernelPatcher)
kp = KernelJBPatcher(data)
# Apply each requested patch
applied = 0
for patch_name in patch_names:
method = getattr(kp, patch_name, None)
if method is None:
print(f"[-] Unknown patch: {patch_name}")
print(f" Available patches:")
for name in sorted(dir(kp)):
if name.startswith("patch_") and callable(getattr(kp, name)):
print(f" {name}")
sys.exit(1)
print(f"\n[*] Applying: {patch_name}")
method()
# Apply the collected patches
for off, patch_bytes, _ in kp.patches:
data[off : off + len(patch_bytes)] = patch_bytes
applied += 1
print(f"\n[+] {applied} patch(es) applied from {len(patch_names)} method(s)")
# Save
save_firmware(kernel_path, im4p, data, was_im4p, original_raw)
print(f"[+] Saved: {kernel_path}")
if __name__ == "__main__":
main()