From f928b5889f38c05d8c833ab46fbe14df24651c9d Mon Sep 17 00:00:00 2001 From: inliver233 Date: Tue, 22 Sep 2026 20:50:01 +0800 Subject: [PATCH] Correct the rasc enum finding: the constant bodies are one query away, not lost The first write-up said asc getclass loses the method bodies of an enum whose constants override an abstract method. Querying the classes JADX itself names in its comment settles it: they exist as PasswordConverter/2/3 and both tools decompile them fully (404-407 B, containing convert/getType). What is real, and narrower: asc's outer-class view does not inline them and says nothing about it, and neither does the Python tool -- so this is a missing convenience step relative to JADX, not a regression against droidasc, and not unrecoverable code. droidasc's outer listing is richer (2,170 B vs 314 B, with \, \() and the constructors explicit). The episode is kept in the record because of how it went wrong: the harness counts literals and pointed at a real difference while misdescribing it, and the hand check turned that into an overstated claim by stopping at the first comparison instead of querying the subclass the other tool had named. --- docs/tool-verification/EXTENSION-rasc.md | 30 ++++++++--- .../references/rasc-and-droidsaw.md | 53 ++++++++++++------- skills/apk-reverse/references/toolchain.md | 15 +++--- 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/docs/tool-verification/EXTENSION-rasc.md b/docs/tool-verification/EXTENSION-rasc.md index 0112503..0aa4bd5 100644 --- a/docs/tool-verification/EXTENSION-rasc.md +++ b/docs/tool-verification/EXTENSION-rasc.md @@ -11,7 +11,7 @@ that produced it. | It is faster than the Python `droidasc` on the same archives | **observed** | 4.0–5.6× for `classes`, up to 16.2× for `findrefs` (tables below) | | It returns the same answers | **observed** | **identical class-definition sets**, 30,768 / 30,768 with **0 differences in either direction**; `findrefs string` identical on one sample, `type` a superset (390 vs 384) | | Its `getclass` decompiles ordinary classes correctly | **observed** | 16 of 17 JADX-judged classes matched literal-for-literal, 0 reorderings | -| **It silently drops the bodies of enum constants that override a method** | **observed** | `org.bouncycastle.crypto.PasswordConverter`: JADX emits three anonymous subclasses with `convert`/`getType`; `rasc` emits a bare constant list, **no warning** | +| **Its outer-class view of an enum does not inline the constant bodies, and says nothing** | **observed — and the first write-up of this row over-claimed; see §The blind spot** | JADX inlines the three anonymous subclasses; `rasc` prints a bare constant list. Both tools decompile `PasswordConverter$1/2/3` **fully** (404–407 B, containing `convert`/`getType`), so what is missing is an inlining step, not the code | | Its own benchmark's 8.0× geometric mean reproduces here | **not reproduced, and explained** | that figure is from a 343 MiB / 567k-class archive; on a 34.8 MB / 30,768-class app the ratio is 4×. Both are real; the small-archive figure is what a normal task hits | ## Environment @@ -102,12 +102,28 @@ missing : ['ASCII', 'PKCS12', 'UTF8'] VERDICT: form difference (enum constants printed as a list, literals not re-emitted) ``` -**The automated verdict understates it, and that is the finding.** The harness counts literals; the -human check shows the literals are gone *because the whole anonymous subclass body is gone* — JADX -prints `ASCII { ... convert() ... getType() ... }` and `rasc` prints `ASCII,`. `rasc` emits no -comment, no TODO, no warning. Output that is clean, plausible and incomplete is the failure shape this -repository's `pitfalls.md` exists to catalogue, so the consequence is a rule, not a caveat: confirm an -enum with no bodies using a second reader. +**The automated verdict understates it, and the hand check then overstated it. Both corrections are +kept.** The harness counts literals, and the literals in JADX's output belong to the anonymous +subclasses it inlines; `rasc`'s outer-class view prints a bare constant list. The first write-up here +said "the method bodies are gone". **That was wrong, and the way it was wrong is worth recording: the +subclasses were never queried.** They exist as their own classes and both tools decompile them fully: + +``` +$ rasc getclass 'Lorg/bouncycastle/crypto/PasswordConverter$1;' -> 404 B, has convert(/getType( +$ droidasc getclass 'Lorg/bouncycastle/crypto/PasswordConverter$1;' -> 460 B, has both +$ ... $2 and $3 the same shape, 406-407 B vs 462-463 B +``` + +So the accurate finding is narrower than the first version and narrower than the harness's phrasing: +**`rasc` does not inline enum-constant bodies into the outer class view, and it does not say so** — +the bodies are one `getclass` away, on the subclass JADX itself names in a comment +(`// from class: ...PasswordConverter.1`). Neither does the Python tool inline; `droidasc`'s outer +listing is just richer (2,170 B vs 314 B, with `$VALUES`, `$values()` and the constructors explicit). + +The general lesson this left behind, and the reason the episode stays in the record: a harness that +counts one artefact (literals) can point at a real difference while misdescribing it, and a hand +check that stops at the first comparison can convert that into an overstated claim. The corrective was +to query the classes the tool we were comparing against had named — which cost one command. ## Not established diff --git a/skills/apk-reverse/references/rasc-and-droidsaw.md b/skills/apk-reverse/references/rasc-and-droidsaw.md index 74b3a63..24fda23 100644 --- a/skills/apk-reverse/references/rasc-and-droidsaw.md +++ b/skills/apk-reverse/references/rasc-and-droidsaw.md @@ -54,15 +54,16 @@ the ones a normal task hits. `python skills/apk-reverse/scripts/rasc_build.py --verify ` reproduces the class-set comparison and is the check to run after any rebuild. -## The blind spot: abstract enum constants lose their bodies +## The blind spot: `getclass` on the outer enum does not inline the constant bodies `bench/jadx_parity.py` from the `rasc` repository compares **string literals** between JADX and `rasc` per class, counting a literal JADX has and `rasc` does not as a lost-code signal. Run here on a fair sample — app-like classes, no obfuscated identifiers, since the harness itself refuses to judge a class JADX cannot decompile — **20 classes picked, 17 judged, 1 with a difference, 0 reorderings**. -The one difference is not a formatting artefact. `org.bouncycastle.crypto.PasswordConverter` is an -enum whose constants each override an abstract method: +The one difference is `org.bouncycastle.crypto.PasswordConverter`, an enum whose constants each +override an abstract method. JADX inlines each constant's anonymous subclass; `rasc` prints the +constant list: ```java // JADX @@ -75,31 +76,45 @@ public enum PasswordConverter implements CharToByteConverter { ``` ```java -// rasc +// rasc -- getclass on the OUTER class public enum PasswordConverter implements org.bouncycastle.crypto.CharToByteConverter { - ASCII, - UTF8, - PKCS12; + ASCII, UTF8, PKCS12; public PasswordConverter() { } public PasswordConverter(PasswordConverter$1 v3) { } } ``` -**Both the per-constant method bodies and the string literals are gone, and `rasc` emits no warning -that anything was dropped.** The output is clean, plausible, and incomplete — the same failure shape -`pitfalls.md` is a catalogue of. So the rule for this layer is a rule, not a preference: +**What is actually lost, measured directly on the constants.** The bodies live in their own classes, +so the question is whether they survive as their own classes. They do: + +| class | `rasc getclass` | `droidasc getclass` | +|---|---|---| +| `PasswordConverter$1` | 404 B, contains `convert(`/`getType(` | 460 B, contains both | +| `PasswordConverter$2` | 407 B, contains both | 463 B, contains both | +| `PasswordConverter$3` | 406 B, contains both | 462 B, contains both | + +So the correct severity is **a missing inlining step in the outer-class view, not unrecoverable code**: +the source is one `getclass` away, on the anonymous subclass JADX names in its own comment +(`// from class: ...PasswordConverter.1`). This reference said "the method bodies are gone" when this +row was first written, and that was wrong — the subclasses were never queried. The two Python-visible +symptoms are the same in both tools (neither inlines), so the difference here is JADX's convenience, +not a `rasc` regression against the Python implementation. + +`droidasc`'s outer output is nevertheless richer than `rasc`'s for the same class — 2,170 B against +314 B, carrying the synthetic `$VALUES`, `$values()` and the constructors explicitly — which matters +when the enum's *shape* is the thing you are reading. + +The rule, now proportional to what was measured: - **Use `rasc getclass` for locating and reading ordinary classes.** That is what it was measured on. -- **When the class is an `enum` with constant-specific bodies, confirm with a second reader** - (`droidasc getclass`, or JADX). A collapsed enum is visible on sight: the constants are printed as a - bare list and no method bodies follow. Treat "enum with no bodies" as *unverified output*, not as - "this enum has no logic". -- **Never treat a single decompiler's output as the definition of what exists.** This is the same rule - the VMP section states about opcode tables, applied to a class body. +- **When the class is an `enum` with constant-specific bodies, read the constants.** A collapsed enum + is visible on sight (bare constant list, no bodies). Either query the anonymous subclasses directly + (`Lpkg/Enum$1;`) or use `droidasc`, whose outer-class listing is more complete. +- **Never treat a single decompiler's output as the definition of what exists.** The same rule the VMP + section states about opcode tables, applied to a class body. -Frequency matters for calibration: 1 of 17 judged classes here, and the other 16 agreed literal for -literal. So the correct posture is **adopt, with the cross-check kept**, not "reject" and not -"drop the Python tool from the kit". +Frequency for calibration: 1 of 17 judged classes here, and the other 16 agreed literal for literal. +Adopt it, and keep the cross-check — but do not describe this as lost code. ## What it does not change diff --git a/skills/apk-reverse/references/toolchain.md b/skills/apk-reverse/references/toolchain.md index 1c4d086..73ee17c 100644 --- a/skills/apk-reverse/references/toolchain.md +++ b/skills/apk-reverse/references/toolchain.md @@ -56,12 +56,15 @@ python scripts/rasc_build.py --build # needs git + rustup; ~2 min python scripts/rasc_build.py --verify app.apk # compares class sets against droidasc, fails on a difference ``` -**The one thing to know before trusting its `getclass`:** an `enum` whose constants override an -abstract method is printed as a bare constant list, and **the per-constant bodies are dropped with no -warning**. Sampling 20 app-like classes from a real app, 17 were judged by the JADX-parity harness and -16 agreed literal for literal; the one that differed was exactly this shape. So: use it to *locate* -and to read ordinary classes, and confirm any enum that shows no bodies with a second reader. The -measurements, the failing class and the build traps are in `references/rasc-and-droidsaw.md`. +**The one thing to know before trusting its `getclass`:** on an `enum` whose constants override an +abstract method, the outer class is printed as a bare constant list and the per-constant bodies are +**not inlined** — no warning, and none in the Python tool either, because both leave those bodies in +their own subclasses. They are one query away (`Lpkg/Enum$1;`, and both tools decompile those fully); +`droidasc`'s outer-class listing is simply richer (2,170 B vs 314 B on the measured class, carrying +`$VALUES`, `$values()` and the constructors). Sampling 20 app-like classes from a real app, 17 were +judged by the JADX-parity harness and 16 agreed literal for literal; the one that differed was exactly +this shape. Use it to *locate* and to read ordinary classes, and read the constants when an enum shows +no bodies. The measurements and the failing class are in `references/rasc-and-droidsaw.md`. ### droidasc (ASC) — ask an APK "who references this?", in one query