3

Let's say I'm currently as root in Bash on a terminal, outside any app like Termux. I'm running Magisk.

Now I want to synchronously invoke Termux's Bash, such that it behaves the same way as it would if I had directly started the Termux app (i.e. with the same permissions and SELinux context and user/group and everything else).

How can I do this?

2

In order to run bash in Termux app's context so that it behaves exactly the same way as if it was started from the app, it must be run at least:

  • With UID/GID of the app assigned by the Package Manager at the time of installation
  • With supplementary group inet (3003), which is required to create network sockets and is assigned to any app at the time of installation which declares android.permission.INTERNET in its AndroidManifest.xml. For details see How Android's permissions mapping with UIDs/GIDs works?.
  • With supplementary group everybody (9997) which is used to control app's read / write access to /sdcard. For details see What is the “u#_everybody” UID?.
  • With supplementary groups <uid>_cache (UID+10000) and all_<uid> (UID+40000) so that app can access its cache, native executable code etc. But these aren't required necessarily since we are not running the app itself.
  • By entering the mount namespace of Termux app (com.termux process) which is required for supgid 9997 to work. If the app isn't running (e.g. if running from adb shell), supgid 1015 can be used to get write access to /sdcard but that's not how the app works.
  • By dropping all Linux capabilities (granular root privileges in Effective, Permitted, Inheritable, Bounding and Ambient sets) as well as setting securebits and process control attribute NO_NEW_PRIVS which make sure that app isn't able to elevate privileges by making use of setuid or file capabilities.
  • With blocked syscalls by applying seccomp-bpf filter.
  • With SELinux context of the app, which is determined at install / runtime based on MMAC (1, 2).
  • By adding the process to relevant cgroups e.g. cpuset, memcg etc. as the Android's framework does.
  • By catching, blocking and ignoring the same signals as the app would do.
  • By setting the environment it relies on. E.g. Termux's bash won't execute without LD_LIBRARY_PATH which it requires for dynamic linking.

There isn't a single commandline tool available which can do all of this sandboxing, it can be achieved fully only programmatically (see Minijail), or might be using multiple tools e.g. nsjail/firejail for setting seccomp filters, runcon to change SELinux context, capsh to alter DAC/capabilities, nsenter to enter mount namespace etc. setpriv from util-linux package can do the maximum:

~# uid=$(stat -c %u /data/data/com.termux)
~# pid=$(pidof -s com.termux)
~# label=$(cat /proc/$pid/attr/current)
~# export LD_LIBRARY_PATH=/data/data/com.termux/files/usr/lib
~# exec nsenter -t $pid -m setpriv --reuid $uid --regid $uid --groups 3003,9997 --bounding-set -all --selinux-label $label -- /system/bin/sh -c 'exec /data/data/com.termux/files/usr/bin/bash'

* nsenter and setpriv are busybox applets but with limited functionality. For aarch64 you may get static binaries here: nsenter, setpriv.

However we need to define shell_exec (label of /system/bin/sh) as entrypoint for execution by untrusted_app context which is not part of stock sepolicy (at least on Pie):

~# supolicy --live 'allow untrusted_app shell_exec file entrypoint'

Executing /system/bin/sh before executing /data/data/com.termux/files/usr/bin/bash is required because /data partition is mounted with nosuid option which prevents SELinux context transition (from magisk to untrusted_app) (3) and you get permission denied. You may consider mounting /data without nosuid to skip this step.
For the same reason --no-new-privs and --selinux-label can't be used in conjunction.

All this happens in native world, nothing in Java stack. So we have no direct control on things like manifest permissions which entirely operate inside Android framework. However manifest permission enforcement is also based on UIDs (4). For instance if Termux was granted android.permission.WRITE_EXTERNAL_STORAGE the bash we are running with Termux's UID will also be able to write to /sdcard.


From your comment:

It doesn't make sense to me how this is possible. Like, consider the output:

u0_a129 ~$ /sbin/su --context=u:object_r:app_data_file:s0:c512,c768 u0_a129 -c /system/bin/id
uid=10129(u0_a129) gid=10129(u0_a129) groups=10129(u0_a129) context=u:r:magisk:s0
u0_a129 ~$ /system/bin/id
uid=10129(u0_a129) gid=10129(u0_a129) groups=10129(u0_a129),3003(inet),9997(everybody),20129(u0_a129_cache),50129(all_a129),99909997(u999_everybody) context=u:r:untrusted_app_27:s0:c512,c768

Well, according to my understanding that's how Unix Discretionary Access Control works. First of all passing --context to Magisk's /sbin/su makes no difference as explained here:

the option still exists for CLI compatibility with apps designed for SuperSU. However the option is silently ignored since it's no longer relevant.

So the context isn't switched as you can see it's still u:r:magisk:s0. Secondly Magisk's su isn't the actual switch user (5), it provides a minimal functionality of standard su binary (the one we have on Linux). For details see How Magisk works?

In your first command it set UID, GID and supplementary groups to the UID you provided. You didn't ask su to set any additional supplementary groups, neither it can.

In second command you see the UID, GID, supplementary groups and SELinxu context which were set by zygote when it forked the app's DVM/ART (com.termux in case of Termux). Supplementary groups are explained above. 99909997 is also explained in the link given above.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.