// // 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) } }