作者:opLW
本文基于6.0以上进行分析、适合有一定Android基础和Linux基础的同学阅读。
目录
一图概括
1. Android权限机制
2. Framework层决定存储权限
3. Native层限制存储访问
一图概括
1. Android权限机制
- 主要类
- 应用获取权限的过程
- Normal权限 应用安装时,
PackageManagerService
会扫描应用的APK,获取AndroidManifest.xml
文件中的Normal权限,允许并保存到PackageManagerService
的mPackages: ArrayMap<String, PackageParser.Package>
(缓存所有包的信息),同时会写入到/data/system/packages.xml
文件中,以便手机重启时快速产生包信息。 - Dangerous权限 上面提到,
PackageManagerService
的成员mPackages
保存所有包的信息包括权限信息。Dangerous级别的权限,在取得用户同意后,除了保存到mPackages
外则是保存到/data/system/users/0/runtime-permissions.xml
文件中。这个文件主要用于保存应用的动态权限。
- Normal权限 应用安装时,
- 参考文章「Android 安全架构」你真的了解权限机制吗?
2. Framework层决定挂载模式
- 通知
StorageManagerInternalImpl
外部存储政策变化
前面简单介绍了Android的权限机制。授予动态权限的代码在PermissionManagerService.grantRuntimePermission
中,这个函数的最后对外部存储权限做了特殊处理:通知StorageManagerInternalImpl
政策变化。private void grantRuntimePermission(...) { .... if (READ_EXTERNAL_STORAGE.equals(permName) || WRITE_EXTERNAL_STORAGE.equals(permName)) { final long token = Binder.clearCallingIdentity(); try { if (mUserManagerInt.isUserInitialized(userId)) { StorageManagerInternal storageManagerInternal = LocalServices.getService( StorageManagerInternal.class); storageManagerInternal.onExternalStoragePolicyChanged(uid, packageName); // StorageManagerInternal的实现类是StorageManagerService.StorageManagerInternalImpl } } finally { Binder.restoreCallingIdentity(token); } } }
- 判断挂载模式 政策发生变化时,
StorageManagerInternalImpl
需要判断挂载模式并通知重新挂载。
判断函数public void onExternalStoragePolicyChanged(int uid, String packageName) { final int mountMode = getExternalStorageMountMode(uid, packageName); remountUidExternalStorage(uid, mountMode); }
getExternalStorageMountMode
在不同版本有不同的实现。-
SDK == 28
public int getExternalStorageMountMode(int uid, String packageName) { int mountMode = Integer.MAX_VALUE; for (ExternalStorageMountPolicy policy : mPolicies) { final int policyMode = policy.getMountMode(uid, packageName); if (policyMode == Zygote.MOUNT_EXTERNAL_NONE) { return Zygote.MOUNT_EXTERNAL_NONE; } mountMode = Math.min(mountMode, policyMode); } if (mountMode == Integer.MAX_VALUE) { return Zygote.MOUNT_EXTERNAL_NONE; } return mountMode; }
就是获取调用每个
ExternalStorageMountPolicy
的getMountMode
方法,并取其中的最小值。具体取值有:MOUNT_EXTERNAL_NONE
=0、MOUNT_EXTERNAL_DEFAULT
=1、MOUNT_EXTERNAL_READ
=2、MOUNT_EXTERNAL_WRITE
=3。实现
ExternalStorageMountPolicy
并添加到mPolicies的地方有AppOpsService
和PackageManagerService
这两个类,其getMountMode
的大概实现就是判断有无相关权限,感兴趣请自行查阅。 -
SDK >= 29 相比于
SDK=28
,判断函数多了下面这一段。根据注释可知在SDK==9
时,ENABLE_ISOLATED_STORAGE
为true。public int getExternalStorageMountMode(int uid, String packageName) { if (ENABLE_ISOLATED_STORAGE) { return getMountMode(uid, packageName); } ...... } private static final boolean ENABLE_ISOLATED_STORAGE = StorageManager.hasIsolatedStorage(); /** * Return if the currently booted device has the "isolated storage" feature * flag enabled. This will eventually be fully enabled in the final * {@link android.os.Build.VERSION_CODES#Q} release. */ public static boolean hasIsolatedStorage() {...}
getMountMode
最终会调用到下面这个函数:private int getMountModeInternal(int uid, String packageName) { try { ...省略部分判断 // Determine if caller is holding runtime permission final boolean hasRead = StorageManager.checkPermissionAndCheckOp(...); final boolean hasWrite = StorageManager.checkPermissionAndCheckOp(...); ...省略部分判断 // Otherwise we're willing to give out sandboxed or non-sandboxed if // they hold the runtime permission final boolean hasLegacy = mIAppOpsService.checkOperation(OP_LEGACY_STORAGE, uid, packageName) == MODE_ALLOWED; if (hasLegacy && hasWrite) { return Zygote.MOUNT_EXTERNAL_WRITE; } else if (hasLegacy && hasRead) { return Zygote.MOUNT_EXTERNAL_READ; } else { return Zygote.MOUNT_EXTERNAL_DEFAULT; } } catch (RemoteException e) {} return Zygote.MOUNT_EXTERNAL_NONE; }
SDK<29
时,只要申请了存储权限就能读写外部存储(下面简称这种模式:旧版存储模式);而SDK>=29
时启用沙箱存储,但为了迁移数据,还是可以继续使用旧版存储模式,需要满足条件: Android – 沙箱适配规则总结。- 其中
hasLegacy
就是判断能否继续使用旧版的存储模式。
-
- 更新挂载模式 上一步通过
getExternalStorageMountMode
判断新的挂载模式,接下来就是更新挂载模式了。
其中public void onExternalStoragePolicyChanged(int uid, String packageName) { final int mountMode = getExternalStorageMountMode(uid, packageName); remountUidExternalStorage(uid, mountMode); } private void remountUidExternalStorage(int uid, int mode) { try { mVold.remountUid(uid, mode); } catch (Exception e) { Slog.wtf(TAG, e); } }
mVold
是Vold(volume Daemon),即Volume
守护进程,用来管理Android
中存储类的热拔插事件,处于Kernel
和Framework
之间,是两个层级连接的桥梁。mVold.remountUid
最终会调用到Kernal
层的VolumeManager.remountUid
方法,下面我们看看Kernel
的操作。 - 参考文章:androidQ sd卡权限详细使用说明
3. Kernel层挂载相关目录
- 根据挂载模式,挂载不同的目录 具体步骤见代码注释。
int VolumeManager::remountUid(uid_t uid, int32_t mountMode) { // 1.根据挂载模式获取子目录名字 std::string mode; switch (mountMode) { case VoldNativeService::REMOUNT_MODE_NONE: mode = "none"; break; case VoldNativeService::REMOUNT_MODE_DEFAULT: mode = "default"; break; case VoldNativeService::REMOUNT_MODE_READ: mode = "read"; break; case VoldNativeService::REMOUNT_MODE_WRITE: case VoldNativeService::REMOUNT_MODE_LEGACY: case VoldNativeService::REMOUNT_MODE_INSTALLER: mode = "write"; break; case VoldNativeService::REMOUNT_MODE_FULL: mode = "full"; break; default: PLOG(ERROR) << "Unknown mode " << std::to_string(mountMode); return -1; } // 2.打开进程目录。/proc目录存放所有进程信息 if (!(dir = opendir("/proc"))) { PLOG(ERROR) << "Failed to opendir"; return -1; } // Poke through all running PIDs look for apps running as UID while ((de = readdir(dir))) { // 3.查找目标进程的目录 ... 省略判断 // 4.fork一个子进程,用于替换目标进程的目录。 if (!(child = fork())) { // 5.通过setns命令,将fork出来的子进程加入到目标进程的命名空间中,加入后便可操作目标进程的空间。 if (setns(nsFd, CLONE_NEWNS) != 0) { PLOG(ERROR) << "Failed to setns for " << de->d_name; _exit(1); } // 6.获取待挂载目录,不同目录具有不同的访问权限 android::vold::UnmountTree("/storage/"); 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 if (mode == "full") { storageSource = "/mnt/runtime/full"; } else { // Sane default of no storage visible _exit(0); } // 6. 将待挂载目录挂载到/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); } ...... } } return 0; }
- 命名空间,个人简单理解为一个抽象的空间。在这个空间里,应用认为只有自己在运行,而实际上进程访问的文件,会映射到不同的文件,因此也具有不同的权限。(可能有理解不到位的地方,还望指教 ~ 。~
- 不同目录的权限 上述步骤根据模式将
/mnt/runtime/default/
、/mnt/runtime/read/
、/mnt/runtime/write
、/mnt/runtime/full
其中的一个挂载到目标进程命名空间的storage下。这几个目录的/emulated/0具有不同的权限,具体如下:其中
everybody
分组对应的gid是9997。进程启动时,系统默认将9997加入到进程的gids中,加入后进程拥有该分组权限。
此后目标进程通过generic_x86:/mnt/runtime/default/emulated # ls -l drwxrwx--x 12 root sdcard_rw 4096 2021-03-10 02:32 0 generic_x86:/mnt/runtime/read/emulated # ls -l drwxr-x--- 12 root everybody 4096 2021-03-10 02:32 0 generic_x86:/mnt/runtime/write/emulated # ls -l drwxrwx--- 12 root everybody 4096 2021-03-10 02:32 0
/storage/emulated/0/..
访问时会因为实际挂载的目录不同,而具有不同的权限。 - 参考文章:Android外部存储空间及动态权限授予原理
万水千山总是情,麻烦手下别留情。
如若讲得有不妥,文末留言告知我,
如若觉得还可以,收藏点赞要一起。