这篇文章是建立在你已经对 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:

on init

# See storage config details at http://source.android.com/tech/storage/

mkdir /mnt/shell/emulated 0700 shell shell

mkdir /storage/emulated 0555 root root

exportEXTERNAL_STORAGE /storage/emulated/legacy

exportEMULATED_STORAGE_SOURCE /mnt/shell/emulated

exportEMULATED_STORAGE_TARGET /storage/emulated

# Support legacy paths

symlink /storage/emulated/legacy /sdcard

symlink /storage/emulated/legacy /mnt/sdcard

symlink /storage/emulated/legacy /storage/sdcard0

symlink /mnt/shell/emulated/0 /storage/emulated/legacy

root@mx5:/mnt # ls -l

drwxr-xr-x root system 2016-07-08 16:20 asec

dr-xr-xr-x root root 2014-08-14 20:41 cd-rom

drwx------ media_rw media_rw 2016-07-08 16:20 media_rw

drwxr-xr-x root system 2016-07-08 16:20 obb

lrwxrwxrwx root root 2016-07-08 16:20 sdcard -> /storage/emulated/legacy

lrwxrwxrwx root root 2016-07-08 16:20 sdcard2 -> /storage/sdcard1

drwx------ root root 2016-07-08 16:20 secure

drwxr-x--- shell sdcard_r 2016-07-08 16:20 shell

root@mx5:/sdcard # ls -l

drwxrwx--x root sdcard_r 2016-07-07 10:33 Android

drwxrwx--- root sdcard_r 2015-01-01 00:00 Customize

drwxrwx--- root sdcard_r 2015-01-01 00:00 DCIM

drwxrwx--- root sdcard_r 2016-07-05 19:55 Download

drwxrwx--- root sdcard_r 2015-01-01 00:00 Movies

drwxrwx--- root sdcard_r 2015-01-01 00:00 Music

drwxrwx--- root sdcard_r 2015-01-01 00:00 Pictures

在 Android L 上 , 访问 sdcard 的安全主要是通过用户组和权限来控制,详细介绍可以阅读本文开头的链接,这里就不赘述了。

Android M:

# set up the global environment

on init

exportANDROID_STORAGE /storage

exportEXTERNAL_STORAGE /sdcard

exportASEC_MOUNTPOINT /mnt/asec

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

static int fuse_setup(struct fuse* fuse, gid_t gid, mode_t mask) {

char opts[256];

fuse->fd = open("/dev/fuse", O_RDWR);

if (fuse->fd == -1) {

ERROR("failed to open fuse device: %s\n", strerror(errno));

return -1;

}

umount2(fuse->dest_path, MNT_DETACH);

snprintf(opts, sizeof(opts),

"fd=%i,rootmode=40000,default_permissions,allow_other,user_id=%d,group_id=%d",

fuse->fd, fuse->global->uid, fuse->global->gid);

if (mount("/dev/fuse", fuse->dest_path, "fuse", MS_NOSUID | MS_NODEV | MS_NOEXEC |

MS_NOATIME, opts) != 0) {

ERROR("failed to mount fuse filesystem: %s\n", strerror(errno));

return -1;

}

fuse->gid = gid;

fuse->mask = mask;

return 0;

}

static void run(const char* source_path, const char* label, uid_t uid,

gid_t gid, userid_t userid, bool multi_user, bool full_write) {

struct fuse_global global;

struct fuse fuse_default;

struct fuse fuse_read;

struct fuse fuse_write;

struct fuse_handler handler_default;

struct fuse_handler handler_read;

struct fuse_handler handler_write;

pthread_t thread_default;

pthread_t thread_read;

pthread_t thread_write;

memset(&global, 0, sizeof(global));

memset(&fuse_default, 0, sizeof(fuse_default));

memset(&fuse_read, 0, sizeof(fuse_read));

memset(&fuse_write, 0, sizeof(fuse_write));

memset(&handler_default, 0, sizeof(handler_default));

memset(&handler_read, 0, sizeof(handler_read));

memset(&handler_write, 0, sizeof(handler_write));

pthread_mutex_init(&global.lock, NULL);

global.package_to_appid = hashmapCreate(256, str_hash, str_icase_equals);

global.uid = uid;

global.gid = gid;

global.multi_user = multi_user;

global.next_generation = 0;

global.inode_ctr = 1;

memset(&global.root, 0, sizeof(global.root));

global.root.nid = FUSE_ROOT_ID; /* 1 */

global.root.refcount = 2;

global.root.namelen = strlen(source_path);

global.root.name = strdup(source_path);

global.root.userid = userid;

global.root.uid = AID_ROOT;

global.root.under_android = false;

strcpy(global.source_path, source_path);

if (multi_user) {

global.root.perm = PERM_PRE_ROOT;

snprintf(global.obb_path, sizeof(global.obb_path), "%s/obb", source_path);

} else {

global.root.perm = PERM_ROOT;

snprintf(global.obb_path, sizeof(global.obb_path), "%s/Android/obb", source_path);

}

fuse_default.global = &global;

fuse_read.global = &global;

fuse_write.global = &global;

global.fuse_default = &fuse_default;

global.fuse_read = &fuse_read;

global.fuse_write = &fuse_write;

snprintf(fuse_default.dest_path, PATH_MAX, "/mnt/runtime/default/%s", label);

snprintf(fuse_read.dest_path, PATH_MAX, "/mnt/runtime/read/%s", label);

snprintf(fuse_write.dest_path, PATH_MAX, "/mnt/runtime/write/%s", label);

handler_default.fuse = &fuse_default;

handler_read.fuse = &fuse_read;

handler_write.fuse = &fuse_write;

handler_default.token = 0;

handler_read.token = 1;

handler_write.token = 2;

umask(0);

if (multi_user) {

/* Multi-user storage is fully isolated per user, so "other"

* permissions are completely masked off. */

if (fuse_setup(&fuse_default, AID_SDCARD_RW, 0006)

|| fuse_setup(&fuse_read, AID_EVERYBODY, 0027)

|| fuse_setup(&fuse_write, AID_EVERYBODY, full_write ? 0007 : 0027)) {

ERROR("failed to fuse_setup\n");

exit(1);

}

} else {

/* Physical storage is readable by all users on device, but

* the Android directories are masked off to a single user

* deep inside attr_from_stat(). */

if (fuse_setup(&fuse_default, AID_SDCARD_RW, 0006)

|| fuse_setup(&fuse_read, AID_EVERYBODY, full_write ? 0027 : 0022)

|| fuse_setup(&fuse_write, AID_EVERYBODY, full_write ? 0007 : 0022)) {

ERROR("failed to fuse_setup\n");

exit(1);

}

}

/* Drop privs */

if (setgroups(sizeof(kGroups) / sizeof(kGroups[0]), kGroups) < 0) {

ERROR("cannot setgroups: %s\n", strerror(errno));

exit(1);

}

if (setgid(gid) < 0) {

ERROR("cannot setgid: %s\n", strerror(errno));

exit(1);

}

if (setuid(uid) < 0) {

ERROR("cannot setuid: %s\n", strerror(errno));

exit(1);

}

if (multi_user) {

fs_prepare_dir(global.obb_path, 0775, uid, gid);

}

if (pthread_create(&thread_default, NULL, start_handler, &handler_default)

|| pthread_create(&thread_read, NULL, start_handler, &handler_read)

|| pthread_create(&thread_write, NULL, start_handler, &handler_write)) {

ERROR("failed to pthread_create\n");

exit(1);

}

watch_package_list(&global);

ERROR("terminated prematurely\n");

exit(1);

}

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 权限的应用可见。为什么这样,请一直看到文章结尾,自然知晓原理。

root@bullhead:/mnt/runtime/default/emulated # ls -l

drwxrwx--x root sdcard_rw 1970-03-04 02:51 0

drwxrwx--x root sdcard_rw 1970-03-04 02:51 obb

root@bullhead:/mnt/runtime/read/emulated # ls -l

drwxr-x--- root everybody 1970-03-04 02:51 0

drwxr-x--- root everybody 1970-03-04 02:51 obb

root@bullhead:/mnt/runtime/write/emulated # ls -l

drwxrwx--- root everybody 1970-03-04 02:51 0

drwxrwx--- root everybody 1970-03-04 02:51 obb

上述 default/read/write 三个目录下的 0 和 obb 目录所赋予的用户组和权限各不相同。

这将为后面不同应用程序的运行时权限打下了基础。注意,这里的 0 代表用户 0 , 这里需要特别说明一下,从 4.0 后 android 也同时逐步引入了多用户的支持。

Step 3: 应用程序启动授权

在 zygote fork app 时,我们为每个运行中的应用创建一个 mount 名字空间,并在其中 bind mount 合适的初始视图。

// 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/

// Create a second private mount namespace for our process

if (unshare(CLONE_NEWNS) == -1) {

ALOGW("Failed to unshare(): %s", strerror(errno));

return false;

}

// Unmount storage provided by root namespace and mount requested view

UnmountTree("/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 {

// Sane default of no storage visible

return true;

}

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;

}

// Mount user-specific symlink helper into place

userid_t user_id = multiuser_get_user_id(uid);

const String8 userSource(String8::format("/mnt/user/%d", user_id));

if (fs_prepare_dir(userSource.string(), 0751, 0, 0) == -1) {

return false;

}

if (TEMP_FAILURE_RETRY(mount(userSource.string(), "/storage/self",

NULL, MS_BIND, NULL)) == -1) {

ALOGW("Failed to mount %s to /storage/self: %s", userSource.string(), strerror(errno));

return false;

}

return true;

}

我们仔细的分析这段代码做了些什么:根据 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

grantRuntimePermission

-> onExternalStoragePolicyChanged

-->remountUidExternalStorage

mConnector.execute("volume", "remount_uid", uid, modeName);然后 /system/vold/CommandListener.cpp

CommandListener::VolumeCmd::runCommand

->sendGenericOkFail(cli, vm->remountUid(uid, mode));重点 /system/vold/VolumeManager.cpp

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

LOG(DEBUG) << "Remounting " << uid << " as mode " << mode;

DIR* dir;

struct dirent* de;

char rootName[PATH_MAX];

char pidName[PATH_MAX];

int pidFd;

int nsFd;

struct stat sb;

pid_t child;

if (!(dir = opendir("/proc"))) {

PLOG(ERROR) << "Failed to opendir";

return -1;

}

// Figure out root namespace to compare against below

if (sane_readlinkat(dirfd(dir), "1/ns/mnt", rootName, PATH_MAX) == -1) {

PLOG(ERROR) << "Failed to readlink";

closedir(dir);

return -1;

}

// Poke through all running PIDs look for apps running as UID

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;

}

if (sb.st_uid != uid) {

goto next;

}

// Matches so far, but refuse to touch if in root namespace

LOG(DEBUG) << "Found matching PID " << de->d_name;

if (sane_readlinkat(pidFd, "ns/mnt", pidName, PATH_MAX) == -1) {

PLOG(WARNING) << "Failed to read namespace for " << de->d_name;

goto next;

}

if (!strcmp(rootName, pidName)) {

LOG(WARNING) << "Skipping due to root namespace";

goto next;

}

// We purposefully leave the namespace open across the fork

nsFd = openat(pidFd, "ns/mnt", O_RDONLY);

if (nsFd < 0) {

PLOG(WARNING) << "Failed to open namespace for " << de->d_name;

goto next;

}

if (!(child = fork())) {

if (setns(nsFd, CLONE_NEWNS) != 0) {

PLOG(ERROR) << "Failed to setns for " << de->d_name;

_exit(1);

}

unmount_tree("/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 {

// Sane default of no storage visible

_exit(0);

}

if (TEMP_FAILURE_RETRY(mount(storageSource.c_str(), "/storage",

NULL, MS_BIND | MS_REC | MS_SLAVE, NULL)) == -1) {

PLOG(ERROR) << "Failed to mount " << storageSource << " for "

<< de->d_name;

_exit(1);

}

// Mount user-specific symlink helper into place

userid_t user_id = multiuser_get_user_id(uid);

std::string userSource(StringPrintf("/mnt/user/%d", user_id));

if (TEMP_FAILURE_RETRY(mount(userSource.c_str(), "/storage/self",

NULL, MS_BIND, NULL)) == -1) {

PLOG(ERROR) << "Failed to mount " << userSource << " 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;

}

上述核心代码和 zygote 中很类似,不再赘述,至此,才算彻底搞清楚了 Androd M 在外置存储上权限控制的改变和多用户多进程下的安全原理。系统使用 setns() 函数来实现上述特性,这要求 Linux 3.8 , 不过 Linux 3.4 加上补丁上也可以支持该功能。

魅族的android m l,Android M 外部存储剖析相关推荐

  1. 【Android取证篇】华为外部存储支持备份的数据类型-支持第三方应用

    [Android取证篇]华为外部存储支持备份的数据类型-支持第三方应用 ​ 数据保存至外置存储卡或USB存储,"无需网络连接"!,支持部分第三方应用的数据备份-[suy] 文章目录 ...

  2. android 外部存储列表,如何获取Android设备的已安装外部存储列表

    我使用/ proc / mounts文件来获取可用存储选项的列表 public class StorageUtils { private static final String TAG = " ...

  3. android逐行写入读取_Android外部存储-读取,写入,保存文件

    android逐行写入读取 Android external storage can be used to write and save data, read configuration files ...

  4. Android 11 高版本 出现外部存储无法访问的问题

    最近在做Android 应用开发,IDE是android studio , 使用的版本配置如下:compileSdk 32 buildToolsVersion '32.0.0' defaultConf ...

  5. Android学习笔记-判断手机外部存储是否可读写

    通过调用Environment的getExternalStorageState()方法来判断外部存储的状态: /* 查检外部存储读取与写入功能是否可用 */ public boolean isExte ...

  6. chromebook刷机_如何获取Android应用以查看Chromebook上的外部存储

    chromebook刷机 Android apps are a great way to expand the sometimes limited capabilities of Chromebook ...

  7. 解析Android内部存储、外部存储的区别

    1.背景 在开发过程中我们都会使用到手机的内部缓存.外部缓存.但有些开发者对这两个存储区域理解还够透彻,以为手机内置的存储卡(不可手机移除)就是内部存储, 可插拔的SD卡就是外部存储,其实这些理解都是 ...

  8. android edittext_基于Android输入法开发,制作一个微信斗图APP

    刘望舒 读完需要 20分钟 速读仅需12分钟 作者:小学生° 来源:搜狐技术产品 01 导读 微信斗图的应用有很多,但大部分都是通过微信分享来实现的,需下载 APP,下载表情并分享到微信联系人,操作步 ...

  9. 魅族的android m l,魅族Flyme邀你参加Android M/L内测,要尝鲜的朋友看这里

    原标题:魅族Flyme邀你参加Android M/L内测,要尝鲜的朋友看这里 12月12日,魅族Flyme 6.7.12.12体验版正式发布,适用于魅族PRO 7/PRO 7 Plus.魅族PRO 6 ...

最新文章

  1. java springcloud版b2b2c社交电商spring cloud分布式微服务(十三)断路器聚合监控(Hystrix Turbine)...
  2. 笔记-项目管理基础知识-项目组织结构
  3. GinWin命令控制台执行指令
  4. 【Python金融量化 7- 100 】、七、计算两只股票方差和相关性
  5. JVM 运行时数据区域总结
  6. 【C#】wpf自定义calendar日期选择控件的样式
  7. NetBeans IDE下载及安装
  8. 批量下载wsdl文件
  9. 云服务器 微信支付开发,WeX5怎么样实现支付宝和微信支付接口
  10. 阿里云邮箱企业版产品知识库
  11. php期末作业总结,期末考试总结与反思(精选6篇)
  12. 可靠性测试项目之可靠性试验
  13. 苹果公司:苹果商店App价格将调整 多个国家地区受影响
  14. android虚拟器没有菜单,网易MuMu模拟器不显示Menu(菜单)键的解决办法
  15. 计算机具有理性和逻辑思维吗,我们的理性和逻辑思维能力真的有那么重要吗?...
  16. CSS“超出显示省略号,后面还能显示其他内容”的解决方案
  17. VS2022安装失败
  18. tableau-计算一个月内的第几周
  19. 课程设计:公交线路管理系统
  20. 低速接口之SPI接口,分类,四种模式,特点

热门文章

  1. 一步步教你轻松学朴素贝叶斯模型算法理论篇1
  2. OC_UITextField
  3. .net分布式系统架构的思路
  4. SpringCloud系列七:Hystrix 熔断机制(Hystrix基本配置、服务降级、HystrixDashboard服务监控、Turbine聚合监控)...
  5. 校园安全责任重大 安防守护迭代升级
  6. 使用功能开关更好地实现持续部署
  7. linux下杀死进程的10种方法
  8. nsTimer的简单用法
  9. Power Designer使用技巧
  10. 【Vue.js 牛刀小试】:第九章 - 组件基础再探(data、props)