docs: catalogue every persistent path the project touches

Adds docs/state.md — a reference doc grouped by location prefix
(/data/adb/modules/, /data/adb/vpnhide_*/, /data/system/vpnhide_*,
/proc/vpnhide_*, app SharedPrefs + filesDir, iptables chains,
external paths we read). Each entry records format, writers,
readers (with file:line cites), lifetime, and mode/owner/SELinux
label where it matters.

Includes a heads-up that Vector LSPosed redirects the app's
SharedPreferences to /data/misc/<vector-uuid>/prefs/<pkg>/, so
inspecting /data/data/<pkg>/shared_prefs/ from a root shell
shows nothing even after the user toggles a setting.

Linked from CLAUDE.md's "Read before touching code" list.
This commit is contained in:
okhsunrog 2026-04-27 23:33:40 +03:00
parent 604e5cbeb4
commit 3c0fe63289
2 changed files with 273 additions and 0 deletions

View file

@ -21,6 +21,7 @@ These short files cover everything specific to this repo. Skipping them leads to
- [CONTRIBUTING.md](CONTRIBUTING.md) — PR process, commit conventions, changelog requirement
- [docs/development.md](docs/development.md) — prereqs, per-module build quickstart, keystore setup, device install, CI lints
- [docs/state.md](docs/state.md) — every persistent path / proc entry / iptables chain the project touches; who writes, who reads, lifetime
- [docs/changelog.md](docs/changelog.md) — changelog storage (`changelog.d/` fragments + history JSON), `./scripts/changelog.py` usage
- [docs/releasing.md](docs/releasing.md) — `./scripts/release.py` usage, version-bump flow
- [kmod/BUILDING.md](kmod/BUILDING.md) — kernel-module build (one-command DDK via `./kmod/build.py`, GKI matrix, troubleshooting)

272
docs/state.md Normal file
View file

@ -0,0 +1,272 @@
# Persistent state — every path the project touches
Reference catalogue for everyone (humans, agents) trying to answer
"where is this stored?" / "who reads X?" / "what survives a reboot?".
Grouped by **location prefix**, because the same path is usually
written by one component and read by another, and grouping by reader
or writer would split the same path across multiple places.
For each entry: format, who writes, who reads, lifetime, mode/owner/
SELinux label when relevant. File:line cites the source of truth.
> **Heads-up:** SELinux contexts shown here are `setfiles`/AOSP
> defaults observed on a Pixel running Magisk. Custom kernels or
> heavily modified ROMs may relabel things; if a write fails, check
> `dmesg | grep avc` first.
---
## 1. Module install dirs — `/data/adb/modules/vpnhide_*/`
These are the standard Magisk/KSU module dirs. Anything inside is
**wiped on module reinstall** — the root manager replaces the whole
tree from the zip. So all *user-managed* state lives outside, in the
persistent dirs of section 2.
### `/data/adb/modules/vpnhide_kmod/`
- `module.prop` — module metadata + stamped `gkiVariant=` and `version=`. Read by app dashboard (`DashboardData.kt:382` `parseModuleProp`).
- `post-fs-data.sh` — runs at boot, attempts `insmod vpnhide_kmod.ko`, writes diagnostics into the persistent dir.
- `service.sh` — runs after boot, reads `targets.txt`, resolves UIDs, writes `/proc/vpnhide_targets`.
- `vpnhide_kmod.ko` — the kernel module binary itself.
### `/data/adb/modules/vpnhide_zygisk/`
- `module.prop` — module metadata.
- `customize.sh` — install-time hook; seeds persistent dir, migrates legacy targets.
- `service.sh` — boot script; copies persistent `targets.txt` into module dir so the Zygisk loader's `get_module_dir()` fd sees it.
- `zygisk/arm64-v8a.so` — Rust cdylib injected into every forked app by NeoZygisk.
- `targets.txt`**boot-time copy** of the canonical persistent file (`/data/adb/vpnhide_zygisk/targets.txt`). Loader reads via fd, not path. (`zygisk/module/service.sh`, `zygisk/src/lib.rs`)
- `debug_logging``"0"` or `"1"`. Written by app (`DebugLoggingPrefs.kt:78-82`), read by zygisk module on init.
### `/data/adb/modules/vpnhide_ports/`
- `module.prop`.
- `customize.sh` — seeds persistent dir on install.
- `service.sh` — calls `vpnhide_ports_apply.sh` after netd is up.
- `vpnhide_ports_apply.sh` — main runtime script. Resolves observers → UIDs, builds & applies iptables rules. Also re-invoked by the app via su when the user taps Save.
- `uninstall.sh` — flushes `vpnhide_out` / `vpnhide_out6` chains.
---
## 2. Module persistent dirs — `/data/adb/vpnhide_*/`
These dirs are **outside** `/data/adb/modules/`, so module reinstalls
don't touch them. They survive Magisk/KSU updates, kernel upgrades,
and even uninstalling+reinstalling the corresponding module. Wiped
only by factory reset.
### `/data/adb/vpnhide_kmod/`
| File | Format | Writer | Reader | Lifetime |
|---|---|---|---|---|
| `targets.txt` | one pkg per line, `#` comments | app via su (Save in Protection); seeded by `kmod/module/customize.sh` | `kmod/module/service.sh` (boot, resolves to UIDs) | persistent |
| `load_status` | `key=value` per line: `timestamp`, `boot_id`, `uname_r`, `gki_variant`, `kmod_version`, `root_manager`, `kprobes`, `kretprobes`, `insmod_exit`, `loaded`, `insmod_stderr` | `kmod/module/post-fs-data.sh` | app dashboard (`KMOD_LOAD_STATUS_FILE` constant in `ShellUtils.kt`); `readKmodLoadStatus` in `DashboardData.kt:482` | overwritten each boot |
| `load_dmesg` | filtered `dmesg` excerpt (text) | `post-fs-data.sh` | dashboard (verbose error display) | overwritten each boot |
`boot_id` in `load_status` is compared against the current
`/proc/sys/kernel/random/boot_id` so the app can tell "this status
was written this boot" vs. "stale from last boot".
### `/data/adb/vpnhide_zygisk/`
| File | Format | Writer | Reader | Lifetime |
|---|---|---|---|---|
| `targets.txt` | one pkg per line | app via su; `zygisk/module/customize.sh` migrates from legacy in-module location | copied into module dir by `zygisk/module/service.sh` at boot | persistent |
### `/data/adb/vpnhide_ports/`
| File | Format | Writer | Reader | Lifetime |
|---|---|---|---|---|
| `observers.txt` | one pkg per line | app via su; seeded by `portshide/module/customize.sh` | `vpnhide_ports_apply.sh` (boot + on Save) | persistent |
### `/data/adb/vpnhide_lsposed/`
| File | Format | Writer | Reader | Lifetime |
|---|---|---|---|---|
| `targets.txt` | one pkg per line | app via su (`ensureSelfInTargets` in `ShellUtils.kt:198` + Protection screens) | `kmod/module/service.sh` and `zygisk/module/service.sh` migrate-from on first boot if their own `targets.txt` is empty | persistent |
LSPosed has no module dir (it's not a Magisk module — it's
hooks installed into system_server by the Vector framework), so
everything lives in this persistent dir directly.
---
## 3. system_server-readable files — `/data/system/vpnhide_*`
This is the coordination channel between the app (writes via su) and
the LSPosed hooks running inside system_server. All files here are
**owned `root:system`, mode 0640, label `system_data_file`** — system_server
reads via the `system` group, untrusted apps fall to "other" and get
EACCES. The dir `/data/system/` itself is mode 0775 traversable by
all, so the per-file restriction matters; a plain 0644 here would be
enumerable + readable.
| File | Format | Writer | Reader | Lifetime |
|---|---|---|---|---|
| `vpnhide_uids.txt` | one UID per line (integer) | `kmod/module/service.sh` and `zygisk/module/service.sh` resolve `targets.txt` → UIDs at boot; app via su after Save | LSPosed hook in system_server; `HookEntry.kt:174` first-call read + FileObserver (`HookEntry.kt:346`) for live reload | persistent (rewritten at boot + on save) |
| `vpnhide_hidden_pkgs.txt` | one pkg per line | app via su when user picks "Apps to hide from PackageManager" | `PackageVisibilityHooks.kt:124` + FileObserver | persistent |
| `vpnhide_observer_uids.txt` | one UID per line | app via su | `PackageVisibilityHooks.kt:111` + FileObserver | persistent |
| `vpnhide_hook_active` | `key=value`: `version`, `boot_id`, `timestamp`, `aosp_sdk`, optional `broken_fields` | `HookEntry.kt:312-339` `writeHookStatusFile` (in system_server) | app dashboard (`DashboardData.kt:805` reads via su); compares `boot_id` to detect stale records | per-boot |
| `vpnhide_debug_logging` | single byte `"0"` or `"1"` | app via su (`DebugLoggingPrefs.kt:69-73`) | LSPosed hooks via `HookLog.reload` + FileObserver (`HookLog.kt:30-43`); also surfaced as a Dashboard warning (`DashboardData.kt:991`) | persistent |
**FileObservers** in `HookEntry.kt` and `PackageVisibilityHooks.kt`
watch `/data/system/` for `CREATE | CLOSE_WRITE | MOVED_TO | MODIFY`
events. Saves from the app trigger inotify, which invalidates the
in-process cache, so a Save propagates to running system_server
hooks **without any IPC or restart**.
---
## 4. Kernel module ABI — `/proc/vpnhide_*`
Created by `kmod/vpnhide_kmod.c` at module init via `proc_create()`,
removed at module exit. Mode `0600`, root-only.
| Path | Format | Writer | Reader (kernel side) |
|---|---|---|---|
| `/proc/vpnhide_targets` | one UID per line; write replaces full set | root userspace: `kmod/module/service.sh:78` writes resolved UIDs at boot; LSPosed app via su after Save | `vpnhide_kmod.c` `targets_write` handler — caches UIDs in kernel memory, consulted by every kretprobe handler |
| `/proc/vpnhide_debug` | `"1"` enables, `"0"` disables verbose `pr_info` from kretprobes | LSPosed Diagnostics screen during Collect-debug-log capture (`DiagnosticsScreen.kt`) | `READ_ONCE(debug_enabled)` in every probe handler |
Both files are **per-boot, in-kernel state only**. Unloading the
module (or rebooting) wipes the cached UIDs; service.sh re-seeds at
next boot from `/data/adb/vpnhide_kmod/targets.txt`.
---
## 5. App-process state — SharedPreferences and `filesDir`
### SharedPreferences `vpnhide_prefs`
Accessed via `context.getSharedPreferences("vpnhide_prefs", MODE_PRIVATE)`
in Kotlin. Keys currently in use:
- `debug_logging: Boolean` — Diagnostics toggle (`DebugLoggingPrefs.kt:21,27-30`).
- `last_seen_version: String` — for "what's new" changelog dialog.
- `help_collapsed_apps_tun: Boolean` and similar — collapse state for help accordions.
> ⚠️ **Vector LSPosed redirects this storage.**
>
> Because `dev.okhsunrog.vpnhide` is registered in LSPosed as its own
> module, the Vector framework hooks `Context.getSharedPreferences()`
> for the app's process and **transparently redirects reads/writes**
> to:
>
> ```
> /data/misc/<vector-uuid>/prefs/dev.okhsunrog.vpnhide/vpnhide_prefs.xml
> ```
>
> The `<vector-uuid>` is a row in `/data/adb/lspd/config/modules_config.db`.
> Owner is the app uid, SELinux label `xposed_data`.
>
> **Consequence when debugging:**
> `/data/data/dev.okhsunrog.vpnhide/shared_prefs/` will be empty even
> when the user has touched the toggle. Writing a fake `vpnhide_prefs.xml`
> at `/data/data/<pkg>/shared_prefs/` from a root shell has **no
> effect** — Vector ignores that path. To inspect or seed prefs:
>
> ```sh
> su -c "find /data/misc -name vpnhide_prefs.xml"
> ```
>
> This is Vector framework behaviour (the modern equivalent of the
> classic `XSharedPreferences` mechanism), not a vpnhide-specific
> quirk.
### `filesDir``/data/user/0/dev.okhsunrog.vpnhide/files/`
| File | Format | Writer | Reader | Lifetime |
|---|---|---|---|---|
| `vpnhide_zygisk_active` | `key=value`: `version`, `boot_id`, `pid`, `timestamp` | Zygisk module (`zygisk/src/lib.rs`) when the VPN Hide app itself is forked under zygisk hooks | App reads from its own `filesDir` to verify zygisk is hooking the app process; compared against current `boot_id` to detect stale heartbeats; `cleanupStaleZygiskStatus` (`ShellUtils.kt:113`) deletes if stale | per-app-launch (overwritten on each fork) |
Owner is the app uid (no su involved on read; Zygisk runs in the
forked app process so it has DAC perms to write into the app's own
filesDir).
### `cacheDir``/data/user/0/dev.okhsunrog.vpnhide/cache/`
Scratch space for short-lived files. Currently used for:
- `vpnhide_lspd_modules_config.db` (`+ -wal`, `+ -shm`) — temporary copies of LSPosed's config DB pulled via su and SQLite-opened read-only, then deleted. See `readLsposedConfig` (`DashboardData.kt:529-612`).
---
## 6. iptables — `vpnhide_out` and `vpnhide_out6`
Two named chains in the `filter` / `OUTPUT` path, IPv4 and IPv6.
Defined in `portshide/module/vpnhide_ports_apply.sh:21-22`.
- **Created/populated** by `vpnhide_ports_apply.sh` via `iptables-restore`
/ `ip6tables-restore` (`--noflush` so other chains aren't touched).
- **Triggered**: at boot (`portshide/module/service.sh`), and on every
Save in the Ports tab of the app (re-runs the apply script via su).
- **Removed** on module uninstall (`portshide/module/uninstall.sh`).
- **Live in kernel memory only** — no persistence across reboot;
must be re-applied each boot.
- Per-UID rules: target observer apps' UIDs get REJECT for connections
to `127.0.0.0/8` and `::1`.
The dashboard's "ports active" check is `iptables -L vpnhide_out -n`
in `DashboardData.kt:690` — chain existence implies the apply script
has run successfully this boot.
---
## 7. External / third-party paths the app reads
Not owned by us, but consulted for diagnostics or wiring.
| Path | Owner / Source | Purpose |
|---|---|---|
| `/data/adb/lspd/config/modules_config.db` (+ `-wal`, `-shm`) | LSPosed framework (LSPosed-Next / Vector) | Module enabled state, scope packages — read by `readLsposedConfig` in `DashboardData.kt:529` for dashboard display |
| `/data/misc/<vector-uuid>/prefs/<pkg>/` | Vector LSPosed | Redirected SharedPreferences — see § 5 |
| `/proc/sys/kernel/random/boot_id` | Linux kernel | Compared everywhere with stored `boot_id` fields to detect "is this record from the current boot?" |
| `/proc/version`, `/proc/modules`, `/proc/config.gz` | Linux kernel | Read by `kmod/module/post-fs-data.sh` for kernel diagnostics |
| `/proc/net/{route,ipv6_route,if_inet6,tcp,tcp6,udp,udp6,dev,fib_trie}` | Linux kernel | Read by `lsposed/native/src/` diagnostics + filtered by Zygisk hooks (`zygisk/src/hooks.rs`) — a memfd substitute is returned when a target app `openat`'s these |
| `/sys/class/net/`, `/sys/class/net/<iface>/operstate` | Linux kernel | `isVpnActiveBlocking` (`ShellUtils.kt:96`) iterates interfaces and checks operstate to detect "VPN is up right now" |
---
## 8. Boot-time write sequence (high-level)
```
post-fs-data.sh phase (root, before zygote):
kmod/module/post-fs-data.sh
→ modprobe / insmod vpnhide_kmod.ko
→ write /data/adb/vpnhide_kmod/load_status
→ write /data/adb/vpnhide_kmod/load_dmesg
service.sh phase (root, after boot_completed-ish):
kmod/module/service.sh
→ resolve /data/adb/vpnhide_kmod/targets.txt → UIDs
→ write /proc/vpnhide_targets
→ write /data/system/vpnhide_uids.txt
zygisk/module/service.sh
→ cp /data/adb/vpnhide_zygisk/targets.txt → /data/adb/modules/vpnhide_zygisk/targets.txt
→ resolve /data/adb/vpnhide_lsposed/targets.txt → UIDs
→ append/merge into /data/system/vpnhide_uids.txt
portshide/module/service.sh
→ wait for netd
→ /data/adb/modules/vpnhide_ports/vpnhide_ports_apply.sh
→ resolve /data/adb/vpnhide_ports/observers.txt → UIDs
→ iptables-restore --noflush (creates vpnhide_out / vpnhide_out6)
system_server start (LSPosed framework injects):
HookEntry.handleLoadPackage
→ install hooks
→ write /data/system/vpnhide_hook_active (with current boot_id)
→ start FileObservers on /data/system/
zygote forks an app (NeoZygisk):
zygisk/src/lib.rs::on_load (in forked process before specialize)
→ read targets via module dir fd
zygisk/src/lib.rs::post_app_specialize
→ if target: install libc hooks
→ if VPN Hide app itself: write filesDir/vpnhide_zygisk_active
```
---
## 9. Lifetime cheat-sheet
| Lifetime class | Examples |
|---|---|
| **In-kernel only (per-boot, volatile)** | `/proc/vpnhide_targets`, `/proc/vpnhide_debug`, iptables `vpnhide_out{,6}` chains |
| **Per-boot** (overwritten each boot by service scripts) | `/data/adb/vpnhide_kmod/load_status`, `/data/adb/vpnhide_kmod/load_dmesg`, `/data/system/vpnhide_uids.txt`, `/data/system/vpnhide_hook_active` |
| **Per-app-launch** (overwritten on each fork) | `<app-filesDir>/vpnhide_zygisk_active` |
| **Persistent — survives reboot, module reinstall, app reinstall** | `/data/adb/vpnhide_*/targets.txt`, `/data/adb/vpnhide_ports/observers.txt`, `/data/system/vpnhide_hidden_pkgs.txt`, `/data/system/vpnhide_observer_uids.txt`, `/data/system/vpnhide_debug_logging` |
| **Persistent — survives reboot but wiped on module reinstall** | everything inside `/data/adb/modules/vpnhide_*/` (Magisk/KSU replaces the tree from the zip) |
| **Persistent — survives reboot but wiped on app reinstall** | SharedPrefs `vpnhide_prefs` — but stored at the Vector-redirected path under `/data/misc/<uuid>/prefs/` |
| **Wiped only by factory reset** | `/data/adb/vpnhide_*/` persistent dirs + their contents |