android开发分享Android存储系统-使用fuse来管理外置存储

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


背景

        android开发分享Android存储系统-使用fuse来管理外置存储不是主要简介fuse文件系统,android开发分享Android存储系统-使用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 = %dn", 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_setupn");             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_setupn");             exit(1);         }     }      /* Drop privs */     if (setgroups(sizeof(kGroups) / sizeof(kGroups[0]), kGroups) < 0) { // 取消特权组,只保留AID_PACKAGE_INFO组,用于读取/data/system/packages.list文件         ERROR("cannot setgroups: %sn", strerror(errno));         exit(1);     }     if (setgid(gid) < 0) { // 设置gid,保证拥有source_path的读写权限         ERROR("cannot setgid: %sn", strerror(errno));         exit(1);     }     if (setuid(uid) < 0) { // 设置gid,保证拥有source_path的读写权限         ERROR("cannot setuid: %sn", 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_createn");         exit(1);     }      watch_package_list(&global);     ERROR("terminated prematurelyn");     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: %sn", 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: %sn", 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=%dn", handler->token, errno);             continue;         }          if ((size_t)len < sizeof(struct fuse_in_header)) {             ERROR("[%d] request too short: len=%zun", 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=%un",                     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 %dn", 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): %sn", 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): %sn", 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  */  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: %sn", 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=%dn", 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: %sn", 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; retryingn");                     sleep(3);                     continue;                 } else {                     ERROR("inotify_add_watch failed: %sn", 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: %sn", 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: %sn", strerror(errno));             return;         }          while (res >= (int) sizeof(*event)) {             int event_size;             event = (struct inotify_event *) (event_buf + event_pos);              TRACE("inotify event: %08xn", 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: %sn", 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 packagesn",             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权限体系来实现。

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/addevelopment/894463.html

(0)
上一篇 2021年10月21日
下一篇 2021年10月21日

精彩推荐