Skip to content

Repository files navigation

regraph

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.


What it does

  1. Reassembles the dynamic-partition super from its sparse chunks.
  2. Locates the ro.debuggable=0 assignment in /system/etc/prop.default and flips the single value byte to 1 (an init.rc trigger with the same text is correctly left alone). Same length → ext4 layout and partition size unchanged.
  3. Recomputes the system dm-verity hashtree + Reed-Solomon FEC over the patched filesystem.
  4. 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).
  5. Rebuilds and re-signs vbmeta.img with the AOSP test key, updating only the system hashtree root digest, and writes that key as the new avb_custom_key (avb_pkmd.bin).
  6. 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.

Requirements

  • Python 3 with numpy (pip install numpy)
  • ~6 GB free disk (the raw super is ~3.9 GB)
  • Docker (optional) only for the full differential oracle

Usage

# 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)
PY

By 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.

Flashing (device you 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 zip

After 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" state

Re-lock with care. A locked bootloader that rejects vbmeta can bootloop and force an unlock (another wipe). Verify first (below), test unlocked, then lock.

How it's verified

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/ -v

Full canonical differential oracle (Docker, ~6 GB scratch):

./verify/run-oracle.sh blazer-install-2026061600-debuggable.zip

It 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.

Layout

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

adb root (the su SELinux domain)

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.

Keeping it working (field notes)

Things confirmed the hard way on a real device:

  • adbd honors 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_service gates purely on getuid()==0 and __android_log_is_debuggable() (which reads ro.debuggable). So flipping the prop is sufficient; you just need a fresh adbd (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 regraph on 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 mangle super flashing (unsupported image size on the tail chunk) and adb root. On aarch64 Linux there's no official Google build — use Alpine android-tools (fastboot/adb 35.0.2, aarch64-native) in a --privileged -v /dev/bus/usb container, or nix shell nixpkgs#android-tools.
  • --in-memory stages the FEC region in RAM (~1.3 GB) instead of a disk temp — use it on machines tight on disk (needs the RAM instead).

Notes & limitations

  • 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=1 is the standard, minimal enabler for adb root, and on this build it is enough (verified by disassembling adbd). It is necessary but not sufficient on builds whose adbd lacks the root path (user-build adbd without ALLOW_ADBD_ROOT); there the only route is patching the binary — which on Android 16 lives inside the signed com.android.adbd APEX (much heavier).
  • Tuned for this image's shape: single-extent system_a, FEC num_roots=2, SHA256_RSA4096 vbmeta with all descriptors in one blob. Other devices may need small generalizations (multi-extent partitions, chained vbmeta).

License

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.

About

Tooling for rebuilding self-signed Android images for Pixel devices, no magisk injection.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages