8.2 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 includeroot,system,shell, andCapEff/CapPrmare non-zero, for example000001ffffffffff. - APatch shell:
uid=0, SELinux context commonlyu:r:magisk:s0, andCapEffis non-zero.
Broken examples:
uid=0(root)butCapEff: 0000000000000000: KernelSU granted UID 0 but no capabilities. Root will still fail ordinary Unix permission checks such as/data/local/tmpor0600files.ls -lZworks butcatfails withPermission denied: SELinux allows metadata/stat but deniesopen/read./data/adb/ksu/bin/ksud: inaccessible or not found: often an SELinuxexecute/searchdenial 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
ls -lZ /data/adb/modules/vpnhide_kmod /data/adb/modules/vpnhide_kpm 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 variant for LSPosed-enabled devices
Use the release APK for cold-start checks on devices where VPN Hide itself is
enabled in LSPosed/Vector. The debug APK is intentionally unminified and can
carry tens of megabytes of dex; LSPosed/Vector may spend longer than Android's
process-start timeout obfuscating that dex before the app attaches, producing
Process ... failed to attach / start timeout in logcat. The release APK is
R8-shrunk and is the representative path for app-start diagnostics.
KernelSU Next Shell profile
If com.android.shell has UID 0 but zero capabilities, fix the KernelSU Next
app profile:
- KernelSU Next app -> Superuser -> Shell (
com.android.shell). - Enable Superuser.
- Use Custom profile.
- Keep mount namespace
Inheritedunless debugging module mounts. - Set
uid = 0,gid = 0. - Set groups to at least
root,system, andshell. - For a dedicated test device, enable all capabilities. Minimal diagnostic subsets are easy to miss; all caps makes failures clearly SELinux-related.
- Keep SELinux context as
u:r:ksu:s0unless 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
- New
susession, then checkidandCapEff. - Check labels with
ls -lZ. - Check whether the failing operation is DAC (
CapEff=0, groups missing) or SELinux (CapEffnon-zero, labels visible,open/read/executedenied). - On KernelSU Next, fix Shell groups/capabilities in the app profile first.
- Apply only the SELinux rules needed for diagnostics.
- Re-test
ksud module list, vpnhide status files, and a write to/data/local/tmp.