Skip to content

Bootloop protection counts off-mode (charger-mode) boots -> Safe Mode falsely triggered on Motorola #9967

Description

@vv19ca0

Device: Motorola moto XT2435 (MediaTek MT6855). Also reproduces on Motorola XT2533 (Qualcomm Snapdragon SM7435).
Android version: 14 (also reproduced on Android 15)
Magisk version name: 30.7 (debug build, reports 30.7:MAGISK:D)
Magisk version code: 30700

Reproduced on a debug build with no modules installed.

Summary

On Motorola devices, off-mode charging (device powered off, plugged into a charger, showing the charging animation) runs a near-full boot that reaches post-fs-data, so magiskd increments the bootloop counter — but a charger boot never reaches boot-complete, so the counter is never reset. Two off-mode-charging boots with no full boot in between raise the counter to 2, and the next normal boot falsely enters Safe Mode (Zygisk turned off, all modules disabled). This happens from ordinary charging-while-off, not a real bootloop.

Root cause (native/src/core/bootstages.rs)

let boot_cnt = self.get_db_setting(DbEntryKey::BootloopCount);   // read
self.set_db_setting(DbEntryKey::BootloopCount, boot_cnt + 1);   // increment
let safe_mode = boot_cnt >= 2 || …;                              // trigger

The counter is cleared only at boot-complete:

// Reset the bootloop counter once we have boot-complete
self.set_db_setting(DbEntryKey::BootloopCount, 0).log_ok();

On Motorola, off-mode charging is implemented as a near-full boot: the bootloader shows its normal (unlocked) warning screen, then the patched boot image + init run through post-fs-data with /data mounted, so magiskd runs post_fs_data (ro.bootmode=charger) and increments BootloopCount. It then stays at the charger UI and never reaches boot-complete, so the counter is never reset. There is no charger/ro.bootmode guard anywhere in the boot-stage code.

Evidence (debug v30.7, no modules)

Two consecutive off-mode-charging boots, ro.bootmode=charger, counter (bootloop DB value) never reset:

charger boot #1 : ro.bootmode=charger   bootloop=1
charger boot #2 : ro.bootmode=charger   bootloop=2

getprop persist.sys.boot.reason.history:

reboot,charger → reboot,charger → cold,powerkey

The next normal boot (magisk.log):

Magisk 30.7(30700) daemon started
** post-fs-data mode running
* Initializing Magisk environment
* Safe mode triggered
** boot-complete triggered

Result: zygisk=0, all modules disabled.

Why it is OEM-dependent, not SoC / not "low battery"

Reproduced on both a MediaTek (MT6855) and a Qualcomm (Snapdragon SM7435) Motorola, so it is not chip-specific — it is Motorola's off-mode-charging implementation running the full init path (mounts /data, runs post-fs-data). OEMs whose off-mode charging uses a lightweight path never run magiskd, so the counter never moves and they are unaffected.

Steps to reproduce (stock/official build, Motorola)

  1. Boot normally: magisk --sqlite "SELECT value FROM settings WHERE key='bootloop'"0.
  2. Power off. Plug in charger → charging animation (off-mode charging).
  3. Unplug (device powers off), plug in again → charging animation. Do this ≥2 times with no boot to system in between.
  4. Power on into the OS → magisk.log shows * Safe mode triggered; Zygisk off; modules disabled.

Suggested fix

Don't count charger-mode boots toward bootloop protection. In post_fs_data:

if get_prop(cstr!("ro.bootmode")) != "charger" {
    let boot_cnt = self.get_db_setting(DbEntryKey::BootloopCount);
    self.set_db_setting(DbEntryKey::BootloopCount, boot_cnt + 1);
    // safe_mode check …
}

i.e. treat androidboot.mode=charger boots as not counting toward bootloop, since they never reach boot-complete by design.

Related

Likely the same root cause as #9530 (closed with no mechanism identified there).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions