Android framework takes care of a lot of things when forking an app from
zygote. In short, it's nearly impossible to run an app process from commandline so that it behaves “exactly the same way” as if the app was properly launched. Continue reading to have an idea of why it's so.
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
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
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.
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.
firejail for setting
runcon to change SELinux context,
capsh to alter DAC/capabilities,
nsenter to enter mount namespace etc.
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'
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
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'
/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
untrusted_app) (3) and you get permission denied. You may consider mounting
nosuid to skip this step.
For the same reason
--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
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.