I am using a Sony Xperia M4 Aqua. As is well known, the internal memory is rather small. Especially the Media directory of WhatsApp uses a lot of precious space, therefore I am trying to move it to the SD card. I am using Android 6 and I have formatted the SD card to have an adopted storage partition.

Adopted storage would be normally used to migrate all /data to it, as discussed here. I am however interested in moving just the single directory WhatsApp/Media somewhere else (possibly in the adopted storage partition), and then bind-mount it to its original location.

For this purpose, I moved the WhatsApp/Media directory to the adopted storage. Then, following this discussion, I have modified the script /system/etc/init.qcom.post_boot.sh adding the following mountcommands (the phone has no support for init.d scripts)

mount -o bind /mnt/expand/4fdb2500-9aa7-44bc-a2c4-80aeae28e764/WhatsAppMedia /storage/emulated/0/WhatsApp/Media
mount -o bind /mnt/expand/4fdb2500-9aa7-44bc-a2c4-80aeae28e764/WhatsAppMedia /mnt/runtime/write/emulated/0/WhatsApp/Media
mount -o bind /mnt/expand/4fdb2500-9aa7-44bc-a2c4-80aeae28e764/WhatsAppMedia /mnt/runtime/read/emulated/0/WhatsApp/Media
mount -o bind /mnt/expand/4fdb2500-9aa7-44bc-a2c4-80aeae28e764/WhatsAppMedia /mnt/runtime/default/emulated/0/WhatsApp/Media
mount -o bind /mnt/expand/4fdb2500-9aa7-44bc-a2c4-80aeae28e764/WhatsAppMedia /data/media/0/WhatsApp/Media

Notice: /mnt/expand/4fdb25.... points to the adopted storage partition.

This only apparently works: if I open a shell with adb I can correctly see that the WhatsApp/Media directory contains the mounted directory. Also, I see no additional views that contains the WhatsApp directory, as can be checked by doing, in an adb shell

find / -type d -name WhatsApp

Nevertheless, WhatsApp is not able to access the Media gallery. For instance, in the chat I just see blurred pictures (like a preview), and clicking on it I do not see the full picture. Moreover, if somebody sends me a picture, all I can see is a blurred preview with the download icon. Clicking on the download icon does not produce anything.

Wrong permissions are presumably the source of problems. For example, some permissions/group ownership appear to be incorrect on some views:

root@E2303:/ # ls -n /storage/emulated/0/WhatsApp
drwxrwx--x 0        1015              2019-09-04 14:37 Backups
drwxrwx--x 0        1015              2019-10-19 02:00 Databases
drwxrwx--x 0        1015              2019-09-04 20:24 Media
root@E2303:/ # ls -n /mnt/runtime/write/emulated/0/WhatsApp
drwxrwx--- 0        9997              2019-09-04 14:37 Backups
drwxrwx--- 0        9997              2019-10-19 02:00 Databases
drwxrwx--x 0        1015              2019-09-04 20:24 Media
root@E2303:/ # ls -n /mnt/runtime/read/emulated/0/WhatsApp
drwxr-x--- 0        9997              2019-09-04 14:37 Backups
drwxr-x--- 0        9997              2019-10-19 02:00 Databases
drwxrwx--x 0        1015              2019-09-04 20:24 Media
root@E2303:/ # ls -n /mnt/runtime/default/emulated/0/WhatsApp
drwxrwx--x 0        1015              2019-09-04 14:37 Backups
drwxrwx--x 0        1015              2019-10-19 02:00 Databases
drwxrwx--x 0        1015              2019-09-04 20:24 Media
root@E2303:/ # ls -n /data/media/0/WhatsApp/                                   
drwxrwxr-x 1023     1023              2019-09-04 14:37 Backups
drwxrwxr-x 1023     1023              2019-10-19 02:00 Databases
drwxrwx--x 0        1015              2019-09-04 20:24 Media

Permissions and group ownership of the Media directory should be the same of the other (non bind-mounted) directories Backups and Databases. Oddly enough, an attempt to remount the directories with the correct gid

root@E2303:/ # mount -o remount, gid=9997 /mnt/runtime/write/emulated/0/WhatsApp/Media
root@E2303:/ # mount -o remount, gid=9997 /mnt/runtime/read/emulated/0/WhatsApp/Media
root@E2303:/ # mount -o remount, gid=1023 /data/media/0/WhatsApp/Media

does not produce any changes in the group ownership: ls -n as above gives identical results.

Even more strange, but maybe unrelated, omitting the space between remount and gid=... results in

mount: Invalid argument

How to bind mount WhatsApp/Media folder from external SD card with correct permissions?


2 Answers 2


I have been using two different approaches (in fact many with small differences) on my older Android versions to mount whole /sdcard/WhatsApp directory from external SD card. I have tested, it works on Android 9 too, but the storage things have changed on Android 10.

Before getting into practical details, we need to keep in mind a few points:

  • /sdcard isn't an actual but emulated filesystem. Android uses sdcardfs (or FUSE) to emulate actual storage on /sdcard. See What is /storage/emulated/0/?
  • Both of the above filesystems have a fixed SELinux context: u:object_r:sdcardfs:s0 (or u:object_r:fuse:s0).
  • Normally /data/media is emulated over /sdcard, but in case of Adoptable Storage when data is migrated, /mnt/expand/[UUID]/media is emulated. See How to free Internal Storage by moving data or using symlink / bind-mount with Adoptable Storage?
  • Files and directories on /sdcard have fixed ownership and permissions which depend on if the app has android.permission.[READ|WRITE]_EXTERNAL_STORAGE permission granted or not. Files have never executable permission. Apps' data directories in /sdcard/Android/data/ have ownership set to respective app's UID. For details see What is the “u#_everybody” UID?

    We assume here that every app is allowed to write to /sdcard by setting ownership 0/9997 (user/group) and permissions 0771/0660 (directories/files).

  • To achieve above said behavior, since Android 6 every app is run in an isolated mount namespace and /storage/emulated is bind mounted to a different VIEW: /mnt/runtime/[default|read|write]/emulated with private/slave mount propagation. So mounting directly to /storage/emulated won't appear in apps' mount namespaces unless you enter every app's mount namespace explicitly. The same is true if you mount from some app's isolated mount namespace. See Partition gets unmounted automatically in Android Oreo.

    We are going to mount from root mount namespace to /mnt/runtime/write/emulated which is propagated to all apps' mount namespaces.

  • read and default views have different permissions than write (1), but mounting to both is usually unnecessary. Permissions READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE belong to same permission group. So granting one to an app through GUI also grants the other, and all apps with Storage permission will only see write view. default is only to let apps (which don't have READ/WRITE Storage permission) traverse /sdcard/Android/data directories. So mounting to default will let such apps just pass through subdirectories on /sdcard/, no files will be visible.

    Also at least with sdcardfs emulation, read and write are bind-mounted (2) from default and mounting to one also mounts to other two. So it's not possible to mount all of three with different permissions.

  • /sdcard does not support Extended Attributes (xattr) and Access Time (atime). Other mount options include nosuid, nodev and noexec. See mount manpage for details.

First of all make sure you are in root mount namespace as explained in the link given above. Or use nsenter to get a root shell in global namespace:

~# [ $(readlink /proc/1/ns/mnt) = $(readlink /proc/self/ns/mnt) ] || busybox nsenter -t 1 -m /system/bin/sh


The straightforward way is to format external SD card as portable storage with exFAT or FAT32. Since these filesystems aren't native to Linux, their in-kernel driver implementations support uid, gid, fmask and dmask mount options. You can use exfat or sdfat drivers with exFAT and vfat with FAT32. Former has also a userspace implementation mount.exfat-fuse which requires only FUSE support from kernel. Check with grep fuse /proc/filesystems.

Let's say /dev/block/sda1 is your exFAT partition:

~# mount -t exfat -o nosuid,nodev,noexec,noatime,context=u:object_r:sdcardfs:s0,uid=0,gid=9997,fmask=0117,dmask=0006 /dev/block/sda1 /mnt/runtime/write/emulated/0/WhatsApp
~# mv /data/media/0/WhatsApp/* /sdcard/WhatsApp/

* Replace u:object_r:sdcardfs:s0 with u:object_r:fuse:s0 or whatever label your /sdcard has.

You can also create multiple partitions on SD card. Or after mounting with required mount options you may also bind mount a directory instead of whole partition. Let's say you first mount SD card to /mnt/my_sdcard, then bind mount WhatsApp directory:

~# mount -o bind /mnt/my_sdcard/WhatsApp /mnt/runtime/write/emulated/0/WhatsApp

Downside with this approach is that vold mounts external SD card on boot, so you need to un-mount first.
Secondly the data on SD card isn't encrypted, though there are multiple ways to encrypt manually. See Decrypting microSD card on another Android device or desktop computer.


An easy way to counter the above said downsides is to make use of kernel's built-in FDE by formatting the SD card as Adoptable Storage, but only if you don't want to migrate all /data/media/ to external SD card. Once formatted, external SD card will be mounted at /mnt/expand/[UUID] (filesystem UUID is a 16 bytes number). But we can't simply bind mount a directory from there to /sdcard because the filesystem on Adoptable Storage is ext4, which follows UNIX permissions model but the apps can't handle those as explained above. Even if you make it work somehow (using chown, chmod etc.), every app will create files with its own UID which won't be accessible to other apps e.g. Gallery may not be able see pictures downloaded by WhatsApp.

For this method to work your kernel must support sdcardfs (check with grep sdcardfs /proc/filesystems). Create a directory on Adopted SD card and emulate it to /sdcard/WhatsApp:

~# mkdir /mnt/expand/[UUID]/media/0/WhatsApp
~# mv /sdcard/WhatsApp/* /mnt/expand/[UUID]/media/0/WhatsApp/
~# restorecon -rv /mnt/expand/[UUID]/media/
~# mount -t sdcardfs -o nosuid,nodev,noexec,noatime,mask=7,gid=9997 /mnt/expand/[UUID]/media/0/WhatsApp /mnt/runtime/write/emulated/0/Whatsapp

Please note that we necessarily need to use path /mnt/expand/[UUID]/media/ because it's labeled as media_rw_data_file (3) (like /data/media (4)) which is allowed by SELinux policy to be accessed by apps (5). If you use a different path, you need to modify SELinux policy. Unlike other filesystems sdcardfs itself doesn't change SELinux context when accessing underlying filesystem.


This method is the most flexible, it works if you want to use:

  • Portable storage but don't want to mount whole partition, instead a directory.
  • Adoptable storage but your kernel doesn't have sdcardfs support.
  • Adoptable storage but don't want to use /mnt/expand/[UUID]/media/ path necessarily.

Use a third party tool named bindfs (which uses FUSE) to simulate the behavior of emulated filesystem. Actually Android's built-in tool /system/bin/sdcard does exactly this on pre-sdcardfs releases but it has some fixed paths and other unnecessary things, so its source code needs to be modified to achieve what we want. You can build bindfs yourself or try this this one.

~# DIR=/mnt/media_rw/[UUID]    # for Portable Storage
~# DIR=/mnt/expand/[UUID]      # for Adoptable Storage
~# mkdir $DIR/WhatsApp
~# mv /sdcard/WhatsApp/* $DIR/WhatsApp/
~# bindfs -o nosuid,nodev,noexec,noatime,context=u:object_r:sdcardfs:s0 -u 0 -g 9997 -p a-rwx,ug+rw,ugo+X --create-with-perms=a-rwx,ug+rw,ugo+X --xattr-none --chown-ignore --chgrp-ignore --chmod-ignore $DIR/WhatsApp /mnt/runtime/write/emulated/0/WhatsApp

Side notes:

  • sdcardfs also works partially with this method except it doesn't support SELinux context= option (so far). So it depends on what is the SELinux label of backing directory on SD card.
  • Other tools like rclone, encfs, sshfs etc. which make use of FUSE can also be mounted inside /sdcard the same way. Related: How to mount rclone on Android?
  • -u and -g options require /etc/passwd and /etc/group to exist on bindfs < v1.14.2.

So you may go with whatever method suits you. Usually in-kernel drivers perform better than userspace solutions, and methods native to Linux kernel are more robust and stable. FUSE (over FUSE) may exert performance penalty sometimes e.g. if SD card itself supports high speed data transfers.

You can place the required mount commands in an init.d script or define and init service. See How to run an executable on boot and keep it running?


  • Apps which don't scan /sdcard filesystem themselves but rely on Android's MediaProvider for any changes may need a forced media scan for new files on mounted filesystem to appear immediately.
  • If you use multiple users or profiles, you need to mount new filesystem for every User_ID. For device owner it's 0.


  • 1
    @user1874594 we have been through similar discussions on your previous questions and could hardly conclude anything. So I don't think I'd be able to explain anything newer than what I've already done. I don't have a 32-bit ARM bindfs binary, you have to build from source. If you can't, let me know and I'll build one for you when I get time. As far as your other queries are concerned, they cannot be explained (again) in comments. You may ask a precise and unique question with all necessary information and I can try to briefly explain steps specific to your situation. Commented Mar 17, 2020 at 3:24
  • 1
    @IrfanLatif Great answer. It works as expected, but it doesn't seem I'm able to unmount a mount I make this way. The original mount command creates the three mounts in /mnt/runtime along with two duplicate mounts in /storage/emulated/0. umount called on these directories returns no errors, but trying to re-mount to the same directory later doesn't propagate to non-root namespaces. SELinux is set to permissive. Only thing that fixes it is a reboot of the tablet. Using LineageOS 16.0 (Android 9) with Magisk.
    – Shamtam
    Commented Oct 17, 2020 at 16:53
  • 1
    This is for an in-car tablet with OTG storage via permanently-mounted USB hub. When powered-off and back on, the OTG storage needs to be unmounted and then remounted, since the device gets re-probed (i.e. /dev/block/sdb1 will disappear on loss of power, and then when power is applied, it will appear again as /dev/block/sdc1).
    – Shamtam
    Commented Oct 17, 2020 at 16:55
  • 1
    @IrfanLatif I made a question here... I'd greatly appreciate any input you can provide: android.stackexchange.com/questions/230436/…
    – Shamtam
    Commented Apr 4, 2021 at 16:16
  • 1
    @IrfanLatif How do you build the bindfs?
    – Jtnqr
    Commented Jun 27, 2021 at 9:33

Android 11+

On my Android 13 phone (but released with Android 11), the above answer by @IrfanLatif no longer works. SDCardFS has been deprecated is/will be replaced by a FUSE filesystem by MediaProvider.

My phone's primary internal storage has its physical storage at /data/media/0, then there is an sdcardfs filesystem on top of that, and a FUSE filesystem (provided by MediaProvider) on top of the sdcardfs. The FUSE fs is the one that is visible to apps. See my addition to @IrfanLatif's Android storage journey for details.

My phone also has a microSD slot, and I wanted to encrypt my sd card (without using adoptable storage). I'll leave the details of the encryption to another answer, but I ended up creating a LUKS partition with an F2FS filesystem inside.

I first tried mounting the encrypted partition on a subdirectory of /data/media/0, that doesn't work because sdcardfs doesn't accept mounts of other devices on its source tree, and returns an "Invalid cross-device link" error. Then I tried mounting on top of the sdcardfs at /mnt/runtime/write/emulated/0/cryptcard, following @IrfanLatif's answer. While it is possible to mount there (and on the other paths at /mnt/runtime/*), doing so doesn't make the new filesystem show up under /sdcard (or the equivalent /storage/emulated/0), /sdcard ignores the mount as if it didn't happen.

After some digging, it turned out that MediaProvider's FUSE filesystem doesn't read from /mnt/runtime/*, instead it has its own mount of the sdcardfs under /storage/emulated/0, in a private mount namespace. So mounting in the master mount namespace didn't affect it.

Mount under MediaProvider

First you need to identify the PID of MediaProvider, so you can access its mount namespace. On my device it uses a command line of rs.media.module, so pgrep rs.media.module gives the PID. The command line name is probably not going to change across Android distributions, but if it does, you can find all processes that implement FUSE mounts using lsof /dev/fuse. That should narrow down your search, and probably there is only one process.

Once you know MediaProvider's PID, you need to be able to enter its mount namespace. For that you either need nsenter or a mount which supports the -N switch.

To put it all together:

Create a mount target folder:

mkdir /storage/emulated/0/myfilesystem

Mount your device to the mount point:

nsenter -m -t `pgrep rs.media.module` -- mount /dev/block/my_device /storage/emulated/0/myfilesystem


mount -N `pgrep rs.media.module` /dev/block/my_device /storage/emulated/0/myfilesystem

The necessary tools (pgrep, lsof, nsenter, mount) can all be installed through Termux if your Android doesn't include them.

Mounting an SDCardFS

To get the same permissions/features/etc I wanted to recreate the same stack as my Android uses. The physical storage I wanted to mount was an F2FS filesystem. For details of the encryption, see my answer to this question.

Because SDCardFS doesn't allow unprivileged users to read its root directory, I made a subfolder on my f2fs filesystem which is what will actually get mounted. For some details about SDCardFS, see this question.

First I made a working directory at /data/root to put some intermediate mounts. NB: on devices using FBE (file based encryption), such a directory is not encrypted!

Then I made subdirectories /data/root/cryptcard and data/root/cryptcard-sdcardfs

Mounting under /data/root should happen in the master mount namespace. If you use Magisk root, you can enter it with su -M. For other root apps, check their docs.

I mount the f2fs filesystem on /data/root/cryptcard:

mount -t f2fs /dev/block/my_device /data/root/cryptcard

If it does not exist yet, create a subdirectory on the device:

mkdir /data/root/cryptcard/mountdir

I am trying to duplicate the stack that my Android uses for its emulated primary external storage, so give the mountdir the same owner, group, permissions and security context as is used on the physical storage of your primary external storage. Owner, group and security context are not strictly necessary as sdcardfs ignores them.

Then we want to create an sdcardfs with the same options as the primary internal storage uses (which also uses the f2fs filesystem containing /data as backing storage). To see its mount options, run

findmnt -N `pgrep rs.media.module` -u /storage/emulated

Then mount an sdcardfs with the same options to /data/root/cryptcard-sdcardfs:

mount -t sdcardfs -o options_from_above_go_here /data/root/cryptcard /data/root/cryptcard-sdcardfs

The above two mounts should also be visible in MediaProvider's mount namespace. You can check that with

findmnt -N `pgrep rs.media.module` -R /data/root

If they don't show up, you could try making those mounts explicitly in MediaProvider's mount namespace.

Finally, bind-mount the subdirectory we want to use to MediaProvider's source tree:

mount -N `pgrep rs.media.module` --bind /data/root/cryptcard-sdcardfs/mountdir /storage/emulated/0/myfilesystem

The filesystem should now be visible under /sdcard/myfilesystem to your shell and your apps.

How to find out where MediaProvider reads from

I expect that MediaProvider keeps reading from /storage/emulated as its source. But if that stops working, here's how to find where MediaProvider reads from.

In one shell session, strace your MediaProvider:

strace -fp `pgrep rs.media.module`

This shows all the system calls that it makes, so make sure your Android is quiet when you try this.

Then, in another shell, do

stat /sdcard/Documents/some_file_that_exists

This should produce output from strace in the first shell, something like

[pid 30441] <... splice resumed> )      = 53 
[pid 30441] read(93, "5\0\0\0\1\0\0\0qa/\0\0\0\0\0\320\213a\314s\0\0\264\314'\0\0\314'\0\0"..., 53) = 53 
[pid 30441] write(2, "unique: 3105137, opcode: LOOKUP "..., 115) = 115 
[pid 30441] newfstatat(AT_FDCWD, "/storage/emulated/0/Documents/some_file_that_exists", {st_mode=S_IFDIR|0770, st_size=3452, ...}, AT_SYMLINK_NOFOLLOW) = 0 
[pid 30441] write(2, "   unique: 3105137, success, out"..., 42) = 42 
[pid 30441] writev(90, [{iov_base="\220\0\0\0\0\0\0\0qa/\0\0\0\0\0", iov_len=16}, {iov_base="\20\0_\314s\0\0\264\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\377\377\377"..., iov_len=128}], 2) = 144 
[pid 30441] splice(90, NULL, 122, NULL, 135168, 0 <unfinished ...>

This is one MediaProvider thread waking up, reading from /dev/fuse, doing the newfstatat system call, sending its response back, and going back to sleep. With some logging interspersed. The interesting part is the newfstatat(AT_FDCWD, "/storage/emulated/0/Documents/some_file_that_exists", system call, which shows us the path MediaProvider is reading from (within its own mount namespace), and where our mount should be created.

Again, the tools (findmnt, strace, stat) are available in Termux.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .