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.
283 lines
10 KiB
Swift
283 lines
10 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|