Rooting, Flashing, & Bootloader Exploits

Persistent Magisk Modules: Architecting Solutions that Survive OTA Updates and Factory Resets

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction

Magisk revolutionized Android rooting by introducing a “systemless” approach, allowing users to modify their device’s behavior without directly altering the read-only system partition. This innovation paved the way for a vibrant module ecosystem, enabling custom kernels, performance tweaks, UI enhancements, and more. However, a common challenge faced by both module developers and advanced users is the ephemeral nature of certain module functionalities. While Magisk modules generally survive simple reboots, ensuring their configurations and custom data persist across Over-The-Air (OTA) updates or factory resets requires a deeper understanding of Magisk’s intricate boot process and its systemless overlay mechanisms.

This expert-level guide delves into the advanced techniques required to architect Magisk modules that maintain their state and functionality, offering robust solutions for true persistence.

Understanding Magisk’s Boot Flow and Module Loading

To build persistent modules, one must first grasp how Magisk operates during boot. Magisk creates a “Magisk partition” (often a loop device) on the /data partition, which hosts its core files, modules, and overlayFS data. During boot, Magisk hooks into the early boot stages to mount its own image and then overlay its modifications onto the original system. This systemless approach is brilliant, but it’s also the source of persistence challenges.

Key Script Execution Stages

  • boot-start.sh

    Executed very early in the boot process, even before the /data partition is decrypted and mounted. This script is rarely used for module persistence as it lacks access to most user data and module files.

  • post-fs-data.sh

    This is a critical script for persistence. It runs after /data is mounted and decrypted, but before system services start. This makes it ideal for applying file-level modifications or setting up environments that need to be ready early.

    #!/system/bin/sh# This script is executed after the /data partition is mounted and decrypted.# Example: Ensure a custom hosts file is symlinked if not already present.MODULE_DIR="${MAGISK_MODULE_PATH}"HOSTS_ORIGINAL="${MODULE_DIR}/system/etc/hosts.magisk"HOSTS_TARGET="/system/etc/hosts"if [ ! -f "${HOSTS_TARGET}" ] || [ ! -L "${HOSTS_TARGET}" ]; then  # Remove existing, non-symlinked hosts file if it's not ours  if [ -f "${HOSTS_TARGET}" ] && [ ! -L "${HOSTS_TARGET}" ]; then    rm -f "${HOSTS_TARGET}"  fi  ln -sf "${HOSTS_ORIGINAL}" "${HOSTS_TARGET}"fi
  • service.sh

    Executed later, after /data is mounted and most system services have started. This script is perfect for starting custom daemons, applying runtime configurations, or making changes that depend on other system components being fully initialized.

    #!/system/bin/sh# This script is executed late in the boot process, after system services start.# Example: Start a custom background service and apply a persistent configuration.MODULE_DIR="${MAGISK_MODULE_PATH}"CONFIG_FILE="${MODULE_DIR}/config/my_service.conf"LOG_FILE="/data/local/tmp/my_service.log"# Ensure log directory existsmkdir -p "$(dirname "${LOG_FILE}")"if [ -f "${CONFIG_FILE}" ]; then  echo "[$(date)] Starting my_service with config: ${CONFIG_FILE}" >> "${LOG_FILE}"  # Replace with actual service start command  # my_custom_service --config "${CONFIG_FILE}" &  echo "[$(date)] my_service started." >> "${LOG_FILE}"else  echo "[$(date)] Error: ${CONFIG_FILE} not found! Cannot start my_service." >> "${LOG_FILE}"fi

The Challenge of Persistence: OTA Updates and Factory Resets

Magisk’s modules leverage overlayFS to apply modifications. When an OTA update occurs, the underlying system partition is overwritten, effectively ‘resetting’ any system-level changes that Magisk overlaid. Magisk’s “survival mode” attempts to preserve root, but modules often need to be re-enabled or even reinstalled, meaning their setup scripts (`customize.sh`, `post-fs-data.sh`, `service.sh`) must run again.

A factory reset, by design, wipes the entire /data partition, where Magisk’s core files and module data reside. This means all module configurations and any custom data stored within the /data partition (outside the module’s core structure) will be obliterated.

Architecting Persistence: Core Strategies

The key to persistence lies in ensuring that either the crucial data itself survives, or that the module is intelligent enough to recreate or re-apply that data when it’s re-enabled or reinstalled.

Strategy 1: Data Re-Application via post-fs-data.sh or service.sh

For scenarios where a module needs to modify a file that might be overwritten by an OTA or requires runtime setup, the `post-fs-data.sh` or `service.sh` scripts are invaluable. The module’s `customize.sh` script (run only during installation) should place the *source* of the persistent data within the module’s own directory structure (e.g., $MODPATH/config/my_persistent_data).

Then, `post-fs-data.sh` or `service.sh` can check for the existence of the desired modification at the target location and re-apply it if missing or incorrect. This makes the module idempotent.

Example: Persisting a custom hosts file

Let’s say you want a custom hosts file to always be active at /system/etc/hosts.

customize.sh (part of your module installer):

# Place the actual hosts file in your module's system overlay.mkdir -p ${MODPATH}/system/etccp /tmp/hosts_template ${MODPATH}/system/etc/hosts

This `hosts` file will be overlaid by Magisk, but if an OTA occurs and `customize.sh` doesn’t run again, the symlink might be broken. A more robust approach involving `post-fs-data.sh` would be:

post-fs-data.sh (part of your module):

#!/system/bin/sh# Re-establish custom hosts file if necessaryMAGISK_HOSTS="${MAGISK_MODULE_PATH}/system/etc/hosts"TARGET_HOSTS="/system/etc/hosts"if [ -f "${MAGISK_HOSTS}" ]; then  if [ -f "${TARGET_HOSTS}" ] && [ ! -L "${TARGET_HOSTS}" ]; then    # Target exists but is not a symlink, likely a stock file, remove it    rm -f "${TARGET_HOSTS}"  fi  if [ ! -L "${TARGET_HOSTS}" ]; then    # Symlink if not already symlinked    ln -sf "${MAGISK_HOSTS}" "${TARGET_HOSTS}"  fi  # Ensure correct permissions if needed  chmod 0644 "${TARGET_HOSTS}"fi

This ensures that even if the original target is restored by an OTA, your module’s script will re-establish the symlink on every boot, providing persistence as long as the module itself is active.

Strategy 2: Leveraging the Module’s Own Data Directory (`/data/adb/modules//`)

This is arguably the most robust method for maintaining module-specific persistent configuration and data across reboots and even OTA updates (assuming the module is re-enabled/reinstalled). Each Magisk module gets its own dedicated directory under `/data/adb/modules//`. Crucially, this directory *survives* OTA updates because it resides on the `/data` partition and Magisk can automatically re-enable modules. It only gets wiped during a factory reset.

Any data (configuration files, custom binaries, logs) stored within $MODPATH/data (which resolves to /data/adb/modules//data) or a custom sub-directory created within $MODPATH will persist as long as the module itself is installed and not explicitly removed or /data is not wiped.

Example: Persistent Configuration for a Custom Daemon

Imagine your module provides a custom daemon that needs a configuration file.

customize.sh (during module installation):

# Define pathsCONFIG_DIR="${MODPATH}/data/config"DEFAULT_CONFIG_SOURCE="${MODPATH}/assets/default_config.ini"PERSISTENT_CONFIG="${CONFIG_DIR}/my_daemon.ini"# Create config directorymkdir -p "${CONFIG_DIR}"chown 0.0 "${CONFIG_DIR}"chmod 0755 "${CONFIG_DIR}"# Copy default config if none exists or if force-installif [ ! -f "${PERSISTENT_CONFIG}" ] || [ "$MAGISK_UPDATE" = "true" ]; then  cp "${DEFAULT_CONFIG_SOURCE}" "${PERSISTENT_CONFIG}"  chown 0.0 "${PERSISTENT_CONFIG}"  chmod 0644 "${PERSISTENT_CONFIG}"fi

service.sh (on every boot):

#!/system/bin/shMODULE_DIR="${MAGISK_MODULE_PATH}"CONFIG_FILE="${MODULE_DIR}/data/config/my_daemon.ini"DAEMON_BINARY="${MODULE_DIR}/system/bin/my_daemon"# Check if config exists and binary is executableif [ -f "${CONFIG_FILE}" ] && [ -x "${DAEMON_BINARY}" ]; then  # Start the daemon with the persistent config  "${DAEMON_BINARY}" --config "${CONFIG_FILE}" &  log_print "My custom daemon started with persistent config."else  log_print "Error: My custom daemon or its config not found. Not starting."fi

Here, the `my_daemon.ini` file located under `$MODPATH/data/config` will persist across reboots and OTA updates. The `service.sh` script simply reads this persistent configuration to start the daemon. User modifications to `my_daemon.ini` will also persist.

Handling OTA Updates and Factory Resets

OTA Updates

Magisk offers a “Direct Install” or “Install to Inactive Slot (After OTA)” option. For modules, if Magisk survives the OTA, modules usually get re-enabled. Your `post-fs-data.sh` and `service.sh` will then run again, ensuring that your persistent logic is re-applied. If Magisk doesn’t survive, a re-flash of Magisk and re-installation/re-enabling of modules will be needed. In either case, Strategy 1 and 2 ensure the necessary files/logic are available to be reapplied.

Factory Resets

This is the ultimate wipe. A factory reset obliterates the entire `/data` partition, meaning anything stored within `/data/adb` (including your modules and their data directories) will be lost. True persistence across a factory reset is generally outside the scope of *systemless* Magisk modules. If a module requires data to survive a factory reset, it would necessitate backup/restore mechanisms (e.g., cloud backup, local storage on an external SD card) that are external to Magisk itself.

Best Practices for Robust Persistent Modules

  • Idempotency

    Your `post-fs-data.sh` and `service.sh` scripts should be idempotent. Running them multiple times (e.g., due to a Magisk update or manual re-execution) should produce the same state without errors or unintended side effects.

  • Error Handling and Logging

    Implement robust error checking and logging within your scripts. Use `log_print` (available in Magisk module scripts) to output messages to the Magisk log, making debugging easier.

    # Example of logging and error handlingif [ -d "${TARGET_DIR}" ]; then  log_print "Target directory ${TARGET_DIR} exists."else  log_print "Error: Target directory ${TARGET_DIR} does not exist!"  exit 1 # Exit script with error codefi
  • Clean Uninstallation

    Ensure your `uninstall.sh` script (if present) thoroughly reverts all changes made by the module, especially any persistent modifications. This maintains system integrity.

  • Clear Documentation

    Provide clear instructions within your module’s `README.md` and comment your scripts extensively. Explain which files are persisted, where, and why.

Conclusion

Architecting persistent Magisk modules requires a nuanced understanding of Magisk’s operational lifecycle and strategic use of its inherent capabilities. By leveraging `post-fs-data.sh` and `service.sh` for re-application logic and, more importantly, by storing module-specific persistent data within the module’s dedicated data directory (`/data/adb/modules//`), developers can create powerful solutions that seamlessly survive OTA updates and provide a stable user experience. While factory resets remain the ultimate reset button, the techniques outlined here provide a robust framework for nearly all other persistence challenges within the Magisk ecosystem.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner