NO NETWORK · NO TELEMETRY · FAILS CLOSED · v0.3.0

Android

Bootloader unlockedDevice environment

Reads the verified boot state the bootloader hands to Android — orange means the bootloader is unlocked.

bootloader_unlockedCompromising by default

At a glance

Platform
Android
Category
Device environment
Source
IntegrityChecks.kt · checkBootloaderUnlocked
Default status
Makes status compromised
Seen firing
Seen firing in A01, A02, A07. Based on validated scenarios whose observed result includes this signal; see below.

How it works

What the code inspects.

Reads three system properties in-process (android.os.SystemProperties, falling back to one getprop dump if that is unavailable): ro.boot.verifiedbootstate (orange = unlocked), ro.boot.flash.locked (0 = unlocked) and ro.boot.vbmeta.device_state (unlocked). Any one of them is enough.

green (locked, stock keys) and yellow (locked with user-installed keys, e.g. GrapheneOS) do not fire. Skipped when the emulator signal fired, because emulators boot unlocked too and are not compromising by default — verified on a development emulator reporting orange (scenario S11).

The properties are readable by any app without root or permissions. Reading them in-process takes microseconds; the getprop fallback spawns a process (~40–100 ms) and is shared with root_dangerous_props, so it runs at most once per check.

Configurable per app at build time: compromised (default), report (returned in signals but never compromising on its own, like emulator) or off (not run). Expo: the config plugin option android.bootloaderUnlocked. Bare React Native: <meta-data android:name="com.deviceintegrity.bootloader_unlocked" android:value="report"/> inside <application>, optionally through a Gradle manifest placeholder so debug and release builds differ. A missing or unknown value counts as compromised.

Independent of the root checks. It answers whether the phone can boot unverified software; root_su_binary, root_management_apps and root_magisk_files answer whether root is installed now. Status is compromised if any active signal is compromising, so setting this to report or off (or ignoring it at runtime) never hides a rooted phone. Each can also fire alone: an unrooted custom-ROM phone reports only this signal.

Verified on a rooted OnePlus Nord 3 (Magisk 31): default → compromised; root signals ignored at runtime → still compromised from this signal alone; report with root ignored → clean, with the signal still listed; off → the signal is gone but the root signals keep the device compromised; ignore: ['bootloader_unlocked'] → compromised from the root signals.

The exploit it addresses

Why this matters.The threat.

An unlocked bootloader boots any software without verifying it. It's the first step to rooting and to replacing the OS entirely, and it's why Play Integrity fails its device verdict on such phones.

It also catches a device that was unlocked and then had root hidden or removed, which the root checks alone would miss.

How it can be bypassed

No single check is sufficient.Known evasions.

A root framework can rewrite these properties for the target app — Magisk's resetprop is what Play Integrity Fix-style modules use — so a hidden setup can report green.

Relaxing it weakens the backstop for hidden root: with Magisk's DenyList on, su and the root mounts disappear and this signal is one of the few left (scenario A02). With it set to report or off, hiding root and renaming the Magisk app would leave a rooted phone reading clean.

An in-process hook (Frida, Xposed/LSPosed) on android.os.SystemProperties.get can return green to this app without touching the real properties.

Hardware-backed key attestation reports the real boot state from the secure hardware and can't be changed by resetprop; for high-value decisions, verify it (or Play Integrity) server-side.