这篇文章是建立在你已经对 Android 外部存储的基础知识有一定了解的基础之上,如果之前并不是太了解这个部分,阅读起来可能会比较费劲,可以先阅读参考下面文章:http://blog.csdn.net/zjbpku/article/details/25161131
Android M 外部存储的变化
从 Android 6.0 开始,Android 支持移动存储(adoptable storage),例如 SD 卡或者 USB 。移动存储可以像内部存储一样加密和格式化,可以存储所有类型的应用数据。
权限变化
是否访问外部存储由各种 Android 权限保护。
- 从 Android 1.0 开始,写访问需要
WRITE_EXTERNAL_STORAGE
权限。 - 从 Android 4.0 开始,读访问需要
READ_EXTERNAL_STORAGE
权限。 - 从 Android 4.4 开始,外部存储设备上的文件,也能够基于目录结构来合成( synthesized )不同的 DAC 权限( owner,group,mode )。这允许应用能够在外部存储上管理一个包相关的目录,而无需
WRITE_EXTERNAL_STORAGE
权限。例如, 应用 com.example.foo 可以自由访问外部存储上的 Android/data/com.example.foo/ 。这种合成权限是通过 fuse 守护来包裹原始存储设备来完成的。 - Android 6.0 引入了新的运行时权限(runtime permissions )模型,用于应用在运行中必要时申请权限。由于新模型包含了 READ/WRITE_EXTERNAL_STORAGE ,因此平台需要在不杀死或者重启运行中的应用的前提下,动态对存储访问授权。
关于运行时权限
Android 6.0 引入了一个新的应用权限模型,期望对用户更容易理解,更易用和更安全。该模型将标记为危险的权限从安装时权限 ( Install Time Permission ) 模型移动到运行时权限模型( Runtime Permissions ):
- 安装时权限模型 ( Android 5.1 以及更早 )。用户在应用安装和更新时,对危险权限授权。但是 OEM 和运行商预装的应用将自动预授权。
- 运行时权限 ( Android 6.0 及以后 )。用户在应用运行时,对应用授予危险权限。由应用决定何时去申请权限(例如,在应用启动时或者用户访问某个特性时),但必须允许用户来授予或者拒绝应用对特定权限组的访问。OEM 和运营商可以预装应用,但是不能对权限进行预授权(例外情况请看这里 Create exception )。
运行时权限提供给用户关于应用所需权限更多的相关上下文和可视性,这也让开发者帮助用户更好的理解:为什么应用需要所请求的权限,授权将有什么样的好处,拒绝将有何种不便。用户可以通过设置中的菜单来撤销应用的权限。
目录的变化
Android L:
1 | on init |
1 | root@mx5:/mnt # ls -l |
1 | root@mx5:/sdcard # ls -l |
在 Android L 上 , 访问 sdcard 的安全主要是通过用户组和权限来控制,详细介绍可以阅读本文开头的链接,这里就不赘述了。
Android M:
1 | # set up the global environment |
1 | lrwxrwxrwx root root 1970-10-09 20:28 sdcard -> /storage/self/primary |
所以,在 Android M 上 , /storage/self 是访问 sdcard 的关键路径。这个路径非常重要,会在后面的原理介绍中讲到。
实现原理
Step 1: sdcard 挂载
不得已,首先得挂一串代码出来:/system/core/sdcard/sdcard.c
1 | static int fuse_setup(struct fuse* fuse, gid_t gid, mode_t mask) { |
sdcard service 是 fuse 的守护进程,在 4.0 以后的 android 版本上,sdcard 都是通过 sdcard 服务来挂载和访问的,而且该服务程序还提供额外的权限控制。
上述代码是 sdcard 的挂载部分,关键代码即根据 vold 传过来的参数来准备好 uid/gid/userid 等信息,并且根据是否为多用户 ( multi_user ),是否 full_write 来准备好下面三个目录的用户组及其对应的权限:
- /mnt/runtime/default/emulated
- /mnt/runtime/read/emulated
- /mnt/runtime/write/emulated
Step 2: 三视图
在第一步结束后,所有挂载的存储设备都会维护三个不同视图:
- /mnt/runtime/default - 对所有的应用、root 命名空间(adb 和其他系统组件)可见,而无需任何权限
- /mnt/runtime/read - 对有
READ_EXTERNAL_STORAGE
权限的应用可见。 - /mnt/runtime/write - 对有
WRITE_EXTERNAL_STORAGE
权限的应用可见。
为什么这样,请一直看到文章结尾,自然知晓原理。
1 | root@bullhead:/mnt/runtime/default/emulated # ls -l |
上述 default/read/write 三个目录下的 0 和 obb 目录所赋予的用户组和权限各不相同。
这将为后面不同应用程序的运行时权限打下了基础。
注意,这里的 0 代表用户 0 , 这里需要特别说明一下,从 4.0 后 android 也同时逐步引入了多用户的支持。
Step 3: 应用程序启动授权
在 zygote fork app 时,我们为每个运行中的应用创建一个 mount 名字空间,并在其中 bind mount 合适的初始视图。
1 | // Create a private mount namespace and bind mount appropriate emulated |
我们仔细的分析这段代码做了些什么:
根据 mount mode 从上面三个已经准备好的路径中选择一个默认的挂载路径,并将这个路径挂到
/storage
上去。注意:这里带的是MS_BIND | MS_REC | MS_SLAVE
参数,这意味着会拷贝命名空间,所以,每个 app 进入的/storage
都是私有的。再根据当前的 user_id ,将
/mnt/user/user_id
bind 到当前/storage
的 self 目录上。
经过上述两步之后,达到了一个什么目的呢?
每个 app 都根据自己的授权,选择了不同权限的 runtime 目录进行访问,而不同用户访问的目录也跟去当前用户的 id 区分开了。
一切都完美的工作起来了,好像可以结束了!
Step 4: 应用程序 runtime 授权
回到我们文章开头介绍的 runtime 权限,我们发现,到目前为止,我们似乎并不能 runtime 控制权限?那我们要如何做呢?
其实方法特别简单,当被授予运行时权限时,vold 在运行中的应用的名字空间上,通过 bind mount 来更新视图。
- 我猜测 runtime 授权的入口代码是这个:
/frameworks/base/services/core/java/com/android/server/pm/PermissionsState.java
1 | grantRuntimePermission |
- 然后 /system/vold/CommandListener.cpp
1 | CommandListener::VolumeCmd::runCommand |
- 重点 /system/vold/VolumeManager.cpp
1 | int VolumeManager::remountUid(uid_t uid, const std::string& mode) { |
上述核心代码和 zygote 中很类似,不再赘述,至此,才算彻底搞清楚了 Androd M 在外置存储上权限控制的改变和多用户多进程下的安全原理。
系统使用
setns()
函数来实现上述特性,这要求 Linux 3.8 , 不过 Linux 3.4 加上补丁上也可以支持该功能。
This is copyright.