From 62dfc80026eab749064032a722d2a281e0545ddb Mon Sep 17 00:00:00 2001 From: moxcomic <37604141+moxcomic@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:39:42 +0800 Subject: [PATCH 1/4] fix: avoid Swift ARC trap in zstd streaming decompression Data.append(contentsOf: ArraySlice) starting from an empty _NSZeroData-backed Data triggered a brk #1 in ObjC-bridged value copy during injection. Switch to raw UnsafeMutableRawPointer + the UnsafePointer-based Data.append to avoid the Sequence/COW path, and harden the loop: break cleanly on streamResult == 0 and fail fast on stalled progress instead of potentially spinning on truncated input. Co-Authored-By: Claude Opus 4.7 (1M context) --- TrollFools/InjectorV3+Preprocess.swift | 43 ++++++++++++++++---------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/TrollFools/InjectorV3+Preprocess.swift b/TrollFools/InjectorV3+Preprocess.swift index eac8e80..2fdd583 100644 --- a/TrollFools/InjectorV3+Preprocess.swift +++ b/TrollFools/InjectorV3+Preprocess.swift @@ -247,29 +247,38 @@ fileprivate enum ZStd { } let chunkSize = max(Int(ZSTD_DStreamOutSize()), 1) - var chunk = [UInt8](repeating: 0, count: chunkSize) + let chunk = UnsafeMutableRawPointer.allocate(byteCount: chunkSize, alignment: 1) + defer { + chunk.deallocate() + } + var output = Data() + try data.withUnsafeBytes { (sourceBuffer: UnsafeRawBufferPointer) throws -> Void in + guard let sourcePtr = sourceBuffer.baseAddress else { + throw SWCompression.DataError.corrupted + } + var input = ZSTD_inBuffer(src: sourcePtr, size: sourceBuffer.count, pos: 0) - try data.withUnsafeBytes { sourceBuffer in - var input = ZSTD_inBuffer(src: sourceBuffer.baseAddress, size: sourceBuffer.count, pos: 0) - var streamResult: size_t = 1 + while true { + var outBuffer = ZSTD_outBuffer(dst: chunk, size: chunkSize, pos: 0) + let streamResult = ZSTD_decompressStream(stream, &outBuffer, &input) + guard ZSTD_isError(streamResult) == 0 else { + throw SWCompression.DataError.corrupted + } - while input.pos < input.size || streamResult != 0 { - let produced = try chunk.withUnsafeMutableBytes { destinationBuffer in - var outBuffer = ZSTD_outBuffer( - dst: destinationBuffer.baseAddress, - size: destinationBuffer.count, - pos: 0 + if outBuffer.pos > 0 { + output.append( + chunk.assumingMemoryBound(to: UInt8.self), + count: Int(outBuffer.pos) ) - streamResult = ZSTD_decompressStream(stream, &outBuffer, &input) - guard ZSTD_isError(streamResult) == 0 else { - throw SWCompression.DataError.corrupted - } - return outBuffer.pos } - if produced > 0 { - output.append(contentsOf: chunk[.. Date: Thu, 23 Apr 2026 11:57:02 +0800 Subject: [PATCH 2/4] fix: eject crash when scanning non-Mach-O files in Frameworks/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit frameworkMachOsInBundle's fallback loop (added in 8a832b4) calls isMachO on every level-2 item inside Frameworks/*.framework/. For non-Mach-O files — Info.plist, .car, .nib, .bin — MachOKit.loadFromFile reaches an internal NSFileHandle.read(offset:swapHandler:) that triggers a Swift runtime trap (brk #1) instead of throwing, so the try? at the call site cannot catch it. Eject ends up crashing inside Bundle.swift:72 on any app whose frameworks contain non-Mach-O files. Gate isMachO with a cheap file-size + magic-bytes pre-check so non-Mach-O files are rejected before MachOKit is invoked. Co-Authored-By: Claude Opus 4.7 (1M context) --- TrollFools/InjectorV3+MachO.swift | 41 ++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/TrollFools/InjectorV3+MachO.swift b/TrollFools/InjectorV3+MachO.swift index e5ff2fb..d4d45d6 100644 --- a/TrollFools/InjectorV3+MachO.swift +++ b/TrollFools/InjectorV3+MachO.swift @@ -10,12 +10,45 @@ import MachOKit import OrderedCollections extension InjectorV3 { - func isMachO(_ target: URL) -> Bool { - if (try? MachOKit.loadFromFile(url: target)) != nil { - true + // Mach-O magic numbers (both native and byte-swapped). + // MachOKit's loadFromFile can hit a Swift runtime trap (brk #1) on files + // that are not Mach-O — `try?` does not catch those — so callers must + // screen with this cheap pre-check before invoking MachOKit. + private static let machOMagics: Set = [ + 0xFEEDFACE, 0xCEFAEDFE, + 0xFEEDFACF, 0xCFFAEDFE, + 0xCAFEBABE, 0xBEBAFECA, + 0xCAFEBABF, 0xBFBAFECA, + ] + + fileprivate func hasMachOMagic(_ target: URL) -> Bool { + guard let size = (try? target.resourceValues(forKeys: [.fileSizeKey]).fileSize), + size >= 32 + else { + return false + } + guard let handle = try? FileHandle(forReadingFrom: target) else { + return false + } + defer { try? handle.close() } + let head: Data? + if #available(iOS 13.4, *) { + head = try? handle.read(upToCount: 4) } else { - false + head = handle.readData(ofLength: 4) + } + guard let data = head, data.count == 4 else { + return false + } + let magic = data.withUnsafeBytes { $0.load(as: UInt32.self) } + return Self.machOMagics.contains(magic) + } + + func isMachO(_ target: URL) -> Bool { + guard hasMachOMagic(target) else { + return false } + return (try? MachOKit.loadFromFile(url: target)) != nil } func isProtectedMachO(_ target: URL) throws -> Bool { From e0fa9395a57dc6ec9bf132d7d466b1bdce3801c7 Mon Sep 17 00:00:00 2001 From: moxcomic <37604141+moxcomic@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:38:27 +0800 Subject: [PATCH 3/4] fix: eject crash in MachOKit DyldCache code path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit frameworkMachOsInBundle's injectedAssetNames loop (added 5ea814a) calls loadedDylibsOfMachO on every Mach-O with a .troll-fools.bak sibling, then iterates MachOKit's loadCommands. On some binaries this reaches LoadCommandsProtocol.infos(of:) → DyldCache.programsTrieEntries → Sequence.programOffsets and triggers a Swift runtime trap (brk #1) that try? cannot catch, killing eject. Stop routing eject through frameworkMachOsInBundle. Add a dedicated collectModifiedMachOs that does a plain Frameworks/ scan for files with a .bak sibling, avoiding every MachOKit load-command path. Also tighten isMachO to a magic-byte check only. MachOKit.loadFromFile reaches the same DyldCache code on some inputs; the 4-byte magic is a sufficient classifier for scan-time filtering. Co-Authored-By: Claude Opus 4.7 (1M context) --- TrollFools/InjectorV3+Eject.swift | 41 +++++++++++++++++++++++++++++-- TrollFools/InjectorV3+MachO.swift | 9 ++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/TrollFools/InjectorV3+Eject.swift b/TrollFools/InjectorV3+Eject.swift index 36522ce..aa403b7 100644 --- a/TrollFools/InjectorV3+Eject.swift +++ b/TrollFools/InjectorV3+Eject.swift @@ -94,8 +94,45 @@ extension InjectorV3 { } fileprivate func collectModifiedMachOs() throws -> [URL] { - try frameworkMachOsInBundle(bundleURL) - .filter { hasAlternate($0) }.elements + // Eject only needs Mach-Os that have a `.troll-fools.bak` sibling (signifying + // prior modification). Routing through frameworkMachOsInBundle drags in + // loadedDylibsOfMachO → MachOKit load-command iteration, which can hit a + // Swift runtime trap (brk #1) inside MachOKit's DyldCache handling that + // `try?` cannot catch. Do a plain filesystem scan instead. + var modifiedMachOs: [URL] = [] + + if hasAlternate(executableURL) { + modifiedMachOs.append(executableURL) + } + + let frameworksURL = bundleURL.appendingPathComponent("Frameworks") + guard let enumerator = FileManager.default.enumerator( + at: frameworksURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) else { + return modifiedMachOs + } + + for case let itemURL as URL in enumerator { + if checkIsInjectedBundle(itemURL) || enumerator.level > 2 { + enumerator.skipDescendants() + continue + } + if itemURL.path.hasSuffix(".\(Self.alternateSuffix)") { + continue + } + let atLevel2 = enumerator.level == 2 + let atLevel1Dylib = enumerator.level == 1 && itemURL.pathExtension.lowercased() == "dylib" + guard atLevel2 || atLevel1Dylib else { + continue + } + if hasAlternate(itemURL) && isMachO(itemURL) { + modifiedMachOs.append(itemURL) + } + } + + return modifiedMachOs } // MARK: - Load Commands diff --git a/TrollFools/InjectorV3+MachO.swift b/TrollFools/InjectorV3+MachO.swift index d4d45d6..018679c 100644 --- a/TrollFools/InjectorV3+MachO.swift +++ b/TrollFools/InjectorV3+MachO.swift @@ -45,10 +45,11 @@ extension InjectorV3 { } func isMachO(_ target: URL) -> Bool { - guard hasMachOMagic(target) else { - return false - } - return (try? MachOKit.loadFromFile(url: target)) != nil + // Magic-byte check only: MachOKit.loadFromFile eagerly touches DyldCache + // code in its parser, which can hit a Swift runtime trap (brk #1) inside + // MachOKit that `try?` does not catch. Magic bytes alone are sufficient + // to classify a file as Mach-O for injection/eject scanning. + hasMachOMagic(target) } func isProtectedMachO(_ target: URL) throws -> Bool { From 46030e06a2eb164672065286f97886ce37808b5a Mon Sep 17 00:00:00 2001 From: moxcomic <37604141+moxcomic@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:04:03 +0800 Subject: [PATCH 4/4] docs: document 2026-04-23 hotfix in README and CHANGELOG Adds bilingual CHANGELOG entry and a README summary covering the three Swift runtime traps fixed on top of 4.3 Build 246: the zstd streaming COW trap on the inject path, MachOKit traps on non-Mach-O files, and the MachOKit DyldCache trap on the eject path. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ README.md | 10 ++++++++++ 2 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 368b9ab..0fc7359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 4.3 Build 246 Hotfix (2026-04-23) + +修复 4.2 Build 225 至 4.3 Build 246 期间引入的三处注入/移除注入崩溃。三处均为 Swift 运行时陷阱(`brk #1`),`try?` 无法捕获。 + +### 修复 + +- **zstd 流式解压崩溃(注入路径)**:`InjectorV3+Preprocess.swift` 中 `ZStd.decompress` 以 `Data()` 起始,再 `append(contentsOf: ArraySlice)`,首次 COW 时在 `_NSZeroData` 背书状态下触发 ARC `brk #1`。改为 `UnsafeMutableRawPointer` 裸缓冲配合 `Data.append(_:count:)`,绕开 Sequence/COW 路径;循环条件收紧为 `streamResult == 0` 退出,无进展时直接抛错,避免截断输入下潜在死循环。 +- **MachOKit 非 Mach-O 文件崩溃(移除注入路径)**:commit `8a832b4` 引入的 Unity 回退扫描对 `Frameworks/*.framework/` 中所有 level-2 文件(`Info.plist`、`.car`、`.nib`、`.bin` 等)调用 `isMachO`,`MachOKit.loadFromFile` 在 `NSFileHandle.read(offset:swapHandler:)` 内部 `brk #1`。`isMachO` 收紧为仅校验前 4 字节 Mach-O / fat magic(8 种变体),非 Mach-O 文件不再进入 MachOKit。 +- **MachOKit DyldCache 路径崩溃(移除注入路径)**:commit `5ea814a` 引入的 `injectedAssetNames` 反查循环对每个带 `.troll-fools.bak` 备份的 Mach-O 调用 `loadedDylibsOfMachO`,MachOKit 的 load commands 迭代进入 `DyldCache.programsTrieEntries` → `Sequence.programOffsets` 触发 `brk #1`。移除注入本不需要该反查。新增独立实现 `collectModifiedMachOs`,仅扫描文件系统中有 `.troll-fools.bak` 兄弟文件的 Mach-O,完全避开 MachOKit load commands 路径。 + +完整根因与修复见 `hotfix-4.3-246` annotated tag。 + +------ + +## 4.3 Build 246 Hotfix (2026-04-23) [EN] + +Fixed three injection/ejection crashes introduced between builds 225 and 246. All three are Swift runtime traps (`brk #1`) that `try?` cannot catch. + +### Fixed + +- **zstd streaming decompression crash (inject)**: `ZStd.decompress` in `InjectorV3+Preprocess.swift` started from an empty `Data()` backed by `_NSZeroData` and grew via `append(contentsOf: ArraySlice)`; the first COW transition triggered an ARC `brk #1`. Rewritten to use a raw `UnsafeMutableRawPointer` buffer with `Data.append(_:count:)` — bypasses the Sequence/COW path. Loop tightened: break on `streamResult == 0` and fail fast on stalled progress instead of potentially spinning on truncated input. +- **MachOKit crash on non-Mach-O files (eject)**: The Unity fallback scan added in `8a832b4` called `isMachO` on every level-2 file inside `Frameworks/*.framework/` (including `Info.plist`, `.car`, `.nib`, `.bin`). `MachOKit.loadFromFile` `brk #1`s inside `NSFileHandle.read(offset:swapHandler:)` on such inputs. `isMachO` is now a 4-byte magic check only (8 Mach-O / fat magic variants) — non-Mach-O files never reach MachOKit. +- **MachOKit DyldCache trap (eject)**: The `injectedAssetNames` diff loop added in `5ea814a` called `loadedDylibsOfMachO` on every Mach-O with a `.troll-fools.bak` sibling; MachOKit's load-command iteration reaches `DyldCache.programsTrieEntries` → `Sequence.programOffsets` and traps. Eject does not need that filter. A dedicated `collectModifiedMachOs` now does a plain filesystem scan for `.bak` siblings, avoiding every MachOKit load-command path. + +Full root-cause notes live in the annotated `hotfix-4.3-246` tag. + +------ + ## 4.3 Build 246 (2026-04-16) 修复二次注入时可能误选已注入动态库作为目标 Mach-O 的问题。 diff --git a/README.md b/README.md index c10a02b..a2386c2 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ PRs are always welcome. - [x] `optool` is buggy so we need to compile a statically linked `install_name_tool` or `llvm-install-name-tool` on iOS to achieve a smaller package size. - [x] Support for `.deb` or `.zip`. +## Hotfix Notes (2026-04-23) + +A hotfix on top of 4.3 Build 246 resolves three injection/ejection crashes introduced between builds 225 and 246. All three surfaced as Swift runtime traps (`brk #1`) that `try?` could not catch: + +- Inject path — zstd streaming decompression tripped ARC during the first COW transition from an empty `Data` (`_NSZeroData`-backed). +- Eject path — the fallback scan called `isMachO` on non-Mach-O files (`Info.plist`, `.car`, `.nib`), reaching a trap inside MachOKit's `NSFileHandle.read`. +- Eject path — the `injectedAssetNames` backup-diff loop reached MachOKit's `DyldCache.programsTrieEntries` parser and trapped there. + +See [CHANGELOG.md](CHANGELOG.md) and the annotated `hotfix-4.3-246` tag for per-bug root cause and fix details. + ## Credits This project is inspired by [Patched-TS-App](https://github.com/34306/Patched-TS-App) by **[Huy Nguyen](https://x.com/Little_34306) and [Nathan](https://x.com/dedbeddedbed)**.