Fork notice. This repository is a compatible fork of
chenxiaolong/my-avbroot-setupused by the PixeneOS build pipeline for LineageOS / non-Pixel ROM compatibility. The fork strategy and the per-hunk decomposition of its delta against upstream are documented inMAINTAINERS.mdanddocs/. Upstream-bound PR drafts live underdocs/upstream-prs/.
This repo describes my personal setup for modifying the OS on my Android devices.
Unlike the norm in the Android modding community, I do not use runtime modifications and instead, prefer to modify the Android image directly. This eliminates the need for privileged code to live on writable storage and avoids giving potential malware a place to persist.
This repo includes the script I use for modifying Android OTAs. Folks should probably not use the script as-is and instead, adapt it to their needs.
Supported patch modules and their compatibility metadata are enumerated by the
declarative module catalog. Catalog entries remain
metadata-only: they neither enable modules nor execute installer scripts.
Modules can be selected explicitly with --module-<name> for unlocked
workflows, or through patch.py's canonical locked profile/lock boundary.
- Host must run Linux or an Android device must be connected via
adb- Needed for running a statically-linked Android executable
- python3
- avbroot (>= version 3.12.0)
- afsr (>= commit adcae036b68684828edf5eb90be1500abd5cf491)
- Custota (>= version 5.2)
- MSD (>= version 1.8)
- BCR (>= version 1.65)
- OEMUnlockOnBoot (>= version 1.1)
- AlterInstaller (>= version 2.0)
The avbroot, afsr, and custota-tool commands must exist in PATH.
This legacy behavior remains the default. Integrators that authenticate those
tools through a separate runner can instead pass an exact JSON argv prefix:
python3 patch.py \
--tool-runner-prefix-json \
'["/usr/bin/python3","/opt/pixene/bootstrap.py","--workdir","/var/tmp/pixene","run"]' \
--input ota.zip --output ota.patched.zipFor each external tool invocation, the helper appends the allowlisted tool
name, a -- separator, and the original arguments. The prefix executable must
be an absolute path. No shell parsing is performed, and loader/debugging
environment variables are removed before an authenticated runner starts.
Install uv or manually set up a Python virtualenv.
The staged module preparation CLI keeps catalog resolution, lock updates, artifact acquisition, verification, and archive inspection separate from OTA mutation:
python3 module-tool.py catalog list --format json
python3 module-tool.py lock verify --lock locks/artifacts.lock.json
python3 module-tool.py artifacts fetch \
--lock locks/artifacts.lock.json --cache .artifact-cache
python3 module-tool.py artifacts verify \
--lock locks/artifacts.lock.json --cache .artifact-cache
python3 module-tool.py resolve \
--profile profiles/lineage.toml --lock locks/artifacts.lock.json \
--format jsonNormal builds consume checked-in locks and never resolve “latest” versions.
Only the explicit lock update <module> command may use a reviewed provider
for floating upstream metadata; this Phase 1 foundation intentionally has no
such provider yet. See the module-tool guide for the
complete command contract and offline/security boundaries.
Then, run the patch script:
uv run patch.py \
--input ota.zip \
--verify-public-key-avb verify_avb_pkmd.bin \
--verify-cert-ota verify_ota.crt \
--sign-key-avb sign_avb.key \
--sign-key-ota sign_ota.key \
--sign-cert-ota sign_cert.key \
--module-custota Custota-<version>-release.zip \
--module-msd MSD-<version>-release.zip \
--module-bcr BCR-<version>-release.zip \
--module-oemunlockonboot OEMUnlockOnBoot-<version>-release.zip \
--module-alterinstaller AlterInstaller-<version>-release.zipThis will:
- Verify the original OTA signatures against the specified verification keys. This includes the OTA signature, the
payload.binsignature, and the signatures of every AVB-enabled partition image. - Extract the
system,vendor, andvendor_bootpartitions. This is all done in userspace. Root access is not needed as nothing is ever mounted by the kernel. - Verify the signatures of BCR and the other modules and copy the necessary files. Commands that would traditionally be run during boot via the module's
service.shandpost-fs-data.shscripts are added as proper init services. - Repack the
system,vendor, andvendor_bootpartitions, re-signing them with the specified AVB key if necessary. - Patch the OTA using avbroot's
--rootlessmode, replacing the 3 modified partitions. This will re-sign other partitions (eg.vbmeta) needed to reestablish AVB's chain of trust and also sign the patched OTA. - Generate the metadata needed to install the OTA using Custota.
When folks refer to "rooting" in the Android community, they're usually not just referring to a process running as UID 0. The term generally also includes the functionality for making runtime code patches (eg. with Zygisk) and making runtime filesystem modifications (eg. Magisk modules).
This is probably where my usage differs from most. I use root access purely for reverse engineering and debugging. As long as I can run a process as UID 0 via adb, that's good enough for me. I don't allow any Android apps to gain root access. In the past, I used my own soft-fork of Magisk with the relevant code and SELinux rules that allows such access completely removed.
Out of the many root-enabled apps I've studied or reverse engineered, the vast majority fail to handle arbitrary inputs properly (especially filenames). For example, some root-supporting file managers turn a seemingly benign action like listing a directory into local privilege escalation. This is trivially exploitable, especially with browsers auto-downloading files with server-provided filenames to /sdcard/Download/.
To avoid repeated root access UI prompts, some apps spawn a long-running shell session, write commands to stdin, and rely on parsing stdout and searching for the shell prompt to determine when commands complete. This approach is prone to desync, which can lead to commands being skipped or other inputs being interpreted as commands.
All in all, I simply do not trust most root-enabled apps to not leave a gaping security hole, so I avoid them entirely. There are apps that do handle root access in what I would consider a more proper way, by spawning a daemon as root and then talking to the daemon over a well defined binary protocol. Unfortunately, this approach is the extreme minority.
For situations where I actually do need to run a process as UID 0, I use Android's official way of getting root access: adb root and /system/xbin/su. To accomplish that, I make a userdebug build of GrapheneOS and set the ro.adb.secure=1 property to retain adb's host key verification: https://github.com/chenxiaolong/grapheneos-patches#secure-adb-for-userdebug-builds.
In the past, I used Zygisk + LSPosed for one reason only: to disable Android 12+'s verified links "feature". With GrapheneOS, I no longer have a use for this because the functionality can effectively be disabled by removing the network permission from the Intent Filter Verification Service system app. This also means I no longer need to flip between enabling and disabling Zygisk because it conflicts with some debugger functionality.
This is, by far, the main reason I "rooted" my device in the past. I've written several apps that only require system app privileges, not root, but Magisk modules are the easiest and most convenient way to install them. Modules allow running scripts during boot and overriding arbitrary filesystem paths via bind mounts (or overlayfs). Unfortunately, while this is incredibly convenient, it also makes it easy for potential malware to persist.
I wanted to have a way to install system apps without breaking Android's security model. My initial thought was to modify Magisk so that it would only load modules from the AVB-protected boot image ramdisk. Since I no longer used Magisk's other functionality, I didn't end up pursuing this approach. Instead, I wrote afsr to make it easy to unpack, modify, and repack ext2/3/4 images (byte-for-byte reproducibly). This way, I can add system apps in a way that preserves the guarantees of Android Verified Boot.
I could accomplish the exact same result by adding the apps I care about to the AOSP build system when building GrapheneOS. I intentionally don't do that because the iteration time for building OTAs with AOSP's build system is so long even when nothing has changed.
Apps that do these things don't remain installed on my devices :)
This repo is licensed under GPL-3.0-only. Please see LICENSE for the full license text.