Rework JB finalization: drop dropbear, auto-bootstrap on first boot (#141)

* fix: build

* fix: remove [trusted=yes] from Havoc apt source

The inline [trusted=yes] option can cause issues with Sileo's
source parser. The apt-get calls already use AllowUnauthenticated
flags, making it redundant.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: main actor crash in VPhoneControl + IPA extraction failures

VPhoneControl: pending request handlers are @MainActor-isolated closures
but were called from DispatchQueue.global() in the read loop and timeout
handler, causing dispatch_assert_queue_fail crashes. Wrap all
pending.handler() calls in DispatchQueue.main.async.

unarchive: the recent ARCHIVE_EXTRACT_SECURE_* hardening (ef02d50) broke
IPA extraction on iOS because:
- SECURE_NOABSOLUTEPATHS: we set absolute output paths on entries
- SECURE_SYMLINKS: iOS system paths (/var, /tmp) are symlinks
- archive_write_header failures were silently swallowed due to if/else if
  structure, making extraction report success with no files extracted

Fix by keeping only SECURE_NODOTDOT, resolving symlinks in extraction
path, fixing header error handling, removing unnecessary ACL/FFLAGS
flags, and surfacing libarchive errors in the install response.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* remove dropbear SSH daemon from guest

Drop all dropbear setup: LaunchDaemon plist injection, host key
generation, daemon deployment, and SSH availability messages.
Guest communication is handled by vphoned over vsock.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: liblaunch compat stub + automatic JB first-boot setup

liblaunch_compat.dylib: stub exporting _launch_active_user_switch
(missing from PCC VM's libSystem.B.dylib) so procursus binaries
like launchctl can load. Deployed to /cores/, loaded via
DYLD_INSERT_LIBRARIES in LaunchDaemon environment and JB profile.

vphone_jb_setup.sh: first-boot script replacing the SSH-based
cfw_install_jb_post.sh. Runs as a LaunchDaemon on first normal
boot and performs all JB finalization: /var/jb symlink,
prep_bootstrap, markers, Sileo, apt setup, TrollStore Lite.
Idempotent with done marker. Logs to /var/log/vphone_jb_setup.log.

Removes the cfw_install_jb_finalize make target and the entire
SSH/iproxy/sshpass-based post-boot flow from setup_machine.sh.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* chore: update AGENTS.md firmware table, gitignore build artifacts

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: launchctl wrapper uses absolute path + timeout to prevent hangs

- Use absolute path to launchctl.real instead of relative dirname,
  fixing "not found" when called via /var/jb/bin/launchctl symlink
- Add 5s timeout so launchctl doesn't hang when launchd is
  unresponsive on PCC VMs — always exits 0 for dpkg postinst compat
- Symlink /var/jb/bin/launchctl -> /var/jb/usr/bin/launchctl so both
  paths work (openssh postinst uses the /bin/ path)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: replace liblaunch_compat dylib stub with iosbinpack64 launchctl symlink

Procursus launchctl crashes on PCC VMs due to missing
_launch_active_user_switch symbol. Rather than a custom dylib stub,
simply symlink iosbinpack64's launchctl into /var/jb — it talks to
launchd fine and always exits 0, which is all dpkg scripts need.

- Remove liblaunch_compat.c, its build target, signing, and deployment
- Remove DYLD_INSERT_LIBRARIES from setup script and plist
- Replace launchctl wrapper with symlinks to /iosbinpack64/bin/launchctl
- Both /var/jb/usr/bin/launchctl and /var/jb/bin/launchctl are covered

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Felipe Cavalcanti
2026-03-08 12:54:00 +08:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 5921cba2ba
commit 32b73cd50b
18 changed files with 312 additions and 348 deletions
+1
View File
@@ -319,6 +319,7 @@ __marimo__/
TODO.md
/references/
scripts/vphoned/vphoned
/cfw_input/
sources/vphone-cli/VPhoneBuildInfo.swift
setup_logs/
/testing_results
+2 -2
View File
@@ -33,9 +33,9 @@ For any changes applying new patches, also update research/0_binary_patch_compar
| --------------- | :---------: | :-------: | ------------------------------------------------------------ |
| **Regular** | 51 patches | 10 phases | `fw_patch` + `cfw_install` |
| **Development** | 64 patches | 12 phases | `fw_patch_dev` + `cfw_install_dev` |
| **Jailbreak** | 126 patches | 14 phases | `fw_patch_jb` + `cfw_install_jb` + `cfw_install_jb_finalize` |
| **Jailbreak** | 126 patches | 14 phases | `fw_patch_jb` + `cfw_install_jb` |
> `cfw_install_jb_finalize` requires booting into the full system, not the ramdisk.
> JB finalization (symlinks, Sileo, apt, TrollStore) runs automatically on first boot via `/cores/vphone_jb_setup.sh` LaunchDaemon. Monitor progress: `/var/log/vphone_jb_setup.log`.
See `research/` for detailed firmware pipeline, component origins, patch breakdowns, and boot flow documentation.
+1 -5
View File
@@ -81,7 +81,6 @@ help:
@echo " make cfw_install Install CFW mods via SSH"
@echo " make cfw_install_dev Install CFW mods via SSH (dev mode)"
@echo " make cfw_install_jb Install CFW + JB extensions (jetsam/procursus/basebin)"
@echo " make cfw_install_jb_finalize Finalize JB bootstrap on live device (symlinks/sileo/apt)"
@echo ""
@echo "Variables: VM_DIR=$(VM_DIR) CPU=$(CPU) MEMORY=$(MEMORY) DISK_SIZE=$(DISK_SIZE)"
@@ -244,7 +243,7 @@ ramdisk_send:
# CFW
# ═══════════════════════════════════════════════════════════════════
.PHONY: cfw_install cfw_install_dev cfw_install_jb cfw_install_jb_finalize
.PHONY: cfw_install cfw_install_dev cfw_install_jb
cfw_install:
cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install.sh" .
@@ -254,6 +253,3 @@ cfw_install_dev:
cfw_install_jb:
cd $(VM_DIR) && $(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb.sh" .
cfw_install_jb_finalize:
$(if $(SSH_PORT),SSH_PORT="$(SSH_PORT)") $(if $(SSH_PASS),SSH_PASS="$(SSH_PASS)") zsh "$(CURDIR)/$(SCRIPTS)/cfw_install_jb_post.sh"
+1 -2
View File
@@ -478,7 +478,7 @@ cp "$TEMP_DIR/vphoned" "$VM_DIR/.vphoned.signed"
echo " [+] vphoned installed (signed copy at .vphoned.signed)"
# Send daemon plists (overwrite on re-run)
for plist in bash.plist dropbear.plist trollvnc.plist rpcserver_ios.plist; do
for plist in bash.plist trollvnc.plist rpcserver_ios.plist; do
scp_to "$INPUT_DIR/jb/LaunchDaemons/$plist" "/mnt1/System/Library/LaunchDaemons/"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/LaunchDaemons/$plist"
done
@@ -518,7 +518,6 @@ rm -f "$TEMP_DIR/seputil" \
echo ""
echo "[+] CFW installation complete!"
echo " Reboot the device for changes to take effect."
echo " After boot, SSH will be available on port 22222 (password: alpine)"
if [[ "$CFW_SKIP_HALT" == "1" ]]; then
echo "[*] CFW_SKIP_HALT=1, skipping halt."
+1 -2
View File
@@ -477,7 +477,7 @@ cp "$TEMP_DIR/vphoned" "$VM_DIR/.vphoned.signed"
echo " [+] vphoned installed (signed copy at .vphoned.signed)"
# Send daemon plists (overwrite on re-run)
for plist in bash.plist dropbear.plist trollvnc.plist rpcserver_ios.plist; do
for plist in bash.plist trollvnc.plist rpcserver_ios.plist; do
scp_to "$INPUT_DIR/jb/LaunchDaemons/$plist" "/mnt1/System/Library/LaunchDaemons/"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/LaunchDaemons/$plist"
done
@@ -517,7 +517,6 @@ rm -f "$TEMP_DIR/seputil" \
echo ""
echo "[+] CFW installation complete!"
echo " Reboot the device for changes to take effect."
echo " After boot, SSH will be available on port 22222 (password: alpine)"
if [[ "$CFW_SKIP_HALT" == "1" ]]; then
echo "[*] CFW_SKIP_HALT=1, skipping halt."
+34 -1
View File
@@ -277,6 +277,40 @@ if [[ -d "$BASEBIN_DIR" ]]; then
echo " [+] BaseBin hooks deployed"
fi
# ═══════════ JB-5 DEPLOY FIRST-BOOT SETUP ══════════════════════
echo ""
echo "[JB-5] Deploying first-boot setup..."
# Deploy first-boot JB setup script + LaunchDaemon
SETUP_SCRIPT="$SCRIPT_DIR/vphone_jb_setup.sh"
SETUP_PLIST="$SCRIPT_DIR/vphone_jb_setup.plist"
if [[ -f "$SETUP_SCRIPT" ]]; then
scp_to "$SETUP_SCRIPT" "/mnt1/cores/vphone_jb_setup.sh"
ssh_cmd "/bin/chmod 0755 /mnt1/cores/vphone_jb_setup.sh"
echo " [+] vphone_jb_setup.sh -> /cores/"
fi
if [[ -f "$SETUP_PLIST" ]]; then
scp_to "$SETUP_PLIST" "/mnt1/System/Library/LaunchDaemons/com.vphone.jb-setup.plist"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/LaunchDaemons/com.vphone.jb-setup.plist"
# Inject into launchd.plist so launchd starts it at boot
echo " Injecting com.vphone.jb-setup into launchd.plist..."
scp_from "/mnt1/System/Library/xpc/launchd.plist" "$TEMP_DIR/launchd.plist"
python3 -c "
import plistlib, sys
with open(sys.argv[1], 'rb') as f:
target = plistlib.load(f)
with open(sys.argv[2], 'rb') as f:
daemon = plistlib.load(f)
target.setdefault('LaunchDaemons', {})['/System/Library/LaunchDaemons/com.vphone.jb-setup.plist'] = daemon
with open(sys.argv[1], 'wb') as f:
plistlib.dump(target, f, sort_keys=False)
" "$TEMP_DIR/launchd.plist" "$SETUP_PLIST"
scp_to "$TEMP_DIR/launchd.plist" "/mnt1/System/Library/xpc/launchd.plist"
ssh_cmd "/bin/chmod 0644 /mnt1/System/Library/xpc/launchd.plist"
echo " [+] com.vphone.jb-setup.plist injected into launchd.plist"
fi
# ═══════════ CLEANUP ═════════════════════════════════════════
echo ""
echo "[*] Unmounting device filesystems..."
@@ -291,6 +325,5 @@ rm -f "$TEMP_DIR/launchd" \
echo ""
echo "[+] CFW + JB installation complete!"
echo " Reboot the device for changes to take effect."
echo " After boot, SSH will be available on port 22222 (password: alpine)"
ssh_cmd "/sbin/halt" || true
-257
View File
@@ -1,257 +0,0 @@
#!/bin/zsh
# cfw_install_jb_post.sh — Finalize JB bootstrap on a normally-booted vphone.
#
# Runs after `cfw_install_jb` + first normal boot. Connects to the live device
# via SSH and sets up procursus symlinks, markers, Sileo, and apt packages.
#
# Every step is idempotent — safe to re-run at any point.
# All binary paths are discovered dynamically (no hardcoded /bin, /sbin, etc.).
#
# Usage: make cfw_install_jb_finalize [SSH_PORT=22222] [SSH_PASS=alpine]
set -euo pipefail
SCRIPT_DIR="${0:a:h}"
# ── Configuration ───────────────────────────────────────────────
SSH_PORT="${SSH_PORT:-22222}"
SSH_PASS="${SSH_PASS:-alpine}"
SSH_USER="root"
SSH_HOST="localhost"
SSH_RETRY="${SSH_RETRY:-3}"
SSHPASS_BIN=""
SSH_OPTS=(
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o PreferredAuthentications=password
-o ConnectTimeout=30
-q
)
# ── Helpers ─────────────────────────────────────────────────────
die() {
echo "[-] $*" >&2
exit 1
}
_sshpass() {
"$SSHPASS_BIN" -p "$SSH_PASS" "$@"
}
_ssh_retry() {
local attempt rc label
label=${2:-cmd}
for ((attempt = 1; attempt <= SSH_RETRY; attempt++)); do
"$@" && return 0
rc=$?
[[ $rc -ne 255 ]] && return $rc
echo " [${label}] connection lost (attempt $attempt/$SSH_RETRY), retrying in 3s..." >&2
sleep 3
done
return 255
}
# Raw ssh — no PATH prefix
ssh_raw() {
_ssh_retry _sshpass ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "$@"
}
# ssh with discovered PATH prepended
ssh_cmd() {
ssh_raw "$RENV $*"
}
# ── Prerequisites ──────────────────────────────────────────────
command -v sshpass &>/dev/null || die "Missing sshpass. Run: make setup_tools"
SSHPASS_BIN="$(command -v sshpass)"
echo "[*] cfw_install_jb_post.sh — Finalizing JB bootstrap..."
echo " Target: ${SSH_USER}@${SSH_HOST}:${SSH_PORT}"
echo ""
# ── Verify SSH connectivity ────────────────────────────────────
echo "[*] Checking SSH connectivity..."
ssh_raw "echo ready" >/dev/null 2>&1 || die "Cannot reach device on ${SSH_HOST}:${SSH_PORT}. Is the VM booted normally?"
echo "[+] Device reachable"
# ── Discover remote PATH ──────────────────────────────────────
# Uses only shell builtins (test -d, echo) — works with empty PATH.
echo "[*] Discovering remote binary directories..."
DISCOVERED_PATH=$(ssh_raw 'P=""; \
for d in \
/var/jb/usr/bin /var/jb/bin /var/jb/sbin /var/jb/usr/sbin \
/iosbinpack64/bin /iosbinpack64/usr/bin /iosbinpack64/sbin /iosbinpack64/usr/sbin \
/usr/bin /usr/sbin /bin /sbin; do \
[ -d "$d" ] && P="$P:$d"; \
done; \
echo "${P#:}"')
[[ -n "$DISCOVERED_PATH" ]] || die "Could not discover any binary directories on device"
echo " PATH=$DISCOVERED_PATH"
# This gets prepended to every ssh_cmd call
RENV="export PATH='$DISCOVERED_PATH' TERM='xterm-256color';"
# Quick sanity: verify we can run ls now
ssh_cmd "ls / >/dev/null" || die "PATH discovery succeeded but 'ls' still not found"
echo "[+] Remote environment ready"
# ═══════════ 1/6 SYMLINK /var/jb ══════════════════════════════
echo ""
echo "[1/6] Creating /private/var/jb symlink..."
# Find 96-char boot manifest hash — use shell glob (no ls dependency)
BOOT_HASH=$(ssh_cmd 'for d in /private/preboot/*/; do \
b="${d%/}"; b="${b##*/}"; \
[ "${#b}" = 96 ] && echo "$b" && break; \
done')
[[ -n "$BOOT_HASH" ]] || die "Could not find 96-char boot manifest hash in /private/preboot"
echo " Boot manifest hash: $BOOT_HASH"
JB_TARGET="/private/preboot/$BOOT_HASH/jb-vphone/procursus"
ssh_cmd "test -d '$JB_TARGET'" || die "Procursus directory not found at $JB_TARGET. Run cfw_install_jb first."
CURRENT_LINK=$(ssh_cmd "readlink /private/var/jb 2>/dev/null || true")
if [[ "$CURRENT_LINK" == "$JB_TARGET" ]]; then
echo " [*] Symlink already correct, skipping"
else
ssh_cmd "ln -sf '$JB_TARGET' /private/var/jb"
echo " [+] /private/var/jb -> $JB_TARGET"
fi
# ═══════════ 2/6 FIX OWNERSHIP / PERMISSIONS ═════════════════
echo ""
echo "[2/6] Fixing mobile Library ownership..."
ssh_cmd "mkdir -p /var/jb/var/mobile/Library/Preferences"
ssh_cmd "chown -R 501:501 /var/jb/var/mobile/Library"
ssh_cmd "chmod 0755 /var/jb/var/mobile/Library"
ssh_cmd "chown -R 501:501 /var/jb/var/mobile/Library/Preferences"
ssh_cmd "chmod 0755 /var/jb/var/mobile/Library/Preferences"
echo " [+] Ownership set"
# ═══════════ 3/6 RUN prep_bootstrap.sh ════════════════════════
echo ""
echo "[3/6] Running prep_bootstrap.sh..."
if ssh_cmd "test -f /var/jb/prep_bootstrap.sh"; then
# Skip interactive password prompt (uses uialert GUI — not usable over SSH)
ssh_cmd "NO_PASSWORD_PROMPT=1 /var/jb/prep_bootstrap.sh"
echo " [+] prep_bootstrap.sh completed"
echo " [!] Terminal password was NOT set (automated mode)."
echo " To set it manually: ssh in and run: passwd"
else
echo " [*] prep_bootstrap.sh already ran (deleted itself), skipping"
fi
# Re-discover PATH after prep_bootstrap.sh may have changed the login shell.
# The shell switch (chsh) can alter which profile scripts run on subsequent SSH
# sessions, so we must refresh RENV to ensure dpkg/apt/uicache are reachable.
echo "[*] Re-discovering remote PATH after bootstrap prep..."
DISCOVERED_PATH=$(ssh_raw 'P=""; \
for d in \
/var/jb/usr/bin /var/jb/bin /var/jb/sbin /var/jb/usr/sbin \
/iosbinpack64/bin /iosbinpack64/usr/bin /iosbinpack64/sbin /iosbinpack64/usr/sbin \
/usr/bin /usr/sbin /bin /sbin; do \
[ -d "$d" ] && P="$P:$d"; \
done; \
echo "${P#:}"')
RENV="export PATH='$DISCOVERED_PATH' TERM='xterm-256color';"
echo " PATH=$DISCOVERED_PATH"
# Fix interactive SSH environment.
# dropbear uses --shell /iosbinpack64/bin/bash and ignores /etc/passwd.
# /iosbinpack64/etc/profile spawns a non-login subshell that only reads ~/.bashrc.
# prep_bootstrap.sh's chsh has no effect because dropbear doesn't consult passwd.
# Create ~/.bashrc so the interactive subshell sources /var/jb/etc/profile (full PATH).
echo "[*] Setting up shell profile for interactive SSH..."
if ! ssh_cmd "test -f /var/root/.bashrc"; then
ssh_cmd "printf '%s\n' '# Source JB environment' '[ -r /var/jb/etc/profile ] && . /var/jb/etc/profile' > /var/root/.bashrc"
echo " [+] /var/root/.bashrc created"
else
echo " [*] /var/root/.bashrc already exists, skipping"
fi
# ═══════════ 4/6 CREATE MARKER FILES ═════════════════════════
echo ""
echo "[4/6] Creating marker files..."
for marker in .procursus_strapped .installed_dopamine; do
if ssh_cmd "test -f /var/jb/$marker"; then
echo " [*] $marker already exists, skipping"
else
ssh_cmd ": > /var/jb/$marker && chown 0:0 /var/jb/$marker && chmod 0644 /var/jb/$marker"
echo " [+] $marker created"
fi
done
# ═══════════ 5/6 INSTALL SILEO ══════════════════════════════
echo ""
echo "[5/6] Installing Sileo..."
SILEO_DEB_PATH="/private/preboot/$BOOT_HASH/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb"
if ssh_cmd "dpkg -s org.coolstar.sileo >/dev/null 2>&1"; then
echo " [*] Sileo already installed, skipping"
else
ssh_cmd "test -f '$SILEO_DEB_PATH'" || die "Sileo deb not found at $SILEO_DEB_PATH. Was it uploaded by cfw_install_jb?"
ssh_cmd "dpkg -i '$SILEO_DEB_PATH'"
echo " [+] Sileo installed"
fi
ssh_cmd "uicache -a 2>/dev/null || true"
echo " [+] uicache refreshed"
# ═══════════ 6/7 APT SETUP ═════════════════════════════════
echo ""
echo "[6/7] Running apt setup..."
HAVOC_LIST="/var/jb/etc/apt/sources.list.d/havoc.list"
if ssh_cmd "test -d /etc/apt/sources.list.d && test ! -d /var/jb/etc/apt/sources.list.d"; then
HAVOC_LIST="/etc/apt/sources.list.d/havoc.list"
fi
HAVOC_SOURCES="$(ssh_cmd "grep -RIl 'havoc.app' /etc/apt /var/jb/etc/apt 2>/dev/null || true")"
if [[ -n "$HAVOC_SOURCES" ]]; then
echo " [*] Havoc source already present:"
echo "$HAVOC_SOURCES" | sed 's/^/ - /'
OTHER_HAVOC_SOURCES="$(printf '%s\n' "$HAVOC_SOURCES" | grep -Fvx "$HAVOC_LIST" || true)"
if [[ -n "$OTHER_HAVOC_SOURCES" ]] && ssh_cmd "test -f '$HAVOC_LIST' && grep -q 'https://havoc.app/' '$HAVOC_LIST'"; then
ssh_cmd "rm -f '$HAVOC_LIST'"
echo " [+] Removed duplicate autogenerated Havoc source: $HAVOC_LIST"
fi
else
ssh_cmd "mkdir -p '${HAVOC_LIST:h}'"
ssh_cmd "printf '%s\n' 'deb [trusted=yes] https://havoc.app/ ./' > '$HAVOC_LIST'"
echo " [+] Havoc source added: $HAVOC_LIST"
fi
echo " [*] Allowing unsigned third-party repos during automated apt refresh"
ssh_cmd "DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngradeToInsecureRepositories=true update -qq"
ssh_cmd "DEBIAN_FRONTEND=noninteractive apt-get -o APT::Get::AllowUnauthenticated=true install -y -qq libkrw0-tfp0 2>/dev/null || true"
echo " [+] apt update + libkrw0-tfp0 done"
ssh_cmd "DEBIAN_FRONTEND=noninteractive apt-get -o APT::Get::AllowUnauthenticated=true upgrade -y -qq 2>/dev/null || true"
echo " [+] apt upgrade done"
# ═══════════ 7/7 INSTALL TROLLSTORE LITE ═══════════════════
echo ""
echo "[7/7] Installing TrollStore Lite..."
if ssh_cmd "dpkg -s com.opa334.trollstorelite >/dev/null 2>&1"; then
echo " [*] TrollStore Lite already installed, skipping"
else
ssh_cmd "DEBIAN_FRONTEND=noninteractive apt-get -o APT::Get::AllowUnauthenticated=true install -y -qq com.opa334.trollstorelite"
echo " [+] TrollStore Lite installed"
fi
ssh_cmd "uicache -a 2>/dev/null || true"
echo " [+] uicache refreshed"
# ═══════════ DONE ═══════════════════════════════════════════
echo ""
echo "[+] JB finalization complete!"
echo " TrollStore Lite is installed automatically during finalization."
echo " Next: open Sileo on device, add source https://ellekit.space, install ElleKit"
echo " Then reboot the device for full JB environment."
+1 -1
View File
@@ -24,7 +24,7 @@ Commands:
Patch launchd jetsam panic guard to avoid initproc crash loop.
inject-daemons <launchd.plist> <daemon_dir>
Inject bash/dropbear/trollvnc into launchd.plist.
Inject bash/trollvnc into launchd.plist.
inject-dylib <binary> <dylib_path>
Inject LC_LOAD_DYLIB into Mach-O binary (thin or universal).
+1 -1
View File
@@ -25,7 +25,7 @@ Commands:
Patch launchd jetsam panic guard to avoid initproc crash loop.
inject-daemons <launchd.plist> <daemon_dir>
Inject bash/dropbear/trollvnc into launchd.plist.
Inject bash/trollvnc into launchd.plist.
inject-dylib <binary> <dylib_path>
Inject LC_LOAD_DYLIB into Mach-O binary (thin or universal).
+2 -2
View File
@@ -37,14 +37,14 @@ def parse_cryptex_paths(manifest_path):
def inject_daemons(plist_path, daemon_dir):
"""Inject bash/dropbear/trollvnc entries into launchd.plist."""
"""Inject bash/trollvnc entries into launchd.plist."""
# Convert to XML first (macOS binary plist -> XML)
subprocess.run(["plutil", "-convert", "xml1", plist_path], capture_output=True)
with open(plist_path, "rb") as f:
target = plistlib.load(f)
for name in ("bash", "dropbear", "trollvnc", "vphoned", "rpcserver_ios"):
for name in ("bash", "trollvnc", "vphoned", "rpcserver_ios"):
src = os.path.join(daemon_dir, f"{name}.plist")
if not os.path.exists(src):
print(f" [!] Missing {src}, skipping")
+3 -50
View File
@@ -486,11 +486,8 @@ send_first_boot_commands() {
local commands=(
"export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11:/usr/games:/iosbinpack64/usr/local/sbin:/iosbinpack64/usr/local/bin:/iosbinpack64/usr/sbin:/iosbinpack64/usr/bin:/iosbinpack64/sbin:/iosbinpack64/bin'"
"mkdir -p /var/dropbear"
"cp /iosbinpack64/etc/profile /var/profile"
"cp /iosbinpack64/etc/motd /var/motd"
"dropbearkey -t rsa -f /var/dropbear/dropbear_rsa_host_key"
"dropbearkey -t ecdsa -f /var/dropbear/dropbear_ecdsa_host_key"
"shutdown -h now"
)
@@ -1033,53 +1030,9 @@ main() {
if [[ "$JB_MODE" -eq 1 ]]; then
echo ""
echo "=== JB Finalize ==="
echo "[*] Booting VM normally for JB bootstrap finalization..."
check_vm_storage_locks
mkdir -p "$LOG_DIR"
: > "$BOOT_LOG"
(make boot >"$BOOT_LOG" 2>&1) &
BOOT_PID=$!
sleep 2
if ! kill -0 "$BOOT_PID" 2>/dev/null; then
echo "[-] make boot exited early during JB finalize stage."
tail -n 40 "$BOOT_LOG" 2>/dev/null || true
die "JB finalize boot failed."
fi
local jb_ssh_port jb_iproxy_pid jb_iproxy_log
local iproxy_bin="${PROJECT_ROOT}/.limd/bin/iproxy"
jb_ssh_port="$(pick_random_ssh_port)" \
|| die "Failed to allocate a random local SSH port for JB finalize"
jb_iproxy_log="${LOG_DIR}/iproxy_jb_${jb_ssh_port}.log"
: > "$jb_iproxy_log"
echo "[*] Waiting for device UDID=${DEVICE_UDID} on USBMux..."
wait_for_iproxy_target_udid
echo "[*] Starting iproxy ${jb_ssh_port} -> 22222 (target_udid=${IPROXY_TARGET_UDID})..."
("$iproxy_bin" -u "$IPROXY_TARGET_UDID" "$jb_ssh_port" 22222 >"$jb_iproxy_log" 2>&1) &
jb_iproxy_pid=$!
sleep 1
if ! kill -0 "$jb_iproxy_pid" 2>/dev/null; then
echo "[-] iproxy exited early. Log:"
tail -n 40 "$jb_iproxy_log" || true
die "iproxy for JB finalize failed to start."
fi
echo "[+] iproxy running (pid=$jb_iproxy_pid, log=$jb_iproxy_log)"
wait_for_device_ssh "$jb_ssh_port" 120
run_make "JB finalize" cfw_install_jb_finalize SSH_PORT="$jb_ssh_port"
halt_device_ssh "$jb_ssh_port"
stop_process_tree "$jb_iproxy_pid" 2>/dev/null || true
echo "[*] Waiting for VM shutdown..."
wait "$BOOT_PID" || true
BOOT_PID=""
echo "[*] JB finalization will run automatically on first normal boot"
echo " via /cores/vphone_jb_setup.sh (LaunchDaemon)."
echo " Monitor progress via vphoned file browser: /var/log/vphone_jb_setup.log"
fi
echo ""
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.vphone.jb-setup</string>
<key>ProgramArguments</key>
<array>
<string>/iosbinpack64/bin/bash</string>
<string>/cores/vphone_jb_setup.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/vphone_jb_setup.log</string>
<key>StandardErrorPath</key>
<string>/var/log/vphone_jb_setup.log</string>
<key>UserName</key>
<string>root</string>
<key>EnablePressuredExit</key>
<false/>
</dict>
</plist>
+198
View File
@@ -0,0 +1,198 @@
#!/bin/bash
# vphone_jb_setup.sh — First-boot JB finalization script.
#
# Deployed to /cores/ during cfw_install_jb.sh (ramdisk phase).
# Runs automatically via LaunchDaemon on first normal boot.
# Idempotent — safe to re-run on subsequent boots.
#
# Logs to /var/log/vphone_jb_setup.log for host-side monitoring
# via vphoned file browser.
set -uo pipefail
LOG="/var/log/vphone_jb_setup.log"
DONE_MARKER="/var/mobile/.vphone_jb_setup_done"
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
die() { log "FATAL: $*"; exit 1; }
# Redirect all output to log
exec > >(tee -a "$LOG") 2>&1
log "=== vphone_jb_setup.sh starting ==="
# ── Check done marker ────────────────────────────────────────
if [ -f "$DONE_MARKER" ]; then
log "Already completed (marker exists), exiting."
exit 0
fi
# ── Environment ──────────────────────────────────────────────
export TERM=xterm-256color
export DEBIAN_FRONTEND=noninteractive
# Discover PATH dynamically
P=""
for d in \
/var/jb/usr/bin /var/jb/bin /var/jb/sbin /var/jb/usr/sbin \
/iosbinpack64/bin /iosbinpack64/usr/bin /iosbinpack64/sbin /iosbinpack64/usr/sbin \
/usr/bin /usr/sbin /bin /sbin; do
[ -d "$d" ] && P="$P:$d"
done
export PATH="${P#:}"
log "PATH=$PATH"
# ── Find boot manifest hash ─────────────────────────────────
BOOT_HASH=""
for d in /private/preboot/*/; do
b="${d%/}"; b="${b##*/}"
if [ "${#b}" = 96 ]; then
BOOT_HASH="$b"
break
fi
done
[ -n "$BOOT_HASH" ] || die "Could not find 96-char boot manifest hash"
log "Boot hash: $BOOT_HASH"
JB_TARGET="/private/preboot/$BOOT_HASH/jb-vphone/procursus"
[ -d "$JB_TARGET" ] || die "Procursus not found at $JB_TARGET"
# ═══════════ 0/7 REPLACE LAUNCHCTL ═════════════════════════════
# Procursus launchctl crashes (missing _launch_active_user_switch symbol).
# iosbinpack64's launchctl talks to launchd fine and always exits 0,
# which is enough for dpkg postinst/prerm script compatibility.
log "[0/7] Linking iosbinpack64 launchctl into procursus..."
IOSBINPACK_LAUNCHCTL=""
for p in /iosbinpack64/bin/launchctl /iosbinpack64/usr/bin/launchctl; do
[ -f "$p" ] && IOSBINPACK_LAUNCHCTL="$p" && break
done
if [ -n "$IOSBINPACK_LAUNCHCTL" ]; then
if [ -f "$JB_TARGET/usr/bin/launchctl" ] && [ ! -L "$JB_TARGET/usr/bin/launchctl" ] && [ ! -f "$JB_TARGET/usr/bin/launchctl.procursus" ]; then
mv "$JB_TARGET/usr/bin/launchctl" "$JB_TARGET/usr/bin/launchctl.procursus"
log " procursus original saved as launchctl.procursus"
fi
ln -sf "$IOSBINPACK_LAUNCHCTL" "$JB_TARGET/usr/bin/launchctl"
mkdir -p "$JB_TARGET/bin"
ln -sf "$IOSBINPACK_LAUNCHCTL" "$JB_TARGET/bin/launchctl"
log " linked usr/bin/launchctl + bin/launchctl -> $IOSBINPACK_LAUNCHCTL"
else
log " WARNING: iosbinpack64 launchctl not found"
fi
# ═══════════ 1/7 SYMLINK /var/jb ═════════════════════════════
log "[1/7] Creating /private/var/jb symlink..."
CURRENT_LINK=$(readlink /private/var/jb 2>/dev/null || true)
if [ "$CURRENT_LINK" = "$JB_TARGET" ]; then
log " Symlink already correct"
else
ln -sf "$JB_TARGET" /private/var/jb
log " /var/jb -> $JB_TARGET"
fi
# ═══════════ 2/7 FIX OWNERSHIP / PERMISSIONS ═════════════════
log "[2/7] Fixing mobile Library ownership..."
mkdir -p /var/jb/var/mobile/Library/Preferences
chown -R 501:501 /var/jb/var/mobile/Library
chmod 0755 /var/jb/var/mobile/Library
chown -R 501:501 /var/jb/var/mobile/Library/Preferences
chmod 0755 /var/jb/var/mobile/Library/Preferences
log " Ownership set"
# ═══════════ 3/7 RUN prep_bootstrap.sh ═══════════════════════
log "[3/7] Running prep_bootstrap.sh..."
if [ -f /var/jb/prep_bootstrap.sh ]; then
NO_PASSWORD_PROMPT=1 /var/jb/prep_bootstrap.sh || log " prep_bootstrap.sh exited with $?"
log " prep_bootstrap.sh completed"
else
log " prep_bootstrap.sh already ran (deleted itself), skipping"
fi
# Re-discover PATH after prep_bootstrap
P=""
for d in \
/var/jb/usr/bin /var/jb/bin /var/jb/sbin /var/jb/usr/sbin \
/iosbinpack64/bin /iosbinpack64/usr/bin /iosbinpack64/sbin /iosbinpack64/usr/sbin \
/usr/bin /usr/sbin /bin /sbin; do
[ -d "$d" ] && P="$P:$d"
done
export PATH="${P#:}"
log " PATH=$PATH"
# ═══════════ 4/7 CREATE MARKER FILES ═════════════════════════
log "[4/7] Creating marker files..."
for marker in .procursus_strapped .installed_dopamine; do
if [ -f "/var/jb/$marker" ]; then
log " $marker already exists"
else
: > "/var/jb/$marker"
chown 0:0 "/var/jb/$marker"
chmod 0644 "/var/jb/$marker"
log " $marker created"
fi
done
# ═══════════ 5/7 INSTALL SILEO ═══════════════════════════════
log "[5/7] Installing Sileo..."
SILEO_DEB_PATH="/private/preboot/$BOOT_HASH/org.coolstar.sileo_2.5.1_iphoneos-arm64.deb"
if dpkg -s org.coolstar.sileo >/dev/null 2>&1; then
log " Sileo already installed"
else
if [ -f "$SILEO_DEB_PATH" ]; then
dpkg -i "$SILEO_DEB_PATH" || log " dpkg -i sileo exited with $?"
log " Sileo installed"
else
log " WARNING: Sileo deb not found at $SILEO_DEB_PATH"
fi
fi
uicache -a 2>/dev/null || true
log " uicache refreshed"
# ═══════════ 6/7 APT SETUP ══════════════════════════════════
log "[6/7] Running apt setup..."
# Determine apt sources directory
HAVOC_LIST="/var/jb/etc/apt/sources.list.d/havoc.list"
if [ -d /etc/apt/sources.list.d ] && [ ! -d /var/jb/etc/apt/sources.list.d ]; then
HAVOC_LIST="/etc/apt/sources.list.d/havoc.list"
fi
if ! grep -rIl 'havoc.app' /etc/apt /var/jb/etc/apt 2>/dev/null | grep -q .; then
mkdir -p "$(dirname "$HAVOC_LIST")"
printf '%s\n' 'deb https://havoc.app/ ./' > "$HAVOC_LIST"
log " Havoc source added: $HAVOC_LIST"
else
log " Havoc source already present"
fi
apt-get -o Acquire::AllowInsecureRepositories=true \
-o Acquire::AllowDowngradeToInsecureRepositories=true \
update -qq 2>&1 || log " apt update exited with $?"
log " apt update done"
apt-get -o APT::Get::AllowUnauthenticated=true \
install -y -qq libkrw0-tfp0 2>/dev/null || true
log " libkrw0-tfp0 done"
apt-get -o APT::Get::AllowUnauthenticated=true \
upgrade -y -qq 2>/dev/null || true
log " apt upgrade done"
# ═══════════ 7/7 INSTALL TROLLSTORE LITE ═════════════════════
log "[7/7] Installing TrollStore Lite..."
if dpkg -s com.opa334.trollstorelite >/dev/null 2>&1; then
log " TrollStore Lite already installed"
else
apt-get -o APT::Get::AllowUnauthenticated=true \
install -y -qq com.opa334.trollstorelite 2>&1 || log " TrollStore install exited with $?"
log " TrollStore Lite installed"
fi
uicache -a 2>/dev/null || true
log " uicache refreshed"
# ═══════════ DONE ════════════════════════════════════════════
: > "$DONE_MARKER"
log "=== vphone_jb_setup.sh complete ==="
+1 -1
View File
@@ -1,3 +1,3 @@
#import <Foundation/Foundation.h>
extern int vp_extract_archive(NSString *archivePath, NSString *extractionPath);
extern int vp_extract_archive(NSString *archivePath, NSString *extractionPath, NSString **errorOutput);
+32 -15
View File
@@ -20,14 +20,15 @@ static int copy_data(struct archive *ar, struct archive *aw) {
}
}
int vp_extract_archive(NSString *archivePath, NSString *extractionPath) {
int vp_extract_archive(NSString *archivePath, NSString *extractionPath, NSString **errorOutput) {
int flags = ARCHIVE_EXTRACT_TIME
| ARCHIVE_EXTRACT_PERM
| ARCHIVE_EXTRACT_ACL
| ARCHIVE_EXTRACT_FFLAGS
| ARCHIVE_EXTRACT_SECURE_SYMLINKS
| ARCHIVE_EXTRACT_SECURE_NODOTDOT
| ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS;
| ARCHIVE_EXTRACT_SECURE_NODOTDOT;
// Resolve symlinks in extractionPath (e.g. /tmp -> /private/tmp on iOS)
// so ARCHIVE_EXTRACT_SECURE_SYMLINKS doesn't reject trusted system symlinks.
NSString *resolvedPath = [extractionPath stringByResolvingSymlinksInPath];
NSLog(@"vphoned: extract %@ -> %@ (resolved: %@)", archivePath, extractionPath, resolvedPath);
struct archive *a = archive_read_new();
archive_read_support_format_all(a);
@@ -39,6 +40,9 @@ int vp_extract_archive(NSString *archivePath, NSString *extractionPath) {
int ret = 0;
if (archive_read_open_filename(a, archivePath.fileSystemRepresentation, 10240) != ARCHIVE_OK) {
NSString *err = [NSString stringWithFormat:@"archive_read_open failed: %s", archive_error_string(a)];
NSLog(@"vphoned: %@", err);
if (errorOutput) *errorOutput = err;
ret = 1;
goto cleanup;
}
@@ -48,30 +52,43 @@ int vp_extract_archive(NSString *archivePath, NSString *extractionPath) {
int r = archive_read_next_header(a, &entry);
if (r == ARCHIVE_EOF) break;
if (r < ARCHIVE_OK)
fprintf(stderr, "%s\n", archive_error_string(a));
if (r < ARCHIVE_WARN) { ret = 1; goto cleanup; }
NSLog(@"vphoned: archive_read_next_header: %s", archive_error_string(a));
if (r < ARCHIVE_WARN) {
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"archive_read_next_header failed: %s", archive_error_string(a)];
ret = 1; goto cleanup;
}
const char *entryPath = archive_entry_pathname(entry);
if (!entryPath) { ret = 1; goto cleanup; }
NSString *currentFile = [NSString stringWithUTF8String:entryPath];
if (!currentFile) { ret = 1; goto cleanup; }
NSString *fullOutputPath = [extractionPath stringByAppendingPathComponent:currentFile];
NSString *fullOutputPath = [resolvedPath stringByAppendingPathComponent:currentFile];
archive_entry_set_pathname(entry, fullOutputPath.fileSystemRepresentation);
r = archive_write_header(ext, entry);
if (r < ARCHIVE_OK)
fprintf(stderr, "%s\n", archive_error_string(ext));
else if (archive_entry_size(entry) > 0) {
NSLog(@"vphoned: archive_write_header(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
if (r < ARCHIVE_WARN) {
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"archive_write_header failed for %@: %s", currentFile, archive_error_string(ext)];
ret = 1; goto cleanup;
}
if (archive_entry_size(entry) > 0) {
r = copy_data(a, ext);
if (r < ARCHIVE_OK)
fprintf(stderr, "%s\n", archive_error_string(ext));
if (r < ARCHIVE_WARN) { ret = 1; goto cleanup; }
NSLog(@"vphoned: copy_data(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
if (r < ARCHIVE_WARN) {
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"copy_data failed for %@: %s", currentFile, archive_error_string(ext)];
ret = 1; goto cleanup;
}
}
r = archive_write_finish_entry(ext);
if (r < ARCHIVE_OK)
fprintf(stderr, "%s\n", archive_error_string(ext));
if (r < ARCHIVE_WARN) { ret = 1; goto cleanup; }
NSLog(@"vphoned: archive_write_finish_entry(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
if (r < ARCHIVE_WARN) {
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"archive_write_finish_entry failed for %@: %s", currentFile, archive_error_string(ext)];
ret = 1; goto cleanup;
}
}
cleanup:
+4 -3
View File
@@ -745,9 +745,10 @@ static int vp_extract_package_to_directory(
NSString *extractionPath,
NSString **detailOutput
) {
int ret = vp_extract_archive(fileToExtract, extractionPath);
NSString *archiveError = nil;
int ret = vp_extract_archive(fileToExtract, extractionPath, &archiveError);
if (ret != 0) {
if (detailOutput) *detailOutput = @"libarchive extraction failed";
if (detailOutput) *detailOutput = archiveError ?: @"libarchive extraction failed";
return 168;
}
return 0;
@@ -796,7 +797,7 @@ NSDictionary *vp_handle_custom_install(NSDictionary *msg) {
certPath = nil;
}
NSString *tmpPackagePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
NSString *tmpPackagePath = [[NSTemporaryDirectory() stringByResolvingSymlinksInPath] stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
if (![[NSFileManager defaultManager] createDirectoryAtPath:tmpPackagePath withIntermediateDirectories:NO attributes:nil error:nil]) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
response[@"msg"] = @"failed to create temporary extraction directory";
+6 -6
View File
@@ -531,7 +531,7 @@ class VPhoneControl {
if let reqId, let pending = removePending(id: reqId) {
if type == "err" {
let detail = msg["msg"] as? String ?? "unknown error"
pending.handler(.failure(ControlError.guestError(detail)))
DispatchQueue.main.async { pending.handler(.failure(ControlError.guestError(detail))) }
continue
}
@@ -543,19 +543,19 @@ class VPhoneControl {
if Self.readFully(fd: fd, buf: buf, count: size) {
let data = Data(bytes: buf, count: size)
buf.deallocate()
pending.handler(.success((msg, data)))
DispatchQueue.main.async { pending.handler(.success((msg, data))) }
} else {
buf.deallocate()
pending.handler(.failure(ControlError.protocolError("failed to read file data")))
DispatchQueue.main.async { pending.handler(.failure(ControlError.protocolError("failed to read file data"))) }
}
} else {
pending.handler(.success((msg, Data())))
DispatchQueue.main.async { pending.handler(.success((msg, Data()))) }
}
continue
}
// Normal response (ok, pong, etc.)
pending.handler(.success((msg, nil)))
DispatchQueue.main.async { pending.handler(.success((msg, nil))) }
continue
}
@@ -602,7 +602,7 @@ class VPhoneControl {
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + timeout) { [weak self] in
guard let self else { return }
guard let pending = removePending(id: id) else { return }
pending.handler(.failure(ControlError.requestTimedOut(type: type, seconds: timeoutSeconds)))
DispatchQueue.main.async { pending.handler(.failure(ControlError.requestTimedOut(type: type, seconds: timeoutSeconds))) }
}
}
@@ -1,3 +1,4 @@
@preconcurrency import Foundation
import SwiftUI
import UniformTypeIdentifiers