Project Overview
apktool_diagnostics.py is a diagnostic tool built on top of apktool, the standard tool for decompiling and rebuilding Android APKs. It checks whether an APK survives a decompile → rebuild round trip without silent data loss, flags known-CVE-style path traversal risks in .arsc resource tables, and diffs .dex output when a rebuild diverges from the original. It's meant to answer one question reliably: "did apktool actually reproduce this APK correctly, or did something quietly go missing?"
Bug Fix or Performance Improvement
While stress-testing the tool against real-world APKs (F-Droid client, NewPipe), I found two bugs in the tool itself — both are the "silent, not loud" kind that are worse than a crash because they let you trust a wrong answer.
Bug 1 — -unbounded allocation while scanning **.arsc string pools (crash / OOM risk).**
The scanner walks a binary resource table looking for ResStringPool chunk headers (0x0001). Because it's a best-effort byte scan rather than a full chunk-tree walk, a coincidental two-byte match on unrelated data could be misread as a real chunk header, handing the parser a garbage stringCount with no sanity check. The following loop would then try to iterate that many entries — on a crafted or corrupted .arsc, that's an unbounded allocation/iteration with no upper bound, i.e. an OOM or hang.
Bug 2 -silent undercount in the round-trip summary (data-loss-masking bug).
The round-trip check classifies problems into two buckets — files missing from the rebuild, and files whose content diverged — then folds both into a single summary dict keyed by category. The original code did summary[k] = len(v) for each source dict in turn. If the same category key showed up in both buckets (which happens), the second write silently clobbered the first instead of adding to it. The tool would report a lower anomaly count than actually existed — exactly the kind of bug that erodes trust in a tool whose entire purpose is catching silent problems.
_
Fix 1_ — bounds-check stringCount before trusting it:
**`if stringCount == 0 or size <= 0 or size > len(data) - off:
off += 4
continue
- # Sanity bounds: a coincidental 0x0001 match on non-chunk
- # bytes can yield a garbage stringCount that would try to
- # allocate/iterate absurdly - bail out instead of OOMing.
- idx_off = off + 28
- if stringCount > 5_000_000 or idx_off + stringCount * 4 > off + size:
- off += 4
- continue is_utf8 = bool(flags & (1 << 8)) strings = []
- idx_off = off + 28 base = off + stringsStart for i in range(stringCount): entry_off = struct.unpack_from('<I', data, idx_off + i * 4)[0]`**
Fix 2 — accumulate instead of overwrite:
`**summary = {}
for src in (missing_classified, diverged_classified):
for k, v in src.items():
- summary[k] = len(v)
- summary[k] = summary.get(k, 0) + len(v)**`
Full diff: zowskyy/apktool-diagnostics@33d7924
My Improvements
For the OOM fix, the guard has two conditions rather than one: stringCount > 5_000_000 catches the "obviously garbage" case cheaply, and idx_off + stringCount * 4 > off + size catches the subtler case where stringCount is plausible-looking but would still read past the chunk's own declared size — the second check is the one that actually matters for a maliciously crafted file, since an attacker tuning the value to dodge a flat cap would still get caught by the bounds check. I moved the check to sit immediately after the existing stringCount == 0 guard rather than tacking it on later, so all the "is this chunk even real" logic lives in one place instead of being scattered.
For the summary bug, the fix is a one-character-class change (= → += in spirit), but finding it required noticing that missing_classified and diverged_classified aren't guaranteed to have disjoint keys — they're independent classifiers that happen to share a category vocabulary. I caught it by deliberately constructing a test APK where a category appeared in both buckets and asserting the summary count against a hand-computed total, rather than trusting the tool's own output as ground truth.
Both bugs were caught by the same practice: never trust a diagnostic tool's clean report without an independent check against a real, adversarial input. The OOM bug only showed up because I fed the scanner a byte sequence deliberately chosen to produce a false chunk-header match; the summary bug only showed up because I hand-verified counts on a fixture built to collide across both classifiers.
Update
The apktool_diagnostics.py tool is now complete, production-ready, and live on GitHub.
Full toolkit: 5 subcommands (security-scan, framework-check, roundtrip, dex-diff, corpus)
GitHub: zowskyy/apktool-diagnostics
Quick start: python3 apktool_diagnostics.py security-scan your-app.apk
The two bugs described in this post have been fixed (commits d362bb2 and 33d7924). Real test results from F-Droid client and NewPipe are included in the repo.
Related: See The Advisory Said It Was Fixed for a full CVE-2026-39973 reproduction using this toolkit.
Top comments (0)