Android: How to Bind Mount under SDCardFS
Extending the trick we used for so long
Back to blog
I am an quite a fan of portable storages. These types of storages have somewhat strange appeal to me. It also have to do with my childhood too of course. Sheesh, I think, so much of our experiences of fondness come from our days when we were wee and happy…
A little introduction
"Bind mounts" which is what I prefer to call it, refers to an action of combining two different paths that direct to a single physical location of the files (my personal laymen words). Although I think this explanation is far superior than mine:
A bind mount is an alternate view of a directory tree. Classically, mounting creates a view of a storage device as a directory tree. A bind mount instead takes an existing directory tree and replicates it under a different point. The directories and files in the bind mount are the same as the original. Any modification on one side is immediately reflected on the other side, since the two views show the same data.
Unlike a hard link or symbolic link, a bind mount doesn't affect what is stored on the filesystem. It's a property of the live system.
As to what he meant by it being a live system, that, I do not know myself. This is because, as far as I may concern, bind mount does affect what is stored on the filesystem for the source path. Maybe he meant for the target path.
Anyway, using bind mounts allow you to mount a directory or file from a filesystem (can be either same or different) to another. This makes the target path to have the same content from the source. So any modifications to the target path will always take effect on the source path without actually modifying the original filesystem of the target path prior to the bind mounting.
This proves exceptionally useful years ago for Android devices. Because Android has Linux kernel at its core, in addition to that the tech at the time have rather small eMMC storage, also with the availability of SD Card slots, enthusiasts manage to "find" this golden Linux feature and made use of them.
They bind mounted paths from SD Card to their internal storage. Profit: internal storage space is not affected!
It spawned a famous application called Foldermount which allows users to manage the mountpoints easily without the need to face the terminal emulator and typing in tedious commands.
SDCardFS and this ancient trick's apparent doom
In January 2017, XDA Developers posted an article about Google's intention in replacing the old FUSE-based emulation in favour of a new filesystem called SDCardFS.
I highly recommend you to read the article. The technicality of how Android handles internal storage has always been mind boggling to me. It is like, Android complicates something that is supposed to be simple…
Personally, after reading such an article, I decided to hop to SDCardFS. One could not resist the fact that this new fresh filesystem will help make your internal storage operations a tad bit faster, if not noticeable. Because I also make use of the bind mount trick, I realised that — the trick no longer works!
Welp, it is because of the quirk with Android's storage emulation! 🙂
I did try to bind mount with a set of different paths, but to no avail. I decided to give up and just use the good old FUSE emulation.
Fast forward a few months later, it is reported on my ts-binds Magisk module XDA thread that it doesn't work on Android Pie. This signaled something important and I felt the urgency to re-investigate the matter.
After 7 hours, I managed to finally find the correct mountpoint to use as variables for the mount bind to work. I am mildly disappointed with how much time I have spent only to get a pretty small result, but I think I should be proud because I did this blindly without peeking through the AOSP source code! 😎😁
The Correct Way
The correct algorithm is specifically pinned down after numerous mount operations on a handful of paths. These paths are the ones that involve the external and internal storages if you mount | grep in terminal. It is found that /mnt/runtime mountpoints are the true paths that we need to mess with! The bind mount will work if the SD Card's default runtime is binded to the Internal's default runtime, and not other combinations (sort of). Afterwards Android will automatically remount the respective read and write runtimes.
In this example, I will attempt to bind WhatsApp's directory from the external storage to the internal storage. Classically, the bind mount would look like this:
mount --rbind /mnt/media_rw/2901-2806/WhatsApp /data/media/0/WhatsApp
In order for bind mount to work for SDCardFS, we instead need to run this:
$ mount --rbind /mnt/runtime/default/2901-2806/WhatsApp /mnt/runtime/default/emulated/0/WhatsApp $ mount | owowhatsthis /mnt/media_rw/2901-2806 on /mnt/runtime/default/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,mask=6) /mnt/media_rw/2901-2806 on /storage/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,mask=6) /mnt/media_rw/2901-2806 on /mnt/runtime/read/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,mask=6) /mnt/media_rw/2901-2806 on /mnt/runtime/write/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,mask=6)
As an added bonus, Android even automatically bind the same relative path to the other read and write runtimes! But if the other runtimes is not appearing after running mount on your device, more explanation later in this section.
Unfortunately, the automated bind mount resulted in the binded path not readable. This is because the group set for the bind mount is "1015", which is not what we wanted for the read and write runtime. SDCardFS have this group named sdcard_rw and its gid is 9997. Thus, we need to set the proper gid for both of the binded path at the other runtimes to such gid. To do this, a remount is required to the bind mount for the read and write runtimes only, and not the default runtime!
$ mount -o remount,gid=9997,mask=6 /mnt/runtime/read/emulated/0/WhatsApp $ mount -o remount,gid=9997,mask=6 /mnt/runtime/write/emulated/0/WhatsApp $ mount /mnt/media_rw/2901-2806 on /mnt/runtime/default/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015) /mnt/media_rw/2901-2806 on /storage/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997) /mnt/media_rw/2901-2806 on /mnt/runtime/read/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997) /mnt/media_rw/2901-2806 on /mnt/runtime/write/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997)
So yes. That is basically it! First, we bind mount from the default runtime of the SD Card to the default runtime of internal storage. Lastly, we remount the automatic bind mounts with the gid as 9997.
Summary Shell Lines
mount --rbind /mnt/runtime/default/2901-2806/WhatsApp /mnt/runtime/default/emulated/0/WhatsApp mount -o remount,gid=9997 /mnt/runtime/read/emulated/0/WhatsApp mount -o remount,gid=9997 /mnt/runtime/write/emulated/0/WhatsApp
Android did not automatically mount other runtimes!
Initially, I assumed Android would automatically do this, because, well it did so on my Android device. Unfortunately another kind developer by the name of VR-25 from XDA discovered that this is not the case. Turns out, there may be differences on how the kernel deal with SDCardFS. Some may auto mount, while others don't.
Kernels are something that I have little knowledge on, therefore I do not have any details about this inconsistency. However, fortunately this should not be an issue if you wish to do bind mounts. All you had to do is to mount the other runtimes manually by yourself.
mount --rbind /mnt/runtime/default/2901-2806/WhatsApp /mnt/runtime/default/emulated/0/WhatsApp mount --rbind /mnt/runtime/read/2901-2806/WhatsApp /mnt/runtime/read/emulated/0/WhatsApp mount --rbind /mnt/runtime/write/2901-2806/WhatsApp /mnt/runtime/write/emulated/0/WhatsApp mount -o remount,gid=9997 /mnt/runtime/read/emulated/0/WhatsApp mount -o remount,gid=9997 /mnt/runtime/write/emulated/0/WhatsApp
Maybe you wondered, what would actually happen if you choose other runtimes than "default"? Truth is, it will still work. But the resulted bind and automated bind uses wrong mask in addition to the mentioned gid above.
The mask determined by vold/Linux (I don't know which is responsible but I highly suspect the former) is based on the src argument of mount --bind. The read and write runtime for the SD Card uses mask=18. Therefore the shell line below will spawn bind mounts with mask=18.
$ mount --rbind /mnt/runtime/write/2901-2806/WhatsApp /mnt/runtime/write/emulated/0/WhatsApp $ mount | owowhatsthis /mnt/media_rw/2901-2806 on /mnt/runtime/default/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,mask=18) /mnt/media_rw/2901-2806 on /storage/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,mask=18) /mnt/media_rw/2901-2806 on /mnt/runtime/read/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,mask=18) /mnt/media_rw/2901-2806 on /mnt/runtime/write/emulated/0/WhatsApp type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,mask=18)
The problem with gid mentioned before is still valid. But, if you didn't remount the mask for the runtime paths and also the /storage/emulated/0/... path, the resulted binded directory will not be writable! This is because that the original /storage/emulated/0/ mountpoint requires mask=6 in order to properly set the binded directory's permissions and group sdcard_rw. Other than such value would set the group as root and the permission to be -rwxrwxr--. Therefore, to remedy, add mask=6 to the remount script shown earlier.
mount -o remount,gid=9997,mask=6 /mnt/runtime/read/emulated/0/WhatsApp mount -o remount,gid=9997,mask=6 /mnt/runtime/write/emulated/0/WhatsApp mount -o remount,gid=9997,mask=6 /storage/emulated/0/WhatsApp
No, chmod -R o+wX on the offending path will not work! This is emulation we are talking. /storage/emulated/0/ is a mount point.
Issues from this?
SDCardFS is basically a whole new way for Android to emulate the internal storage. I mean, judging by the sheer number of mount options alone for the internal path, we can already have a glimpse of how complex the emulation may be as opposed to the old userspace FUSE.
One of the reason why the internal storage is emulated in the first place is to cater with Android's permissions for apps. This specific reason became more visible with SDCardFS.
The only issue I have found so far is that you cannot move files from the binded path to another path of the same storage, at least with a third party file managers. For example, you binded the Download directory of SD Card to the internal storage. If you tried to move a file, say Internal/Download/cat.jpg to Internal/Pictures, you will face an unknown error. So does moving Internal/dog.jpg into Internal/Download. This do not happen with the built-in "Files" app.
I am not really that clever 😅 Hence my best guess is that, this minor issue could be something to do with permissions (either Android or *NIX, I don't know).
What about the old (raw) paths?
SDCardFS is a new virtual filesystem, and therefore the real paths are still fine just as it always was with FUSE-based. I mean, if you directly do any file operations on /mnt/media_rw/2901-2806 and /data/media/0, the changes will take effect as if there are no difference for the runtime paths. The runtime paths are mere mountpoints anyway that originate from the raw paths (/mnt & /data/media).
My Magisk module for example, still grab the folderlist file in the internal storage using the old absolute path, which is /data/media/0/ts-binds-folderlist.txt instead of the runtime path. It also copies a log file from /data/ts-binds to /data/media/0/ts-binds.log. But of course, you will need to set the correct UID, GID, and permissions on the files.
In addition to the storages, I think it is quite important for me to remind that OBB directory is also emulated under internal storage. The raw path is at /data/media/obb, and if you inspect the /mnt/runtime/default/emulated directory, you can find obb in it. In essence, if you want to mount bind from the OBB folder, you will also need to remount the bind mounts similar to the method detailed in the previous sections.
Granted, there may be a better, more elegant solution. Unfortunately, reading a source code, especially for a big project like Android is overwhelming for me. That is not even considering my level of understanding in tracing OOP code... Hopefully if someone finds a better alternative, they would share it publicly so we could together make use of it.
I hope this entry prove to be helpful if you are searching for a method to bind directorys between your internal storage and external storage.
It is one of my pleasure to be able to extend this most used trick in Android's history for a reasonable time in the future.
Get notified of new posts