Commit Graph
81 Commits
Author SHA1 Message Date
root 90d9bd3e57 Audit the ref drop folder a third time, and measure what it can measure
ref/ still held the sixteen write-ups absorbed on 2026-10-24, with unchanged mtimes, so this pass
re-read them instead of re-absorbing. Nine generalisable mechanisms were still missing: Pyarmor's
outer container layer and its three nonce scopes (plus the live dump being structurally incomplete),
fixed-width arithmetic in the 12-byte key VM, Python compiled to native extensions, the rebuilt
libil2cpp ELF fingerprint (JNI_OnLoad / own DT_INIT / SHT_LOUSER), a container VM's block schedule,
driving String::NewLen over every literal index as an oracle, VMs that live in the managed layer,
array literals materialised by InitializeArray instead of .rodata, framework crypto primitives as
the hook target, a comparison gate as a prefix oracle, and the debugger's own stop policy
(SIGPWR/SIGXCPU suspend-to-crash, one database per activity).

One absorbed claim was wrong and is corrected: python-bytecode-recovery section 5 said an unset
nonce[15] decrypts the first block plausibly and then diverges. Counter mode has no such state --
the same nonce differing in that one byte changes the keystream from block 0, measured at 16/16
bytes.

verify_ref_claims_10c.py carries the three device-free checks. Its first draft was decorative -- the
comparison check counted work inside the loop it was measuring -- so the comparators are now
compiled at -O2, every check has a negative control that must come out empty, and the verdict
travels as a return value rather than through a global list. tests/unit/test_verify_ref_claims_10c.py
injects the wrong claim into each check and requires the failure.

Registration: two capability rows (il2cpp-managed-layer-bytecode-vm unverified,
comparison-cost-oracle observed for the host half), two evidence-summary rows, one new and three
extended route-inventory rows, three coverage bullets. SKILL.md and routing.md unchanged; the record
is EXTENSION-ref-absorption-2026-10c.md. Gates and pytest green (796 passed).
2026-10-27 00:33:49 +08:00
root 068280a180 Verify the absorbed claims on the host and label them per claim
Adds docs/tool-verification/fixtures/verify_absorbed_claims.py: seven device-free checks covering the mask identity, the PKCS#7 length law, zero-fill versus skipped offsets, the hidden-salt search, compression drift, Python percent-encoding, and AES CTR/CBC/ECB separation.

The AES run falsified an absorbed claim: zero prefix-XOR does not imply a stream cipher (CBC and ECB show it too). The discriminator is a one-byte flip -- 1 changed byte for CTR, all following blocks for CBC, one block for ECB -- and protocol-reverse section 7 now states that, with the capability row recording the correction.

Labels are now per claim rather than per section: observed where a host command produced output, inferred for rules derived from the mechanisms, unverified for anything needing a device, a JDK, a lz4 binding or a VM sample. native-vm-recovery's claim-strength paragraph is aligned with the repository definition, and the cipher row moves to inferred.

Gates and pytest green.
2026-10-25 12:19:37 +08:00
root 45ba13c0f3 Absorb three write-ups on dump timing, native VM devirtualization and encrypted bodies
vm1: dump window at android_dlopen_ext entry, raw-syscall reader, zero-filled holes; predicate-first patching with tiered ordering; module-filtered signal attribution.

vm2: devirtualization to a straight-line (op, args) program with constant table, output map and template; version binding; per-node validation.

vm3: three black-box cipher probes, involution wrappers, encoding/compression drift; a certificate-derived salt inside the signature digest.

Also adds pitfalls P49-P50, three capability rows (all partial/unverified), the evidence/route records and EXTENSION-ref-absorption-2026-10b.md. Renumbers native-vm-recovery section 11 to 12. Gates and pytest green.
2026-10-25 12:10:41 +08:00
dabao1955 f497180e35 Fix the corpus count, recalibrate the token estimate, add a PR template
- check_budget.py counted scripts/__pycache__/*.pyc in its corpus note, so the
  same commit reported 27 scripts on a machine that had run the suite and 16
  on a clean checkout. The routing gate said 16 throughout; two gates
  disagreeing about one tree was the finding.
- TOKENS_PER_CHAR was calibrated against a 67,011-character SKILL.md and the
  docstring's arithmetic (0.30 characters-to-tokens) did not describe that
  measurement. Re-measured here: 54,983 chars -> 12,906 tokens = 4.260; the
  constant and both comments now carry the number this tree measures.
- test_budget_measurement.py pins the corpus exclusion, so a bytecode cache
  cannot re-enter the count and quietly change it again.
- .github/PULL_REQUEST_TEMPLATE.md carries what CONTRIBUTING.md asks a
  contributor for: what was run, the four labels, what has to move with the
  change, and the boundary that was not exercised.
2026-10-24 14:55:10 +08:00
root f3863f070c Absorb sixteen write-ups on IL2CPP, linkers, VM protectors and Pyarmor
Add four references for protected IL2CPP metadata, linker-level SO repair, VM-protector idioms and Python bytecode recovery, plus the generalizable mechanisms they exposed in the existing topic files.

Register the routes in the symptom index, routing tables, capability matrix, evidence summary, coverage docs and both READMEs. Every absorbed claim stays unverified: no metadata was decrypted, no loader reimplemented, no VM traced and no Pyarmor sample unpacked here.
2026-10-24 14:17:54 +08:00
dabao1955 bd6dd0c59c Absorb the Unity and IDA debug-server write-ups
Two reference files, both absorbed rather than measured and labelled unverified
throughout:

- unity-runtime.md: Mono versus IL2CPP from the shipped files, Il2CppDumper over
  libil2cpp.so + global-metadata.dat with the metadata magic as the encryption
  check, the four-step IDA import order, frida-il2cpp-bridge, the
  dnSpy-Unity-mono patch set, and the audit probes for a report deliverable.
- ida-so-debugging.md: ABI-matched dbgsrv, root server, am start -D plus the
  JDWP handshake that actually releases the process, and the
  extractNativeLibs=false failure where the library is mapped from base.apk and
  no breakpoint can bind.

The registration surface moves with them: two symptom rows mirrored into
routing.md (with its stale row counts corrected), the reference index, a Unity
section in framework-runtimes.md, a debugger row in dynamic-frida.md's
alternatives table, pitfall P48 for the silent no-bind, the capability matrix
(blocked-unity-il2cpp-react-native-ios narrowed, two partial/unverified rows
added), evidence-summary, coverage-and-limits, route-inventory,
known-limitations and both READMEs.
2026-10-21 22:19:26 +08:00
dabao1955 aa9af54854 Absorb gaps from three public reverse-engineering skills
Batch 1 (full): Kotlin original-name recovery (new reference + zero-dep
kotlin_name_recovery.py mining @DebugMetadata/@Metadata.d2/jadx rename
comments, with -o/-p/--grep query modes); jadx advanced flags and a
Fernflower/Vineflower second-engine tier in toolchain.md; BuildConfig
sweep habit in recon.md; WebSocket/long-connection state-chain section
in protocol-reverse.md.

jsvmp-only (batches 2-3): A/B/C recovery ladder with black-box reuse
boundary and a wire-parameter family index in jsvmp-recovery.md; new
js-ast-deobfuscation.md pipeline layer (family detection, per-family
pass order, safe-rewrite rules, minimal bootstrap eval, residue metrics)
with js_obf_detect.py weighted detector.

Wiring: routing.md/SKILL.md symptom+index rows, coverage-and-limits,
route-inventory, evidence-summary, capability-matrix (+3 entries,
labels kept honest: scripts observed on fixtures, absorbed methodology
unverified). 12 new unit tests.

Also: silence two environment-dependent mypy errors (yaml/frida
sentinel assignments) and bump mypy python_version 3.9 -> 3.10 with the
trade-off recorded.

Gates: check_repo/check_refs/check_budget/check_commands/check_routing
PASS; pytest 787 passed; flake8 clean; mypy clean.
2026-10-20 11:54:59 +08:00
dabao1955 8f32bb5e29 Bump actions version 2026-10-20 10:35:15 +08:00
root 9c2b23f7ce Revert "[temp] Use codeberg machine"
This reverts commit 2a6d972a0f.
2026-10-20 10:27:53 +08:00
root 00e11ee9af Revert "[temp] Fix workflow error"
This reverts commit b8dbd60b02.
2026-10-20 10:27:44 +08:00
dabao1955 b8dbd60b02 [temp] Fix workflow error 2026-10-20 04:22:07 +02:00
dabao1955 2a6d972a0f [temp] Use codeberg machine 2026-10-20 04:20:08 +02:00
root caa981b7e7 Absorb the jsvmp series: JS VMP recovery, and a round-trip-tested loader decoder
A new documented route for logic protected as a private JS opcode stream (jsvmp):
recognition signals, the call-it-first route, stack- and register-machine reading,
AST instruction extraction, the four JS evaluation traps, constant folding and
control-flow recovery. Every procedure is inferred (absorbed from three public
write-ups, anonymized; none run on a real target) and labelled so.

The one shipped piece is the loader decoder (base64 -> varint -> ZigZag), measured
against its own round trip (2,016 values exact, canonical varint/ZigZag layouts
pinned, truncation raises) and it decodes the write-up's own sample literal end to
end. Wired into the symptom index, routing tables, coverage ledgers and the
capability matrix; RN/Hermes still not covered, with the jsvmp carve-out stated.

Also clears two warnings: an E302 in the new script, and the long-standing
check_commands warning where a tool-verification transcript quoted check_budget.py
with its result inline (annotations moved to a continuation line, content intact).

Ran: check_repo/check_refs/check_routing/check_commands/check_budget (all exit 0),
flake8 clean, pytest 763 passed / 199 skipped / 25 xfailed.
2026-10-19 22:33:01 +08:00
dabao1955 eca9c19ee4 Fourth audit pass: seccomp as anti-instrumentation, and the anti-debug sweep
Two focused audits: the seccomp article under the lens of seccomp as the thing
that blocks your tools (not just a sandbox fact), and a corpus-wide sweep for
anti-debug material against the detection file that owns the topic.

The seccomp audit's strongest finding was repo-internal: advanced-unpacking
claimed seccomp filters were among the 5.10-gated layers "closed" on the 4.14
reference device -- while the repo's own extension notes observed Seccomp: 2
active on that very device, and the hardening file says "any modern kernel".
The contradiction is resolved in favour of the measurement: seccomp is now
named as the one syscall-layer mechanism NOT closed there. The hardening row
gains the full decision set (TRACE hands a syscall to your tracer; KILL_PROCESS
is the action that impersonates a detector), the bionic-installs-at-init timing
with its unverified-network marker (the cnb.cool mirror carries frameworks/base,
libcore and art but not system/seccomp or bionic -- the URLs to retry are
recorded at the claim), the external-tracer-exempt mechanism note (a filter
evaluates the installing process's own syscalls, so "they seccomp-blocked my
attach" is structurally wrong), and Seccomp_filters>1 as the target-installed-
filter signal.

The detection funnel gains the fourth cause its environment table lacked: the
process's own filter denying the agent's syscall -- Seccomp: 2 is the normal
state of every zygote-forked app, and the discriminator is a SIGSYS/SYS_SECCOMP
tombstone naming YOUR syscall, vanishing in the control run. This also finally
interprets the Seccomp field anti_detect_probe.js has been capturing all along.
Snowblind (2024) lands as a named pattern only -- recognition strings in the
third-party-builds marker table and a Step-2C rationale row; the filter+handler
implementation stays out on the stated line: recovery changes the analyst's
observation channel, weaponization makes the target's policy stop applying.
svc_scan.py learns syscall 277; seccomp-tools joins the toolchain table as an
offline filter reader with unverified grades.

The anti-debug sweep's verdict was the opposite shape, stated plainly: on this
topic the corpus is the shallow party and the repo is the owner -- TracerPid,
loader discipline, Frida-detection specifics are all already covered deeper,
and no corpus-driven table was manufactured. What it did add: the
explain-the-target half of SELinux policy reading (wildcard grants, attribute
memberships, missing type transitions -- why the app reaches what you thought
was closed, with the read/inject boundary drawn), the magiskpolicy one-liner
for the measured frida-server denial, and the three kernel-observation gate
knobs (kptr_restrict, dmesg_restrict, perf_event_paranoid) in the ladder rule.

Gates re-run: routing coherent, 414 section references zero dangling, repo
clean (after check_repo caught a broken string literal this pass introduced --
the gate did its job), pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 22:08:14 +08:00
dabao1955 ce227a13c5 Third audit pass: WebView as a probing channel, source-verified against AOSP
Three re-audits under new lenses -- the WebView/JS-bridge as an active probe for
hidden APIs and core logic (not just a config risk surface), environment blind
spots, and jvm-art internals -- each instructed to verify claims against the
cnb.cool AOSP mirror. The subagents' network was denied, so the mirror checks
were run in the main session: WebView/WebView.java and WebSettings.java javadocs,
platform.xml, art/runtime/hidden_api.cc, ActivityThread.java, and
libcore's DexPathList.java were all fetched and cited.

The probing lens rewrote system-ipc-audit.md section 3 around what the audit
found: the bridge-inventory hook over-reported (it printed every declared method;
since API 17 only @JavascriptInterface-annotated public methods are exposed --
javadoc-verified, with the targetSdk<17 legacy caveat), the risk table gained a
defaults column so absence stops being read as off (setAllowFileAccess defaults
true through targetSdk 29; the FileURLs pair has been false-and-deprecated since
API 16), and the DevTools entry was split into its two channels (chrome://inspect
self-discovers; the forward is for /json, socket found via /proc/net/unix) with
the process-wide and debuggable-auto-enable gates stated. New: the five-step
probing playbook -- global diff, dictionary probe (enumeration of the injected
object returns empty on modern WebView), arity/type oracle with the parallel
logcat watch (a Java exception stack can name the handler class without any
hook), return-oracle calibration, and the origin-gate read -- each routed back to
the Java class for an ordinary hook.

jvm-art: the hidden-API boundary sentence in dynamic-frida.md was directionally
right and mechanically wrong; it now states the real policy (targetSdk>=28
gating; greylist warn-only; blocklist and greylist-max-X deny -- verified against
hidden_api.cc), the caller-domain mechanism that explains why Frida's bridge
passes where app reflection fails, and the debuggable/policy exemptions. The
classloader walk (pathList -> dexElements -> Element.dexFile, field names
verified against current main) joins the frida-dexdump route as the hand-rolled
fallback that also says which loader owns a dex.

Environment: wireless debugging/pairing (Android 11+, pairing vs connect ports,
plaintext-TCP caveat), the -d downgrade gate (INSTALL_FAILED_VERSION_DOWNGRADE is
a build-type fact), --user N multi-user handling, and run-as as the cheapest
non-root inspection route. One auditor correction was REJECTED by verification:
current-main platform.xml assigns the shell uid only INTERNET, so the proposed
"shell holds INSTALL_PACKAGES/INJECT_EVENTS" rewrite was itself wrong; the
existing measured claim stays, now with the platform fact recorded beside it.

Gates re-run: routing coherent, 412 section references zero dangling, repo clean,
pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 21:52:42 +08:00
dabao1955 2bb54a72fb Second audit pass: fill gaps and correct drift, with verification where possible
Five read-only re-audits of the source material under a different lens -- not "is
it covered" but gap-filling, drift correction, and a user-assisted framing in
which the AI names the hook point or command and the human builds, runs, and
pastes back. The lens change earned its keep: the audits found drift that the
coverage lens could not, including two errors in what this repository had itself
absorbed.

Drift corrected, worst first:

- native-and-so.md: the absorbed syscall example loaded #0x47 (71, sendfile) --
  the source article's "real libc fragment" mislabels its own bytes, and the
  absorption inherited it. Now #0x40 with the mislabel named, the bionic wrapper
  epilogue (cmn x0,#0x1,lsl#12; b.cs) added as the discriminator between libc's
  own svc sites and a protector's inline ones, and the "no X8 load means data"
  claim softened to match svc_scan.py's nr=? semantics.
- system-ipc-audit.md: the generated-proxy example named installPackage, dead on
  modern IPackageManager -- swapped for getPackageInfo (already this skill's own
  hook) and IPackageInstallerSession.commit. Transaction-code guidance now says
  read the TRANSACTION_* constants, do not count the .aidl. getCallingUid gains
  its app-process reading semantics (own uid; uid 1000 on relayed callbacks --
  the "the system is calling my app" misread). service call gains the full type
  tags, the automatic interface token, and the default-binder-context boundary.
  The binderfs path variant and binder_calls_stats enable are recorded.
- packers.md: "dlopen not found = your bug" was one cause wearing three faces --
  now the namespace/DT_NEEDED/ABI triage with the classloader-namespace logcat
  discriminator and the 7.0+/8.0 gates; plus the extractNativeLibs="false"
  caveat on add-a-file tests, matching what so_constpatch.py already refuses.
- lsposed-and-modules.md: LSPatch paragraph re-labeled per evidence -- the
  delivery-class reasoning stays repo reasoning, the absorbed facts now carry a
  network-verified marker (upstream README: dex+so insertion, rootless, min
  Android 9; LSPosed active as of 2026-10-14). The api:100 parenthetical no
  longer implies a rewrite requirement.
- dynamic-frida.md: the LD_PRELOAD row gains its version gates (7.0+, Treble 8.0).

Gaps filled: signed-vs-unsigned branch suffixes and PAC/BTI recognition with the
SIGILL-on-patch consequence (native-and-so); the on-disk oat/vdex artifacts
check with the InMemory and quickened-opcodes gates, cross-linked from the
"decoder you did not write" rule (advanced-unpacking); the hook-point map --
funnel vs hop, ATMS/install/session gates stated (system-ipc-audit); the
interactive gdbserver escape hatch in detection Step 2, with the
frida-specific-vs-tracer-generic discrimination, and the user-session command
core in the toolchain row; the Hook vocabulary table translating instructions
into MethodHookParam calls, javadoc-verified, closing the one point where the
user-assisted loop forced a repo exit (lsposed-and-modules); cross-links from
the neutralization and TLS hook-body tables to their delivery vehicles.

Gates re-run: routing coherent, section references zero dangling and zero
warnings, repo clean, pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 21:32:41 +08:00
dabao1955 8c00c88035 Add the AArch64 reading layer the native file assumed
native-and-so.md taught how to find and hook JNI calls in a stripped library but
never said what the listing itself means: registers were named only incidentally
(pitfalls P43 assumes "the AArch64 argument registers" as known), the frame
pattern appeared in examples without explanation, and the addressing shapes were
nowhere. The source notes' ARM64 essentials 00-02 are exactly that missing layer,
and the original scoping pass had filed the whole series under "language teaching"
after reading only its debugging chapter -- same silent-truncation failure mode
the article audit already caught twice.

New section "Reading AArch64 -- registers, addressing, frames", placed after the
call-site recipe it underlies: the register role/saved-by table with the Wn-
zeroes-the-high-half truncation trap, the AAPCS64 argument rules including the
separate floating-point channel and the JNI consequence (env X0, thiz X1, user
args X2 -- the reason the vtable shape above loads from X0), the canonical
prologue/epilogue with the X29 chain identified as what every backtrace walks,
the six addressing shapes on sight (including SXTW-vs-UXTW and the adrp+add PIC
pattern the source notes omit but every string xref wears), movz/movk immediates
with the byte-patching consequence, and the mov-x8-plus-svc syscall shape tied
back to what svc_scan.py actually locates -- including the data-not-code reading
of an svc hit with no X8 load nearby.

03 (GCC inline-asm syntax) and 05-07 (ROP, shellcode, overflow exploitation)
stay out by design: development and exploitation material, not lookup value for
reading a library during a reversing task. Provenance and strength labels at the
point of use, retrieval date included. Gates re-run: routing coherent, 409
section references zero dangling, repo clean, pytest 751 passed / 194 skipped /
25 xfailed.
2026-10-19 21:09:36 +08:00
dabao1955 2f6b26f4dd Close the gaps an independent audit of the source articles found
The milksu integration was audited a second time, independently: four read-only
passes, each taking a group of the source articles and verifying the coverage
claims against the whole repository (references and scripts), with instructions
to contradict rather than confirm. Their findings corrected the prior pass in
both directions and drove every change here.

The audit's most useful correction was against the integration itself: the
ActivityThread.currentApplication() Context idiom had been reported missing
while it ships working in scripts/rpc_template.js and scripts/sig_probe.py --
the pass had audited only references/. The fix is a cross-link, not absorption.
Two more claims were narrowed (advanced-unpacking covers the classloader lab,
not the dynamic-proxy lab; readelf coverage lives in toolchain.md, not the
files first proposed), one attribution was repaired, and one provenance note
was added: the absorbed Binder hook is not the article's script -- the source
hooks Binder.transact with server-side getCallingUid() semantics inside; the
form kept here is corrected and now says so.

What the audit found genuinely absent, now integrated (all labelled unverified
or inferred with the retrieval date at the point of use):

- system-ipc-audit.md: generated-proxy hooking (decoded arguments beside the
  raw transact), the debugfs binder node/ref view for "why does my hook never
  fire", two new Intent signature rows (intent-scheme handoff, ClipData URI
  grants), am start/broadcast action-data-extras driving forms, the Intent
  monitor with live nested-Intent detection, the deep-link getData triage,
  three config rows the earlier table silently truncated (JavaScript, DOM
  storage, mixed content), the loadUrl dynamic hook, the logcat Console
  fallback channel, the four-surface static WebView pass, and the attestation
  KeyDescription/OID reading with the teeEnforced extraction discriminator.
- lsposed-and-modules.md: LSPatch as the fourth route -- module-class
  authoring, repack-class delivery, runs on unrooted phones; plus existence
  proofs for the resource-hook and constructor/field/hook-all API surface.
- dynamic-frida.md: the scripts' own Context idiom cross-linked with the
  Android 9+ hidden-API reflection boundary; dynamic proxy as an interception
  primitive; the LD_PRELOAD-closed-on-Android row in the non-live table.
- toolchain.md: the lldb-server remote recipe, with the attach-is-detection
  pointer.
- java2c-and-jni-sinking.md: the __<proto> mangling decode route step 2
  silently depended on.
- advanced-unpacking.md: the local-reference cap hazard on active-invocation
  loops.
- native-and-so.md: the JNI width/type map beside the vtable shape.
- recon.md: the MODE_WORLD_READABLE/WRITABLE row the earlier table dropped.

Gates re-run: routing coherent, 409 section references with zero dangling,
repo clean, pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 21:05:08 +08:00
dabao1955 526d487f1a Fill the reading-layer gaps the IPC reference exposed
The first milksu integration only took the system-boundary material and dismissed the
workflow content as overlapping deeper files. That dismissal was asserted, not audited;
auditing it found five places where the repository owns the procedure but not the
reference material the procedure leans on:

- dex-patching.md: a reading-level smali cheat sheet (type descriptors, register
  conventions and the .registers coupling, the five instruction families that carry
  control flow, obfuscation signatures). The file taught how to edit a method you can
  already read; nothing taught the reading.
- recon.md: the sensitive-behavior grep table (shell-outs, JS bridges, mutable
  PendingIntents, WebView width, dynamic dex loading, intent redirection) -- the
  signature-check section had one pattern, the behaviour sweep had none.
- detection-and-anti-analysis.md: the named Java-level root checks and their one-line
  neutralizations, as the concrete form of Step 2A's "simple, localised check" --
  scoped to the named detector, never a blind stack.
- tls-and-cert.md: the two OkHttp-path client sites (CertificatePinner.check,
  SSLContext.init) that the OkHttp section previously pointed at without naming, each
  annotated with what it honestly buys.
- native-and-so.md: the JNIEnv vtable call shape for reading JNI calls in a stripped
  library, with the offset-calibration caveat.

All absorbed material is marked unverified with the retrieval date (milksu.org,
2026-10-19) at the point of use; syntax and layout facts that follow from documented
platform behaviour are marked inferred. Gates re-run: routing coherent, 409 section
references with zero dangling, repo clean, pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 20:29:03 +08:00
dabao1955 60224a26b9 Add the system-IPC audit reference (Binder, Intent, WebView, SELinux, attestation)
The reference set owned the app's own wire (protocol-reverse.md) and the in-process
runtime (dynamic-frida.md), but not the platform boundary between them: what the app
asks system_server to do over Binder, which Intent/JS-bridge inputs an unmodified
build exposes, why an avc: denied is blocking instrumentation, and what a Keystore
attestation check means for a rooted-device workflow. That gap is where several
long detours start -- a hook that could never fire because SELinux says the domain
cannot reach the target, or a "detection" that is actually a server-verified
attestation no client patch can satisfy.

Everything here is absorbed from an external notes source (milksu.org, Android
internals notes, retrieved 2026-10-19) and is labelled accordingly: the commands and
hook scripts are unverified in the coverage-and-limits.md sense -- reported by someone
else, never run by this repository -- while the platform mechanisms they rest on
(AIDL codegen arithmetic, kernel-filled caller identity, FLAG_IMMUTABLE since
Android 12) are inferred from documented AOSP behaviour, with a note that each fails
loudly if wrong. The strength note at the top of the file states this before any
content, and each script carries its own unverified marker, so a reader cannot
mistake a starting point for a measured procedure.

Routing gains one row next to server-api.md. Gates re-run: check_routing.py
coherent (53 rows / 51 files), check_refs.py 0 dangling over 408 section
references, check_repo.py clean, pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 20:25:12 +08:00
inliver233 1b94651b1e Fix the bare path that turned CI red
The README named `references/routing.md` without the `skills/apk-reverse/` prefix, and
`check_repo.py` requires that form in the README. CI was right; the commit that introduced it was
pushed without re-running the gates, which is the one step CONTRIBUTING.md asks a contributor to run
and I skipped.

No behaviour changes: the path is now spelled the way the gate requires in both language editions.
The local reproduction is the same as CI's, so this was a process failure rather than a
platform-dependent gate — worth stating plainly, because "it passed locally" is exactly the excuse
the gate cannot hear.
2026-10-16 19:29:14 +08:00
inliver233 7d6b4c7fe9 Do not overstate what the progressive split buys
SKILL.md loads in full; a reference loads when the symptom index or the routing table sends you to
one. That is usually described as cheaper context, and only half of it has been measured here: the
always-loaded body has a measured size and a gate that fails when it grows.

The other half has not been measured -- how many references one real question pulls in, and whether
fetching them one at a time costs more than the alternative would have. A question that needs four
references is not obviously cheaper than one longer file, and each reference also carries the index
row you have to read to find it.

The split's economy is therefore `unknown` and the body budget is `observed`. Saying so is the point:
the label discipline is worth nothing if the repository exempts its own design choices from it.
2026-10-16 19:25:02 +08:00
inliver233 905544ea89 Name the three path namespaces, and where a crossing is legal
A path in this skill lives in one of three places -- the host filesystem, the device filesystem, or an
APK entry name -- and the same string means something different in each. Two properties decide most
of the failures that follow:

  * a host path does not exist on the device and vice versa; `adb push`/`adb pull` is the only place
    a translation happens, and it is the caller's to make;
  * an entry name is always `/`-separated and case-sensitive whatever the host OS is, so a path built
    with `os.path.join` matches nothing on Windows.

The symptom is a "no such file" naming a path you can see exists -- on the other side of the boundary.
`environment.md` now says to ask which namespace a string is in before asking whether the file is
there, and points at `validate_device_path`, which refuses a host-shaped path before it reaches the
device shell where it would fail far from the caller.

The leak scan caught the first draft: the example in the table was a real-looking POSIX home path and
`scan_leaks.py` flagged it as target identity. The scanner was right and the example was wrong; it now
uses a placeholder.
2026-10-16 19:23:59 +08:00
inliver233 5c3ba9cd2e Stop reporting a tool's resource failure as a finding about the target
A JVM reader that dies on a large or deeply nested input -- OutOfMemoryError, an OOM kill, a
container limit, a timeout -- prints an empty result that looks exactly like a reader that read the
file and found nothing. That is the `present but unreadable` case SKILL.md §Coverage names, and it is
where a wrong "that class is not in there" gets recorded.

The new section in toolchain.md says what a failed analysis actually supports (a claim about the
route, not about the artifact) and the three things to check before a negative from a heavy tool
becomes a finding: the exit code and stderr rather than stdout alone, a re-run with room to work, and
the indexed-query route that answers most location questions without materialising anything. A
deadline you set yourself gets the same reading -- it reported nothing about the target.

Also: the README claimed the scripts "work identically on Windows, macOS and Linux", while every gate
run, every test result and every figure in the evidence record comes from one Windows host. The claim
now carries the strength it actually has -- measured on Windows, `unverified` elsewhere -- and names
the usual suspects (path separators, `os.pathsep` in `APKREV_TOOLS`, console encodings, ADB shell
quoting) so a first run on another platform has somewhere to look.
2026-10-16 19:20:45 +08:00
inliver233 6ab905b2c8 Four-state labels, and the negatives that used to look alike
Every claim carried one of three labels, and the third was doing two jobs. `unverified` meant
both "someone asserted it and nobody here paid for the counters" and "no evidence was gathered
at all" -- different statements with different next actions. This adds `unknown` for the second.

The distinction that pays for itself lives inside a *negative* result, because an empty probe has
three causes and only one of them is a finding:

  absent                  -- coverage reached the question, nothing is there
  present but unreadable  -- the thing is there and this route cannot read it
  unknown                 -- nothing covered the question

The middle one is the dangerous one: it produces byte-identical output to the first, and it is the
normal case on the input this skill is aimed at. A packed APK returning four classes is not an APK
with four classes, and "the string was not in the dex" is a statement about a probe. The rule now
says to name the probe's coverage and run a control that must hit before writing "not present", and
to record `unknown` about the probe rather than `absent` about the target.

Also in this pass:

  * P47 -- two tools disagree about the same artifact, and the disagreement is your parser. One
    extraction rule reused across outputs that do not share a format: an unescaped `$` dropped every
    descriptor containing one, and dotted names were compared against DEX descriptors so one side
    parsed to nothing. The comfortable failure is the one that confirms what you already expected,
    which is why it survived the sanity check it should not have.
  * dexsql: a cache is second-hand evidence and it ages silently. A stale cache answers for the old
    artifact with no error, and `--trusted-input` skips revalidating the recorded source inputs --
    so a cached run supports "true of the artifact the cache was built from", not "true of the
    artifact in front of me".
  * ete-diff-tracing: a divergence is a location, not a cause. The differential method produces a
    positional claim, and three readings it does not authorise are written down beside the step that
    produces it.
  * CONTRIBUTING.md -- the gates, what a green run does and does not establish, the sync rules a
    change has to keep (a new reference must be named in the inventory; renaming a heading breaks
    every section reference that pointed at it), and the two failure modes to watch when editing the
    gates: a limit that cannot fail, and a gate whose answer varies with the runner. README links it
    from both language editions.
2026-10-16 19:17:31 +08:00
inliver233 b8aa0a529b Make the budget gate's hard index-row limit actually fail
`check_index_rows` read `'warn' if hard else 'warn'` -- both branches identical -- so
INDEX_ROW_HARD was defined, computed, and unable to fail anything: an index row could grow
without bound while every gate stayed green.

Found by injecting an over-long row into SKILL.md and requiring the gate to fail. That is the
only way a dead threshold shows itself; a gate that has only ever been observed passing is not
evidence that it can see anything.

A test now pins both sides -- a row past the soft limit warns, a row past the hard limit fails
-- and it was checked against the pre-fix behaviour, where it fails.
2026-10-16 19:17:21 +08:00
inliver233 ca38fa393d Merge PR #7 "Pass stage2" (dabao1955/stage2)
Absorbs four public reverse-engineering write-ups into the skill, and completes
the xvmp IR-virtualizer work started in the hardware pass. 20 files, +1151/-151.

What it adds:

  * references/native-vm-recovery.md -- the recovery procedure for a native VM:
    VM-existence signals, separating stacked CFF/VM dispatchers, register
    anchoring with triple verification, nested-VM segment stacks, STORE-backward
    operand slicing with byte provenance, and the clean-re-VM validation loop.
    Absorbed, so every step is labelled inferred in the file itself.
  * pitfalls P43-P46, including the Go stack-ABI one (a watchpoint hit whose
    argument is not in x0-x7 because the frame holds it) and the observer whose
    crash silently disarms every observer installed after it.
  * vm_handler_slice.py: a nested-dispatch diagnostic (a block repeating 4+ times
    inside one segment is a second dispatcher running under the first), plus the
    fix for a latent bug this pass's own review found -- diagnostics() had been
    measuring len() of the (run, closed) tuples since the open-tail change, which
    is always 2, quietly disabling the spin-loop note on the real code path.
  * evidence for a completed workbench toolchain and a measured end-to-end run,
    recorded in EXTENSION-sampling-crash.md SS5-SS6 and EXTENSION-absorption-kx.md.

Verified on the merged tree, not on the PR branch alone:

  * check_repo.py, check_refs.py, check_commands.py, check_routing.py and
    check_budget.py pass with zero problems; flake8 and mypy are clean;
  * pytest: 772 passed, 172 skipped, 25 xfailed;
  * the code change was reproduced independently of the PR's own tests:
    nested_loop_segments() flags the repeat-inside-one-segment shape in both the
    (run, closed) and the legacy bare-run form, and diagnostics() now reports
    avg_segment_len over the run -- 5.0 on a fixture whose tuples are length 2,
    where the pre-fix code reported 2.0.

One defect fixed during the merge: the PR's own record says the local copies of
the absorbed write-ups under `.ref/` are "git-ignored in effect", but .gitignore
had no such rule -- it rested on nobody having run `git add -A` yet. The rule is
now explicit, with the reason stated beside it, the same way this file handles
tools/ and the user's own input material.
2026-10-16 18:34:59 +08:00
dabao1955 cd1df50a2a Complete the xvmp IR virtualizer end to end and measure the de-virtualization chain on a real fixture
The public xvmp tree ships a compiler half that was never compiled (no TU
includes the virtualizer headers; on dev trees they do not build), a weak
empty vm_exec stub, silent skips for unsupported IR, and a companion article
describing an earlier prototype that is not in the tree. This change completes
and measures the chain:

- Completed toolchain, merged into all four omp branches of that tree: gated
  new-PM pass wiring (-mllvm -irobf-irvm, section .irvm), FORMAT v3 bytecode
  (8-byte aligned slots, explicit load/store widths, stable CC_ cast kinds,
  per-index GEP byte scales, branch condition = operand 0), a strong-symbol
  vm_exec interpreter with a 23-check unit suite, and a VMP.h user header.
  Virtualized vs native output identical at -O0/-O1/-O2 on all four
  branches. One branch's ungated IRVM call in Obfuscation::runOnModule
  (rewrote .irvm functions in plain native builds) removed; IRVM is
  strictly flag-gated.

- Fixture measured end to end (fco+bcfobf+subobf+IRVM build): native-identical
  output; 17,961-step guest trace via VMP_TRACE; 24.6M-instruction host trace
  via a ptrace single-stepper; vm_handler_slice yields 95,184 segments / 18
  unique signatures; the fla outer state machine separates as a shuttle class,
  round-boundary segments split the trajectory, positional alignment names
  every handler segment (59/59 in a round; counts match the guest histograms
  exactly), zero unmapped with --opcode-map.

- Tooling hardening measured on the real fixture: opcode maps key by
  signature (first-block keys collide under a shared dispatch successor);
  apply_map keeps first-block keys as fallback, regression tests added.

- Environment boundary recorded: frida stalker does not work in these
  containers (measured past the syscall layer: primitives pass, agent loads,
  zero denied syscalls; the failure is inside the injected agent's hook
  machinery), an outer ptrace tracer conflicts with frida spawn, and the
  ptrace stepper is the working host capture path. Design lesson: a pure
  virtualized call can still be LICM-hoisted by the compiling optimizer.

Records: EXTENSION-sampling-crash.md SS5-SS6 (final-state rewrite),
known-limitations.md, ete-diff-tracing.md SS5, vm_handler_slice.py header,
capability matrix.
2026-10-16 14:45:03 +08:00
inliver233 4aaa586725 Restore the trailing newline PR #6 dropped from evals.json
Every other JSON file in the tree ends with a newline; the hardware pass
removed this one, so the file diffed as 'no newline at end of file'. Content
unchanged -- the eval entry it adds (id 4, the hardware-layer route) parses.
2026-10-16 02:07:50 +08:00
inliver233 e151d7d605 Merge PR #6 "Hardware pass (stage 1)" (dabao1955/feat/ete)
Merges the hardware-layer pass onto the dexsql commit: arm64 hardware
watchpoints, ETE differential tracing, kernel-side event injection, SPE/BRBE
sampling and crash forensics, address symbolization and interpreter-trace
de-slicing. 33 files, 6 new scripts, 6 new test modules, 4 new reference
documents.

The only conflict was in evidence/capability-matrix.json: both sides appended
to the same array tail. Resolved by keeping both -- the PR's seven entries
(44 rows total now) -- and re-validated as JSON before staging.

Verified on the merged tree, not on the PR branch alone:

  * check_repo.py, check_refs.py, check_commands.py, check_routing.py and
    check_budget.py all pass with zero problems;
  * pytest: 766 passed, 172 skipped, 25 xfailed -- the 141 tests this PR adds
    all pass alongside the 625 that were already there;
  * the PR's host-side claims were reproduced by hand, not taken on trust:
    `hw_watch.py plan --addr 0x1004 --len 4 --access w` returns
    `cr: 7701` (WCR 0x1E15) exactly as the record says, both refusals exit 2
    with the documented reason strings, and the kernel scaffold's
    generate/verify round trip returns RESULT=ok.

One documentation defect was found and fixed in the merge: EXTENSION-kernel-instrument.md
recorded `verified 3 kernel-side file(s)` while the same page's own generate
output lists four (kpm/src/kwatch.c, kpm/Makefile, lkm/kwatch.c, lkm/Makefile)
and the tool reports 4. The page now says 4.

Everything the PR ships that needs a device or a kernel toolchain stays
labelled unverified in the files themselves, which is the property that made
it mergeable.
2026-10-16 02:05:59 +08:00
inliver233 cbbeb58343 Adopt dexsql as a third indexer, parity-checked against rasc, droidasc and ddc
LSPosed/dexsql exposes the whole dex as SQL tables (callers, field accesses,
numeric constants, debug lines, annotations, hiddenapi flags) plus the APK
manifest and resources.arsc, plus an MCP server. It is a third indexer at the
same stage as ASC, so it is registered the same way:

  * scripts/capabilities.py gains tool:dexsql and the dex_query_sql capability,
    so doctor.py reports it instead of guessing. INSTALL_HINTS replaces the
    rasc-only hardcoded path list, so a release download or a from-source build
    that landed outside PATH is not reported BLOCKED.
  * references/dexsql-sql-query-layer.md carries the table model, the recipes,
    the MCP tool list and the boundaries; references/toolchain.md gains the
    Tier-1 row and a section; routing.md, SKILL.md, both READMEs and the
    evidence record are updated in step.

What was measured, on four archives (1.4 MB / 1,396 classes, 4.3 MB / 647,
38.7 MB / 4 hardened, 34.8 MB / 30,768):

  * class-definition sets identical to rasc, droidasc and ddc on all four,
    zero differences in either direction;
  * string cross-reference agrees with ASC site for site -- and the match
    semantics differ, which is recorded as a trap (`=` is not `findrefs`);
  * decode emits the same instruction sequence as ddc disasm, line for line;
  * the persistent cache is NOT a speed switch: 0.121 s -> 0.098 s on a 34.8 MB
    archive, a net loss on a small one. Recorded as measured, not as sold.

Full class listing, best of 3: dexsql 0.136 s against droidasc 0.200 s and
rasc 0.055 s on the 34.8 MB archive, so it does not replace rasc for a class
list -- it answers the joins and aggregates the other two cannot express.
2026-10-16 02:01:36 +08:00
root ab22b225ff Hardware pass: hardware watchpoints, ETE differential tracing, kernel-side event injection
Add the layer under the software tooling, in the repository's shape: what runs
on a host is measured, and every device-bound or kernel-bound piece is a gated,
labelled template.

- scripts/hw_watch.py: arm64 hardware watchpoints/execution breakpoints. WCR/
  BCR encoders with the 8-byte-window and alignment rules (unit-tested to the
  canonical 0x1FFD/0x3F05), a self-contained on-device ptrace helper that
  compiles clean as a static aarch64 ELF (observed: cross gcc 16.2), adb
  orchestration, and hit decoding to module+offset. The stealth ledger is
  stated three times on purpose: the ptrace route leaves TracerPid visible.
- scripts/ete_capture.py: CoreSight ETE capture with the five gates (kernel
  era, coresight sysfs, cs_etm PMU, perf-with-CORESIGHT, permission), the
  perf/OpenCSD build recipe, and the per-thread record wrapper whose
  --control one-instruction run must pass before any real capture.
- scripts/trace_diff.py: differential reduction of two execution traces into
  first-divergence / divergent regions / blocks-unique-to-each-side.
  Producer-agnostic: Stalker BLK logs (raw or run_probe-wrapped) and decoded
  cs_etm address streams alike, host-tested on synthetic flattened-dispatcher
  fixtures.
- scripts/kernel_instrument_scaffold.py: KPM/LKM watchpoint + register-rewrite
  templates plus an honest userspace carrier; every kernel file carries a
  NOT COMPILED AND NOT LOADED banner that verify enforces, and the
  register-rewrite action is fenced as experimental in the template itself.
- references/hardware-watchpoints.md, references/ete-diff-tracing.md,
  references/kernel-event-injection.md: the mechanism, the gates, the traps,
  and the composition recipes (watch the key buffer; diff two runs; flip the
  decision at the trap), with hand-offs instead of duplication.
- Registration: 3 symptom rows (SKILL.md + routing mirror), 3 reference rows,
  4 script rows, coverage/route-inventory/evidence rows, capability-matrix
  entries, known-limitations entry, eval case 4, README trees and coverage
  paragraphs in both languages, and three EXTENSION evidence records stating
  exactly what was measured on this host (encoders, compile, fixtures,
  generate/verify) and what was not (no watchpoint armed, no cs_etm stream,
  nothing kernel-side compiled or loaded anywhere).

Hardware pass 2: SPE/BRBE crash forensics, address symbolization, VM de-virtualization

Calibrated against the two public obfuscator implementations the user supplied
(dpkg123/ollvm-aosp-toolchain: Hikari-family and Pluto-family flattening, the
xvmp IR virtualizer at 63fec41f/9b7cf75e; fetched read-only into
tools/_work/refs, git-ignored). Source facts observed, runtime inferred.

- scripts/trace_addr2sym.py: runtime addresses -> symbol + static offset,
  zero-dependency ELF64 (PT_LOAD vaddr<->file-off conversion, .dynsym/.symtab,
  stripped section+off fallback). Host-verified against a real cross-compiled
  aarch64 .so with readelf as the independent cross-check, plus a hand-built
  minimal ELF covering vaddr!=offset; accepts hw_watch hits, trace mod+off
  keys, or raw address lists. Fixes on the way: --base int coercion, and
  nearest-below symbol semantics for PCs in inter-symbol gaps.
- scripts/vm_handler_slice.py: slice an interpreter trace into handler
  segments (dispatcher detection = histogram top + predecessor diversity; the
  first 4x-margin rule refused the calibrated fixture itself and was replaced),
  signature clustering, open-tail handling, and the guest-opcode trajectory via
  an optional handler->opcode map. Calibrated against xvmp's documented shape:
  vm_exec(code,size,vm_args,num) stubs, boxed arguments observable before
  interpretation, 11 opcodes + 19 arithmetic sub-ops, const-materialization
  dispatch as a distinct segment.
- references/hardware-sampling-and-crash.md: SPE (data flow without a known
  address; PC + data address per sample), BRBE (branch records when a crash
  stack cannot be trusted), the crash loop (tombstone -> branch records ->
  symbolize), and what each adds to the OLLVM/VMP routes. All
  documented-and-gated: no SPE sample or BRBE record was captured here.
- Flattening-recipe corrections from source: Hikari keeps a frame-resident
  switchVar written through an indirect pointer with scramble32-permuted cases;
  Pluto stores switchVar/switchXor rolling-XOR so memory only ever holds noise
  -- hardware-watchpoints.md's state-variable recipe now distinguishes the
  families, and ete-diff-tracing.md carries the family table plus the xvmp
  shape. AntiDebugging injects raw svc inline asm with randomized fields, so
  byte signatures cannot survive it; semantic site-location can.
- Registration: 3 symptom rows (untrustworthy crash stack; runtime addresses
  needing symbols; interpreter loop reading as noise), reference/script rows,
  coverage/evidence/capability-matrix entries, README trees in both languages,
  and EXTENSION-sampling-crash.md recording exactly what was measured (real .so
  symbolization, slicer fixtures, the slicer's self-found dispatcher-rule bug)
  and what was not (no SPE/BRBE capture, no hardened binary ever traced).

Fix all PR #1 review findings by severity; un-entrench the tests that pinned them

Major (device-breaking / wrong results):
- hw_watch: match the arm64 user_hwdebug_state slot ABI (address register
  first, control second -- the swap armed nothing correctly)
- hw_watch: every post-seize error path now clears slots and detaches;
  PTRACE_O_EXITKILL turned an analysis failure into a SIGKILL of the target
- hw_watch: forward genuine target SIGTRAPs (raise/tgkill/int3); only ptrace
  event stops are consumed -- the reference's signal-preservation promise
- hw_watch: run checks push/build/chmod and the helper's exit status; a dead
  capture is RESULT=blocked, never ok-no-hits
- hw_watch: decode offsets from the module base, not each mapping's start
- ete_capture: --control now requires the control record to DECODE
  (perf script event lines), the documented gate, not just a zero exit
- ete_capture: -o before the -- workload separator in --cmd mode; all
  values shell-quoted into su -c; stale remote perf.data removed first;
  perf exit status checked (timeout's 124 is the normal window end)
- kernel scaffold: --set-reg restricted to x0..x30; the rewrite is gated by
  its own breakpoint event so a watchpoint hit cannot run it; LKM pins the
  task (get/put_task_struct) across registration and calls the four-argument
  register_user_hw_breakpoint; --break rows initialize .addr/.len/.bp_type
  (there is no .pc member); --id validated against the module-id grammar
- trace_addr2sym: exact module matching (path or basename, never substring);
  load bias aligned against the PT_LOAD (bias = start - (foff - p_offset)
  - p_vaddr); PCs outside the ELF span are refused instead of landing on the
  last symbol; --json stdout is a pure document (RESULT= in text mode only)
- trace_diff: block keys need 0x or >=8 hex digits (PIDs/counts no longer
  parse); vm_handler_slice: dispatcher needs predecessor diversity, no
  frequency-margin fallback; both JSON outputs pure (check_commands' rule)

Minor/nitpick:
- capability-matrix + evidence-summary: trace reduction status ok->partial
  (no real-target producer ran it; the file's own definition of ok)
- hardware-sampling-and-crash.md and the script examples carry --elf
- kernel-event-injection.md no longer claims the callback's first statement
  is a filter (it prints unfiltered; the filter is a required change)
- Elf64 caches symbol addresses for per-row bisect
- docstring coverage of the touched surface: 16.67% -> 100%

Tests: every json.loads workaround stripping RESULT= is gone; new regression
tests cover the slot ABI, release-on-error, SIGTRAP forwarding, non-first
mapping offsets, -o placement and su -c quoting, the control decode gate,
x31/--id refusals, the guarded rewrite, outside-ELF PCs and bias math,
short-token refusal and the dispatcher margin rule. The minimal-ELF fixture
now packs a full 8-field Elf64_Phdr (its 7-field pack silently shifted
p_memsz onto p_align). 739 passed; flake8/mypy and all five gates clean.

Address the follow-up review on 0bd8dfe (6 findings)

- ete_capture: two-layer quoting for the control-record, decode and cleanup
  commands -- shlex.quote spliced between literal single quotes broke out of
  su -c for any spaced output name; --seconds must be positive (timeout 0
  disables the timer), refused before any adb check
- kernel scaffold: --set-reg now refuses sp/pc as well (the template has no
  rewrite path for them; a generate that silently drops the action is the
  misleading kind of success); both templates arm with a sampling period
  (LKM via hw_breakpoint_init, KPM spells out sample_period=1) -- a zero
  period is a non-sampling event whose callback never fires: armed, silent
- trace_addr2sym: contains_vaddr tests each PT_LOAD's loaded span instead
  of the whole-file extent; a PC in the unmapped gap between segments is
  refused rather than named after the nearest symbol below
- trace_diff: a bare all-digit token is indistinguishable from a date or
  count, so block keys need 0x or a non-decimal hex digit; a date-only log
  stays refused as a capture failure

Tests: two-layer quoting with a spaced control name, --seconds refusal,
sp/pc in the rejection list, sampling-period assertions for both templates,
a two-segment gap fixture, and a date-only parser case. 743 passed; flake8,
mypy and all five gates clean.
2026-10-15 15:33:28 +08:00
newliver666 7b6c6932a9 README: add a theme-aware header, live badges, and a Simplified Chinese edition 2026-09-24 17:22:21 +08:00
inliver233 971a961217 Register the ASC indexers as capabilities so G2 reports them instead of guessing
The kit documented rasc and told the reader to reach for it first, but nothing knew whether it was
present: doctor.py had no entry for either indexer, so an environment gate could not answer "is the
fastest indexer available here", and the reference file had no way to say where a built binary lives.

  capabilities.py  two new capabilities, dex_index_rust and dex_index_python, plus the two tool atoms
                   they depend on. rasc's install line is its build command, because that is what
                   acquiring it means; its cost is marked measured (a clean clone built in 117 s here)
                   rather than estimated. The probe also accepts the work-area path that
                   rasc_build.py builds into, so a machine that followed the kit's own instructions
                   is not reported as BLOCKED.
  doctor.py        reports both, with the next action on the blocked one. Verified: OK with the built
                   binary present, BLOCKED with the build command when it is moved away.
  toolchain.md     says the capability is registered, so G2 does not have to guess.

Also fixed, found while verifying this: `doctor.py --json` printed the RESULT= token after the JSON
document, so its output was not parseable by the tool the flag exists for. Same defect that was fixed
in check_commands.py; the status stays a field in the document and the exit code carries the meaning.

Gates: check_repo 0, check_refs 0 dangling, check_routing coherent, check_budget 0, check_commands 0
drift, pytest 625 passed, flake8 0, mypy clean over 55 files.
2026-09-22 20:57:10 +08:00
inliver233 f928b5889f 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.
2026-09-22 20:50:01 +08:00
inliver233 481089600d CI: do not self-upgrade pip in the test job 2026-09-22 20:01:04 +08:00
inliver233 a1ada96cc3 Lint clean: adopt the PR's type/style gate, apply it to the current tree, and drop its path filter
PR #3 proposed adding mypy and flake8 to CI, with configurations for both. The idea is right and the
configurations are kept. Three things about the PR itself needed correcting, and all three were found
by running the gate rather than reading the diff:

  1. It was written against an older tree. Its own edits reference code that no longer exists there
     (`unknown, positions, warnings = ...`, a `struct` import at line 857 that is used 600 lines
     later), so merging it as-is would have removed a live import. The fixes were re-derived against
     the current tree instead, and the PR is superseded.
  2. It added a `paths:` filter to the workflow, watching only `*.py` plus the two lint configs. That
     would stop `check_refs.py` and `check_routing.py` -- documentation gates -- from running on a
     documentation-only commit, which is exactly the change they exist to catch. The workflow keeps
     the PR's `actions/*@v7` bump and gains no filter; the reason is written next to the trigger.
  3. `pip install --upgrade pip pytest` was dropped to just the packages, because upgrading pip in CI
     is not a goal and can fail for reasons unrelated to this repository.

What the gate found on the current tree, and what was done about it -- 70 flake8 findings and 3 mypy
errors, none of which were cosmetic-only:

  F401 (23)  unused imports, removed. These were safe to delete because the import statement itself
             cannot have a side effect that matters here.
  F841 (12)  unused bindings, handled three ways rather than one. Dead code was deleted
             (`mt_mcp_probe.print_waiting_help`'s `host`, `repack.py`'s `signer_jar`); a computed
             value that was then forgotten was **wired up** (`rasc_build.py` now names the linker
             target in its build-failure hint instead of discarding it); and a binding that is
             intentional kept its place with `# noqa: F841` and the reason -- `java2c_probe` reads
             dex header fields one by one against the format spec, and deleting three of them to
             please a linter would have made the parser stop lining up with the format it implements.
  E741 (14)  the ambiguous loop name `l`, renamed with an AST pass that resolves each binding to its
             own scope. A regex rename would have been wrong: several scopes bind their own `l`.
             That pass had a defect of its own -- it treated a comprehension target as living in the
             comprehension scope while leaving references to it in the enclosing one -- which
             produced seven `undefined name 'l'`. Found by running flake8, fixed, and the counting
             bug is documented in `check_routing.py`, whose own `tables()` had the same class of
             error in the other direction.
  style      continuation indents, blank lines between top-level definitions, and lines over length.
             The line limit is 160, not 120, and the number is a **measurement**: this repository's
             secret-scanner rule table holds raw regex source of 148 and 156 characters, which cannot
             be split without changing the pattern.
  mypy (3)   two missing annotations on module-level containers, and one optional-dependency rebind
             (`dexutil = None` after `except ImportError`) that is the repository's own idiom and
             now says so with a `type: ignore[assignment]` and the reason. No code was reordered to
             satisfy the checker.

Verified on a venv containing only `mypy` and `flake8` -- the poorest environment the configuration
must work in, and the one CI uses: **flake8 reports 0, mypy reports "Success: no issues found in 55
source files"**. Behaviour is unchanged: 625 passed / 163 skipped / 25 xfailed, the same five gates
green (check_repo 0 problem, check_refs 0 dangling, check_routing coherent, check_budget 0 failure,
check_commands 0 drift), and every key script still prints help.

  .flake8        new, with the line-length and complexity numbers and their reasons
  mypy.ini       new; optional imports declared, nothing excluded from the analysis
  ci.yml         `actions/*@v7`; new `lint` job on one interpreter; no path filter
  30 files       the findings above
2026-09-22 19:56:34 +08:00
inliver233 9b159253c6 Adopt rasc (the Rust ASC) as the indexer, with its measured blind spot written down
The Python `droidasc` indexer works and is one `pip install` away, but the same project has a Rust
re-implementation on its `rust` branch, and this pass answers whether it can be used instead of the
Python one. It can, with one documented caveat. Everything below was measured here; nothing is taken
from the upstream README's numbers.

**What was built.** `rasc` publishes no artifact -- no release asset, and `cargo install rasc`
fetches an unrelated maths parser -- so adopting it means building it. rustup (gnu host, rustc 1.98.1)
plus a 64-bit MinGW-w64 gcc 16.1.0 as the linker, `cargo build --release` in **117 s**, producing a
2.13 MB binary from `MG1937/ASC@e809c7b` (rust branch, sha256 `1becdb86…c0264b`). The build traps are
recorded because they cost time here: the host's existing MinGW is **32-bit** and fails with
`sorry, unimplemented: 64-bit mode not compiled in`, E:\vs2022 has no VC tools, and the registered WSL2
distribution cannot start (`HCS_E_SERVICE_NOT_AVAILABLE`) -- so neither the MSVC nor the Linux path
was available.

**What it answers.** Same CLI shape as the Python tool (`classes` / `manifest` / `getclass` /
`findrefs {string,type,method,field}`), and on this repository's archives the answers match:

  MASTG UnCrackable-Level3 (1.4 MB)   classes 5.6x   findrefs string 15.6x   findrefs type 16.2x
  a real 34.8 MB app (30,768 classes) classes 4.0x   -- class sets IDENTICAL, 0 differences either way

`scripts/rasc_build.py` wraps the whole path -- `--check`, `--build`, `--verify <apk>` -- so the
verification travels with the kit instead of living in a note: `--verify` compares the class-definition
set against `droidasc` and fails on any difference. It resolves the repository root by walking up
rather than assuming a layout, prepends a non-default `CARGO_HOME` to PATH, and reports a missing
toolchain as exit 3 with the install step.

**The blind spot, and why the rule is a rule.** The rasc project ships its own JADX-parity harness.
Run here on a fair sample (app-like classes; the harness refuses to judge a class JADX cannot
decompile), 17 of 20 were judged and 16 matched literal-for-literal with 0 reorderings. The one
difference is not cosmetic: `org.bouncycastle.crypto.PasswordConverter` is an enum whose constants
override a method, JADX emits the three anonymous subclasses with their `convert` / `getType` bodies,
and **rasc emits a bare constant list -- the bodies are gone and no warning is emitted**. Clean,
plausible, incomplete output is precisely the failure shape this repository catalogues, so:

  use rasc to locate, and to read ordinary classes; confirm an enum that shows no bodies with a
  second reader; keep droidasc and JADX as the cross-checks the skill already requires.

The automated verdict said "literals rasc lost: 1", which understates it -- counting literals cannot
see a dropped body. That is recorded in the evidence file rather than smoothed over.

**What changed in the kit.** `references/rasc-and-droidsaw.md` (the measurements, the failing class,
the build traps, provenance); `scripts/rasc_build.py`; `toolchain.md` Tier 1 now lists `rasc` first
with `droidasc` as the fallback and the cross-check; README structure and script tables; the evidence
record `docs/tool-verification/EXTENSION-rasc.md`.

**One inconsistency this exposed and fixed.** `check_repo.py` looked for a reference or script name in
`SKILL.md` + the README only, while this repository's reference inventory lives in
`references/routing.md` -- so a correctly registered file was reported as orphaned, and the script
half of that check was not actually running. The registration surface is now SKILL.md + README +
`references/routing.md`, nested reference directories are read too, and a planted unregistered script
was used to confirm the check still fires.

Gates: check_repo 0 problem, check_refs 0 dangling / 0 warning, check_routing coherent, check_budget
0 failure / 0 warning, check_commands 154 checked / 0 drift, pytest 625 passed / 163 skipped /
25 xfailed.
2026-09-22 19:38:50 +08:00
inliver233 b098904228 README: add a disclaimer for learning-only, authorized use 2026-09-22 18:44:03 +08:00
inliver233 5c57553186 README: acknowledge the LINUX DO community 2026-09-22 18:41:00 +08:00
inliver233 5aa4c97050 Merge PR #2 (dabao1955): expand the CI matrix, and finish the dcc.py declaration
The PR diagnosed the same CI failure this branch had just fixed, from the other end, and two of its
three changes are kept.

  .github/workflows/ci.yml   KEPT. The matrix was ["3.11", "3.13"] while the frontmatter claims
                             Python 3.9+; it is now 3.11-3.14. Both new versions were checked here
                             before taking it: the gates and the full suite pass on 3.12 and 3.14.
  check_commands.py          KEPT. dcc.py added to EXTERNAL_SCRIPTS. This branch reached the same
                             conclusion structurally -- recognition by filename needs the gitignored
                             tools/ tree to exist -- and the explicit entry is the better belt for
                             the same braces, because it names the situation instead of inferring it.
                             Both spellings are declared: the bare dcc.py the java2c record uses
                             and the ./dcc.py this branch''s record uses.
  its new test               KEPT, with one assertion corrected. It pinned workbench-after-cd when
                             the workbench was present and xternal when it was not, so it asserted
                             one mechanism at a time -- and would have failed on any machine with
                             tools/ present once the declaration above exists, i.e. it was itself
                             machine-dependent, which is the defect the test guards. It now asserts
                             the property both mechanisms produce.

Verified on the merged tree, in both shapes: with tools/ present, 619 passed / 158 skipped / 25
xfailed and all five gates exit 0; with tools/ hidden to imitate a clean checkout, 596 passed / 181
skipped / 25 xfailed and all five gates still exit 0. The second shape is the one CI runs, and it is
the check that was missing when the previous commit was pushed.
2026-09-22 17:12:45 +08:00
inliver233 dd8057cfcb Make the command gate machine-independent, and calibrate the budget fallback
CI went red on `1dc1d9b` in both jobs while the identical command passed locally, and the reason is
worth keeping: the checks depended on state that only exists on a developer's machine.

check_commands.py -- the gate's verdict depended on a gitignored directory
  The previous commit's fix recognised a workbench script by matching its name against a walk of
  `tools/`. That tree is gitignored, so a clean checkout has none of it: `dcc.py` matched locally and
  skipped, and on CI nothing matched, it fell through to `missing-script`, and the gate reported
  drift for three quoted lines. A check whose verdict changes with a directory CI does not have is
  not a check. Two changes remove the dependency:

    * `qualified_basenames()` collects every `.py` the *document itself* writes with a path, so a
      bare name the document has already located elsewhere is a skip, computed from the text.
    * `./dcc.py` is a declared upstream entry point, checked before the workbench-name test.
    * a bare name after a `cd` that resolves nowhere is `after-cd-unresolved` -- unverifiable, which
      is not the same claim as broken, and is now reported as such instead of as drift.

  Verified by running the whole CI sequence against a tree with `tools/` hidden: five gates exit 0,
  the suite passes. That experiment is the one that should have been run before the first push.

check_budget.py -- the measurement was installation-dependent
  The gate measures the always-loaded body in tokens, preferring `tiktoken` and falling back to
  `characters / N`. `N` had been guessed at 3.6 rather than calibrated, so a machine with tiktoken
  saw ~11.1k tokens and passed while a machine without it saw ~14.2k and warned -- and CI's 3.11 job
  is the one without it. `N` is now the measured 4.285 characters per token (47,629 chars -> 11,114
  tokens on SKILL.md), both paths agree within one token, and `tests/unit/test_budget_measurement.py`
  forces both paths and asserts they do not straddle the budget.

  CI now installs `tiktoken` so the comparison exercises the real tokenizer instead of skipping.

Also: two `.py` files are formatted for consistency, and the reconstruction record documents the
CI-only failure, since "it passed on my machine" is the lesson rather than an aside.

Gates: check_repo 0 problem, check_refs 0 dangling / 0 warning, check_routing coherent,
check_budget 0 failure / 0 warning (both measurement paths), check_commands 154 checked / 0 drift,
pytest 617 passed on 3.14 and 609 on 3.11.
2026-09-22 17:09:22 +08:00
dabao1955 b0ff2605fd Fix failing ci tests 2026-09-22 11:20:22 +08:00
inliver233 1dc1d9b707 Fix the command gate's blind spot, and make --json actually be JSON
A reviewer reported seven drifting commands in the evidence record. Reproduced, the report split
three ways, and the middle category was the useful one: the gate had been scoring them clean.

Not reproducible, with the measurement:

  tests/run_tests.py -q   runs. The wrapper forwards its arguments to pytest
                          (`args = list(argv[1:]) or ["-q"]`), and documents that in its own usage.
  dcc.py                  already written as `cd tools/_work/bench/repos/dcc` followed by
                          `python dcc.py ...` -- an external checkout, not a shipped script.

Real, and fixed here: seven commands in two evidence records named a **workbench** script with no
path at all (`analyze_pair.py`, `make_skeleton.py`, `ttd_attach.py`, `b2_measure.py`). The files
exist under `tools/`, which .gitignore excludes, so a reader copying one from the repository root
gets "No such file" -- and two of those documents wrote the correct path elsewhere, so they
disagreed with themselves.

**The actual defect was in `check_commands.py`, not in the seven lines.** `resolve_script()` tried
the document's own directory first, so a bare `analyze_pair.py` inside `docs/tool-verification/`
resolved to `tools/_work/bench/unpack/b3/analyze_pair.py` -- a real file -- and passed. Resolution
now follows the order a reader would try (repository root, then the skill's scripts, then the
document's own directory, which is what `../x.py` in a nested README legitimately means); a bare
name that only resolves under `tools/` is `unqualified-workbench`; and that class counts as **drift,
not as a skip**, because it is a one-token fix that a reader cannot run. The first attempt classified
it as a low-confidence warning, which would have left all seven in place.

One exception is honoured and pinned: a code block opening with `cd tools/_work/bench/repos/dcc` is
telling the reader where to stand, so a bare `python dcc.py` after it is correct.
`documented_commands()` now reports whether a `cd` preceded the command in the same block, and that
case is skipped as `workbench-after-cd`.

Two defects of this pass's own, found while fixing it:

  --json emitted the RESULT= token after the JSON document, so `--json | jq` failed with "Extra
  data". The token is gone from that path; the status is a field in the document, and the exit code
  carries the same meaning.
  The fix's own substitution left four literal backslashes before `$` in the two files it edited --
  a character a reader would copy by mistake. Removed, and verified absent.

`tests/integration/test_check_commands_gate.py` pins the resolution order, the drift classification,
the `cd` exception, and a clean pass over the committed tree, so this cannot come back silently.

Gates: check_commands 153 checked / 0 drift, check_repo 0 problem, check_refs 0 dangling / 0 warning,
check_routing coherent, check_budget 0 failure / 0 warning, pytest 613 passed / 158 skipped / 25
xfailed.
2026-09-22 02:18:40 +08:00
inliver233 fe5bdee087 Merge: keep the maintenance pass and the reconstruction pass, both
Two passes met on this branch and neither is discarded. `d0fe0ea` (inliver233) hardened
`build_scripts.py` -- 37 findings that were mostly noise, down to 0, with two defects found in its
own first implementation by an 18-case table -- generalised three absolute paths out of the evidence
record, and added a corpus-size note to `check_budget.py`. `2beade6` (this pass) rewrote
`check_budget.py`'s measurement, added four gates, rewrote `so_constpatch.py`'s zip writer,
replaced 59 out-of-package references, and shrank the always-loaded body.

Resolution, file by file:

  check_budget.py   the only real conflict. Their corpus note and this pass's rewritten measurement
                    are kept as separate functions and separate notes, because they measure different
                    quantities: what a task loads versus what the repository carries. Collapsing them
                    would let a growing corpus hide behind a green always-loaded count -- which is
                    the defect their note exists to prevent.
  build_scripts.py  theirs, taken as-is, including the case table.
  REPO-DECISIONS.md both sections: their maintenance-pass record follows this pass's decision record.
  three extensions  their absolute-path generalisations, on top of this pass's convergence edits.
  everything else   this pass's versions, which supersede (SKILL.md, the routing file, tests/, the
                    skill's evidence/ and references/).

One regression this merge introduced and this commit fixes: `EXTENSION-test-harness.md` carried a
device serial in a quoted `adb devices` transcript. It is `<DEVICE>` now, and `check_repo.py`'s
content scan -- the gate added in the same pass -- is what caught it.

Gates on the merged tree: check_repo 0 problem, check_refs 0 dangling / 0 warning, check_routing
coherent, check_budget 0 failure / 0 warning (6 notes, including both corpus thresholds),
check_commands 146 checked / 0 drift, pytest 608 passed / 158 skipped / 25 xfailed.
2026-09-22 01:43:07 +08:00
inliver233 2beade67f2 Reconstruction pass: make the gates tell the truth, make the package self-contained, keep the one-hop symptom index
An external audit of this repository landed on four structural findings, and this pass is the
response to them. Everything below was re-measured on this tree before being acted on; two of the
audit's claims needed a correction of my own, and both corrections are noted in place.

**The gates were optimistic, and one of them did not read content at all.**

  doctor.py    the capability table was hard-coded (`repack + sign = bool(java) and python`), so a
               machine with a JDK and no zipalign/apksigner was told it could re-sign; the script
               inventory was a hand-written list of 28 against 51 files on disk, omitting the nine
               most important scripts. It now scans the directory (`registered/checked = 52 / 52`,
               unjudgeable entries counted as `unknown`, never skipped) and resolves capability as a
               real requires-closure, reporting BLOCKED with the missing piece and an installable
               next step. Measured: the old doctor reports `[OK] repack + sign` on a JDK-only PATH
               where keytool, jarsigner, zipalign, apksigner and all eight smali jars are absent.
               The report is allowed to look bad; that is the point.
  check_commands.py (new)
               anchor checks prove a section exists, never that a command runs. This parses each
               script's argparse table with `ast` and checks every documented invocation against it.
               146 commands / 41 interfaces, 0 drift. It caught `sig_probe.py --live --pkg`, which
               does not exist -- the reference had been wrong since it was written.
  check_routing.py (new)
               the symptom index now has one home (`SKILL.md`, because symptom -> file must stay one
               hop) and the inventory has another (`references/routing.md`). This gate proves the
               mirror agrees, that every reference file is named, and that every script is named.

**so_constpatch.py was corrupting APKs it rebuilt.** The writer copied filename and timestamp onto a
fresh ZipInfo and recompressed the whole archive: 435-entry real package, `extra` lost on 205 entries,
`external_attr` rewritten on all 435, 132 untouched entries recompressed, and `resources.arsc` pushed
to an unaligned offset. Android's own `zipalign -c -v 4` fails the old output and passes the new one;
a 26-case fixture suite pins it, and `extractNativeLibs="false"` is now refused by default with a
pointer at `repack.py`. Fixing it exposed the same defect in `repack.py`, where `^lib/[^/]+\.so$`
cannot match `lib/arm64-v8a/*.so` -- so its alignment handling had never once fired on a real APK.

**The package was not self-contained.** 59 references pointed at the repository root, which `npx
skills add` does not install. They now converge on `references/evidence-summary.md` (capability,
conclusion, strength, evidence that ships) plus `evidence/` with the machine-readable companions: 8
references remain and each is explicitly labelled "repository root, not shipped". A drill in which
the new files were copied to an isolated directory and asked three questions answered all three.

**SKILL.md: 572 lines / 15,733 tokens -> 398 lines / ~10,900 tokens**, without losing the two things
that had to stay. The symptom index remains inline; the reference and script indexes moved to the
on-demand routing file. The freed budget went into making the rules *stronger*, not shorter: the
constraints section is now a table of thirteen non-negotiable constraints each paired with the check
that catches a violation, and the tooling obligations are instructions whose violation is a defect
(hedging language in SKILL.md: 1 line, down from a paragraph of hedged advice). Long references gained
a navigation head so a reader can decide from the top whether to load one.

**New: an executable test layer.** `tests/` (unit, CLI contract, no-device integration) plus
`pytest.ini`, `tests/run_tests.py`, a minimal CI job, and `evals/evals.json` written but never run --
stated as such in the file. 608 passed / 158 skipped / 25 xfailed. The 25 xfails are a work order, not
a pass: thirteen scripts report a missing input by exiting 1 instead of 2, seven let a traceback
escape, four treat `--help` as an error, and `preflight --json` lacks a `status` field. A regression
row for `dex_patch_bytes.py` reproduces a previous artifact byte-for-byte
(`67ae1e58e7a44777e5ac8f9f823ab21e…`) and additionally pins the four bytes that may move.

**Two real defects the new tests found in the tooling itself.** `check_repo.py` decoded subprocess
output with the locale encoding, so on a GBK console a script printing an em dash in its usage line
was reported as crashed; it now decodes UTF-8 explicitly. And a CLI contract test drove a *real
attached device* through `coldstart.py` -- logcat, screenshots, five orphan `adb logcat` processes --
which is fixed twice over: child processes run in a private temporary cwd, and the fifteen
device-bound scripts are never executed beyond `--help`/argument parsing.

**Quoting on the device side is now one implementation.** `devsh.su` escaped only double quotes, so
`;`, `$( )` and backticks in a caller-supplied value still reached the device shell while
`coldstart.Device.shell` quoted correctly. Both use `scripts/device_shell.py` now: POSIX single-quote
escaping, argv arrays by default, and identifier validation that refuses a malformed package, path or
component with exit 2 instead of a traceback. The integration test that used to pin the *defect* (it
asserted `su -c "…"` with `\"` escapes) now asserts the property, across five metacharacter payloads.

Gates: check_repo 0 problems (including the content-reading identity scan over git ls-files),
check_refs 0 dangling / 0 warning, check_routing coherent (53 symptom / 45 reference / 54 script rows),
check_budget 0 failure / 0 warning with the always-loaded body now measured in tokens rather than in
"narrative lines with the index excluded", check_commands 146 checked / 0 drift, pytest 608 passed.

Not done, and stated rather than implied: CI has never run on a runner; the evals have never been
executed; the 25 xfails are open; no device-side install/launch was performed in this pass.
2026-09-22 01:37:51 +08:00
inliver233 d0fe0ea337 Harden the path audit, generalise three absolute paths, and gate the corpus size
An external review found the evidence record honest and the gates real, but raised three
concrete defects. Each is fixed at its cause rather than at its symptom, and the reasoning
is recorded in docs/tool-verification/REPO-DECISIONS.md.

build_scripts.py -- the audit reported 37 findings, most of them noise
  It matched any `X:\...` literal, so `C:\Program Files\...`, `C:\Python314\...` and literal
  placeholders all counted as machine-specific leftovers. A gate whose output is mostly noise
  stops being read -- the same failure scan_leaks.py names when it refuses to exempt the
  RFC 5737 ranges. Two explicit exemption families are added instead of a looser regex:
  GENERIC_PATH_PREFIXES (install locations that exist on every host of that platform) and
  `_leaks_nothing()` (also exempts an already-redacted path, since a placeholder or an
  ellipsis means a normalisation pass already ran).

  Two traps surfaced only because the change was tested rather than merely run. BARE_ABS stops
  at whitespace, so `C:\Program Files\...` arrives truncated as `C:\Program` and a one-way
  `startswith` misses the exemption; the comparison is now bidirectional under a minimum
  length. And with a lookahead of only `[a-z0-9]` the optional `\\?` backtracks to empty, the
  next character is the separator, it passes, and a real personal path is swallowed -- the
  lookahead has to exclude the separator as well. Both were caught by the case table, not by
  the audit run.

  Result: 37 -> 0 findings, exit 2 -> exit 0. The real hits (`E:\apk-reverse\...`,
  `E:\agb_apktool.jar`) were fixed in the documents, not by widening the list.

Three absolute paths on the published surface
  EXTENSION-desensitization.md, EXTENSION-extraction-shell-bench.md and EXTENSION-vmp-diff.md
  quoted a workbench root, a tool jar and an SDK location absolutely.
  docs/tool-verification/ already states the rule -- a record may say a run happened at a
  `tools/...` path, written relatively -- so the three files were made to match the rule they
  were already under. The tool-jar and SDK cases are rephrased rather than placeholder-ised,
  so no reader has to learn a new token.

check_budget.py -- blind to the half of the problem it was not measuring
  The gate budgets the always-loaded part and could not see the corpus, where a new reference
  costs exactly one index line: the corpus can double while the narrative count stays green.
  Two note-level thresholds now report it. Notes, not failures -- a large corpus is a choice,
  a growth nobody measured is the defect. The first run reports 47 references + 55 scripts =
  102 files, 31,423 lines.

A cross-tool inconsistency this pass ran into, recorded rather than papered over
  build_scripts.py now exempts any `<...>` placeholder; scan_leaks.py recognises a fixed token
  list. An invented placeholder in a source comment passed the first and was read by the
  second as the very personal name its `user_path_windows` rule exists to catch, turning
  check_repo.py red. That is by design -- a strict list is what stops a real name hiding
  behind a plausible-looking placeholder -- but the two tools disagree about what "already
  redacted" means, and the disagreement stays invisible until someone writes a new token.
  Rephrased here; a future pass may want one shared list.

Verification
  - build_scripts.py: 37 -> 0 findings on a clean clone, exit 0.
  - An 18-case table pins both directions, 0 failures: generic and already-redacted paths are
    exempt, while a real workbench path, a real personal name after `C:\Users\`, a real POSIX
    home and a real project path still report. It found two defects in the first
    implementation.
  - All four gates pass: check_repo 0 problem, check_refs 0 dangling / 0 warning,
    check_budget 0 failure / 0 warning / 3 notes, build_scripts 0 findings.
2026-09-21 23:18:17 +08:00
inliver233 6cffc3d2ac Absorption pass: take the mechanics from four external skill repositories, keep our discipline
Four projects were cloned for comparison (reverse-skill, awesome-game-security,
android-reverse-engineering-claude-skill, MobileRE-Skill) and read for what they do that this
repository does not. None is vendored and none becomes a runtime dependency: what crosses over is a
phase order, a checklist or a command pattern, with the source URL and the retrieval date recorded in
the file that absorbed it.

The rule applied to every candidate: does this make a judgement easier or a verification harder? The
repository is a decision-and-verification layer, and the four things kept are all of that kind.

  new  scripts/scan_leaks.py            target identity scanned before publishing, with an explicit
                                        do-not-anonymize list and four RESULT=/exit states
  new  references/precedents/           the positive half of the record: route including dead ends, a
                                        grade per assertion, measured pits, and a write-back checklist
  new  scripts/svc_scan.py              does a libc-level hook structurally see this call at all
  new  scripts/anti_detect_probe.js     observer-only: name the check, and report the environment it reads
  new  references/desensitization-and-leak-scans.md
  new  references/dart-aot.md sections   the 32-bit snapshot's real string-record format

  check_repo.py now delegates the identity scan to scripts/scan_leaks.py and runs it over
  git ls-files, so the gate reads content instead of layout for the first time. An explicit file list
  cannot be narrowed by the default work-area exemptions -- that defect was caught by the gate itself.

Two claims that this repository had already written down turned out to be wrong, and both are
recorded as refuted rather than quietly rewritten:

  dart-aot.md   the armv7 string table was documented as [len*2 u32le][UTF-16LE]. It is not. The same
                extractor keeps 4,980 chained entries on arm64 and zero on armv7, and the byte layer
                shows the 32-bit record is [header u32][byte-count u32le][UTF-8 payload]: two adjacent
                literals carry 0x46 (70) and 0x48 (72), exactly their byte lengths. The zero was a
                format mismatch, not an empty table. The engine-version banner on the same target is
                66 bytes of "0.0.1" with a blank build id, so the documented regex returns a
                version-shaped lie.
  leak scan     the pass that added the scanner found 26 strong hits inside its own evidence file on
                the day it was written -- the fixture's literal match values, quoted to prove the
                rules fire. The corpus may contain values; the published surface may not.

Measured, not asserted. The check that fires was named on a public MASTG target: three arms with one
variable, the control alive at the end and the probe arm dead in about 300 ms, with strstr("frida") at
t=303 ms and no tombstone. Reproducibility is bounded honestly -- one full sequence in three arms, on
a target whose whole window is sub-second. svc_scan.py's two independent decoders agree on an
identical 214-site set, and ptrace-free dumping gained a negative boundary: a target whose dex is
deflated inside its APK has no whole-image dex in memory to export, for reasons that say nothing about
protection.

Also in this pass: the detector-locating pipeline (detection-and-anti-analysis.md Step 3) with its
branch conditions; the layered Java-to-svc descent and the dump's honesty boundary in
advanced-unpacking.md; a sourced capability-gap list in toolchain.md where every row is graded
URL-verified or measured; the RESULT=-token convention; and SKILL.md opening with an immediate-action
block, because the symptom index and the gates were sitting in the part of a long file nobody reads.

Gates: check_repo.py 0 problem, check_refs.py 0 dangling / 0 warning, check_budget.py 0 failure /
0 warning / 1 pre-existing note. The scan reports RESULT=leaks_found_strong_only, exit 0: four
endpoint/weak hits, all of them addresses from the RFC 5737 documentation range in usage examples.
2026-09-21 22:51:09 +08:00
inliver233 fd2a1150be Structure: sink the evidence record out of SKILL.md, and gate the budget
SKILL.md was 658 lines against the Anthropic Agent Skills guidance of keeping the body under 500, and the growth sat where it costs most: the Coverage section had accumulated the evidence for every claim plus a per-pass verification history (153 lines / 11.8 KB), and Hand-off had grown two knowledge tables. Both now load on demand, with SKILL.md keeping the decision-level statement and a pointer.

  Coverage   153 -> 68 lines   new references/coverage-and-limits.md
  Hand-off    51 -> 19 lines   new references/handoff-boundaries.md
  SKILL.md   658 -> 543 lines  (404 narrative + 139 index)
  index rows 24.0 KB -> ~19 KB  descriptions now say when to load, not what is inside

check_budget.py is the substance of this pass. check_repo.py proves consistency, and consistency has a natural counter-pressure: a broken path fails loudly and someone fixes it. Bloat has none -- every pass adds a reference, an index row and a coverage claim, and nothing in the repository notices. The new gate measures narrative lines separately from index lines, because 139 of SKILL.md's lines are the discoverability tax for 86 bundled files and counting them against a prose budget would push the skill to drop files it needs. Conclusions restated beyond the benchmark matrix, the evidence record and the coverage statement are reported as notes.

No measured content changed in this pass: every claim, strength label and negative result is where it was, and the two new references carry the moved text verbatim plus the corrections the benchmark pass produced.
2026-09-21 22:12:58 +08:00
inliver233 fa2f62107f docs: stop citing workbench paths that are not shipped
Eight citations in the reference documents pointed at tools/ paths, and .gitignore excludes tools/ -- so neither an installed skill nor a clone of this repository can open them. Each is now either an inline statement of the fact or a pointer to the evidence file under docs/tool-verification/ that actually holds it. One of the eight was introduced by the benchmark pass itself (protocol-reverse.md cited a workbench script).

The evidence records keep quoting tools/_work/... deliberately: there they are operation records of where a run happened. docs/tool-verification/README.md now states that distinction and the rule that follows from it -- a reference document never cites a tools/ path as its evidence, even when the work happened there.
2026-09-21 22:01:28 +08:00