Pure Rust Android Property Manipulation
Get. Set. Delete. Stealth. Nuke. No Magisk required.
<img src="https://img.shields.io/badge/version-v0.6.0-blue?style=for-the-badge" alt="v0.6.0">
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
<img src="https://img.shields.io/badge/Rust-stable-orange?style=for-the-badge&logo=rust" alt="Rust">
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
[!NOTE] resetprop-rs is a standalone reimplementation of Magisk's
resetpropin pure Rust. It does not depend on Magisk, forked bionic, or any custom Android symbols. Works with any root solution: KSU, Magisk, APatch, or baresu.
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.
🔓 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: 165 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, long-value create, A64 encoder vectors, ELF parsing, ptrace primitives.
Property Operations
- [x] Get: single property or list all
- [x] Set: direct mmap write, bypasses property_service
- [x] Set (init-style): --init zeros the serial counter, mimicking how init writes ro.* props at boot
- [x] Stealth Set: --stealth / -st suppresses serial bump, global serial, and futex wake. Combine with -p for stealth persist
- [x] Conditional Set (if-diff): --if-diff writes only when the property exists and the current value differs
- [x] Conditional Set (if-match): --if-match NEEDLE writes only when the current value equals NEEDLE and differs from the new value
- [x] Conditional Delete: --delete-if-exist NAME deletes only when present, exit 0 on absent
- [x] Delete: trie node detach + value/name wipe + orphan pruning
- [x] Hexpatch Delete: dictionary-based name destruction, serial-preserving
- [x] Nuke: --nuke / -nk count-preserving stealth delete (delete + replacement + compact in one atomic operation)
- [x] Compact: --compact defragments arenas after deletes, reclaiming space
- [x] Persistent Properties: -p writes to both memory and /data/property/persistent_properties on disk; -P reads directly from the persist file
- [x] Batch Load: -f flag loads name=value pairs from file
- [x] Wait: --wait NAME [VALUE] blocks until a property exists or matches, with optional --timeout
- [x] Privatize: remap areas as MAP_PRIVATE for per-process COW isolation
Library API
- [x] PropArea: single property file: open, get, set, set_stealth, delete, nuke, hexpatch, compact, foreach
- [x] PropSystem: multi-file scan across /dev/__properties__/
- [x] PersistStore: read/write the on-disk persistent property store (protobuf + legacy format)
- [x] Typed errors: NotFound, AreaCorrupt, PermissionDenied, AreaFull, Io, ValueTooLong, PersistCorrupt
- [x] RO fallback: automatically falls back to read-only when write access is denied
- [x] Context-aware routing: parses property_info binary trie for O(depth) area lookup instead of O(n) scan
- [x] appcompat_override: dual-writes to Android 14+ override areas, preserving write mode (stealth, init, etc.)
- [x] Bionic fallback: falls back to __system_property_* via dlsym when mmap reads are unavailable
- [x] Wait: PropSystem::wait() blocks on property changes via bionic or futex
Format Support
- [x] Short values: ≤91 bytes, inline in prop_info
- [x] Long values: Android 12+, ≥92 bytes via self-relative arena offset. Create, read, and overwrite are all supported. A long prop is byte-identical to one init writes (kLongFlag, legacy error message in value[], serial length byte = error-message length). Read-only (ro.*) long props read back in full via getprop; mutable long props return the bionic legacy error string to __system_property_get/getprop by design, and the full value only via __system_property_read_callback or this tool
- [x] Serial protocol: spin-wait on dirty bit, verification loop for concurrent reads
- [x] Length-first comparison: matches AOSP's cmp_prop_name exactly
Stealth
- [x] Three-signal suppression: stealth writes zero per-prop serial, skip global serial bump, and suppress futex wake
- [x] Count-preserving nuke: delete + plausible replacement + compaction in one operation; enumeration count unchanged
- [x] Runtime harvest: replacement names drawn from the device's own property vocabulary (unfingerprintable)
- [x] Randomized selection: OS-seeded entropy picks different names each run
- [x] 3-tier fallback: harvest pool → static dictionary (~95 words) → dot-split compound generation
- [x] Plausible value: replaced/mangled properties show value 0 instead of empty string
- [x] Name consistency: trie segments and prop_info name written from same source (no cross-validation mismatch)
- [x] Length-bucketed: replacement is always exact same byte length as original
- [x] Shared segment detection: skips renaming prefixes used by other properties
- [x] Arena compaction: defragments holes left by deleted properties, eliminating forensic gaps
Seal
- [x] 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
- [x] Tier B default (-sl / --seal): per-prop hook on __system_property_update inside init; only the sealed prop freezes, neighbors continue to update normally
- [x] 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
- [x] Repeatable seal: --seal A v1 --seal B v2 locks several props in one run as a single multi-entry lock list inside init, instead of one process per prop
- [x] Dry-run (--check): --seal NAME --check resolves __system_property_update in init and validates the splice site without writing anything
- [x] Observe init (--observe-init): ptrace init (PID 1) and print its /dev/kmsg writes for a window (default 5s, aarch64); shows which init thread services a write before you seal it
- [x] -st semantics unchanged: pure stealth write, no ptrace, no hook, no arena remap; 100% back-compat for existing scripts
- [x] 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.
- [x] Detection signature (Tier B installed hook): a rooted observer inspecting /proc/1/maps sees the hook body as a PROT_R|X mapping backed by an anonymous memfd whose inode shows the kernel's standard (deleted) suffix: /memfd:resetprop-hook (deleted). The tracee creates the memfd, this process fills it via /proc/<pid>/fd, and init maps it PROT_R|X (hook.rs:451, install_memfd_hook_page). There is no on-disk file, so nothing is written to or unlinked from /data. The memfd is mapped PROT_R|X rather than as an anonymous RWX page so init's process:execmem SELinux class is not exercised on stock Xiaomi HyperOS policies.
- [x] 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
- [x] 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.
- [x] 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.
[!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
sh
adb push resetprop-arm64-v8a /data/local/tmp/resetprop-rs
adb shell chmod +x /data/local/tmp/resetprop-rssh
adb shell su -c /data/local/tmp/resetprop-rs[!WARNING] Do NOT name the binary
resetpropif you're on KernelSU or Magisk. Both ship their ownresetpropin/data/adb/ksu/bin/or/sbin/, and your shell will resolve to theirs instead of this one. Either: - Name itresetprop-rs(recommended) - Use the full path:/data/local/tmp/resetprop-rs- Place it earlier in$PATHthan the KSU/Magisk binary
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.
resetprop-rs [OPTIONS] [NAME] [VALUE]
# 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
```sh
resetprop-rs -n ro.build.type user
resetprop-rs --init ro.build.fingerprint "google/raven/raven:14/..."
resetprop-rs -p persist.sys.timezone UTC
resetprop-rs --stealth ro.build.type user resetprop-rs -st ro.build.type user # short alias
resetprop-rs --stealth -p persist.sys.timezone UTC resetprop-rs -st -p persist.sys.timezone UTC # short alias
$ claude mcp add resetprop-rs \
-- python -m otcore.mcp_server <graph>