FUSE passthrough

Android 12 supports FUSE passthrough, which minimizes FUSE overhead to achieve performance comparable to direct access to the lower file system. FUSE passthrough is supported in the android12-5.4, android12-5.10, and android-mainline (testing only) kernels, which means that support for this feature depends on the kernel used by the device and the version of Android the device is running:

  • Devices upgrading from Android 11 to Android 12 can't support FUSE passthrough as the kernels for these devices are frozen and they can't move to a kernel that's been officially upgraded with the FUSE passthrough changes.

  • Devices launching with Android 12 can support FUSE passthrough when using an official kernel. For such devices, the Android framework code that implements FUSE passthrough is embedded in the MediaProvider mainline module, which is automatically upgraded. Devices that don't implement MediaProvider as a mainline module (for example, Android Go devices), can also access MediaProvider changes as they are publicly shared.

FUSE versus SDCardFS

File system in Userspace (FUSE) is a mechanism that allows operations performed on a FUSE file system to be outsourced by the kernel (FUSE driver) to a userspace program (FUSE daemon), which implements the operations. Android 11 deprecated SDCardFS and made FUSE the default solution for storage emulation. As part of this change, Android implemented its own FUSE daemon to intercept file accesses, enforce extra security and privacy features, and manipulate files at runtime.

While FUSE performs well when dealing with cacheable information such as pages or attributes, it introduces performance regressions when accessing external storage that are especially visible in mid and low-end devices. These regressions are caused by a chain of components cooperating in the implementation of the FUSE file system, as well as multiple switches from kernel space to user space in communications between the FUSE driver and the FUSE daemon (as compared to direct access to the lower file system that is leaner and completely implemented in the kernel).

To mitigate these regressions, apps can use splicing to reduce data copying and use the ContentProvider API to get direct access to lower file system files. Even with these and other optimizations, read and write operations might see reduced bandwidth when using FUSE when compared to direct access to the lower file system — especially with random read operations, where no caching or read-ahead can help. And apps that directly access storage through the legacy /sdcard/ path continue to experience noticeable performance drops, especially when performing IO-intensive operations.

SDcardFS userspace requests

Using SDcardFS can speed up the storage emulation and permission checks of FUSE by removing the user space call from the kernel. Userspace requests follow the path: Userspace → VFS → sdcardfs → VFS → ext4 → Page cache/Storage.

FUSE Passthrough SDcardFS

Figure 1. SDcardFS userspace requests

FUSE userspace requests

FUSE was initially used to enable storage emulation and to allow apps to transparently use either the internal storage or an external sdcard. Using FUSE introduces some overhead because each userspace request follows the path: Userspace → VFS → FUSE driver → FUSE daemon → VFS → ext4 → Page cache/Storage.

FUSE Passthrough FUSE

Figure 2. FUSE userspace requests

FUSE passthrough requests

Most file access permissions are checked at file open time, with additional permissions checks occurring when reading from and writing to that file. In some cases, it's possible to know at file open time that the requesting app has full access to the requested file, so the system doesn't need to continue forwarding read and write the requests from the FUSE driver to the FUSE daemon (as that would only move data from one place to another).

With FUSE passthrough, the FUSE daemon handling an open request can notify the FUSE driver that the operation is allowed and that all subsequent read and write requests can be directly forwarded to the lower file system. This avoids the extra overhead of waiting for the user space FUSE daemon to reply to the FUSE driver requests.

A comparison of FUSE and FUSE passthrough requests is shown below.

FUSE Passthrough Comparison

Figure 3. FUSE request versus FUSE passthrough request

When an app performs a FUSE file system access, the following operations occur:

  1. The FUSE driver handles and enqueues the request, then presents it to the FUSE daemon that handles that FUSE file system through a specific connection instance on the /dev/fuse file, which the FUSE daemon is blocked from reading.

  2. When the FUSE daemon receives a request to open a file, it decides whether FUSE passthrough should be available for that particular file. If it's available, the daemon:

    1. Notifies the FUSE driver about this request.

    2. Enables FUSE passthrough for the file using the FUSE_DEV_IOC_PASSTHROUGH_OPEN ioctl, which must be performed on the file descriptor of the opened /dev/fuse.

  3. The ioctl receives (as a parameter) a data structure that contains the following:

    • File descriptor of the lower file system file that's the target for the passthrough feature.

    • Unique identifier of the FUSE request that is currently being handled (must be open or create-and-open).

    • Extra fields that can be left empty and are meant for future implementations.

  4. If the ioctl succeeds, the FUSE daemon completes the open request, the FUSE driver handles the FUSE daemon reply, and a reference to the lower file system file is added to the FUSE file within the kernel. When an app requests a read/write operation on a FUSE file, the FUSE driver checks if the reference to a lower file system file is available.

    • If a reference is available, the driver creates a new Virtual File System (VFS) request with the same parameters targeting the lower file system file.

    • If a reference isn't available, the driver forwards the request to the FUSE daemon.

The above operations occur for read/write and read-iter/write-iter on generic files and read/write operations on memory-mapped files. FUSE passthrough for a given file exists until that file is closed.

Implement FUSE passthrough

To enable FUSE passthrough on devices running Android 12, add the following lines to the $ANDROID_BUILD_TOP/device/…/device.mk file of the target device.

# Use FUSE passthrough
PRODUCT_PRODUCT_PROPERTIES += \
    persist.sys.fuse.passthrough.enable=true

To disable FUSE passthrough, omit the above configuration change or set persist.sys.fuse.passthrough.enable to false. If you've previously enabled FUSE passthrough, disabling it prevents the device from using FUSE passthrough but the device remains functional.

To enable/disable FUSE passthrough without flashing the device, change the system property using ADB commands. An example is shown below.

adb root
adb shell setprop persist.sys.fuse.passthrough.enable {true,false}
adb reboot

For additional help, refer to the reference implementation.

Validate FUSE passthrough

To validate that MediaProvider is using FUSE passthrough, check logcat for debugging messages. For example:

adb logcat FuseDaemon:V \*:S
--------- beginning of main
03-02 12:09:57.833  3499  3773 I FuseDaemon: Using FUSE passthrough
03-02 12:09:57.833  3499  3773 I FuseDaemon: Starting fuse...

The FuseDaemon: Using FUSE passthrough entry in the log ensures that FUSE passthrough is in use.

The Android 12 CTS includes CtsStorageTest, which includes tests that trigger FUSE passthrough. To run the test manually, use atest as shown below:

atest CtsStorageTest