上篇博客介紹了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)。它的框架圖如下:

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)限需要:
- 在AndroidManifest.xml聲明android.permission.MANAGE_EXTERNAL_STORAGE權(quán)限
- 使用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);