欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-03-21 22:23:13
本文不是主要简介fuse文件系统,本文主要是将Android如何使用fuse文件系统来做到灵活的权限管理。 Android对于外置存储的管理,需要实现以下几个目标:1、读写外置存储需要 android.permission.READ_EXTERNAL_STORAGE和android.permission.WRITE_EXTERNAL_STORAGE, 这两个权限是运行时权限,可以动态的授予和撤销, 所以主存储目...

背景

        本文不是主要简介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文档 。 我们只简单介绍下:
Android存储系统-使用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存储系统-使用fuse来管理外置存储

         如上图所示,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目录 umask gid Android目录权限 其他目录权限
thread_default /mnt/runtime/default/${label} 0006 AID_SDCARD_RW 771 770
thread_read /mnt/runtime/read/${label} 0027 AID_EVERYBODY 750 750
thread_write /mnt/runtime/write/${label} 0007 AID_EVERYBODY 770 770

        如上面表格所示,如果应该应用程序没有获得读写外置存储权限,那么它操作/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|下 packagenameuiduid{packagename}目录及其子目录uid设置成应用对应的uid。这样应用程序就可以对属于自己的{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权限体系来实现。

本文地址:https://blog.csdn.net/woai110120130/article/details/107778056