Repack a GrapheneOS factory image so that adb root works — and nothing
else changes. The only behavioural difference is ro.debuggable=1 in the system
partition; the image stays dm-verity–enforcing and is re-signed with the
public AOSP test key, so the bootloader can be re-locked with that key.
No su, no Magisk, no kernel patches, no SELinux changes, no disabling of adb
key authorization.
The single goal of this project is to match the security profile of a userdebug image of GrapheneOS without building from scratch (which takes a massive amount of disk space). It will not implement sudo-based root or anything of the sort.
Target verified on: blazer (Pixel 10 Pro), Android 16, build
BP4A.260205.001 / 2026061600.
- Reassembles the dynamic-partition
superfrom its sparse chunks. - Locates the
ro.debuggable=0assignment in/system/etc/prop.defaultand flips the single value byte to1(an init.rc trigger with the same text is correctly left alone). Same length → ext4 layout and partition size unchanged. - Recomputes the system dm-verity hashtree + Reed-Solomon FEC over the patched filesystem.
- Patches those bytes back into the sparse chunks in place (size-stable, so
the chunks stay valid and ≤ the 256 MiB download limit — no re-split, no
multi-GB temporary
super). - Rebuilds and re-signs
vbmeta.imgwith the AOSP test key, updating only thesystemhashtree root digest, and writes that key as the newavb_custom_key(avb_pkmd.bin). - Repackages an otherwise byte-identical factory zip.
Everything is pure-Python on the runtime path (stdlib + numpy); the canonical AOSP tools are used only to verify the output.
- Python 3 with
numpy(pip install numpy) - ~6 GB free disk (the raw
superis ~3.9 GB) - Docker (optional) only for the full differential oracle
# produce the adb-root-enabled, re-signed image
python3 -m regraph.cli repack blazer-install-2026061600.zip \
-o blazer-install-2026061600-debuggable.zip
# structural self-verification (+ native avbtool oracle)
python3 - <<'PY'
from regraph.verifycmd import verify_zip
verify_zip("blazer-install-2026061600-debuggable.zip",
orig_zip="blazer-install-2026061600.zip", oracle=True)
PYBy default the repack signs with the bundled
regraph/keys/testkey_rsa4096.pem (the public AOSP
external/avb/test/data key). Pass --key your_key.pem to use your own.
The repacked zip flashes exactly like the original (flash-all.sh is
unchanged). The bootloader must be unlocked to flash a non-GrapheneOS-signed
image:
fastboot flashing unlock # WIPES the device
./flash-all.sh # from inside the repacked zipAfter boot:
adb root # -> "adbd is running as root"
adb shell id # -> uid=0(root)Optionally re-lock with the custom key already flashed as avb_custom_key:
fastboot flashing lock # boots to the yellow "custom key" stateRe-lock with care. A locked bootloader that rejects vbmeta can bootloop
and force an unlock (another wipe). Verify first (below), test unlocked, then
lock.
Each algorithm is checked against an independent ground truth — most of it before any modification, using values Google's own tools already put in the image:
| Module | Checked against | Result |
|---|---|---|
verity hashtree |
original image's stored system root 36d2114d… |
reproduced exactly |
verity FEC |
original image's stored 10.4 MB FEC | reproduced byte-for-byte |
rsa signing |
openssl dgst -verify |
"Verified OK" |
avb pubkey encode |
avbtool extract_public_key |
byte-identical |
avb rebuild+sign |
avbtool verify_image |
"Successfully verified … vbmeta struct" |
liblp |
lpdump |
system_a @ sector 2048, 2601488 sectors — exact match |
| whole output | structural self-consistency | 12/12 checks pass |
Fast unit tests (synthetic inputs vs avbtool/openssl):
python3 -m pytest tests/ -vFull canonical differential oracle (Docker, ~6 GB scratch):
./verify/run-oracle.sh blazer-install-2026061600-debuggable.zipIt reassembles super with simg2img, dumps the layout with lpdump, extracts
system_a with lpunpack, recomputes the hashtree root with avbtool over the
patched filesystem, and verifies the vbmeta signature — all with the
canonical AOSP tools. The oracle image is aarch64-native (Alpine android-tools)
and is never on the device flashing path.
regraph/
cli.py repack pipeline + CLI
factoryzip.py read/rewrite the install zip
sparse.py Android sparse: reader, in-place patch, writer/splitter
liblp.py dynamic-partition (super) metadata parser
ext4patch.py locate + flip ro.debuggable=0 -> 1 (assignment vs trigger)
verity.py dm-verity hashtree + numpy-vectorised RS(255,253) FEC
avb.py vbmeta parse / rebuild / sign
rsa.py PEM parse, PKCS#1 v1.5 sign, AVB public-key encode (pure Python)
oracle.py native avbtool oracle
verifycmd.py structural verification
keys/ vendored AOSP test key (public, debug)
verify/ Dockerfile + avbtool + oracle scripts
tests/ pytest differential suite
ro.debuggable=1 enables debuggable adb but not full adb root on GrapheneOS — that needs the su SELinux domain re-added to the (vendor) policy. The complete, verified recipe + split-machine scripts are in selinux/. Confirmed working: uid=0(root), context=u:r:su:s0, SELinux still Enforcing.
regraph repack --mode adb-root-only additionally comments the framework-start lines in /system/etc/init/hw/init.rc (class_start main/late_start, trigger zygote-start — size-stable space→#) to produce a headless GrapheneOS base (init + class core/hal + adbd, no zygote/system_server) for building a custom userspace. Note: validate A/B boot-success marking (update_verifier) on first flash, or the bootloader may roll the slot back.
Things confirmed the hard way on a real device:
adbdhonors the change, but lives in an APEX. On Android 16 the binary is/apex/com.android.adbd/bin/adbd(not/system/bin). It is not hardened —restart_root_servicegates purely ongetuid()==0and__android_log_is_debuggable()(which readsro.debuggable). So flipping the prop is sufficient; you just need a freshadbd(it caches debuggability once per process) — a normal boot of the patched slot does that.- GrapheneOS auto-updates revert it. Automatic updates are on by default; a seamless A/B OTA writes a clean GrapheneOS system to the other slot and switches to it, wiping the patch and re-signing with GrapheneOS's key. Turn auto-updates off (Settings → System → System update) or it reverts within hours.
- You can't downgrade back. Once the device boots a newer build it bumps the
AVB rollback index, so the older patched slot becomes "corrupt" and the
bootloader refuses it (enforced even while unlocked). After any update you must
re-run
regraphon that build's factory zip and reflash — same command, e.g.regraph repack blazer-install-<new>.zip -o <new>-debuggable.zip --in-memory. - Use current
fastboot/adb(≥ 35.0.1). Distro packages are years old and manglesuperflashing (unsupported image sizeon the tail chunk) andadb root. On aarch64 Linux there's no official Google build — use Alpineandroid-tools(fastboot/adb 35.0.2, aarch64-native) in a--privileged -v /dev/bus/usbcontainer, ornix shell nixpkgs#android-tools. --in-memorystages the FEC region in RAM (~1.3 GB) instead of a disk temp — use it on machines tight on disk (needs the RAM instead).
- The AOSP test key is public — it provides no secrecy. Security here comes
from
avb_custom_key+ a locked bootloader trusting that key; anyone can produce an image it accepts, so treat such a device as developer hardware. ro.debuggable=1is the standard, minimal enabler foradb root, and on this build it is enough (verified by disassemblingadbd). It is necessary but not sufficient on builds whoseadbdlacks the root path (user-build adbd withoutALLOW_ADBD_ROOT); there the only route is patching the binary — which on Android 16 lives inside the signedcom.android.adbdAPEX (much heavier).- Tuned for this image's shape: single-extent
system_a, FECnum_roots=2,SHA256_RSA4096vbmeta with all descriptors in one blob. Other devices may need small generalizations (multi-extent partitions, chained vbmeta).
regraph is licensed under the Apache License 2.0; see NOTICE
for third-party attributions. Apache-2.0 is compatible with the vendored AOSP
components — avbtool (MIT) and the AVB test key (Apache-2.0) — and the NumPy
runtime dependency (BSD-3-Clause).
GPL-licensed tools the pipeline shells out to (magiskpolicy, debugfs, and the
on-device Alpine userspace) are invoked as separate programs — not bundled or
linked — so they do not affect regraph's own licensing.