背景
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文档 。 我们只简单介绍下:
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目录 | 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|下 {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