Fix Patchless for cloudOS 26.4 (#336)

* Fix Patchless for cloudOS 26.4

by dynamically matching and downloading the required apfs_sealvolume version.

* patchless: Add `seal.log` for `apfs_sealvolume` command

---------

Co-authored-by: zqxwce <[email protected]>
This commit is contained in:
Maximilian Paß
2026-05-28 13:55:05 +03:00
committed by GitHub
co-authored by zqxwce
parent d15d218fe2
commit 3b08751029
4 changed files with 133 additions and 45 deletions
+101
View File
@@ -6,6 +6,7 @@
# - direct iPhone IPSW URLs or local file paths # - direct iPhone IPSW URLs or local file paths
# - version/build selectors for the target device # - version/build selectors for the target device
# - listing of all downloadable IPSWs for the target device # - listing of all downloadable IPSWs for the target device
# - The "VARIANT" environment variable to download apfs_sealvolume for the patchless mode
# #
# Listing and selection are resolved through the `ipsw` CLI already used # Listing and selection are resolved through the `ipsw` CLI already used
# elsewhere in this repo, so the script can work with the full downloadable # elsewhere in this repo, so the script can work with the full downloadable
@@ -410,6 +411,99 @@ extract() {
cp -R "$cache" "$out" cp -R "$cache" "$out"
} }
download_apfs_sealvolume() {
local src="$1"
local base ios_version filename PROJECT_DIR TOOLS_PREFIX TMP_DIR bn ver BUILD BUILD_MANIFEST RAMDISK_PATH RAMDISK_IM4P RAMDISK MOUNT
base="$(basename "$src")"
ios_version="$(awk -F_ 'NF >= 2 { print $2 }' <<<"$base")"
if [[ -z "$ios_version" ]]; then
echo "Error: could not determine iOS version from filename: $base" >&2
return 1
fi
filename="apfs_sealvolume_${ios_version}"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
TOOLS_PREFIX="$PROJECT_DIR/.tools"
if [[ -f "$TOOLS_PREFIX/$filename" ]]; then
echo "$filename already present"
else
echo "Downloading $filename"
(
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
# List matching macOS version to the iOS version
while IFS= read -r url; do
bn="$(basename "$url")"
ver="${bn#*_}"
ver="${ver%%_*}"
[[ "$ver" == "$ios_version" ]] || continue
BUILD="$(awk -F_ '{print $3}' <<<"$bn")"
break
done < <(
ipsw download appledb \
--os macOS \
--version $ios_version \
--urls
)
if [[ -z "${BUILD:-}" ]]; then
echo "Error: failed to determine macOS build from available URLs" >&2
exit 1
fi
# Download BuildManifest first
ipsw download appledb \
--os macOS \
--build $BUILD \
--pattern "^BuildManifest.plist\$" \
--output "$TMP_DIR"
BUILD_MANIFEST="$(find "$TMP_DIR" -name BuildManifest.plist -print -quit)"
if [ -z "$BUILD_MANIFEST" ]; then
echo "Failed to locate BuildManifest.plist"
exit 1
fi
RAMDISK_PATH="$(/usr/bin/plutil -extract 'BuildIdentities.0.Manifest.RestoreRamDisk.Info.Path' raw -o - "$BUILD_MANIFEST")"
if [ -z "$RAMDISK_PATH" ]; then
echo "Failed to read RestoreRamDisk path from BuildManifest"
exit 1
fi
# Download the ramdisk referenced by BuildManifest
ipsw download appledb \
--os macOS \
--build $BUILD \
--pattern "$RAMDISK_PATH" \
--output "$TMP_DIR"
RAMDISK_IM4P="$(find "$TMP_DIR" -path "*${RAMDISK_PATH}" -print -quit)"
if [ -z "$RAMDISK_IM4P" ]; then
echo "Failed to locate downloaded ramdisk: $RAMDISK_PATH"
exit 1
fi
RAMDISK="$TMP_DIR/ramdisk.dmg"
ipsw img4 im4p extract --output "$RAMDISK" "$RAMDISK_IM4P"
MOUNT=$(hdiutil attach -readonly -nobrowse "$RAMDISK" | awk 'END{ print$NF}')
cp "$MOUNT/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs_sealvolume" \
"$TOOLS_PREFIX/$filename"
hdiutil detach "$MOUNT" >/dev/null 2>&1 || true
)
echo " Downloaded: $TOOLS_PREFIX/$filename"
echo " Resigning $filename"
codesign --force --sign - "$TOOLS_PREFIX/$filename"
fi
}
LIST_FIRMWARES="${LIST_FIRMWARES:-0}" LIST_FIRMWARES="${LIST_FIRMWARES:-0}"
IPHONE_DEVICE="${IPHONE_DEVICE:-$DEFAULT_IPHONE_DEVICE}" IPHONE_DEVICE="${IPHONE_DEVICE:-$DEFAULT_IPHONE_DEVICE}"
IPHONE_VERSION="${IPHONE_VERSION:-}" IPHONE_VERSION="${IPHONE_VERSION:-}"
@@ -537,6 +631,13 @@ echo ""
fetch "$IPHONE_SOURCE" "$IPHONE_IPSW_PATH" fetch "$IPHONE_SOURCE" "$IPHONE_IPSW_PATH"
fetch "$CLOUDOS_SOURCE" "$CLOUDOS_IPSW_PATH" fetch "$CLOUDOS_SOURCE" "$CLOUDOS_IPSW_PATH"
VARIANT="${VARIANT:-}"
if [[ "$VARIANT" == "less" ]]; then
download_apfs_sealvolume "$IPHONE_SOURCE"
else
echo "==> Downloading apfs sealvolume (skipped — patchless variant only)"
fi
IPHONE_CACHE="${IPSW_DIR}/${IPHONE_DIR}" IPHONE_CACHE="${IPSW_DIR}/${IPHONE_DIR}"
CLOUDOS_CACHE="${IPSW_DIR}/${CLOUDOS_DIR}" CLOUDOS_CACHE="${IPSW_DIR}/${CLOUDOS_DIR}"
+3 -6
View File
@@ -1053,11 +1053,7 @@ main() {
install_brew_deps install_brew_deps
ensure_python_linked ensure_python_linked
if [[ "$LESS_MODE" -eq 1 ]]; then run_make "Project setup" setup_tools
VARIANT=less run_make "Project setup" setup_tools
else
run_make "Project setup" setup_tools
fi
run_make "Project setup" build run_make "Project setup" build
fi fi
@@ -1066,10 +1062,11 @@ main() {
export PATH="$PROJECT_ROOT/.venv/bin:$PATH" export PATH="$PROJECT_ROOT/.venv/bin:$PATH"
run_make "Firmware prep" vm_new run_make "Firmware prep" vm_new
run_make "Firmware prep" fw_prepare
if [[ "$LESS_MODE" -eq 0 ]]; then if [[ "$LESS_MODE" -eq 0 ]]; then
run_make "Firmware prep" fw_prepare
run_make "Firmware patch" "$fw_patch_target" run_make "Firmware patch" "$fw_patch_target"
else else
VARIANT=less run_make "Firmware prep" fw_prepare
run_make_sudo "Firmware patch" "$fw_patch_target" run_make_sudo "Firmware patch" "$fw_patch_target"
fi fi
+4 -37
View File
@@ -25,7 +25,7 @@ ensure_repo_submodule() {
# ── Brew packages ────────────────────────────────────────────── # ── Brew packages ──────────────────────────────────────────────
echo "[1/5] Checking brew packages..." echo "[1/4] Checking brew packages..."
BREW_PACKAGES=(aria2 gnu-tar openssl@3 ldid-procursus sshpass zstd) BREW_PACKAGES=(aria2 gnu-tar openssl@3 ldid-procursus sshpass zstd)
BREW_MISSING=() BREW_MISSING=()
@@ -45,7 +45,7 @@ fi
# ── Trustcache ───────────────────────────────────────────────── # ── Trustcache ─────────────────────────────────────────────────
echo "[2/5] trustcache" echo "[2/4] trustcache"
TRUSTCACHE_BIN="$TOOLS_PREFIX/bin/trustcache" TRUSTCACHE_BIN="$TOOLS_PREFIX/bin/trustcache"
if [[ -x "$TRUSTCACHE_BIN" ]]; then if [[ -x "$TRUSTCACHE_BIN" ]]; then
@@ -74,7 +74,7 @@ fi
# ── insert_dylib ─────────────────────────────────────────────── # ── insert_dylib ───────────────────────────────────────────────
echo "[3/5] insert_dylib" echo "[3/4] insert_dylib"
INSERT_DYLIB_BIN="$TOOLS_PREFIX/bin/insert_dylib" INSERT_DYLIB_BIN="$TOOLS_PREFIX/bin/insert_dylib"
if [[ -x "$INSERT_DYLIB_BIN" ]]; then if [[ -x "$INSERT_DYLIB_BIN" ]]; then
@@ -90,41 +90,8 @@ fi
# ── Python venv ──────────────────────────────────────────────── # ── Python venv ────────────────────────────────────────────────
echo "[4/5] Python venv" echo "[4/4] Python venv"
zsh "$SCRIPT_DIR/setup_venv.sh" zsh "$SCRIPT_DIR/setup_venv.sh"
# ── APFS sealvolume (patchless variant only) ──────────────────────
VARIANT="${VARIANT:-}"
if [[ "$VARIANT" == "less" ]]; then
echo "[5/5] apfs sealvolume"
if [[ -f "$TOOLS_PREFIX/apfs_sealvolume" ]]; then
echo " apfs_sealvolume already present"
else
TMP_DIR="$(mktemp -d)"
ipsw download appledb \
--os macOS \
--build 25D2140 \
--pattern "094-33864-054.dmg" \
--output "$TMP_DIR"
RAMDISK_IM4P="$TMP_DIR/25D2140__MacOS/094-33864-054.dmg"
RAMDISK="$TMP_DIR/ramdisk.dmg"
ipsw img4 im4p extract --output "$RAMDISK" "$RAMDISK_IM4P"
MOUNT=$(hdiutil attach -readonly -nobrowse "$RAMDISK" | awk 'END{ print$NF}')
cp "$MOUNT/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs_sealvolume" \
"$TOOLS_PREFIX/apfs_sealvolume"
hdiutil detach "$MOUNT" >/dev/null 2>&1 || true
rm -rf "$TMP_DIR"
echo " Downloaded: $TOOLS_PREFIX/apfs_sealvolume"
echo " Resigning apfs_sealvolume"
codesign --force --sign - "$TOOLS_PREFIX/apfs_sealvolume"
fi
else
echo "[5/5] apfs sealvolume (skipped — patchless variant only)"
fi
echo "" echo ""
echo "All tools installed." echo "All tools installed."
@@ -15,6 +15,7 @@ import Img4tool
enum ProcessError: Error { enum ProcessError: Error {
case failed(Int32, String) case failed(Int32, String)
case notExecutable(String)
} }
extension Data { extension Data {
@@ -394,6 +395,18 @@ public final class CryptexFilesystemPatcher: Patcher {
return im4pPath return im4pPath
} }
private func identifyApfsSealvolume() throws -> URL {
let iosVersion = try getProductVersion()
let path = self.vphoneCliDirectory.appending(path: ".tools/apfs_sealvolume_\(iosVersion)")
guard FileManager.default.fileExists(atPath: path.path) else {
throw FirmwareManifest.ManifestError.fileNotFound(path.path)
}
guard FileManager.default.isExecutableFile(atPath: path.path) else {
throw ProcessError.notExecutable(path.path)
}
return path
}
func createDigestAndHash(filesystem: URL, mtree: URL, remap: Bool) throws -> (URL, URL) { func createDigestAndHash(filesystem: URL, mtree: URL, remap: Bool) throws -> (URL, URL) {
let (device, mount) = try attachImage(path: filesystem) let (device, mount) = try attachImage(path: filesystem)
defer { try? detachImage(deviceNode: device) } defer { try? detachImage(deviceNode: device) }
@@ -402,6 +415,7 @@ public final class CryptexFilesystemPatcher: Patcher {
let digestDbPath = tmpDir.appending(path: "digest.db") let digestDbPath = tmpDir.appending(path: "digest.db")
let rootHashPath = tmpDir.appending(path: "root_hash") let rootHashPath = tmpDir.appending(path: "root_hash")
let mtreeRemapPath = tmpDir.appending(path: "mtree_remap.xml") let mtreeRemapPath = tmpDir.appending(path: "mtree_remap.xml")
let sealLogPath = tmpDir.appending(path: "seal.log")
// We want to get the nanosecond timestamp of the last modification before the mtree collection. // We want to get the nanosecond timestamp of the last modification before the mtree collection.
// We know that we remove directories in /private/var in removeSpecificSystemFiles last. // We know that we remove directories in /private/var in removeSpecificSystemFiles last.
@@ -423,13 +437,14 @@ public final class CryptexFilesystemPatcher: Patcher {
FileManager.default.createFile(atPath: mtreeRemapPath.path, contents: remapContent.data(using: .utf8)) FileManager.default.createFile(atPath: mtreeRemapPath.path, contents: remapContent.data(using: .utf8))
try unmount(mount: mount) try unmount(mount: mount)
let sealvolume = self.vphoneCliDirectory.appending(path: ".tools/apfs_sealvolume") let sealvolume = try identifyApfsSealvolume()
FileManager.default.createFile(atPath: sealLogPath.path, contents: nil)
_ = try runProcess(sealvolume.path, [ _ = try runProcess(sealvolume.path, [
"-R", mtreeRemapPath.path, "-R", mtreeRemapPath.path,
"-U", digestDbPath.path, // Save digest records "-U", digestDbPath.path, // Save digest records
"-M", rootHashPath.path, // Save root hash "-M", rootHashPath.path, // Save root hash
device device
]) ], output: sealLogPath)
return (digestDbPath, rootHashPath) return (digestDbPath, rootHashPath)
} }
@@ -615,6 +630,14 @@ public final class CryptexFilesystemPatcher: Patcher {
return remaining.joined(separator: "/") return remaining.joined(separator: "/")
} }
func getProductVersion() throws -> String {
let root = try parsePlist(data: buildManiest)
guard let productVersion = root["ProductVersion"] as? String else {
throw FirmwareManifest.ManifestError.missingKey("ProductVersion in BuildManifest")
}
return productVersion
}
func getTrustcachePath() throws -> String { func getTrustcachePath() throws -> String {
let path = self.restoreDir.appending(path: "iPhone-BuildManifest.plist") let path = self.restoreDir.appending(path: "iPhone-BuildManifest.plist")
let manifest = try getBuildIdentityManifest(path: path) let manifest = try getBuildIdentityManifest(path: path)