vpnhide/docs/adb-root-debugging.md
Danila Gornushko 5902c0f86c
Some checks are pending
CI / setup (push) Waiting to run
CI / lint (push) Blocked by required conditions
CI / kmod-activator (push) Blocked by required conditions
CI / kmod (android12-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.15) (push) Blocked by required conditions
CI / kmod (android14-5.15) (push) Blocked by required conditions
CI / kmod (android14-6.1) (push) Blocked by required conditions
CI / kmod (android15-6.6) (push) Blocked by required conditions
CI / kmod (android16-6.12) (push) Blocked by required conditions
CI / kmod-qemu (android12-5.10) (push) Blocked by required conditions
CI / kmod-qemu (android13-5.10) (push) Blocked by required conditions
CI / kmod-qemu (android13-5.15) (push) Blocked by required conditions
CI / kmod-qemu (android14-5.15) (push) Blocked by required conditions
CI / kmod-qemu (android14-6.1) (push) Blocked by required conditions
CI / kmod-qemu (android15-6.6) (push) Blocked by required conditions
CI / kmod-qemu (android16-6.12) (push) Blocked by required conditions
CI / kpm-qemu (android12-5.10) (push) Blocked by required conditions
CI / kpm-qemu (android13-5.10) (push) Blocked by required conditions
CI / kpm-qemu (android13-5.15) (push) Blocked by required conditions
CI / kpm-qemu (android14-5.15) (push) Blocked by required conditions
CI / kpm-qemu (android14-6.1) (push) Blocked by required conditions
CI / kpm-qemu (android15-6.6) (push) Blocked by required conditions
CI / kpm-qemu (android16-6.12) (push) Blocked by required conditions
CI / kpm-qemu-legacy (4.14) (push) Blocked by required conditions
CI / kpm-qemu-legacy (4.19) (push) Blocked by required conditions
CI / kpm-qemu-legacy (5.4) (push) Blocked by required conditions
CI / kpm (push) Blocked by required conditions
CI / zygisk (push) Blocked by required conditions
CI / lsposed (push) Blocked by required conditions
CI / portshide (push) Blocked by required conditions
CI / release (push) Blocked by required conditions
Upgrade diagnostic captures (#227)
* feat: upgrade diagnostic captures

Collect richer debug and full-logcat bundles with device, backend, hook, portshide, and kernel partition evidence.

Add an explicit kernel image export for maintainer-requested kernel analysis.

* fix: harden diagnostic capture sessions

* fix(lsposed): harden diagnostic export error paths from review

Address correctness findings from a review of the diagnostic-capture upgrade,
all validated on a Pixel 4a via the agent bridge (kernel/debug/logcat exports).

- Kernel image export: wrap exportKernelImagesZip so a zip/IO failure returns
  null (and cleans the parts dir) instead of escaping and stranding the export
  button disabled; add try/finally at the DiagnosticsScreen call site. Cap the
  per-partition dd (skip unknown/oversized partitions, bound with count=) so a
  mislabelled or huge partition can't fill /data.
- HookDiagnostics: drive counter deltas off an explicit "baseline captured"
  flag instead of baselineCounters.isEmpty(), so a fresh boot with an empty
  baseline no longer reports every delta as n/a. Add HookDiagnosticsTest.
- DebugShellSnapshot: raise the 20s snapshot timeout to 60s and preserve+flag a
  timeout-truncated section instead of silently dropping it and every later one.
- LogcatRecorder: delete the intermediate raw logcat file after zipping and on
  failed start, so repeated captures stop leaking multi-MB files into cache.
- DebugExport: rethrow CancellationException before the generic catch so
  navigating away mid-capture cancels cleanly instead of logging a false error.
- DebugCaptureLogging: when the on-disk canonical config is present but
  unparseable, refuse the targets-snapshot rebuild (which would reset settings
  and clear autoHiddenPackages); fall back to an app-process-only toggle.
- Dashboard: don't warn "ports rules not active" for a module the user disabled
  via their manager (new ports_disabled probe, mirrors the activator's guard).
- activator write_atomic: per-process temp name so a boot-time and app-triggered
  activation can't corrupt each other's load_status.
- Drop the wasted multi-user pm enumeration from the counter baseline command.
2026-07-02 16:25:53 +03:00

8.6 KiB

ADB root debugging

How to make adb shell su -c ... useful on rooted test devices, and how to debug the common "root but Permission denied" failures.

This is test-device guidance. Do not ship broad diagnostic SELinux policy or all-capability shell profiles as part of production modules.

What "root" must include

For diagnostics, UID 0 is not enough. A useful adb root shell needs all three:

  • UID/GID/groups that can pass Unix DAC checks.
  • Linux capabilities, especially DAC override/read-search.
  • SELinux allow rules for the file labels being read.

Check all three before assuming a device path is inaccessible:

adb -s SERIAL shell 'su -c "
id
getenforce
echo root_shell_pid=\$\$
grep -E \"Cap(Inh|Prm|Eff|Bnd|Amb)\" /proc/\$\$/status
ls -ldZ /data /data/adb /data/adb/modules /data/system
head -c 120 /data/system/vpnhide_config.json 2>&1
cat /data/adb/vpnhide_kpm/load_status 2>&1 | head
"'

Command chains and quoting

When running more than one command through su -c, quote the whole remote command for adb shell, then quote the payload passed to su -c.

Do not write chained commands like this:

adb -s SERIAL shell su -c 'id; iptables -S OUTPUT'

The local shell strips those quotes before adb sends the command, so Android's remote shell can parse it as su -c id; iptables -S OUTPUT. Only id runs under su; the rest runs as the ordinary adb shell (uid=2000, u:r:shell:s0, usually with no useful capabilities).

Use one of these forms instead:

adb -s SERIAL shell "su -c 'id; iptables -S OUTPUT'"
adb -s SERIAL shell 'su -c "id; iptables -S OUTPUT"'

When checking the root shell itself, prefer /proc/\$\$/status inside the su -c payload. /proc/self/status describes the process that opens the file (cat, grep, etc.), which can hide quoting mistakes while debugging command chains.

Healthy examples:

  • KernelSU Next shell: uid=0, groups include root, system, shell, and CapEff/CapPrm are non-zero, for example 000001ffffffffff.
  • APatch shell: uid=0, SELinux context commonly u:r:magisk:s0, and CapEff is non-zero.

Broken examples:

  • uid=0(root) but CapEff: 0000000000000000: KernelSU granted UID 0 but no capabilities. Root will still fail ordinary Unix permission checks such as /data/local/tmp or 0600 files.
  • ls -lZ works but cat fails with Permission denied: SELinux allows metadata/stat but denies open/read.
  • /data/adb/ksu/bin/ksud: inaccessible or not found: often an SELinux execute/search denial or a PATH/mount namespace issue, not a missing file.

vpnhide paths worth checking

adb -s SERIAL shell 'su -c "
cat /data/system/vpnhide_config.json
cat /data/system/vpnhide_lsposed_state 2>/dev/null
cat /data/adb/vpnhide_kmod/load_status 2>/dev/null
cat /data/adb/vpnhide_kpm/load_status 2>/dev/null
cat /data/adb/vpnhide_ports/load_status 2>/dev/null
cat /data/adb/vpnhide_ports/load_log 2>/dev/null
ls -lZ /data/adb/modules/vpnhide_kmod /data/adb/modules/vpnhide_kpm /data/adb/modules/vpnhide_ports 2>&1
"'

The canonical JSON is /data/system/vpnhide_config.json, not /data/adb. /data/adb is still important for module files, KPM status, superkey storage, KernelSU/APatch CLIs, and LSPosed module state.

APK variants for LSPosed/Vector smoke tests

Use the default debug APK (:app:assembleDebug) or the release APK for cold-start checks. The default debug APK remains debuggable, but it is R8/resource-shrunk so LSPosed/Vector has less dex to prepare after the APK is updated as an Xposed module.

The rawDebug variant (:app:assembleRawDebug) keeps the old unminified debug behavior for Studio/debugger work. It can be much larger; after updating that APK, LSPosed/Vector may spend long enough preparing module code before the app attaches that Android logs Process ... failed to attach / start timeout. That symptom does not mean VPN Hide is in its own LSPosed scope. The expected module scope is still System Framework only.

KernelSU Next Shell profile

If com.android.shell has UID 0 but zero capabilities, fix the KernelSU Next app profile:

  1. KernelSU Next app -> Superuser -> Shell (com.android.shell).
  2. Enable Superuser.
  3. Use Custom profile.
  4. Keep mount namespace Inherited unless debugging module mounts.
  5. Set uid = 0, gid = 0.
  6. Set groups to at least root, system, and shell.
  7. For a dedicated test device, enable all capabilities. Minimal diagnostic subsets are easy to miss; all caps makes failures clearly SELinux-related.
  8. Keep SELinux context as u:r:ksu:s0 unless you know the target domain exists.

After changing the profile, open a new adb shell su -c ... session and verify:

adb -s SERIAL shell 'su -c "
id
echo root_shell_pid=\$\$
grep -E \"Cap(Prm|Eff|Bnd)\" /proc/\$\$/status
echo ok >/data/local/tmp/vpnhide_diag_write_test
cat /data/local/tmp/vpnhide_diag_write_test
rm /data/local/tmp/vpnhide_diag_write_test
"'

For SELinux read access in the u:r:ksu:s0 domain, apply a narrow Shell policy through KernelSU Next:

adb -s SERIAL shell 'cat >/data/local/tmp/vpnhide-ksu-shell.te <<EOF
allow ksu adb_data_file dir { getattr search open read }
allow ksu adb_data_file file { getattr open read map execute execute_no_trans }
allow ksu adb_data_file lnk_file { getattr open read }
allow ksu system_file dir { getattr search open read }
allow ksu system_file file { getattr open read map execute execute_no_trans }
allow ksu system_file lnk_file { getattr open read }
allow ksu system_data_file dir { getattr search open read }
allow ksu system_data_file file { getattr open read map }
allow ksu packages_list_file file { getattr open read map }
allow ksu shell_data_file dir { getattr search open read write add_name remove_name }
allow ksu shell_data_file file { getattr open read write create append unlink setattr map }
EOF'

adb -s SERIAL shell 'su -c "
PATH=/data/adb/ksu/bin:\$PATH
ksud sepolicy apply /data/local/tmp/vpnhide-ksu-shell.te
POLICY=\$(tr \"\n\" \";\" </data/local/tmp/vpnhide-ksu-shell.te)
ksud profile set-sepolicy com.android.shell \"\$POLICY\"
"'

ksud sepolicy apply is live. ksud profile set-sepolicy makes the same Shell policy persistent across reboot.

Do not manually create random diagnostic directories under /data/adb/modules/ on KernelSU Next. Use app profiles or a properly installed module zip. Broken module metadata can make ksud module list fail.

APatch and Magisk-style shells

APatch shells usually run in u:r:magisk:s0 and have non-zero capabilities by default. If SELinux blocks /data/adb or /data/system, use magiskpolicy:

adb -s SERIAL shell 'cat >/data/local/tmp/vpnhide-magisk-shell.te <<EOF
allow magisk adb_data_file dir { getattr search open read }
allow magisk adb_data_file file { getattr open read map execute execute_no_trans }
allow magisk adb_data_file lnk_file { getattr open read }
allow magisk system_file dir { getattr search open read }
allow magisk system_file file { getattr open read map execute execute_no_trans }
allow magisk system_file lnk_file { getattr open read }
allow magisk system_data_file dir { getattr search open read }
allow magisk system_data_file file { getattr open read map }
allow magisk packages_list_file file { getattr open read map }
EOF'

adb -s SERIAL shell 'su -c "
PATH=/data/adb/ap/bin:/data/adb/magisk:\$PATH
magiskpolicy --live --apply /data/local/tmp/vpnhide-magisk-shell.te
"'

For persistence on APatch/Magisk test devices, install a small test-only module with sepolicy.rule. APatch loads active modules' sepolicy.rule at boot. Keep such modules out of release zips.

KPM and KernelPatch diagnostics

Useful KPatch-Next checks:

adb -s SERIAL shell 'su -c "
PATH=/data/adb/modules/KPatch-Next/bin:/data/adb/ksu/bin:\$PATH
kpatch hello
kpatch kpm list
cat /data/adb/vpnhide_kpm/load_status
dmesg | grep -iE \"vpnhide|kpatch|kpm|KernelPatch\" | tail -n 80
"'

kpatch kpm ctl0 exits with the KPM handler return value. Current vpnhide KPM builds return 0 after applying config. Older builds returned the configured target count, so exit status: 15 can mean "15 targets configured", not failure. Actual parse failures commonly surface as 255 because the KPM returned -1.

Quick recovery checklist

  1. New su session, then check id and CapEff.
  2. Check labels with ls -lZ.
  3. Check whether the failing operation is DAC (CapEff=0, groups missing) or SELinux (CapEff non-zero, labels visible, open/read/execute denied).
  4. On KernelSU Next, fix Shell groups/capabilities in the app profile first.
  5. Apply only the SELinux rules needed for diagnostics.
  6. Re-test ksud module list, vpnhide status files, and a write to /data/local/tmp.