安卓存儲(chǔ)權(quán)限原理

上篇博客介紹了FileProvider是如何跨應(yīng)用訪問(wèn)文件的。這篇博客我們來(lái)講講安卓是如何控制文件的訪問(wèn)權(quán)限的。

內(nèi)部?jī)?chǔ)存

由于安卓基于Linux,所以最簡(jiǎn)單的文件訪問(wèn)權(quán)限控制方法就是使用Linux的文件權(quán)限機(jī)制.例如應(yīng)用的私有目錄就是這么實(shí)現(xiàn)的。

安卓系統(tǒng)為每個(gè)安卓的應(yīng)用都分配了一個(gè)用戶和用戶組,我們可以通過(guò)ps命令查看運(yùn)行中的應(yīng)用對(duì)應(yīng)的用戶:

USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
...
u0_a66        2685  1085 3914640  70688 SyS_epoll_wait      0 S me.linw.demo
...

這里的u0_a66指的是應(yīng)用的user name,它表示該應(yīng)用是user 0(這里指的是安卓多用戶模式下的主用戶,和前面講的Linux用戶不是同一個(gè)概念)下面的應(yīng)用id是66.由于通應(yīng)用程序的user id都是從10000開(kāi)始,所以這個(gè)應(yīng)用的user id是10066.可以從/data/system/packages.list文件中確認(rèn):

me.linw.demo 10066 1 /data/user/0/me.linw.demo default:targetSdkVersion=30 3003

應(yīng)用的私有目錄為/data/data/${包名}/,可以看到安卓系統(tǒng)給應(yīng)用創(chuàng)建了一個(gè)權(quán)限為700的目錄,文件的owner和group都只屬于這個(gè)應(yīng)用,這樣就保證了每個(gè)應(yīng)用的私有目錄只有自己可以訪問(wèn):

# ls -l /data/data/ | grep me.linw.demo
drwx------ 5 u0_a66 u0_a66 4096 2023-03-07 19:32 me.linw.demo

SharedUserId

當(dāng)然也可以在AndroidManifest.xml里面配置android:sharedUserId讓他們是用同一個(gè)User:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.linw.demo2"
    android:sharedUserId="test.same.user">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.linw.demo"
    android:sharedUserId="test.same.user">

這樣的話兩個(gè)應(yīng)用的user就是一樣的,就能相互訪問(wèn)私有目錄了:

drwx------ 4 u0_a66 u0_a66 4096 2023-03-10 17:07 me.linw.demo2
drwx------ 5 u0_a66 u0_a66 4096 2023-03-10 16:53 me.linw.demo

外部存儲(chǔ)

外部存儲(chǔ)的文件系統(tǒng)幾經(jīng)變更。從早期的FUSE到Android 8改為性能更優(yōu)的SDCardFS,再到Android 11上為了更細(xì)的管理文件權(quán)限又換回FUSE。各個(gè)安卓版本的實(shí)現(xiàn)細(xì)節(jié)也稍有差異,過(guò)于老舊的版本也沒(méi)有學(xué)習(xí)的必要,這里只拿比較有代表性的Android 8和Android 11進(jìn)行源碼分析。

Android 11以前

安卓11以前的外部存儲(chǔ)權(quán)限控制做的比較粗糙。應(yīng)用申請(qǐng)了WREAD_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE就可以對(duì)外部存儲(chǔ)進(jìn)行讀寫(xiě)。

這個(gè)外部存儲(chǔ)一般指的是/storage/emulated/目錄,它為每個(gè)用戶分配了一個(gè)子目錄。例如0子目錄就是user 0(主用戶)的外部存儲(chǔ)目錄.

這里我們用一個(gè)shellExec在進(jìn)程里面執(zhí)行命令協(xié)助我們理解外部存儲(chǔ)的管理原理:

public void shellExec(String shell) throws IOException {
    InputStream is = Runtime.getRuntime().exec(shell).getInputStream();
    BufferedReader reader = new BufferedReader(new InputStreamReader(is));
    StringBuilder sb = new StringBuilder();
    char[] buff = new char[1024];
    int ch;
    while ((ch = reader.read(buff)) != -1) {
        sb.append(buff, 0, ch);
    }
    reader.close();
    Log.d("ExecShell", shell);
    Log.d("ExecShell", sb.toString());
}

申請(qǐng)READ_EXTERNAL_STORAGE權(quán)限之后執(zhí)行ls -l /storage/emulated/0/就可以看到熟悉的外部存儲(chǔ)目錄結(jié)構(gòu):

shellExec("ls -l /storage/emulated/0/");


03-11 17:02:26.861  3411  3411 D ExecShell: ls -l /storage/emulated/0/
03-11 17:02:26.861  3411  3411 D ExecShell: total 40
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Alarms
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 3 root everybody 4096 2023-03-08 14:13 Android
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 DCIM
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2023-03-07 19:49 Download
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Movies
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Music
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Notifications
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2023-03-07 19:46 Pictures
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Podcasts
03-11 17:02:26.861  3411  3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Ringtones

這里可以看到雖然這些目錄的user是root,但是所屬的group是everybody,即所有人對(duì)這些目錄都有r-x的權(quán)限可讀可進(jìn)入文件夾。

而如果申請(qǐng)了WRITE_EXTERNAL_STORAGE權(quán)限之后再執(zhí)行ls -l /storage/emulated/0/就會(huì)看見(jiàn)group的權(quán)限變成了rwx可讀可寫(xiě)可進(jìn)入文件夾。

03-11 17:10:44.146  3646  3646 D ExecShell: ls -l /storage/emulated/0/
03-11 17:10:44.146  3646  3646 D ExecShell: total 40
03-11 17:10:44.146  3646  3646 D ExecShell: drwxrwx--- 2 root everybody 4096 2022-04-24 20:25 Alarms
03-11 17:10:44.146  3646  3646 D ExecShell: drwxrwx--- 3 root everybody 4096 2023-03-08 14:13 Android
...

也就是說(shuō)不同的權(quán)限下應(yīng)用看到/storage/emulated/0/的文件權(quán)限是不一樣的,這一點(diǎn)又是怎么做的的呢?

/mnt/runtime目錄

這里先介紹/mnt/runtime下的三個(gè)目錄:

mount | grep /mnt/runtime
/data/media on /mnt/runtime/default/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid)
/data/media on /mnt/runtime/read/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=23,derive_gid)
/data/media on /mnt/runtime/write/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=7,derive_gid)

可以看到/mnt/runtime/default/emulated、/mnt/runtime/read/emulated、/mnt/runtime/write/emulated都掛載了/data/media。只不過(guò)他們的gid、和mask不盡相同。

group

其實(shí)這三個(gè)目錄都是通過(guò)bind mount機(jī)制(普通的mount只能掛載設(shè)備,但是bind mount可以掛載目錄)掛載的/data/media目錄,gid指的是掛載之后修改文件系統(tǒng)下文件的group:

# ls -l /data/media
total 8
drwxrwx--- 12 media_rw media_rw 4096 2023-03-11 16:51 0
drwxrwxr-x  2 media_rw media_rw 4096 1970-01-01 08:00 obb

# ls -l /mnt/runtime/default/emulated
total 8
drwxrwx--x 12 root sdcard_rw 4096 2023-03-11 16:51 0
drwxrwx--x  2 root sdcard_rw 4096 1970-01-01 08:00 obb

# ls -l /mnt/runtime/read/emulated
total 8
drwxr-x--- 12 root everybody 4096 2023-03-11 16:51 0
drwxr-x---  2 root everybody 4096 1970-01-01 08:00 obb

# ls -l /mnt/runtime/write/emulated
total 8
drwxrwx--- 12 root everybody 4096 2023-03-11 16:51 0
drwxrwx---  2 root everybody 4096 1970-01-01 08:00 obb

可以看到原本/data/media下的文件group是media_rw(id=1023),但掛載之后/mnt/runtime/default/emulated的group是sdcard_rw(id=1015),/mnt/runtime/read/emulated、/mnt/runtime/write/emulated的group是everybody(id=9997)。

這些group的id可以在android_filesystem_config.h看到:

// http://androidxref.com/8.0.0_r4/xref/system/core/include/private/android_filesystem_config.h

...
#define AID_SDCARD_RW 1015       /* external storage write access */
...
#define AID_MEDIA_RW 1023        /* internal media storage write access */
...
#define AID_EVERYBODY 9997 /* shared between all apps in the same profile */
...

mask

而mask則是用來(lái)重新定義文件的rwx權(quán)限的,掛載后文件的權(quán)限通過(guò)0775 & ~mask計(jì)算得到(注意這里的0775指定是8進(jìn)制的775,即十進(jìn)制的509):

// https://android.googlesource.com/kernel/common.git/+/experimental/android-4.9/fs/sdcardfs/sdcardfs.h

static inline int get_mode(struct vfsmount *mnt, struct sdcardfs_inode_info *info) {
    ...
    int visible_mode = 0775 & ~opts->mask;
    ...
}

所以:

/mnt/runtime/default/emulated的權(quán)限為0775 & ~6:

0775 =  111111101 = 111111101
~6   = ~000000110 = 111111001 
------------------------------
                    111111001 = rwxrwx--x

/mnt/runtime/read/emulated的權(quán)限為0775 & ~23:

0775 =  111111101 = 111111101
~23  = ~000010111 = 111101000
------------------------------
                    111101000 = rwxr-x---

/mnt/runtime/default/emulated的權(quán)限為0775 & ~7:

0775 =  111111101 = 111111101
~7   = ~000000111 = 111111000
------------------------------
                    111111000 = rwxrwx---

綜上所述:

  • /mnt/runtime/default/emulated : 普通應(yīng)用由于不在media_rw組,只有進(jìn)入子目錄的權(quán)限,并不能讀寫(xiě)。
  • /mnt/runtime/read/emulated : 普通應(yīng)用屬于everybody組,有r-x權(quán)限
  • /mnt/runtime/default/emulated : 普通應(yīng)用屬于everybody組,有rwx權(quán)限

外部存儲(chǔ)讀寫(xiě)權(quán)限原理

實(shí)際上外部存儲(chǔ)路徑/storage/emulated是通過(guò)掛載前面所說(shuō)的三個(gè)目錄去實(shí)現(xiàn)不同的訪問(wèn)權(quán)限的。

在Zygote進(jìn)程fork應(yīng)用進(jìn)程的時(shí)候會(huì)通過(guò)Linux的bind mount機(jī)制為應(yīng)用在私有掛載空間掛載/storage目錄:

// https://cs.android.com/android/platform/superproject/+/android-8.0.0_r1:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

// Create a private mount namespace and bind mount appropriate emulated
// storage for the given user.
static bool MountEmulatedStorage(uid_t uid, jint mount_mode,
        bool force_mount_namespace) {
    // See storage config details at http://source.android.com/tech/storage/

    String8 storageSource;
    if (mount_mode == MOUNT_EXTERNAL_DEFAULT) {
        storageSource = "/mnt/runtime/default";
    } else if (mount_mode == MOUNT_EXTERNAL_READ) {
        storageSource = "/mnt/runtime/read";
    } else if (mount_mode == MOUNT_EXTERNAL_WRITE) {
        storageSource = "/mnt/runtime/write";
    } else if (!force_mount_namespace) {
        // Sane default of no storage visible
        return true;
    }

    // Create a second private mount namespace for our process
    if (unshare(CLONE_NEWNS) == -1) {
        ALOGW("Failed to unshare(): %s", strerror(errno));
        return false;
    }

    ...

    if (TEMP_FAILURE_RETRY(mount(storageSource.string(), "/storage",
            NULL, MS_BIND | MS_REC | MS_SLAVE, NULL)) == -1) {
        ALOGW("Failed to mount %s to /storage: %s", storageSource.string(), strerror(errno));
        return false;
    }

    ...
}

系統(tǒng)根據(jù)應(yīng)用的外部存儲(chǔ)權(quán)限傳入不同的mount_mode:

  • 沒(méi)有權(quán)限掛載/mnt/runtime/default
  • 有READ_EXTERNAL_STORAGE權(quán)限掛載/mnt/runtime/read
  • 有WRITE_EXTERNAL_STORAGE權(quán)限掛載/mnt/runtime/write

由于使用了unshare所以掛載的/storage實(shí)際是在應(yīng)用的私有掛載空間,即每個(gè)應(yīng)用掛載的/storage是僅自己可見(jiàn)其他應(yīng)用不可見(jiàn)的。

而這里使用了MS_REC參數(shù),所以會(huì)遞歸掛載子目錄,即:/mnt/runtime/default掛載到/storage的同時(shí)/mnt/runtime/default/emulated也會(huì)掛載到/storage/emulated

間接掛載

不過(guò)通過(guò)mount命令可以看到/storage/emulated實(shí)際上也是掛載了/data/media,而不是前面說(shuō)的三個(gè)目錄:

03-11 17:13:36.495  3778  3778 D ExecShell: mount
...
03-11 17:13:36.495  3778  3778 D ExecShell: /data/media on /storage/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=7,derive_gid)
...

這是由于bind mount的特性,并不能看到間接掛載的過(guò)程。例如我們可以將/mnt/runtime/default/emulated通過(guò)bind mount掛載到/data/test/,然后用mount命令可以看到/data/test也是掛載了/data/media:

# mount --bind /mnt/runtime/default/emulated  /data/test
# mount | grep /data/test
/data/media on /data/test type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid)

運(yùn)行時(shí)權(quán)限

Android 6之后導(dǎo)入了運(yùn)行時(shí)權(quán)限,READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE需要在運(yùn)行時(shí)申請(qǐng).

所以應(yīng)用在第一次啟動(dòng)的時(shí)候還沒(méi)有外部存儲(chǔ)的權(quán)限,掛載的是/mnt/runtime/default.

當(dāng)運(yùn)行時(shí)權(quán)限申請(qǐng)成功之后就會(huì)觸發(fā)StorageManagerInternalImpl.onExternalStoragePolicyChanged然后去給這個(gè)應(yīng)用重新掛載/storage/emulated:

// https://cs.android.com/android/platform/superproject/+/android-8.0.0_r1:frameworks/base/services/core/java/com/android/server/StorageManagerService.java
private final class StorageManagerInternalImpl extends StorageManagerInternal {
    ...
    @Override
    public void onExternalStoragePolicyChanged(int uid, String packageName) {
        final int mountMode = getExternalStorageMountMode(uid, packageName);
        remountUidExternalStorage(uid, mountMode);
    }
    ...
}

private void remountUidExternalStorage(int uid, int mode) {
    waitForReady();

    String modeName = "none";
    switch (mode) {
        case Zygote.MOUNT_EXTERNAL_DEFAULT: {
            modeName = "default";
        } break;

        case Zygote.MOUNT_EXTERNAL_READ: {
            modeName = "read";
        } break;

        case Zygote.MOUNT_EXTERNAL_WRITE: {
            modeName = "write";
        } break;
    }

    try {
        mConnector.execute("volume", "remount_uid", uid, modeName);
    } catch (NativeDaemonConnectorException e) {
        Slog.w(TAG, "Failed to remount UID " + uid + " as " + modeName + ": " + e);
    }
}

最終會(huì)調(diào)用到VolumeManager::remountUid從proc查找應(yīng)用進(jìn)程對(duì)應(yīng)的私有掛載空間,重新根據(jù)權(quán)限掛載/storage:

// https://cs.android.com/android/platform/superproject/+/android-8.0.0_r1:system/vold/VolumeManager.cpp

int VolumeManager::remountUid(uid_t uid, const std::string& mode) {
    ...

    if (!(dir = opendir("/proc"))) {
        PLOG(ERROR) << "Failed to opendir";
        return -1;
    }

    ...

    // 遍歷/proc的子目錄
    while ((de = readdir(dir))) {
        pidFd = -1;
        nsFd = -1;

        pidFd = openat(dirfd(dir), de->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
        if (pidFd < 0) {
            goto next;
        }
        if (fstat(pidFd, &sb) != 0) {
            PLOG(WARNING) << "Failed to stat " << de->d_name;
            goto next;
        }

        // 對(duì)比uid,查找uid對(duì)應(yīng)的進(jìn)程目錄
        if (sb.st_uid != uid) {
            goto next;
        }

        ...

        // 讀取私有掛載空間的id
        nsFd = openat(pidFd, "ns/mnt", O_RDONLY); // not O_CLOEXEC
        if (nsFd < 0) {
            PLOG(WARNING) << "Failed to open namespace for " << de->d_name;
            goto next;
        }

        // 開(kāi)啟子進(jìn)程實(shí)現(xiàn)并發(fā)
        if (!(child = fork())) {
            // 進(jìn)入應(yīng)用進(jìn)程的私有掛載空間
            if (setns(nsFd, CLONE_NEWNS) != 0) {
                PLOG(ERROR) << "Failed to setns for " << de->d_name;
                _exit(1);
            }

            // 解除/storage掛載
            unmount_tree("/storage");

            // 根據(jù)權(quán)限掛載對(duì)應(yīng)目錄
            std::string storageSource;
            if (mode == "default") {
                storageSource = "/mnt/runtime/default";
            } else if (mode == "read") {
                storageSource = "/mnt/runtime/read";
            } else if (mode == "write") {
                storageSource = "/mnt/runtime/write";
            } else {
                // Sane default of no storage visible
                _exit(0);
            }

            //重新掛載/storage
            if (TEMP_FAILURE_RETRY(mount(storageSource.c_str(), "/storage",
                    NULL, MS_BIND | MS_REC, NULL)) == -1) {
                PLOG(ERROR) << "Failed to mount " << storageSource << " for "
                        << de->d_name;
                _exit(1);
            }
            ...

            _exit(0);
        }

        if (child == -1) {
            PLOG(ERROR) << "Failed to fork";
            goto next;
        } else {
            TEMP_FAILURE_RETRY(waitpid(child, nullptr, 0));
        }

next:
        close(nsFd);
        close(pidFd);
    }
    closedir(dir);
    return 0;
}

缺點(diǎn)

這種權(quán)限管理的方式比較粗獷,一旦獲取了讀寫(xiě)的權(quán)限就能對(duì)外部存儲(chǔ)的任意目錄進(jìn)行讀寫(xiě),例如應(yīng)用的外部存儲(chǔ)路徑/storage/emulated/0/Android/data/${包名}/:

shellExec("ls -l /storage/emulated/0/Android/data");

03-12 18:48:12.809  2934  2934 D ExecShell: ls -l /storage/emulated/0/Android/data
03-12 18:48:12.809  2934  2934 D ExecShell: total 4
03-12 18:48:12.809  2934  2934 D ExecShell: drwxrwx--- 3 u0_a15 everybody 4096 2023-03-12 18:47 com.android.launcher3

獲取到讀取權(quán)限之后就能對(duì)其他應(yīng)用的外部存儲(chǔ)路徑進(jìn)行讀寫(xiě)了。因此一些敏感的信息一般不會(huì)寫(xiě)入到下面方法獲取出來(lái)的路徑:

public File getExternalFilesDir(String type)
public File[] getExternalFilesDirs(String type) 
public File getExternalCacheDir() 
public File[] getExternalCacheDirs() 
public File[] getExternalMediaDirs()

Android 11以后

安卓11為了更好的管控外部存儲(chǔ)的權(quán)限,廢棄了READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE,使用分區(qū)存儲(chǔ)(Scoped Storage)的去管理外部存儲(chǔ):

使用分區(qū)存儲(chǔ)的應(yīng)用可具有以下訪問(wèn)權(quán)限級(jí)別(實(shí)際訪問(wèn)權(quán)限因?qū)崿F(xiàn)而異)。

- 對(duì)自己的文件擁有讀取和寫(xiě)入訪問(wèn)權(quán)限(沒(méi)有權(quán)限限制)
- 對(duì)其他應(yīng)用的媒體文件擁有讀取訪問(wèn)權(quán)限(需要具備 READ_EXTERNAL_STORAGE 權(quán)限)
- 只有在用戶直接同意的情況下,才允許對(duì)其他應(yīng)用的媒體文件擁有寫(xiě)入訪問(wèn)權(quán)限(系統(tǒng)圖庫(kù)以及符合“所有文件訪問(wèn)權(quán)限”獲取條件的應(yīng)用除外)
- 對(duì)其他應(yīng)用的外部應(yīng)用數(shù)據(jù)目錄沒(méi)有讀取或?qū)懭朐L問(wèn)權(quán)限

應(yīng)用端具體的適配方法在網(wǎng)上有很多文章有提及,無(wú)非是通過(guò)MediaStore或者SAF去訪問(wèn)外部存儲(chǔ),我這邊就不做介紹了。這篇博客主要介紹系統(tǒng)端是如何實(shí)現(xiàn)外部存儲(chǔ)的權(quán)限管理的。

FUSE

為了實(shí)現(xiàn)分區(qū)存儲(chǔ),前面的bind mount機(jī)制是無(wú)法做到這么細(xì)致的管理的。所以在Android 11谷歌又廢棄了Android 8導(dǎo)入的SDCardFS,回歸FUSE機(jī)制。

FUSE是由Linux Kernel提供的一種文件系統(tǒng)。它的框架圖如下:

1.png

Linux為了支持多種文件系統(tǒng)(如EXT4, NTFS, FAT等)抽象了一個(gè)虛擬文件系統(tǒng)層(VFS),FUSE就是其中的一種.

從上面的框架圖可以看到,在用戶空間會(huì)有一個(gè)FUSE daemon進(jìn)程監(jiān)聽(tīng)對(duì)FUSE文件系統(tǒng)的操作,然后對(duì)其進(jìn)行轉(zhuǎn)發(fā)給到其他的文件系統(tǒng)。

由于是在FUSE是kernel提供的機(jī)制,所以無(wú)論應(yīng)用是通過(guò)java還是native方法去操作的文件,安卓都可以在FUSE daemon對(duì)文件的操作請(qǐng)求進(jìn)行權(quán)限鑒別和攔截。

FUSE daemon

例如使用FileOutputStream在外部存儲(chǔ)創(chuàng)建文件的時(shí)候會(huì)回調(diào)到FuseDaemon的pf_create:

// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/jni/FuseDaemon.cpp
static void pf_create(fuse_req_t req,
                      fuse_ino_t parent,
                      const char* name,
                      mode_t mode,
                      struct fuse_file_info* fi) {
      ...
      if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
          fuse_reply_err(req, ENOENT);
          return;
      }
  
      TRACE_NODE(parent_node, req);
  
      const string child_path = parent_path + "/" + name;
  
      int mp_return_code = fuse->mp->InsertFile(child_path.c_str(), req->ctx.uid);
      if (mp_return_code) {
          fuse_reply_err(req, mp_return_code);
          return;
      }

      ...
      // Let MediaProvider know we've created a new file
      fuse->mp->OnFileCreated(child_path);
      ...
}

從代碼上我們看到首先它會(huì)調(diào)用is_app_accessible_path去判斷應(yīng)用的訪問(wèn)權(quán)限:

const std::regex PATTERN_OWNED_PATH(
    "^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)/([^/]+)(/?.*)?",
    std::regex_constants::icase);

static bool is_app_accessible_path(MediaProviderWrapper* mp, const string& path, uid_t uid) {
  // 系統(tǒng)權(quán)限的應(yīng)用會(huì)被允許訪問(wèn), FuseDaemon進(jìn)程自己也允許訪問(wèn)
  if (uid < AID_APP_START || uid == MY_UID) {
      return true;
  }

  //應(yīng)用不能直接訪問(wèn)/storage/emulated,只能訪問(wèn)它的子目錄,例如/storage/emulated/0
  if (path == "/storage/emulated") {
      return false;
  }

  std::smatch match;
  if (std::regex_match(path, match, PATTERN_OWNED_PATH)) {
      const std::string& pkg = match[1];
      ...
      if (!mp->IsUidForPackage(pkg, uid)) {
          // /storage/emulated/0/Andrdoi/data/${包名} 這樣的目錄不允許其他應(yīng)用訪問(wèn)
          PLOG(WARNING) << "Invalid other package file access from " << pkg << "(: " << path;
          return false;
      }
  }
  return true;
}

然后會(huì)調(diào)用fuse->mp->InsertFile去通過(guò)jni回調(diào)到j(luò)ava層的MediaProvider.insertFileIfNecessaryForFuse去插入文件:

// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/jni/MediaProviderWrapper.cpp
int MediaProviderWrapper::InsertFile(const string& path, uid_t uid) {
    ...
    return insertFileInternal(env, media_provider_object_, mid_insert_file_, path, uid);
}

int insertFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_insert_file,
                       const string& path, uid_t uid) {
    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
    int res = env->CallIntMethod(media_provider_object, mid_insert_file, j_path.get(), uid);
    ...
}

MediaProviderWrapper::MediaProviderWrapper(JNIEnv* env, jobject media_provider) {
    ...
    media_provider_class_ = env->FindClass("com/android/providers/media/MediaProvider");
    ...
    mid_insert_file_ = CacheMethod(env, "insertFileIfNecessary", "(Ljava/lang/String;I)I",
                               /*is_static*/ false);
    ...
}

jmethodID MediaProviderWrapper::CacheMethod(JNIEnv* env, const char method_name[],
                                            const char signature[], bool is_static) {
    jmethodID mid;
    string actual_method_name(method_name);
    actual_method_name.append("ForFuse");
    if (is_static) {
        mid = env->GetStaticMethodID(media_provider_class_, actual_method_name.c_str(), signature);
    } else {
        mid = env->GetMethodID(media_provider_class_, actual_method_name.c_str(), signature);
    }
    ...
}

目錄隔離

insertFileIfNecessaryForFuse會(huì)通過(guò)文件的后綴解析出mimeType(例如.jpg就是圖片類型,.mp4就是視頻類型),然后創(chuàng)建contentUri調(diào)用insertFileForFuse:

// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
    ...
    final String mimeType = MimeUtils.resolveMimeType(new File(path));
    ...
    final Uri contentUri = getContentUriForFile(path, mimeType);
    final Uri item = insertFileForFuse(path, contentUri, mimeType, /*useData*/ false);
    if (item == null) {
        return OsConstants.EPERM;
    }
  ...
}

private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType,
            boolean useData) {
    ContentValues values = new ContentValues();
    values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf());
    values.put(MediaColumns.MIME_TYPE, mimeType);
    values.put(FileColumns.IS_PENDING, 1);

    if (useData) {
        values.put(FileColumns.DATA, path);
    } else {
        values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
        values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
        values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
    }
    return insert(uri, values, Bundle.EMPTY);
}

insert里面會(huì)對(duì)文件類型和存放的路徑做校驗(yàn),也就是說(shuō)外部存儲(chǔ)公共目錄下只能存放特定類型的文件,例如Movies下只能放視頻文件、Music下只能放音頻文件、Pictures下只能放圖片文件等。你不能將png的圖片放到/storage/emulated/0/Movies下:

03-14 19:48:04.683  1774  2181 E MediaProvider: java.lang.IllegalArgumentException: MIME type image/png cannot be inserted into content://media
/external_primary/video/media; expected MIME type under video/*

這個(gè)校驗(yàn)是在insert里面調(diào)用ensureFileColumns方法去檢查的:

// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
        @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)
        throws VolumeArgumentException, VolumeNotFoundException {
  ...
  else if (defaultMediaType != actualMediaType) {
      final String[] split = defaultMimeType.split("/");
      throw new IllegalArgumentException(
              "MIME type " + mimeType + " cannot be inserted into " + uri
                      + "; expected MIME type under " + split[0] + "/*");
  }
  ...
}

而像/storage/emulated/0/Android/media/${包名}這樣的外部媒體私有路徑也會(huì)被攔截下來(lái):

03-14 20:11:49.541  1774  2038 E MediaProvider: java.lang.IllegalArgumentException: Primary directory Android not allowed for content://media/external_primary/file; allowed directories are [Download, Documents]

它同樣是在ensureFileColumns里面攔截的:

// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
        @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)
        throws VolumeArgumentException, VolumeNotFoundException {
    ...
    // Consider allowing external media directory of calling package
    if (!validPath) {
        final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath());
        if (pathOwnerPackage != null) {
            validPath = isExternalMediaDirectory(res.getAbsolutePath()) &&
                  isCallingIdentitySharedPackageName(pathOwnerPackage);
        }
    }
    ...
    if (!validPath) {
        throw new IllegalArgumentException(
              "Primary directory " + primary + " not allowed for " + uri
                      + "; allowed directories are " + allowedPrimary);
    }
    ...
}

private boolean isExternalMediaDirectory(@NonNull String path) {
    final String relativePath = extractRelativePath(path);
    if (relativePath != null) {
        return relativePath.startsWith("Android/media");
    }
    return false;
}
// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java
public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
        "(?i)^/storage/[^/]+/(?:[0-9]+/)?"
        + PROP_CROSS_USER_ROOT_PATTERN
        + "Android/(?:data|media|obb)/([^/]+)(/?.*)?");

public static @Nullable String extractPathOwnerPackageName(@Nullable String path) {
    if (path == null) return null;
    final Matcher m = PATTERN_OWNED_PATH.matcher(path);
    if (m.matches()) {
        return m.group(1);
    }
    return null;
}

從上面的錯(cuò)誤日志可以看出來(lái)Download, Documents是公共目錄。實(shí)際上這兩個(gè)目錄不會(huì)檢查文件類型,可以存放所有類型的文件。

媒體數(shù)據(jù)庫(kù)

另外我們看到insertFileForFuse里面會(huì)創(chuàng)建ContentValues去調(diào)用insert,這里的代碼其實(shí)和應(yīng)用層使用MediaStore去訪問(wèn)外部存儲(chǔ)基本一致了。

insert的意思實(shí)際上是插入到MediaProvider數(shù)據(jù)庫(kù),所以我們可以從MediaProvider數(shù)據(jù)庫(kù)通過(guò)文件類型查找文件(例如音樂(lè)播放器可以通過(guò)MediaProvider查找到手機(jī)上的所有音頻文件):

// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
public @Nullable Uri insert(@NonNull Uri uri, @Nullable ContentValues values,
        @Nullable Bundle extras) {
    ...
    return insertInternal(uri, values, extras);
    ...
}

private @Nullable Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
            @Nullable Bundle extras) throws FallbackException {
    ...
    final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null);
    ...
    switch (match) {
        case IMAGES_MEDIA: {
            ..
            newUri = insertFile(qb, helper, match, uri, extras, initialValues,
                    FileColumns.MEDIA_TYPE_IMAGE);
            break;
        }

        case IMAGES_THUMBNAILS: {
            ...

            rowId = qb.insert(helper, initialValues);
            if (rowId > 0) {
                newUri = ContentUris.withAppendedId(Images.Thumbnails.
                        getContentUri(originalVolumeName), rowId);
            }
            break;
        }

        case VIDEO_THUMBNAILS: {
            ...

            rowId = qb.insert(helper, initialValues);
            if (rowId > 0) {
                newUri = ContentUris.withAppendedId(Video.Thumbnails.
                        getContentUri(originalVolumeName), rowId);
            }
            break;
        }

        case AUDIO_MEDIA: {
            ...
            newUri = insertFile(qb, helper, match, uri, extras, initialValues,
                    FileColumns.MEDIA_TYPE_AUDIO);
            break;
        }
        ...
    }
    ...
}

private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
        int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
        int mediaType) throws VolumeArgumentException, VolumeNotFoundException {
    ...
    rowId = insertAllowingUpsert(qb, helper, values, path);
    ...
}


private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb,
        @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)
        throws SQLiteConstraintException {
    return helper.runWithTransaction((db) -> {
        ...
        return qb.insert(helper, values);
        ...
    }
}

文件隔離

雖然前面講到Download, Document是公共目錄,誰(shuí)都可以往里面寫(xiě)入文件。但是正常情況下普通應(yīng)用只能讀取自己寫(xiě)入的問(wèn)題,沒(méi)有權(quán)限讀取其他應(yīng)用寫(xiě)入的文件:

1774  2181 E MediaProvider: Permission to access file: //storage/emulated/0/Download/OtherAppFile.txt is denied

這是因?yàn)榇蜷_(kāi)文件的時(shí)候會(huì)觸發(fā)到FuseDaemon的pf_open:

static void pf_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
  ...
  std::unique_ptr<FileOpenResult> result = fuse->mp->OnFileOpen(
            build_path, io_path, ctx->uid, ctx->pid, node->GetTransformsReason(), for_write,
            !for_write /* redact */, true /* log_transforms_metrics */);
  ...
}

最終去到MediaProvider.onFileOpenForFuse在里面調(diào)用checkAccess檢查訪問(wèn)權(quán)限:

public FileOpenResult onFileOpenForFuse(String path, String ioPath, int uid, int tid,
        int transformsReason, boolean forWrite, boolean redact, boolean logTransformsMetrics) {
  ...
  try {
    ...
    checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
    ...
  } catch (IllegalStateException | SecurityException e) {
      Log.e(TAG, "Permission to access file: " + path + " is denied");
      return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
              mediaCapabilitiesUid, new long[0]);
  } 
  ...
}

checkAccess最終最一堆的權(quán)限檢查,如果沒(méi)有符合的就拋出SecurityException異常:

private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file,
        boolean isWrite) throws FileNotFoundException {
    enforceCallingPermission(uri, extras, isWrite);
    ...
}

private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
        boolean forWrite) {
    ...
    enforceCallingPermissionInternal(uri, extras, forWrite);
    ...
}

private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras,
            boolean forWrite) {
    ...
    if (checkCallingPermissionGlobal(uri, forWrite)) {
        // Access allowed, yay!
        return;
    }
    ...
    throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri);
}

其中checkCallingPermissionGlobal會(huì)檢測(cè)android.permission.MANAGE_EXTERNAL_STORAGE權(quán)限,也就是文件管理器可以讀取外部存儲(chǔ)的所有公有文件的原理(例如Android/data/${包名}下的文件在前面的判斷里面會(huì)跳出所以還是不能訪問(wèn)):

private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
    ...
    // Apps that have permission to manage external storage can work with all files
    if (isCallingPackageManager()) {
        return true;
    }
    ...
}

開(kāi)啟文件管理器權(quán)限需要:

  1. 在AndroidManifest.xml聲明android.permission.MANAGE_EXTERNAL_STORAGE權(quán)限
  2. 使用Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION啟動(dòng)設(shè)置頁(yè)面讓用戶手動(dòng)打開(kāi)該應(yīng)用的文件管理權(quán)限:
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容