Android存储系统-使用fuse来管理外置存储

背景

        本文不是主要简介fuse文件系统,本文主要是将Android如何使用fuse文件系统来做到灵活的权限管理。 Android对于外置存储的管理,需要实现以下几个目标:

1、读写外置存储需要 android.permission.READ_EXTERNAL_STORAGE和android.permission.WRITE_EXTERNAL_STORAGE, 这两个权限是运行时权限,可以动态的授予和撤销, 所以主存储目录的权限管理需要动态支持。

2、主存储目录下的${userid}/Android/obb, ${userid}/Android/data, ${userid}/Android/media 下的应用程序包名目录不需要读写存储卡权限。比如 com.androiud.ss.ugc.aweme这个应用程序读写 ${userid}/Android/data/com.androiud.ss.ugc.aweme 目录是不需要申请权限的。
但是不同应用对应的${userid}/Android/data/${package} 目录是权限隔离的,不能相互访问的。

3、除${userid}/Android/obb/${package}目录外,相同应用程序(相同包名)不同用户( ${userid} )的目录也是权限隔离的。但是${userid}/Android/obb/${package}是跨用户( ${userid} ) 共享的。

         要实现上述目标,使用传统文件系统是比较困难的,要使用十分复杂的组管理才能达到目标。为了实现更灵活的权限管理能力,Android引入了fuse文件系统。 fuse文件系统是一个用户空间的文件系统,需要内核级别的支持。关于fuse文件系统的详细说明请参考Linux内核fuse文档 。 我们只简单介绍下:
fuse
         fuse的基本架构如图,Linux为了支持多种文件系统,添加了一个VFS层,通过VFS层进行数据结构抽象,来将文件操作请求转发给具体的文件系统,如常见的ext4,ntfs,fat文件系统等。fuse也是这些常见文件系统中的一种。 不像ext4,ntfs,fat等常见的文件系统,会直接将磁盘上的数据进行抽象组织提供给vfs层,供用户操作,fuse文件系统是将这些操作的请求转发给用户空间的fuse file-system deamon进程,由fuse file-system deamon进程来组织vfs需要的数据。 这样就大大的提高了文件系统的灵活性, 在FUSE file-system daemon中可以提供超级灵活的策略,来对权限,文件信息等数据进行组织。 举个例子,我们将/mnt/runtime/default/emulated目录挂载为fuse文件系统,当我们向/mnt/runtime/default/emulated目录写入数据的时候,我们可以将数据实际写入其他的file system,比如ext4 文件系统下的目录/data/media(前提是FUSE file-system deamon 进程有权限)。 所以fuse文件系统可以提供灵活的文件位置管理。权限管理也是fuse文件系统的一大优势,FUSE file-system deamon进程可以动态的给一个目录设置权限,构造文件权限信息返回给VFS做权限验证。 总之fuse文件系统最大的优势就是灵活。 要实现这套功能,我们可以想象, FUSE driver必然和FUSE file-system deamon 之间有一套通信协议,来处理VFS的文件操作请求。可以想象的一次文件写操作可能是下面这样的(还是以/mnt/runtime/default/emulated为例):

/mnt/runtime/default/emulated是一个fuse文件系统,Application要在/mnt/runtime/default/emulated目录下创建a.txt文件。 通过open系统调用请求到VFS, VFS将请求转发给FUSE driver, FUSE driver将请求转发给FUSE file-system daemon进程。 然后FUSE file-system daemon返回一个节点信息(/mnt/runtime/default/emulated目录的节点信息inode), 然后FUSE driver将信息给VFS层,VFS层做一系列的权限校验等操作,最终确认可以创建文件,然后将创建文件请求发送给FUSE driver, FUSE driver将请求发送给FUSE file-system daemon进程,FUSE file-system daemon在/data/media/ 创建a.txt文件,这个过程又会通过open调用VFS层,不过这次VFS层请求/data/media/ 目录对应的ext4文件系统来创建a.txt文件, 所以应用程序请求创建/mnt/runtime/default/emulated/a.txt文件,实际上创建的是/data/media/a.txt文件。假如FUSE file-system daemon进程对/data/media/a.txt看到的权限是777,它可以让应用程序看到的/mnt/runtime/default/emulated/a.txt的权限是711。假如FUSE file-system daemon看到的/data/media/a.txt的属主和属组是都是AID_SDCARD_RW,它可以让应用程序看到的/mnt/runtime/default/emulated/a.txt数组和属主都是AID_EVERYBODY。 文件的读写删除最终都是由FUSE file-system daemon来完成,不管实际要求操作的应用程序是否有权限,只要FUSE file-system daemon进程有权限即可完成相关操作,所以控制权都在FUSE file-system daemon进程手上。

Android 实现

         知道了fuse的灵活性之后,我们再来说明Android如何使用fuse来实现外置存储的管理目标。 下面我们拿使用/data分区来模拟主存储的情况进行说明。

1、为了实现动态的外置存储权限,Android会挂载三个目录到/dev/fuse来对应同一个外置存储,三个目录分别是/mnt/runtime/default/${label}, /mnt/runtime/read/${label},/mnt/runtime/write/${label}, 这三个目录代表不同的权限,当一个应用进程取得了读外置存储的权限,那么它将使用 /mnt/runtime/read/${label} 目录来操作外置存储,当一个应用程序取得了写外置存储的权限,那么它将使用/mnt/runtime/write/${label}目录来操作外置存储。当一个应用程序没有获取操作外置存储的权限,将使用/mnt/runtime/default/${label}目录来操作主存储。三个fuse目录最终都会作用于外置存储的media目录,只不过对目录下的可进行的操作权限是不同的。Android 的FUSE file-system daemon会根据应用程序进程使用的fuse目录来决定是否可以读写外置存储的media目录下的数据。

2、${userid}/Android/obb, ${userid}/Android/data, ${userid}/Android/media 下的${package} 权限管理则根据/data/system/packages.list文件中的内容来完成。 如果一个应用程序操作fuse目录,FUSE file-system daemon处理文件请求的时候可以获取操作文件的进程的uid,并根据/data/system/packages.list下的内容找到uid对应的包名,如果进程操作的报名和uid相对应,则允许操作,否则拒绝操作。

3、不同userid对应的相同应用的uid不同,根据 2中规则,即可实现 相同应用程序(相同包名)不同用户( ${userid} )的目录的权限隔离。

整体架构

         我们回过头来看下EmulatedVolume挂载时如何启动FUSE file-system daemon,关于EmulatedVolume相关知识请参考Android存储系统-MountService 和vold 对外置存储的管理(1)

system/vold/EmulatedVolume.cpp

static const char* kFusePath = "/system/bin/sdcard";

status_t EmulatedVolume::doMount() {
    // We could have migrated storage to an adopted private volume, so always
    // call primary storage "emulated" to avoid media rescans.
    std::string label = mLabel;
    if (getMountFlags() & MountFlags::kPrimary) {
        label = "emulated";
    }

    mFuseDefault = StringPrintf("/mnt/runtime/default/%s", label.c_str());
    mFuseRead = StringPrintf("/mnt/runtime/read/%s", label.c_str());
    mFuseWrite = StringPrintf("/mnt/runtime/write/%s", label.c_str());

    setInternalPath(mRawPath);
    setPath(StringPrintf("/storage/%s", label.c_str()));

    if (fs_prepare_dir(mFuseDefault.c_str(), 0700, AID_ROOT, AID_ROOT) ||
            fs_prepare_dir(mFuseRead.c_str(), 0700, AID_ROOT, AID_ROOT) ||
            fs_prepare_dir(mFuseWrite.c_str(), 0700, AID_ROOT, AID_ROOT)) {
        PLOG(ERROR) << getId() << " failed to create mount points";
        return -errno;
    }

    dev_t before = GetDevice(mFuseWrite);

    if (!(mFusePid = fork())) {
        if (execl(kFusePath, kFusePath,
                "-u", "1023", // AID_MEDIA_RW
                "-g", "1023", // AID_MEDIA_RW
                "-m",
                "-w",
                mRawPath.c_str(),
                label.c_str(),
                NULL)) {
            PLOG(ERROR) << "Failed to exec";
        }

        LOG(ERROR) << "FUSE exiting";
        _exit(1);
    }

    if (mFusePid == -1) {
        PLOG(ERROR) << getId() << " failed to fork";
        return -errno;
    }

    while (before == GetDevice(mFuseWrite)) {
        LOG(VERBOSE) << "Waiting for FUSE to spin up...";
        usleep(50000); // 50ms
    }

    return OK;
}

         根据代码我们可以得出启动FUSE file-system daemon 的命令为:

/system/bin/sdcard -u 1023 -g 1023 -m -w ${mRawPath} ${label}

        这里-u 参数指定uid 为1023 是media账号的uid, -g指定gid是media组的gid, -m表示支持多用户, -w表示全写模式, mRawPath就是外置存储的目标目录,比如/data/media 。 ${label} 用于区分fuse 目录,比如/dev/fuse 会挂载到/mnt/runtime/write/${label},用于 ${mRawPath} 的写访问路径。 知道这些我们就来看看sdcard的代码吧,它是FUSE file-system daemon 的实现。

         如上图所示,Android的sdcard进程中有四个线程, 其中main线程在创建了其他三个线程后调用watch_package_list()函数,来读取/data/system/packages_list.xml文件的内容。sdcard进程通过读取/data/system/packages_list.xml的内容,来维护程序包名和uid的关系,针对外置存储下 ${user_id}/Android 目录的动态权限做处理,基本思路就是当应用程序读写${user_id}/Android/media|obb|data/${package_name} 下文件的时候,把该目录下的文件属主都改成应用程序的uid,作为文件的属主基本上就获得了文件的读写权限,这样应用程序就可以不需要获取外置存储读写权限来读写${user_id}/Android/media|obb|data/${package_name} 下的文件了(因为应用程序就是属主)。

packages.list文件内容。

com.android.soundrecorder 10021 0 /data/data/com.android.soundrecorder
com.android.sdksetup 10020 0 /data/data/com.android.sdksetup
com.android.launcher 10005 0 /data/data/com.android.launcher
com.android.defcontainer 10009 0 /data/data/com.android.defcontainer
com.android.smoketest 10044 0 /data/data/com.android.smoketest

        其他三个线程,每个线程负责一个fuse目录的读写请求。thread_default线程负责/mnt/runtime/default/${label} 目录的读写,thread_read 负责 /mnt/runtime/read/${label}目录的读写, thread_write负责 /mnt/runtime/read/${label}目录的读写,三个线程都是接收fuse driver的文件操作请求,然后将请求转化为对source_path 文件夹的读写。 不同点就是三个线程默认的umask和gid不同。 这里我列了一张表格,如下

表格1-1

线程管理的fuse目录umaskgidAndroid目录权限其他目录权限
thread_default/mnt/runtime/default/${label}0006AID_SDCARD_RW771770
thread_read/mnt/runtime/read/${label}0027AID_EVERYBODY750750
thread_write/mnt/runtime/write/${label}0007AID_EVERYBODY770770

        如上面表格所示,如果应该应用程序没有获得读写外置存储权限,那么它操作/mnt/runtime/default/${label}文件夹下的文件,经由 thread_default线程处理。thread_default线程会将/mnt/runtime/default/${label}/${user_id}/${Android} 下的所有文件权限设置为 771,也就是属主和属组可读写执行,其他用户可执行。/mnt/runtime/default/${label}/${user_id}/下的其他文件权限设置成770,表示其他组不可执行。另外thread_default线程会将/mnt/runtime/default/${label}下的所有文件属组映射成AID_SDCARD_RW。 一般的应用程序不属于AID_SDCARD_RW组,所以无法进行读写,但是属于AID_SDCARD_RW组的特权程序还是可以读写的。

        如果一个应用程序获取了读外置存储权限,而没有获取写外置存储权限,则它看到的目录是/mnt/runtime/read/${label}目录, 经由 thread_read线程处理。thread_read线程会将/mnt/runtime/read/${label}/${user_id}/下的所有文件权限设置为 750,也就是属主可读写执行,属组可读可执行,其他组无任何权限。另外thread_read线程会将/mnt/runtime/default/${label}下的所有文件属主映射成AID_EVERYBODY。 一般的应用程序属于AID_EVERYBODY组,所以可以进行读操作,对于${user_id}/Android/media|obb|data/${package_name} 目录,由于属主就是应用程序本身的uid,所以读写执行都可以。

        如果一个应用程序获取了读权限,而没有获取写外置存储权限,则它看到的目录是/mnt/runtime/write/${label}目录, 经由 thread_write线程处理。thread_write线程会将/mnt/runtime/write/${label}/${user_id}/下的所有文件权限设置为770,也就是属主属组可读写执行,其他组无任何权限。另外thread_write线程会将/mnt/runtime/default/${label}下的所有文件属主映射成AID_EVERYBODY。 一般的应用程序属于AID_EVERYBODY组,所以可以进行读写执行操作。

        上面介绍了sdcard卡对外置存储权限管理的设计思路,下面我们顺着代码来看一遍。

代码分析

system/core/sdcard/sdcard.c

int main(int argc, char **argv) {
    ......
    rlim.rlim_cur = 8192;
    rlim.rlim_max = 8192;
    if (setrlimit(RLIMIT_NOFILE, &rlim)) {
        ERROR("Error setting RLIMIT_NOFILE, errno = %d\n", errno);
    }

   .......
    run(source_path, label, uid, gid, userid, multi_user, full_write);
}

         上述代码省略了参数解析, 最终调用run函数, run函数的参数和我们调用sdcard程序传参含义一样。 source_path为外置存储上的路径。

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; //用于记录全局信息,主要是fuse路径和 source_path(外置存储的关系)
    struct fuse fuse_default; // 没有读写权限的fuse 信息
    struct fuse fuse_read; // 读权限的fuse信息
    struct fuse fuse_write; // 写权限的fuse信息
    struct fuse_handler handler_default;    
    struct fuse_handler handler_read;
    struct fuse_handler handler_write;
    pthread_t thread_default; //无权限fuse工作线程。
    pthread_t thread_read; //读权限fuse工作线程。
    pthread_t thread_write; // 写权限fuse工作线程。

    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); // 保存包名和uid关系的hash map,用于${source_path}/Android目录权限管理
    global.uid = uid;  //fuse 根目录属主
    global.gid = gid; // fuse 根目录属组
    global.multi_user = multi_user; // 是否支持多用户
    global.next_generation = 0; 
    global.inode_ctr = 1; // inode 号

    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; // 创建者的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; //支持多用户设置根目录权限为PERM_PRE_ROOT
        snprintf(global.obb_path, sizeof(global.obb_path), "%s/obb", source_path);
    } else {
        global.root.perm = PERM_ROOT; //不支持多用户设置根目录权限为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;

    1. 设置挂载路径
    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) { // 取消特权组,只保留AID_PACKAGE_INFO组,用于读取/data/system/packages.list文件
        ERROR("cannot setgroups: %s\n", strerror(errno));
        exit(1);
    }
    if (setgid(gid) < 0) { // 设置gid,保证拥有source_path的读写权限
        ERROR("cannot setgid: %s\n", strerror(errno));
        exit(1);
    }
    if (setuid(uid) < 0) { // 设置gid,保证拥有source_path的读写权限
        ERROR("cannot setuid: %s\n", strerror(errno));
        exit(1);
    }

    if (multi_user) { // 创建obb共享文件夹。
        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)) { // 启动三个线程用于处理三个fuse目录的文件操作请求
        ERROR("failed to pthread_create\n");
        exit(1);
    }

    watch_package_list(&global);
    ERROR("terminated prematurely\n");
    exit(1);
}

         run函数准备三个fuse目录,分别是/mnt/runtime/default/${lable},/mnt/runtime/read/${lable}. /mnt/runtime/default/${lable},然后通过fuse_setup进行挂载。 最后创建三个线程用于处理三个目录的读写请求。 我们先来看下fuse的挂载。

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;
}

         挂载使用mount函数来进行, 挂载参数为fd=%i,rootmode=40000,default_permissions,allow_other,user_id=%d,group_id=%d。这里一个至关重要的参数fuse->fd是通过打开/dev/fuse得到的,它的作用类似一个上下文,绑定该目录到这个文件描述后,以后fuse 内核驱动上来的文件操作请求都会写入fuse->fd, 只要读取fuse->fd就能获得fuse 内核驱动上来的文件操作请求。 另外还值得注意的一点,fuse_setup的第二个参数为gid,对于 /mnt/runtime/default/${lable} 目录设置的gid为AID_SDCARD_RW,一般应用程序不属于该组。/mnt/runtime/read/${lable} 和/mnt/runtime/write/${lable}目录设置的gid为AID_EVERYBODY,所有的应用程序都属于该组。 fuse服务线程调用的函数为start_handler,用于处理文件操作请求。参数fuse_handler用于保存当前线程处理的fuse的上下文信息。

static void* start_handler(void* data)
{
    struct fuse_handler* handler = data;
    handle_fuse_requests(handler);
    return NULL;
}

static void handle_fuse_requests(struct fuse_handler* handler)
{
    struct fuse* fuse = handler->fuse;
    for (;;) {
        // 1 监听 fuse driver 发上来的请求,写入handler->request_buffer
        ssize_t len = TEMP_FAILURE_RETRY(read(fuse->fd,
                handler->request_buffer, sizeof(handler->request_buffer)));
        if (len < 0) {
            if (errno == ENODEV) {
                ERROR("[%d] someone stole our marbles!\n", handler->token);
                exit(2);
            }
            ERROR("[%d] handle_fuse_requests: errno=%d\n", handler->token, errno);
            continue;
        }

        if ((size_t)len < sizeof(struct fuse_in_header)) {
            ERROR("[%d] request too short: len=%zu\n", handler->token, (size_t)len);
            continue;
        }
        // 2 将消息转成fuse_in_header + data 两部分
        const struct fuse_in_header *hdr = (void*)handler->request_buffer;
        if (hdr->len != (size_t)len) {
            ERROR("[%d] malformed header: len=%zu, hdr->len=%u\n",
                    handler->token, (size_t)len, hdr->len);
            continue;
        }
        
        const void *data = handler->request_buffer + sizeof(struct fuse_in_header);
        size_t data_len = len - sizeof(struct fuse_in_header);
        __u64 unique = hdr->unique;

        // 3 处理请求
        int res = handle_fuse_request(fuse, handler, hdr, data, data_len);

        /* We do not access the request again after this point because the underlying
         * buffer storage may have been reused while processing the request. */

        if (res != NO_STATUS) {
            if (res) {
                TRACE("[%d] ERROR %d\n", handler->token, res);
            }
            fuse_status(fuse, unique, res);
        }
    }
}

        start_handler 调用handle_fuse_requests来接收处理fuse driver送上来的请求, handle_fuse_requests 通过打开的/dev/fuse文件描述符来读取fuse driver送上来的请求, 然后将请求转换成fuse_in_header + data 两部分,交给handle_fuse_request函数还实际处理。

static int handle_fuse_request(struct fuse *fuse, struct fuse_handler* handler,
        const struct fuse_in_header *hdr, const void *data, size_t data_len) {
    switch (hdr->opcode) {
    case FUSE_LOOKUP: { /* bytez[] -> entry_out */
        const char* name = data;
        return handle_lookup(fuse, handler, hdr, name);
    }
    .......
    case FUSE_OPEN: { /* open_in -> open_out */
        const struct fuse_open_in *req = data;
        return handle_open(fuse, handler, hdr, req);
    }

    case FUSE_READ: { /* read_in -> byte[] */
        const struct fuse_read_in *req = data;
        return handle_read(fuse, handler, hdr, req);
    }
    .......
    case FUSE_MKDIR: { /* mkdir_in, bytez[] -> entry_out */
        const struct fuse_mkdir_in *req = data;
        const char *name = ((const char*) data) + sizeof(*req);
        return handle_mkdir(fuse, handler, hdr, req, name);
    }
    ......
    default: {
        TRACE("[%d] NOTIMPL op=%d uniq=%"PRIx64" nid=%"PRIx64"\n",
                handler->token, hdr->opcode, hdr->unique, hdr->nodeid);
        return -ENOSYS;
    }
}

        handle_fuse_request 处理的消息类型比较多,这里只介FUSE_MKDIR消息,用于创建文件夹。

         我们先来看看对创建文件夹请求的处理。

static int handle_mkdir(struct fuse* fuse, struct fuse_handler* handler,
        const struct fuse_in_header* hdr, const struct fuse_mkdir_in* req, const char* name)
{
    struct node* parent_node;
    char parent_path[PATH_MAX];
    char child_path[PATH_MAX];
    const char* actual_name;

    pthread_mutex_lock(&fuse->global->lock);
    
    // 1 找到父节点(parent_path为对应真实要操作目录, source_path路径)
    parent_node = lookup_node_and_path_by_id_locked(fuse, hdr->nodeid,
            parent_path, sizeof(parent_path));
    TRACE("[%d] MKDIR %s 0%o @ %"PRIx64" (%s)\n", handler->token,
            name, req->mode, hdr->nodeid, parent_node ? parent_node->name : "?");
    pthread_mutex_unlock(&fuse->global->lock);

    // 2 父节点不存在或者要创建的文件路径太长 返回-ENOENT
    if (!parent_node || !(actual_name = find_file_within(parent_path, name,
            child_path, sizeof(child_path), 1))) {
        return -ENOENT;
    }
    // 3 进一步权限检查
    if (!check_caller_access_to_name(fuse, hdr, parent_node, name, W_OK)) {
        return -EACCES;
    }
   // 4 真正创建文件夹
    __u32 mode = (req->mode & (~0777)) | 0775;
    if (mkdir(child_path, mode) < 0) {
        return -errno;
    }

    // 5 如果新建目录是 Android/data 和 /Android/obb 目录,添加.nomedia文件,禁止media扫描
    /* When creating /Android/data and /Android/obb, mark them as .nomedia */
    if (parent_node->perm == PERM_ANDROID && !strcasecmp(name, "data")) {
        char nomedia[PATH_MAX];
        snprintf(nomedia, PATH_MAX, "%s/.nomedia", child_path);
        if (touch(nomedia, 0664) != 0) {
            ERROR("Failed to touch(%s): %s\n", nomedia, strerror(errno));fuse_setup
            return -ENOENT;
        }
    }
    if (parent_node->perm == PERM_ANDROID && !strcasecmp(name, "obb")) {
        char nomedia[PATH_MAX];
        snprintf(nomedia, PATH_MAX, "%s/.nomedia", fuse->global->obb_path);
        if (touch(nomedia, 0664) != 0) {
            ERROR("Failed to touch(%s): %s\n", nomedia, strerror(errno));
            return -ENOENT;
        }
    }
    // 6 构造回包
    return fuse_reply_entry(fuse, hdr->unique, parent_node, name, actual_name, child_path);
}

         handle_mkdir 创建文件夹分为6步:
1、 lookup_node_and_path_by_id_locked函数: 找到要创建的文件夹父节点,也就是source_path对应的目录节点。
2、 父节点不存在或者要创建的文件路径太长 返回-ENOENT。
3、check_caller_access_to_name 进一步检查权限。
4、使用mkdir 在source_path 目录下真正创建目录。
5、如果创建的目录是/Android/obb 或者/Android/data文件夹,创建.nomedia文件,禁止media provider扫描。
6、 fuse_reply_entry构造回包。

         我们按照上面6步往下看

static struct node* lookup_node_and_path_by_id_locked(struct fuse* fuse, __u64 nid,
       char* buf, size_t bufsize)
{
   struct node* node = lookup_node_by_id_locked(fuse, nid);
   if (node && get_node_path_locked(node, buf, bufsize) < 0) {
       node = NULL;
   }
   return node;
}

static struct node *lookup_node_by_id_locked(struct fuse *fuse, __u64 nid)
{
   if (nid == FUSE_ROOT_ID) {
       return &fuse->global->root;
   } else {
       return id_to_ptr(nid);
   }
}

static inline void *id_to_ptr(__u64 nid)
{
   return (void *) (uintptr_t) nid;
}

         lookup_node_and_path_by_id_locked 函数有限调用lookup_node_by_id_locked函数根据nid获取node节点,也就是lookup_node_by_id_locked函数所做的工作, lookup_node_by_id_locked函数会判断如果nid是FUSE_ROOT_ID,对应的node节点就是fuse->global->root,也即是source_path根目录对应的节点。 如果nid不是FUSE_ROOT_ID,那么他就是一个node指针,通过id_to_ptr函数进行强制转换为node数据结构。 get_node_path_locked函数则根据node结构找到对应的source_path 路径。

 315 /* Gets the absolute path to a node into the provided buffer.
 316  *
 317  * Populates 'buf' with the path and returns the length of the path on success,
 318  * or returns -1 if the path is too long for the provided buffer.
 319  */
 320 static ssize_t get_node_path_locked(struct node* node, char* buf, size_t bufsize) {
 321     const char* name;
 322     size_t namelen;
 323     if (node->graft_path) {
 324         name = node->graft_path;
 325         namelen = node->graft_pathlen;
 326     } else if (node->actual_name) {
 327         name = node->actual_name;
 328         namelen = node->namelen;
 329     } else {
 330         name = node->name;
 331         namelen = node->namelen;
 332     }
 333 
 334     if (bufsize < namelen + 1) {
 335         return -1;
 336     }
 337 
 338     ssize_t pathlen = 0;
 339     if (node->parent && node->graft_path == NULL) {
 340         pathlen = get_node_path_locked(node->parent, buf, bufsize - namelen - 1);
 341         if (pathlen < 0) {
 342             return -1;
 343         }
 344         buf[pathlen++] = '/';
 345     }
 346 
 347     memcpy(buf + pathlen, name, namelen + 1); /* include trailing \0 */
 348     return pathlen + namelen;
 349 }                                                                                     

         通过node获取真实路径的过程主要是从node->graft_path 或者node->actual_name 或者node->name选择一个作为路径。 在sdcard的run函数中有一句话 global.root.name = strdup(source_path); ,所以如果是root节点就是对应的目录就是source_path,其他节点将对应root节点下的子目录树下的节点。node->graft_path 主要是为了实现跨用户共享目录,也就是Android/obb目录的特点,后面讲obb目录的时候再来详细说明。 actual_name则是因为Android希望外置存储不区分大小写,但是source_path可能是一个支持大小写的文件系统。所以这里name为一个用户传过来的节点名称,而node->actual_name代表source_path上的文件的真实名称。 339行-347行,主要用于将找到绝对路径,所以顺着node节点向上寻找,来填充路径,由于node->graft_path 本上就是一个绝对路径,所以不需要向上寻找。

         找到父节点之后就是判断父节点的可用性,这里有一个find_file_within函数,用于判断父节点是否可用:

static char* find_file_within(const char* path, const char* name,
        char* buf, size_t bufsize, int search)
{
    size_t pathlen = strlen(path);
    size_t namelen = strlen(name);
    size_t childlen = pathlen + namelen + 1;
    char* actual;

    if (bufsize <= childlen) {
        return NULL;
    }

    memcpy(buf, path, pathlen);
    buf[pathlen] = '/';
    actual = buf + pathlen + 1;
    memcpy(actual, name, namelen + 1);

    if (search && access(buf, F_OK)) {
        struct dirent* entry;
        DIR* dir = opendir(path);
        if (!dir) {
            ERROR("opendir %s failed: %s\n", path, strerror(errno));
            return actual;
        }
        while ((entry = readdir(dir))) {
            if (!strcasecmp(entry->d_name, name)) {
                /* we have a match - replace the name, don't need to copy the null again */
                memcpy(actual, entry->d_name, namelen);
                break;
            }
        }
        closedir(dir);
    }
    return actual;
}

         这里首先判断文件要创建的文件路径长度是否合法,不合法返回NULL, 如果合法,在来看下能否找到actual_name。actual_name作为返回值返回。

/* Kernel has already enforced everything we returned through
 * derive_permissions_locked(), so this is used to lock down access
 * even further, such as enforcing that apps hold sdcard_rw. */
static bool check_caller_access_to_name(struct fuse* fuse,
        const struct fuse_in_header *hdr, const struct node* parent_node,
        const char* name, int mode) {
    /* Always block security-sensitive files at root */
    if (parent_node && parent_node->perm == PERM_ROOT) {
        if (!strcasecmp(name, "autorun.inf")
                || !strcasecmp(name, ".android_secure")
                || !strcasecmp(name, "android_secure")) {
            return false;
        }
    }

    /* Root always has access; access for any other UIDs should always
     * be controlled through packages.list. */
    if (hdr->uid == 0) {
        return true;
    }

    /* No extra permissions to enforce */
    return true;
}

        check_caller_access_to_name这一步主要判断是否要操作autorun.inf, .android_secure, android_secure这三个文件, 由于这三个文件比较敏感,禁止任何程程序通过fuse去读写。 其他的一律放行。最后比较值得看的函数就是 fuse_reply_entry构造回包函数了。

static int fuse_reply_entry(struct fuse* fuse, __u64 unique,
        struct node* parent, const char* name, const char* actual_name,
        const char* path)
{
    struct node* node;
    struct fuse_entry_out out;
    struct stat s;

    if (lstat(path, &s) < 0) {
        return -errno;
    }

    pthread_mutex_lock(&fuse->global->lock);
    //1 创建新节点
    node = acquire_or_create_child_locked(fuse, parent, name, actual_name);
    if (!node) {
        pthread_mutex_unlock(&fuse->global->lock);
        return -ENOMEM;
    }
    memset(&out, 0, sizeof(out));
    // 2 设置新节点属性
    attr_from_stat(fuse, &out.attr, &s, node);
    out.attr_valid = 10;
    out.entry_valid = 10;
    out.nodeid = node->nid;
    out.generation = node->gen;
    pthread_mutex_unlock(&fuse->global->lock);
    // 3 返回数据给fuse driver
    fuse_reply(fuse, unique, &out, sizeof(out));
    return NO_STATUS;
}

        fuse_reply_entry 函数也是分三步来构造请求:
1、 acquire_or_create_child_locked 为新创建的目录构造node节点。
2、attr_from_stat 设置属性节点。
3、构造回包。

        我们详细看下:

static struct node* acquire_or_create_child_locked(
        struct fuse* fuse, struct node* parent,
        const char* name, const char* actual_name)
{
    struct node* child = lookup_child_by_name_locked(parent, name);
    if (child) {
        acquire_node_locked(child);
    } else {
        child = create_node_locked(fuse, parent, name, actual_name);
    }
    return child;
}

static void acquire_node_locked(struct node* node)
{
    node->refcount++;
    TRACE("ACQUIRE %p (%s) rc=%d\n", node, node->name, node->refcount);
}

        如果node已经存在,引用计数加1, 不存在则调用create_node_locked来创建。

struct node *create_node_locked(struct fuse* fuse,
        struct node *parent, const char *name, const char* actual_name)
{
    struct node *node;
    size_t namelen = strlen(name);

    // Detect overflows in the inode counter. "4 billion nodes should be enough
    // for everybody".
    if (fuse->global->inode_ctr == 0) { // inode number溢出,返回NULL
        ERROR("No more inode numbers available");
        return NULL;
    }

    node = calloc(1, sizeof(struct node)); // 申请node内存
    if (!node) {
        return NULL;
    }
    node->name = malloc(namelen + 1); // 设置名字
    if (!node->name) {
        free(node);
        return NULL;
    }
    memcpy(node->name, name, namelen + 1);
    if (strcmp(name, actual_name)) { // 设置 actual
        node->actual_name = malloc(namelen + 1);
        if (!node->actual_name) {
            free(node->name);
            free(node);
            return NULL;
        }
        memcpy(node->actual_name, actual_name, namelen + 1);
    }
    node->namelen = namelen;
    node->nid = ptr_to_id(node); // 将指针强转为nid
    node->ino = fuse->global->inode_ctr++; //分配inode号
    node->gen = fuse->global->next_generation++;

    node->deleted = false;

    derive_permissions_locked(fuse, parent, node); // 设置权限
    acquire_node_locked(node); //引用计数加1
    add_node_to_parent_locked(node, parent); // 设置父子关系。
    return node;
}

        node的创建也比较简单,基本都当注释写在代码上了,这其中有derive_permissions_locked函数用于设置node节点的权限,是Android fuse daemon权限管理的核心。分析derive_permissions_locked函数之前我们先来看下另外一个函数, 在sdcard启动过程中run函数最后调用了 watch_package_list(&global)函数,我们先来分析这个函数。

static const char* const kPackagesListFile = "/data/system/packages.list";

static void watch_package_list(struct fuse_global* global) {
    struct inotify_event *event;
    char event_buf[512];

    int nfd = inotify_init();
    if (nfd < 0) {
        ERROR("inotify_init failed: %s\n", strerror(errno));
        return;
    }

    bool active = false;
    while (1) {
        if (!active) {  
            // 使用inotify观察/data/system/packages.list文件删除事件,当安装或者卸载应用后,该文件就会重建。
            int res = inotify_add_watch(nfd, kPackagesListFile, IN_DELETE_SELF);
            if (res == -1) {
                if (errno == ENOENT || errno == EACCES) {
                    /* Framework may not have created yet, sleep and retry */
                    ERROR("missing packages.list; retrying\n");
                    sleep(3);
                    continue;
                } else {
                    ERROR("inotify_add_watch failed: %s\n", strerror(errno));
                    return;
                }
            }

            /* Watch above will tell us about any future changes, so
             * read the current state. */
            if (read_package_list(global) == -1) { // 读取新的/data/system/packages.list文件
                ERROR("read_package_list failed: %s\n", strerror(errno));
                return;
            }
            active = true;
        }

        int event_pos = 0;
        int res = read(nfd, event_buf, sizeof(event_buf));
        if (res < (int) sizeof(*event)) {
            if (errno == EINTR)
                continue;
            ERROR("failed to read inotify event: %s\n", strerror(errno));
            return;
        }

        while (res >= (int) sizeof(*event)) {
            int event_size;
            event = (struct inotify_event *) (event_buf + event_pos);

            TRACE("inotify event: %08x\n", event->mask);
            if ((event->mask & IN_IGNORED) == IN_IGNORED) {
                /* Previously watched file was deleted, probably due to move
                 * that swapped in new data; re-arm the watch and read. */
                active = false;
            }

            event_size = sizeof(*event) + event->len;
            res -= event_size;
            event_pos += event_size;
        }
    }
}

        这个函数虽然很长,但是核心逻辑就是通过inotify机制观察/data/system/packages.list文件变化,当文件变化后进行读取。大部分逻辑都是处理inotify的,最主要的是read_package_list函数。

static int read_package_list(struct fuse_global* global) {
    pthread_mutex_lock(&global->lock);
    // 1 删除global->package_to_appid这个hashmap中的所有项目,这个map中存储了报名和uid的对应关心
    hashmapForEach(global->package_to_appid, remove_str_to_int, global->package_to_appid);

    // 2 打开/data/system/packages.list文件
    FILE* file = fopen(kPackagesListFile, "r");
    if (!file) {
        ERROR("failed to open package list: %s\n", strerror(errno));
        pthread_mutex_unlock(&global->lock);
        return -1;
    }

    char buf[512];
    while (fgets(buf, sizeof(buf), file) != NULL) {
        char package_name[512];
        int appid;
        char gids[512];

        if (sscanf(buf, "%s %d %*d %*s %*s %s", package_name, &appid, gids) == 3) {
            char* package_name_dup = strdup(package_name); // 3 解析后添加到hashmap
            hashmapPut(global->package_to_appid, package_name_dup, (void*) (uintptr_t) appid);
        }
    }

    TRACE("read_package_list: found %zu packages\n",
            hashmapSize(global->package_to_appid));
    fclose(file);

    // 4 更新权限
    /* Regenerate ownership details using newly loaded mapping */
    derive_permissions_recursive_locked(global->fuse_default, &global->root);

    pthread_mutex_unlock(&global->lock);
    return 0;
}

        read_package_list 函数读取packages.list文件,然后把package_name 和uid的关系保存在global->package_to_appid这个hashmap中,最后调用derive_permissions_recursive_locked 来更新已创建的节点权限(以打开的文件权限)。

static void derive_permissions_recursive_locked(struct fuse* fuse, struct node *parent) {
    struct node *node;
    for (node = parent->child; node; node = node->next) {
        derive_permissions_locked(fuse, parent, node);
        if (node->child) {
            derive_permissions_recursive_locked(fuse, node);
        }
    }
}

       derive_permissions_recursive_locked目录就是递归的遍历打开的节点,然后调用derive_permissions_locked来更新节点的权限,这里我们又回到了derive_permissions_locked函数,derive_permissions_locked的函数主要的目的就是将${user_id}/Android/data|media|obb|/${packagename}目录的及其子目录的属主设置为 ${packagename}应用程序的uid,这样它就不需要申请读写外置存储权限来进行读写操作了。

static void derive_permissions_locked(struct fuse* fuse, struct node *parent,
        struct node *node) {
    appid_t appid;

    /* By default, each node inherits from its parent */
    node->perm = PERM_INHERIT; // 权限默认集成父目录
    node->userid = parent->userid; // userid默认继承
    node->uid = parent->uid; //uid默认继承。root节点的uid为 AID_SDCARD_RW
    node->under_android = parent->under_android; // under_android默认集成

    /* Derive custom permissions based on parent and current node */
    switch (parent->perm) {
    case PERM_INHERIT: // 继承父目录的权限
        /* Already inherited above */
        break;
    case PERM_PRE_ROOT:  // 根目录下的文件节点权限设置为PERM_ROOT ( 一般为\${userid} 目录)
        /* Legacy internal layout places users at top level */
        node->perm = PERM_ROOT;
        node->userid = strtoul(node->name, NULL, 10);
        break;
    case PERM_ROOT: // \${userid} 目录下的文件权限
        /* Assume masked off by default. */
        if (!strcasecmp(node->name, "Android")) {  // 如果是Android目录,设置权限为PERM_ANDROID,并设置node->under_android = true,表示是Android目录,或者Android目录下的子目录
            /* App-specific directories inside; let anyone traverse */
            node->perm = PERM_ANDROID;
            node->under_android = true;
        } // 其他目录继承父目录权限
        break;
    case PERM_ANDROID: // Android目录下
        if (!strcasecmp(node->name, "data")) { // data目录下的目录权限为 PERM_ANDROID_DATA
            /* App-specific directories inside; let anyone traverse */
            node->perm = PERM_ANDROID_DATA;
        } else if (!strcasecmp(node->name, "obb")) {  // obb目录下的目录权限为 PERM_ANDROID_OBB
            /* App-specific directories inside; let anyone traverse */
            node->perm = PERM_ANDROID_OBB;
            /* Single OBB directory is always shared */
            // obb目录下的目录权限为 graft_path有值且为绝对路径,可以参考下run里面对 fuse->global->obb_path的设置
            node->graft_path = fuse->global->obb_path;
            node->graft_pathlen = strlen(fuse->global->obb_path);
        } else if (!strcasecmp(node->name, "media")) { // media目录权限为PERM_ANDROID_MEDIA
            /* App-specific directories inside; let anyone traverse */
            node->perm = PERM_ANDROID_MEDIA;
        }
        break;
    case PERM_ANDROID_DATA:
    case PERM_ANDROID_OBB:
    case PERM_ANDROID_MEDIA: 
         // ${user_id}/Android/data|media|obb|下 ${packagename}目录的下文件的uid都设置成应用的uid 
        appid = (appid_t) (uintptr_t) hashmapGet(fuse->global->package_to_appid, node->name);
        if (appid != 0) {
            node->uid = multiuser_get_uid(parent->userid, appid);
        }
        break;
    }
}

       derive_permissions_locked函数分析通过注释写到了代码中,主要实现的功能就是 ${user_id}/Android/data|media|obb|下 p a c k a g e n a m e 目 录 及 其 子 目 录 u i d 设 置 成 应 用 对 应 的 u i d 。 这 样 应 用 程 序 就 可 以 对 属 于 自 己 的 {packagename}目录及其子目录uid设置成应用对应的uid。这样应用程序就可以对属于自己的 packagenameuiduid{user_id}/Android/data|media|obb|下 ${packagename}进行随意的操作了。

       我们回过头来再来看attr_from_stat函数。该函数主要实现 表格1-1中 的权限管理

static void attr_from_stat(struct fuse* fuse, struct fuse_attr *attr,
        const struct stat *s, const struct node* node) {
    // 1 基本属性从 文件的stat中获取
    attr->ino = node->ino;
    attr->size = s->st_size;
    attr->blocks = s->st_blocks;
    attr->atime = s->st_atim.tv_sec;
    attr->mtime = s->st_mtim.tv_sec;
    attr->ctime = s->st_ctim.tv_sec;
    attr->atimensec = s->st_atim.tv_nsec;
    attr->mtimensec = s->st_mtim.tv_nsec;
    attr->ctimensec = s->st_ctim.tv_nsec;
    attr->mode = s->st_mode;
    attr->nlink = s->st_nlink;

    attr->uid = node->uid;  // uid设置为node的uid,这个uid在derive_permissions_locked函数中设置。

    if (fuse->gid == AID_SDCARD_RW) {
        /* As an optimization, certain trusted system components only run
         * as owner but operate across all users. Since we're now handing
         * out the sdcard_rw GID only to trusted apps, we're okay relaxing
         * the user boundary enforcement for the default view. The UIDs
         * assigned to app directories are still multiuser aware. */
        attr->gid = AID_SDCARD_RW;
    } else {
        attr->gid = multiuser_get_uid(node->userid, fuse->gid);
    }

    int visible_mode = 0775 & ~fuse->mask;
    if (node->perm == PERM_PRE_ROOT) { //多用户模式下根目录设置为711权限模式,也就是属主可读写(也就是只有该用户可读写)
        /* Top of multi-user view should always be visible to ensure
         * secondary users can traverse inside. */
        visible_mode = 0711;
    } else if (node->under_android) { // Android 目录禁止其他用户读权
        /* Block "other" access to Android directories, since only apps
         * belonging to a specific user should be in there; we still
         * leave +x open for the default view. */
        if (fuse->gid == AID_SDCARD_RW) {
            visible_mode = visible_mode & ~0006;
        } else {
            visible_mode = visible_mode & ~0007;
        }
    }
    int owner_mode = s->st_mode & 0700; // 最终还要取实际文件的属主权限来做位与,因为sdcard程序的属主如果不能操作source_path对应的文件, 文件的读写操作也是无法完成的。
    int filtered_mode = visible_mode & (owner_mode | (owner_mode >> 3) | (owner_mode >> 6));
    attr->mode = (attr->mode & S_IFMT) | filtered_mode;
}

       attr_from_stat函数的逻辑读者对照 表格1-1 来分析下相信就会明白。我也在上面关键的地方写了注释。

总结

        Android使用fuse来作为Android的位置存储管理系统,主要通过灵活的gid,uid以及rwx权限体系来实现。

  • 5
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值