Bug description
[Bug] BytecodeMode.FULL produces duplicate classes + empty DEX files, and classMap removal becomes unreliable when combined with other patches
Component
morphe-patcher — BytecodeMode.FULL (BytecodePatchContext.compileFull) and DexReadWrite.writeMultiDexFile
Problem 1 — FULL mode emits duplicate classes and empty DEX files
When compiling a large multi-dex app with BytecodeMode.FULL, the output contains extra DEX files filled with duplicate classes:
- The last few output DEX files contain classes that are already present in other DEX files. The duplicated entries are almost exclusively nested inner classes (e.g.
Foo$a$b).
- After running a "remove duplicate classes" pass (e.g. in MT Manager), those DEX files shrink to ~140-byte empty files that can be deleted, after which the app runs perfectly.
- In other words, the entire content of these trailing DEX files is a duplicate of classes that already exist elsewhere.
Impact: the user must manually clean up duplicate classes before the APK installs/runs correctly. This step should not be necessary.
Reproduction
- Compile with
BytecodeMode.FULL.
- Use a large multi-dex app whose class count far exceeds
MIN_CLASSES_PER_SEGMENT × MAX_THREADS (so multi-threaded segmentation kicks in).
- Remove a number of classes via
classMap.
- Build.
- Inspect the output: the trailing DEX files contain duplicate nested classes; after a duplicate-class cleanup they become empty 140-byte files.
Suspected code (DexReadWrite.kt)
processSegment(...): after the per-segment loop, writeDexPoolToTemp(currentDexPool, ...) is called unconditionally, so an empty pool still produces an empty DEX file.
- Multi-threaded segmentation (
splitIntoMutableSegments + parallel processSegment): on cross-segment class-reference boundaries, some classes appear to be written by more than one segment's DexPool. (Root cause unconfirmed — needs investigation; each class is assigned to a single segment in the split logic, so the duplication is unexpected.)
Problem 2 — FULL-mode class removal is unreliable when multiple patches run together
- When a single patch removes classes from
classMap under FULL mode, the removal works: the classes are absent from the final output.
- When the same removal runs alongside other patches (multiple bytecode patches in the same build), the removed classes reappear in the final output — they are not deleted.
This suggests an interaction problem in FULL compilation when multiple patches share the same BytecodePatchContext. (Patches execute in name alphabetical order via context.executablePatches.sortedBy { it.name }; the removing patch runs last, yet its classMap.remove does not take effect on the final compiled output in the multi-patch case.)
Reproduction
BytecodeMode.FULL.
- Build with only the class-removing patch enabled → removed classes are gone (correct).
- Build with the same patch plus other patches enabled → removed classes remain in the output.
Expected behavior
writeMultiDexFile should not emit DEX files containing duplicate classes.
- No empty DEX files should be produced (or empty pools should be skipped).
classMap removals under FULL mode should reliably take effect, including when multiple patches run in the same build.
Notes
Both problems are specific to BytecodeMode.FULL. STRIP_SAFE/STRIP_FAST do not exhibit them (they only rewrite the small set of modified classes, so segmentation stays single-segment), but FULL is currently the only mode that can fully delete classes from the output, which is why these issues surface there.
Version of Morphe (Manager) and the app you are patching
Environment
- Morphe Manager:
1.21.0-dev.2
- Device: Android 16 (API 36), arm64-v8a
- Target app: large multi-dex application (
com.twitter.android, ~203,000 classes, 18 original DEX files)
Morphe logs
-
This report was actually generated by AI, but I’ve reviewed it thoroughly. The issue was identified in the project I wrote: https://github.com/sjshb57/pairip-patches
If there’s anything else you need me to do, please let me know. I’ll respond promptly. I apologize for the inconvenience.
Acknowledgements
Bug description
[Bug]
BytecodeMode.FULLproduces duplicate classes + empty DEX files, andclassMapremoval becomes unreliable when combined with other patchesComponent
morphe-patcher—BytecodeMode.FULL(BytecodePatchContext.compileFull) andDexReadWrite.writeMultiDexFileProblem 1 — FULL mode emits duplicate classes and empty DEX files
When compiling a large multi-dex app with
BytecodeMode.FULL, the output contains extra DEX files filled with duplicate classes:Foo$a$b).Impact: the user must manually clean up duplicate classes before the APK installs/runs correctly. This step should not be necessary.
Reproduction
BytecodeMode.FULL.MIN_CLASSES_PER_SEGMENT × MAX_THREADS(so multi-threaded segmentation kicks in).classMap.Suspected code (
DexReadWrite.kt)processSegment(...): after the per-segment loop,writeDexPoolToTemp(currentDexPool, ...)is called unconditionally, so an empty pool still produces an empty DEX file.splitIntoMutableSegments+ parallelprocessSegment): on cross-segment class-reference boundaries, some classes appear to be written by more than one segment'sDexPool. (Root cause unconfirmed — needs investigation; each class is assigned to a single segment in the split logic, so the duplication is unexpected.)Problem 2 — FULL-mode class removal is unreliable when multiple patches run together
classMapunder FULL mode, the removal works: the classes are absent from the final output.This suggests an interaction problem in FULL compilation when multiple patches share the same
BytecodePatchContext. (Patches execute innamealphabetical order viacontext.executablePatches.sortedBy { it.name }; the removing patch runs last, yet itsclassMap.removedoes not take effect on the final compiled output in the multi-patch case.)Reproduction
BytecodeMode.FULL.Expected behavior
writeMultiDexFileshould not emit DEX files containing duplicate classes.classMapremovals under FULL mode should reliably take effect, including when multiple patches run in the same build.Notes
Both problems are specific to
BytecodeMode.FULL.STRIP_SAFE/STRIP_FASTdo not exhibit them (they only rewrite the small set of modified classes, so segmentation stays single-segment), but FULL is currently the only mode that can fully delete classes from the output, which is why these issues surface there.Version of Morphe (Manager) and the app you are patching
Environment
1.21.0-dev.2com.twitter.android, ~203,000 classes, 18 original DEX files)Morphe logs
Acknowledgements