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:
1 parent
fb07aa4558
commit
18a67ec171
21 files changed
+1223
-102
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user