Android--深入理解沙箱存储机制

作者:opLW
本文基于6.0以上进行分析、适合有一定Android基础和Linux基础的同学阅读。

目录

一图概括
1. Android权限机制
2. Framework层决定存储权限
3. Native层限制存储访问

一图概括

在这里插入图片描述

1. Android权限机制
  • 主要类
    Android29
  • 应用获取权限的过程
    • Normal权限 应用安装时,PackageManagerService会扫描应用的APK,获取AndroidManifest.xml文件中的Normal权限,允许并保存到PackageManagerServicemPackages: ArrayMap<String, PackageParser.Package>(缓存所有包的信息),同时会写入到/data/system/packages.xml文件中,以便手机重启时快速产生包信息。
    • Dangerous权限 上面提到,PackageManagerService的成员mPackages保存所有包的信息包括权限信息。Dangerous级别的权限,在取得用户同意后,除了保存到mPackages外则是保存到/data/system/users/0/runtime-permissions.xml文件中。这个文件主要用于保存应用的动态权限。
  • 参考文章「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;
          }
      

      就是获取调用每个ExternalStorageMountPolicygetMountMode方法,并取其中的最小值。具体取值有:MOUNT_EXTERNAL_NONE=0、MOUNT_EXTERNAL_DEFAULT=1、MOUNT_EXTERNAL_READ=2、MOUNT_EXTERNAL_WRITE=3。

      实现ExternalStorageMountPolicy并添加到mPolicies的地方有AppOpsServicePackageManagerService这两个类,其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);
         }
     }
    
    其中mVoldVold(volume Daemon),即Volume守护进程,用来管理Android中存储类的热拔插事件,处于KernelFramework之间,是两个层级连接的桥梁。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外部存储空间及动态权限授予原理

万水千山总是情,麻烦手下别留情。
如若讲得有不妥,文末留言告知我,
如若觉得还可以,收藏点赞要一起。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值