Android 6.0 存储权限管理

Android 6.0 存储权限管理

官方说明

先翻译一段Android的官方文档,原文在:https://source.android.com/devices/storage/
Android 6.0开始支持运行时权限管理的功能。运行时权限管量中当然也包括对READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE这两个权限的动态管理。系统需要提供在不杀掉或重启已经运行的应用的情况下去动态授权的机制。目前系统是通过维护三个View来实现的:

  • /mnt/runtime/default: 针对对于存储权限没有特殊需求的情况。这也是adbd的其它系统组件使用的方式。
  • /mnt/runtime/read:对于申请READ_EXTERNAL_STORAGE权限的应用可见。
  • /mnt/runtime/write:对于申请WRITE_EXTERNAL_STORAGE权限的应用可见。

在Zygote fork的时刻,我们为每个运行的应用创建一个命名空间,然后将其绑定到上面所进的三个View中作为初始的View。在运行时获得新的授权后,vold会跳转到这个装载的命名空间并重新绑定新的View. 需要注意的一点是,如果权限降级,则一定会导致应用被杀。

setns()方法从Linux 3.8移植到了3.4就专为干这事儿。有个PermissionsHostTest的CTS测试用例用来保证这个功能的有效性。

在Android 6.0,第三方应用没有访问sdcard_r和sdcard_rw GID的权限。作为替代,通过上面所讲的View的方式来控制。使用everybody GID跨用户的交互会被阻止。

代码实现

看了上面的介绍,我们来看代码中是如何实现的。实现这个功能的函数在framework/base/core/jni/com_android_internal_os_Zygote.cpp中的MountEmulatedStorage函数。

294// Create a private mount namespace and bind mount appropriate emulated
295// storage for the given user.
296static bool MountEmulatedStorage(uid_t uid, jint mount_mode,
297        bool force_mount_namespace) {
298    // See storage config details at http://source.android.com/tech/storage/

第一步,先调用unshare系统调用去禁止同享命名空间。
unshare函数定义于sched.h中,用于将部分进程上下文信息不共享父进程的,CLONE_NEWNS是指定不共享命名空间。

下面是ARM v8a AArch64下时调用unshare系统调用的代码:

5ENTRY(unshare)
6    mov     x8, __NR_unshare
7    svc     #0
8
9    cmn     x0, #(MAX_ERRNO + 1)
10    cneg    x0, x0, hi
11    b.hi    __set_errno_internal
12
13    ret
14END(unshare)
15

下面是第一步的代码:

300    // Create a second private mount namespace for our process
301    if (unshare(CLONE_NEWNS) == -1) {
302        ALOGW("Failed to unshare(): %s", strerror(errno));
303        return false;
304    }

第二步,调用UnmountTree函数:

首先解释一下为什么要做unmount,在init.rc里面,root namespace已经默认地mount /mnt/runtime/default到/storage了,

请看init.rc的片段:

244on post-fs
245    start logd
246    # once everything is setup, no need to modify /
247    mount rootfs rootfs / ro remount
248    # Mount shared so changes propagate into child namespaces
249    mount rootfs rootfs / shared rec
250    # Mount default storage into root namespace
251    mount none /mnt/runtime/default /storage slave bind rec

下面看代码:

306    // Unmount storage provided by root namespace and mount requested view
307    UnmountTree("/storage");

我们转到UnmountTree函数:

static int UnmountTree(const char* path) {
    size_t path_len = strlen(path);

    FILE* fp = setmntent("/proc/mounts", "r");

setmntent函数定义如下:

#include <stdio.h>
#include <mntent.h>

FILE *setmntent(const char *filename, const char *type);

用于获取系统mount的信息。

具体去读每一行的时候使用getmntent()函数。
结束时调用endmntent()函数,相当于fclose()。

getmntent读取的是一个mntent结构体的结构,该结构定义于<mntent.h>中:

struct mntent {
    char *mnt_fsname;   /* name of mounted file system */
    char *mnt_dir;      /* file system path prefix */
    char *mnt_type;     /* mount type (see mntent.h) */
    char *mnt_opts;     /* mount options (see mntent.h) */
    int   mnt_freq;     /* dump frequency in days */
    int   mnt_passno;   /* pass number on parallel fsck */
};

下面代码中,调用getmntent函数将目录信息读出来放在一个列表中:

    if (fp == NULL) {
        ALOGE("Error opening /proc/mounts: %s", strerror(errno));
        return -errno;
    }

    // Some volumes can be stacked on each other, so force unmount in
    // reverse order to give us the best chance of success.
    std::list<std::string> toUnmount;
    mntent* mentry;
    while ((mentry = getmntent(fp)) != NULL) {
        if (strncmp(mentry->mnt_dir, path, path_len) == 0) {
            toUnmount.push_front(std::string(mentry->mnt_dir));
        }
    }
    endmntent(fp);

接着,通过调用umount2函数将这些目录都unmount掉。

    for (auto path : toUnmount) {
        if (umount2(path.c_str(), MNT_DETACH)) {
            ALOGW("Failed to unmount %s: %s", path.c_str(), strerror(errno));
        }
    }
    return 0;
}

umount2函数原型如下,用于unmount文件系统。

#include <sys/mount.h>

int umount2(const char *target, int flags);

第三步,我们从UnmountTree中回来,按照上面所讲的几种模式,分别设置不同路径名:

309    String8 storageSource;
310    if (mount_mode == MOUNT_EXTERNAL_DEFAULT) {
311        storageSource = "/mnt/runtime/default";
312    } else if (mount_mode == MOUNT_EXTERNAL_READ) {
313        storageSource = "/mnt/runtime/read";
314    } else if (mount_mode == MOUNT_EXTERNAL_WRITE) {
315        storageSource = "/mnt/runtime/write";
316    } else {
317        // Sane default of no storage visible
318        return true;
319    }

第四步,根据第三步的模式值,重新mount。

320    if (TEMP_FAILURE_RETRY(mount(storageSource.string(), "/storage",
321            NULL, MS_BIND | MS_REC | MS_SLAVE, NULL)) == -1) {
322        ALOGW("Failed to mount %s to /storage: %s", storageSource.string(), strerror(errno));
323        return false;
324    }

第五步,针对多用户的情况,额外需要做符号链接。

326    // Mount user-specific symlink helper into place
327    userid_t user_id = multiuser_get_user_id(uid);
328    const String8 userSource(String8::format("/mnt/user/%d", user_id));
329    if (fs_prepare_dir(userSource.string(), 0751, 0, 0) == -1) {
330        return false;
331    }
332    if (TEMP_FAILURE_RETRY(mount(userSource.string(), "/storage/self",
333            NULL, MS_BIND, NULL)) == -1) {
334        ALOGW("Failed to mount %s to /storage/self: %s", userSource.string(), strerror(errno));
335        return false;
336    }
337
338    return true;
339}

mount_mode参数

我们开始看这两个参数,首先看mount_mode是从哪里获取的.
这往上一找,就是ActivityManagerService的startProcessLocked,我们摘录个片断看下:

...
3280            int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;
3281            if (!app.isolated) {
3282                int[] permGids = null;
3283                try {
...
3285                    final IPackageManager pm = AppGlobals.getPackageManager();
3286                    permGids = pm.getPackageGids(app.info.packageName, app.userId);
3287                    MountServiceInternal mountServiceInternal = LocalServices.getService(
3288                            MountServiceInternal.class);
3289                    mountExternal = mountServiceInternal.getExternalStorageMountMode(uid,
3290                            app.info.packageName);
3291                } catch (RemoteException e) {
3292                    throw e.rethrowAsRuntimeException();
3293                }
...

然后我们看getExternalStorageMountMode,定义在frameworks/base/services/core/java/com/android/server/MountService.java里,遍历所有的Policy,取其中最小的为最终结果。

3505        public int getExternalStorageMountMode(int uid, String packageName) {
3506            // No locking - CopyOnWriteArrayList
3507            int mountMode = Integer.MAX_VALUE;
3508            for (ExternalStorageMountPolicy policy : mPolicies) {
3509                final int policyMode = policy.getMountMode(uid, packageName);
3510                if (policyMode == Zygote.MOUNT_EXTERNAL_NONE) {
3511                    return Zygote.MOUNT_EXTERNAL_NONE;
3512                }
3513                mountMode = Math.min(mountMode, policyMode);
3514            }
3515            if (mountMode == Integer.MAX_VALUE) {
3516                return Zygote.MOUNT_EXTERNAL_NONE;
3517            }
3518            return mountMode;
3519        }

下面我们再看PM中是如何为policy赋值的,实现在frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java中,通过checkUidPermission的结果来决定policy的结果:

14731        MountServiceInternal mountServiceInternal = LocalServices.getService(
14732                MountServiceInternal.class);
14733        mountServiceInternal.addExternalStoragePolicy(
14734                new MountServiceInternal.ExternalStorageMountPolicy() {
14735            @Override
14736            public int getMountMode(int uid, String packageName) {
14737                if (Process.isIsolated(uid)) {
14738                    return Zygote.MOUNT_EXTERNAL_NONE;
14739                }
14740                if (checkUidPermission(WRITE_MEDIA_STORAGE, uid) == PERMISSION_GRANTED) {
14741                    return Zygote.MOUNT_EXTERNAL_DEFAULT;
14742                }
14743                if (checkUidPermission(READ_EXTERNAL_STORAGE, uid) == PERMISSION_DENIED) {
14744                    return Zygote.MOUNT_EXTERNAL_DEFAULT;
14745                }
14746                if (checkUidPermission(WRITE_EXTERNAL_STORAGE, uid) == PERMISSION_DENIED) {
14747                    return Zygote.MOUNT_EXTERNAL_READ;
14748                }
14749                return Zygote.MOUNT_EXTERNAL_WRITE;
14750            }
14751
14752            @Override
14753            public boolean hasExternalStorage(int uid, String packageName) {
14754                return true;
14755            }
14756        });
14757    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容