Skip to content

Enginex0/resetprop-rs

Repository files navigation

🔧 resetprop-rs

Pure Rust Android Property Manipulation

Get. Set. Delete. Stealth. Nuke. No Magisk required.

v0.5.0 Android 10+ Rust Telegram


Note

resetprop-rs is a standalone reimplementation of Magisk's resetprop in pure Rust. It does not depend on Magisk, forked bionic, or any custom Android symbols. Works with any root solution: KSU, Magisk, APatch, or bare su.


🧬 What is resetprop-rs?

Android system properties live in mmap'd shared memory at /dev/__properties__/. Each file is a 128KB arena containing a prefix trie with BST siblings, the same data structure since Android 10.

Magisk's resetprop can manipulate these, but it's locked into Magisk's build system. It depends on a forked bionic with custom symbols (__system_property_find2, __system_property_delete, etc.) that don't exist in stock Android. You can't extract it as a standalone binary.

resetprop-rs reimplements the entire property area format in pure Rust. No bionic symbols. No Magisk dependency. Ships as a ~320KB static binary and an embeddable library crate.

It also introduces operations no existing tool provides: --stealth for detection-resistant writes, --nuke for count-preserving stealth deletes, --hexpatch-delete for dictionary-based name destruction, and --wait for blocking property watches. Under the hood, it parses the property_info binary trie for O(depth) area resolution and dual-writes to Android 14+ appcompat_override areas automatically.


🔥 Why resetprop-rs?

🔓 Truly Standalone: Zero runtime dependencies. No Magisk, no forked libc, no JNI. A single static binary that works on any rooted Android device.

🥷 Stealth Set: Writes property values with zeroed serial counter, no global serial bump, and no futex wake. To detection apps, the property looks like it was written by init at boot. Combine with -p for stealth persist to disk.

💀 Nuke: Count-preserving stealth delete. Removes the target property, inserts a plausible replacement (drawn from the device's own property vocabulary), and compacts the arena. Property count stays identical. Zero forensic traces.

🔮 Hexpatch Delete: Overwrites property name bytes with realistic dictionary words instead of detaching trie nodes. Trie structure stays intact. Serial counters preserved. Invisible to __system_property_foreach.

📦 Embeddable Library: resetprop crate with typed errors, no anyhow. Drop it into your Rust project and manipulate properties programmatically.

Tiny Footprint: ~320KB ARM64, ~240KB ARMv7. Hand-rolled CLI parser, panic=abort, LTO, single codegen unit. Only dependency: libc.

🧪 Tested Off-Device: 133 unit tests against synthetic property areas. Verified: get, set, overwrite, delete, hexpatch, stealth, nuke, compaction, context parsing, trie integrity, serial preservation, name consistency, conditional primitives, A64 encoder vectors, ELF parsing, ptrace primitives.


✨ Features

Property Operations

  • Get: single property or list all
  • Set: direct mmap write, bypasses property_service
  • Set (init-style): --init zeros the serial counter, mimicking how init writes ro.* props at boot
  • Stealth Set: --stealth / -st suppresses serial bump, global serial, and futex wake. Combine with -p for stealth persist
  • Conditional Set (if-diff): --if-diff writes only when the property exists and the current value differs
  • Conditional Set (if-match): --if-match NEEDLE writes only when the current value equals NEEDLE and differs from the new value
  • Conditional Delete: --delete-if-exist NAME deletes only when present, exit 0 on absent
  • Delete: trie node detach + value/name wipe + orphan pruning
  • Hexpatch Delete: dictionary-based name destruction, serial-preserving
  • Nuke: --nuke / -nk count-preserving stealth delete (delete + replacement + compact in one atomic operation)
  • Compact: --compact defragments arenas after deletes, reclaiming space
  • Persistent Properties: -p writes to both memory and /data/property/persistent_properties on disk; -P reads directly from the persist file
  • Batch Load: -f flag loads name=value pairs from file
  • Wait: --wait NAME [VALUE] blocks until a property exists or matches, with optional --timeout
  • Privatize: remap areas as MAP_PRIVATE for per-process COW isolation

Library API

  • PropArea: single property file: open, get, set, set_stealth, delete, nuke, hexpatch, compact, foreach
  • PropSystem: multi-file scan across /dev/__properties__/
  • PersistStore: read/write the on-disk persistent property store (protobuf + legacy format)
  • Typed errors: NotFound, AreaCorrupt, PermissionDenied, AreaFull, Io, ValueTooLong, PersistCorrupt
  • RO fallback: automatically falls back to read-only when write access is denied
  • Context-aware routing: parses property_info binary trie for O(depth) area lookup instead of O(n) scan
  • appcompat_override: dual-writes to Android 14+ override areas, preserving write mode (stealth, init, etc.)
  • Bionic fallback: falls back to __system_property_* via dlsym when mmap reads are unavailable
  • Wait: PropSystem::wait() blocks on property changes via bionic or futex

Format Support

  • Short values: ≤91 bytes, inline in prop_info
  • Long values: Android 12+, >92 bytes via self-relative arena offset
  • Serial protocol: spin-wait on dirty bit, verification loop for concurrent reads
  • Length-first comparison: matches AOSP's cmp_prop_name exactly

Stealth

  • Three-signal suppression: stealth writes zero per-prop serial, skip global serial bump, and suppress futex wake
  • Count-preserving nuke: delete + plausible replacement + compaction in one operation; enumeration count unchanged
  • Runtime harvest: replacement names drawn from the device's own property vocabulary (unfingerprintable)
  • Randomized selection: OS-seeded entropy picks different names each run
  • 3-tier fallback: harvest pool → static dictionary (~95 words) → dot-split compound generation
  • Plausible value: replaced/mangled properties show value 0 instead of empty string
  • Name consistency: trie segments and prop_info name written from same source (no cross-validation mismatch)
  • Length-bucketed: replacement is always exact same byte length as original
  • Shared segment detection: skips renaming prefixes used by other properties
  • Arena compaction: defragments holes left by deleted properties, eliminating forensic gaps

Seal

  • Two-tier seal: stealth write + ptrace-driven lock against init-mediated writers; no setprop, property_service, or bionic __system_property_set caller can revert a sealed prop
  • Tier B default (-sl / --seal): per-prop hook on __system_property_update inside init; only the sealed prop freezes, neighbors continue to update normally
  • Tier A fallback (-sla / --seal-arena): arena-level MAP_PRIVATE|MAP_FIXED remap in init; guaranteed to work but freezes every prop in the same arena as a side-effect
  • -st semantics unchanged: pure stealth write, no ptrace, no hook, no arena remap; 100% back-compat for existing scripts
  • Bypass surface (direct-mmap writers): callers that write /dev/__properties__/<ctx> directly route around init and defeat both tiers. Confirmed bypass vectors: KernelSU's /data/adb/ksu/bin/resetprop for every ro.* key and every -n / --skip-svc write (its dispatch branch at sys_prop.rs:580 forces direct-mmap for those paths, landing bytes via core::ptr::copy_nonoverlapping at mmap_prop_area.rs:277), Magisk's resetprop (same pattern), and resetprop-rs itself invoked against a sealed prop. Seal protects against init-mediated reverts, not against another root tool writing the arena inode. The threat model is init-routed writers, not every root process on the device.
  • Detection signature (Tier B installed hook): a rooted observer inspecting /proc/1/maps sees the hook body as a PROT_R|X file-backed mapping whose backing inode has been unlinked: /data/adb/resetprop-rs/hook-<pid>-<nanos>.bin (deleted). The on-disk file is written, opened + mmap'd in init, then immediately unlinked from the host so the kernel keeps the inode alive only via init's mapping reference (hook.rs:428-488). This path is used instead of an anonymous RWX mapping so init's process:execmem SELinux class is not exercised on stock Xiaomi HyperOS policies.
  • In-session only: SealRecord lives in process memory; seals do not persist across reboots, SystemProperties::Reload, or init restart. Re-run --seal / --seal-arena after every boot
  • Attach-window stall: the first -sl / -sla call on a process ptrace-attaches to init and installs the trampoline in a 15–40 ms window on modern ARM64 handsets; any thread that blocks on init for a property write during that window waits out the full stall (zygote, system_server, init-launched daemons included). Subsequent calls skip the ELF parse and remote mmap, so they complete substantially faster.
  • Futex waiters stall silently on sealed props: __system_property_wait(pi, ...) waits on init's private serial copy; a sealed prop's serial never bumps in the caller's view, so waiters never wake. Aligned with seal intent (a sealed prop should not notify of spurious updates); downstream test authors must not use waiter-based probes on sealed props.

📋 Requirements

Important

Write operations (set, delete, hexpatch) require root access with appropriate SELinux context. Read operations (get, list) work for any user since property files are world-readable.

You need:

  1. Android 10 or above
  2. Root access (KernelSU, Magisk, APatch, or equivalent)
  3. ARM64, ARMv7, x86_64, or x86 device/emulator

🚀 Quick Start

Setup

  1. Download the binary for your architecture from Releases
  2. Push to device:
    adb push resetprop-arm64-v8a /data/local/tmp/resetprop-rs
    adb shell chmod +x /data/local/tmp/resetprop-rs
  3. Run with root:
    adb shell su -c /data/local/tmp/resetprop-rs

Warning

Do NOT name the binary resetprop if you're on KernelSU or Magisk. Both ship their own resetprop in /data/adb/ksu/bin/ or /sbin/, and your shell will resolve to theirs instead of this one. Either:

  • Name it resetprop-rs (recommended)
  • Use the full path: /data/local/tmp/resetprop-rs
  • Place it earlier in $PATH than the KSU/Magisk binary

For shell scripts and modules

If you bundle this binary in a KSU module or boot script, always call it by full path:

RESETPROP="/data/adb/modules/mymodule/resetprop-rs"
$RESETPROP -st ro.build.type user        # stealth set
$RESETPROP -nk ro.lineage.version        # count-preserving delete
$RESETPROP -st -p persist.sys.timezone UTC  # stealth + persist

Do not rely on bare resetprop in scripts. It will silently use KSU/Magisk's version, which lacks --stealth, --nuke, --hexpatch-delete, --init, -p, and -P.


📖 CLI Reference

resetprop-rs [OPTIONS] [NAME] [VALUE]

Reading properties

# List all properties
resetprop-rs

# Get a single property
resetprop-rs ro.build.type

# List persistent properties from disk (/data/property/)
resetprop-rs -P

# Get a single persistent property from disk
resetprop-rs -P persist.sys.timezone

Writing properties

# Set a property (direct mmap write, bypasses property_service)
resetprop-rs -n ro.build.type user

# Set with zeroed serial counter (mimics how init writes ro.* at boot)
resetprop-rs --init ro.build.fingerprint "google/raven/raven:14/..."

# Set and persist to disk (survives reboot)
resetprop-rs -p persist.sys.timezone UTC

# Stealth set (zeroed serial, no global serial bump, no futex wake)
resetprop-rs --stealth ro.build.type user
resetprop-rs -st ro.build.type user          # short alias

# Stealth set + persist to disk
resetprop-rs --stealth -p persist.sys.timezone UTC
resetprop-rs -st -p persist.sys.timezone UTC  # short alias

# Batch set from file (one name=value per line, # comments allowed)
resetprop-rs -f props.txt

# Batch set with init-style serial
resetprop-rs --init -f props.txt

Sealing properties

# Tier B seal (default): per-prop hook inside init; only the sealed prop freezes.
resetprop-rs --seal ro.telephony.default_network 0
resetprop-rs -sl ro.telephony.default_network 0

# Tier A seal (fallback): arena-level MAP_PRIVATE in init. Freezes every prop in
# the same arena. Invoke manually after --seal reports a Tier B install failure.
resetprop-rs --seal-arena ro.telephony.default_network 0
resetprop-rs -sla ro.telephony.default_network 0

# Remove a seal
resetprop-rs --unseal ro.telephony.default_network
resetprop-rs --unseal-arena ro.telephony.default_network

# List active seals. Prop = Tier B per-prop hook; Arena = Tier A arena remap.
resetprop-rs --seals
# [ro.telephony.default_network]: [Prop] /dev/__properties__/u:object_r:telephony_prop:s0

SealRecord lives in process memory only, so --seals always reports empty from a fresh invocation. To confirm a seal is active on the device, inspect init's mappings and read the prop back:

# Tier B leaves a file-backed hook page in init as a deleted inode.
adb shell 'grep "resetprop-rs" /proc/1/maps'
# 7f8036b000-7f8036c000 r-xp ... /data/adb/resetprop-rs/hook-1-<nanos>.bin (deleted)

# Confirm the prop value is pinned to the sealed value.
adb shell 'getprop ro.telephony.default_network'
# 0

Seals do not persist across reboots. They live only in the running init's memory. To keep a prop sealed across boots, re-apply from a KSU/Magisk module's post-fs-data.sh:

#!/system/bin/sh
# /data/adb/modules/my-telephony-lock/post-fs-data.sh
/data/adb/modules/my-telephony-lock/bin/resetprop-rs \
  --seal ro.telephony.default_network 0 \
  || /data/adb/modules/my-telephony-lock/bin/resetprop-rs \
       --seal-arena ro.telephony.default_network 0

The || chain falls back to Tier A if Tier B refuses. Useful on builds where init's libc.so shape breaks the ELF walker (HookInstallFailed, ElfParse, or SymbolNotFound error classes). Tier A freezes every prop in the same arena as a side effect; for ro.telephony.default_network on stock Xiaomi the arena is telephony_prop, so all ro.telephony.* reads would then return their pre-seal values.

Deleting properties

# Delete (detaches trie node, zeroes value and name, prunes orphans)
resetprop-rs -d ro.debuggable

# Delete from both memory and persist file
resetprop-rs -p -d persist.sys.timezone

# Nuke: count-preserving stealth delete (delete + replacement + compact)
resetprop-rs --nuke ro.lineage.version
resetprop-rs -nk ro.lineage.version          # short alias

# Nuke from both memory and persist file
resetprop-rs -p --nuke persist.sys.timezone

# Hexpatch delete (replaces name with dictionary words, keeps trie intact)
resetprop-rs --hexpatch-delete ro.lineage.version

# Compact arenas (reclaim space from deleted properties)
resetprop-rs --compact

Waiting for properties

# Wait for a property to exist (blocks until set by any process)
resetprop-rs --wait sys.boot_completed

# Wait for a property to equal a specific value
resetprop-rs --wait sys.boot_completed 1

# Wait with a timeout (exits with error if not met in time)
resetprop-rs --wait ro.crypto.state encrypted --timeout 30

Options

Flag Description
-n No-op (compatibility with Magisk's resetprop)
--init Zero the serial counter when writing (mimics init for ro.* properties)
--stealth, -st Stealth set: zeroed serial, no global serial bump, no futex wake
--if-diff With positional NAME VALUE: write only when the property exists and the current value differs
--if-match NEEDLE With positional NAME VALUE: write only when the current value equals NEEDLE and differs from VALUE
--delete-if-exist NAME Delete NAME only when the property is present; exit 0 on absent
--seal NAME VALUE, -sl NAME VALUE Tier B seal (default): stealth write + per-prop init hook. Does not persist across reboots.
--seal-arena NAME VALUE, -sla NAME VALUE Tier A seal (fallback): stealth write + arena-level MAP_PRIVATE in init. Broader blast radius; no auto-fallback. Invoke manually after --seal reports a Tier B install failure.
--unseal NAME Remove NAME from the Tier B in-init lock list.
--unseal-arena NAME Revert Tier A privatization for the arena holding NAME.
--seals List active seals (name, tier, arena).
-p Persist mode: write/delete affects both memory and /data/property/ on disk
-P Read from the persist file on disk, not from the mmap'd property area
-d NAME Delete a property
--nuke NAME, -nk NAME Count-preserving stealth delete (delete + replacement + compact). Combine with -p for persist
--hexpatch-delete NAME Stealth delete with dictionary-based name replacement
--compact Defragment arenas after deletes
-f FILE Load name=value pairs from a file
--wait NAME [VALUE] Wait for property to exist or equal VALUE. Prints the value on success
--timeout SECS Timeout for --wait in seconds (default: no timeout, waits forever)
--dir PATH Use a custom property directory instead of /dev/__properties__/
-v Verbose output
-h, --help Show help

📚 Library Usage

Add to your Cargo.toml:

[dependencies]
resetprop = "0.5"

Or from git:

[dependencies]
resetprop = { git = "https://github.com/Enginex0/resetprop-rs" }
use resetprop::{PropSystem, PersistStore};

let sys = PropSystem::open()?;

// read
if let Some(val) = sys.get("ro.build.type") {
    println!("{val}");
}

// write (direct mmap, bypasses property_service)
sys.set("ro.build.type", "user")?;

// write with zeroed serial (mimics init for ro.* props)
sys.set_init("ro.build.fingerprint", "google/raven/...")?;

// stealth write (zeroed serial, no global serial bump, no futex wake)
sys.set_stealth("ro.build.type", "user")?;

// stealth write + persist to disk
sys.set_stealth_persist("persist.sys.timezone", "UTC")?;

// write to both memory and disk
sys.set_persist("persist.sys.timezone", "UTC")?;

// delete
sys.delete("ro.debuggable")?;
sys.delete_persist("persist.sys.timezone")?;

// nuke: count-preserving stealth delete
sys.nuke("ro.lineage.version")?;
sys.nuke_persist("persist.sys.timezone")?;

// hexpatch delete (dictionary-based name destruction)
sys.hexpatch_delete("ro.custom.prop")?;

// compact arenas after deletes
sys.compact()?;

// wait for a property to equal a value (30s timeout)
use std::time::Duration;
if let Some(val) = sys.wait("sys.boot_completed", Some("1"), Some(Duration::from_secs(30))) {
    println!("boot completed: {val}");
}

// enumerate
for (name, value) in sys.list() {
    println!("[{name}]: [{value}]");
}

// read persist file directly
let store = PersistStore::load()?;
for record in store.list() {
    println!("{}: {}", record.name, record.value);
}

🏗️ Building

Requires Android NDK for cross-compilation:

export ANDROID_NDK_HOME=/path/to/ndk
./build.sh

Outputs stripped binaries to out/:

ABI Binary
arm64-v8a resetprop-arm64-v8a
armeabi-v7a resetprop-armeabi-v7a
x86_64 resetprop-x86_64
x86 resetprop-x86

The build uses opt-level=s, LTO, panic=abort, strip, and single codegen unit for minimal binary size.

No NDK? Fork the repo and go to Actions → Build → Run workflow: GitHub builds all four ABIs for you. Download them from the workflow artifacts.


🧠 How It Works

Property Area Format

Each file in /dev/__properties__/ is a 128KB mmap'd arena:

┌─────────────────────────────────────┐
│ Header (128 bytes)                  │
│   [0x00] bytes_used: u32            │
│   [0x04] serial: AtomicU32          │
│   [0x08] magic: 0x504f5250 "PROP"   │
│   [0x0C] version: 0xfc6ed0ab        │
├─────────────────────────────────────┤
│ Arena (bump-allocated, append-only) │
│   ┌─ prop_trie_node ──────────┐    │
│   │ namelen(4) prop(4) left(4)│    │
│   │ right(4) children(4)      │    │
│   │ name[namelen+1] (aligned) │    │
│   └───────────────────────────┘    │
│   ┌─ prop_info ───────────────┐    │
│   │ serial(4) value[92]       │    │
│   │ name[] (full dotted name) │    │
│   └───────────────────────────┘    │
└─────────────────────────────────────┘

Property names split on dots into a prefix trie. Each trie level uses a BST for siblings, compared length-first then lexicographically (not standard strcmp).

Stealth Set

Standard set() bumps three detection signals: per-property serial, global serial (via notify()), and futex wake. Any monitoring app can observe these. Stealth set suppresses all three:

resetprop-rs --stealth ro.build.type user
  1. Write value with zeroed serial counter ((value.len() << 24), same encoding as init)
  2. Skip notify() entirely (no global serial bump via bump_serial_and_wake)
  3. Skip futex_wake() on the property's serial address
  4. New properties created via stealth are already silent (the allocation path never wakes)

The result is indistinguishable from a property written by init at boot. Combine with -p to also persist to /data/property/persistent_properties.

Nuke (Count-Preserving Delete)

Standard delete leaves a gap in the enumeration count. Hexpatch preserves count but leaves renamed trie segments. Nuke achieves both: zero artifacts AND preserved count.

Before: ro.lineage.version = "18.1"  (2389 props total)
After:  (original gone, plausible replacement added, 2389 props total)
  1. Delete the target property (detach trie node, wipe prop_info, prune orphans)
  2. Scan the area for existing property prefixes, pick the busiest prefix (most natural to add a sibling)
  3. Generate a plausible leaf segment (harvest pool → dictionary → compound generation)
  4. Insert the replacement via stealth write with value "0" (zeroed serial, no wake)
  5. Compact the arena to eliminate holes from the deletion
  6. Net result: target gone, replacement blends in, count unchanged, no detection signals fired

Hexpatch Delete

An alternative stealth delete that keeps the trie structure intact by overwriting name bytes in-place:

Before: ro.lineage.version = "18.1"
After:  ro.codec.charger = "0"
  1. Harvest all property segments from the device into a length-bucketed pool
  2. Walk the trie path, collecting each segment's node offset
  3. For each non-shared segment, pick a same-length replacement (harvest → dict → dot-split compound)
  4. Overwrite name bytes in-place, randomized selection each run
  5. Write mangled name to prop_info from the same chosen segments (single source of truth)
  6. Set value to 0 with correct serial encoding
  7. No serial bump, no futex wake

Context-Aware Area Resolution

Android stores properties across multiple files in /dev/__properties__/, one per SELinux context. The file property_info is a binary trie that maps property name prefixes to the correct area file.

resetprop-rs parses this trie on startup, enabling O(depth) lookup by property name instead of scanning every area file linearly. The algorithm walks the trie segment-by-segment (split on .), checking node contexts, prefix entries, child nodes, and exact matches at each level.

On Android 8-9 (pre-serialized format), it falls back to parsing text property_contexts files. If neither is available, it falls back to the original linear scan.

appcompat_override (Android 14+)

Android 14 introduced /dev/__properties__/appcompat_override/, a mirror directory containing duplicate property areas for app compatibility. When a property is set or deleted, the system writes to both the main area and the corresponding override file.

resetprop-rs detects this directory and automatically dual-writes to the mirror when performing set, set_init, set_stealth, or delete operations. The override write uses the same mode as the main write (stealth for stealth, init for init). Override failures are silently ignored.

Bionic Fallback

On Android, if a property can't be found via direct mmap (e.g., areas not directly accessible), resetprop-rs falls back to bionic's __system_property_find and __system_property_read_callback loaded via dlsym at runtime. This is a secondary path; the primary pure mmap path is always tried first.

The --wait command also uses bionic's __system_property_wait when available, falling back to futex_wait on the property's serial address (or the global serial for properties that don't exist yet).


📱 Compatibility

Status
Android 10 – 15
Architecture ARM64, ARMv7, x86_64, x86
Value format Short (≤91B) + Long (Android 12+, >92B)
Root KernelSU, Magisk, APatch, any su

💬 Community

Telegram


🙏 Credits

Contributors


📄 License

This project is licensed under the MIT License.


🔧 Because the best property manipulation is the one init never noticed.

About

Pure Rust implementation of Android resetprop with hexpatch-delete capability

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors