本文只是记录了早期sd卡访问权限的一些分析,以及当时sdcardfs的一些状态,部分细节未做深入分析。

内置SD卡:是指我们用户文件系统一个目录,是呈现给用户可使用的一个空间,也称为内置SD卡,当然这里面有虚拟层面的意义。所以也叫emulated sdcard

外置SD卡:就是我们平常见到的TF卡,可插拔,用于扩展手机ROM空间。

sdcardfs是最初由三星开发的用于取代android的用户态fuse文件系统,来提高使用性能

先摆出两个问题

1. 问题

问题1:第三方APP是否可以随意读写内置sd卡

问题2:第三方APP是否可以随意读写外置sd卡

2. 测试方法

做一个简单app来测试以上两个问题,代码如下:

File file = new File("/mnt/shell/emulated/0", "abc.txt"); //用于在内置SD卡创建一个新文件adc.txt
//File file = new File("/storage/extSdCard", "abc.txt");  //用于在外置SD卡创建一个新文件abc.txt
try {
	file.createNewFile();
} catch (IOException e) {
	// TODO Auto-generated catch block
Log.d("test", e.getMessage()); //显示出错的log
	e.printStackTrace();
}

测试结果分为两部分
内置SD卡:
03-01  05:51:22.837  D  9009  9009  test:  open failed: EACCES (Permission denied)
外置SD卡:
03-01  05:51:42.277  D  9009  9009  test:  open failed: EACCES (Permission denied)
非常遗憾,两者情况系统都提示错误,原因都是没有访问权限。
所以对于上面的问题,第三方APP并不可以随意的写内置SD卡,同时也不可以随意写外置SD卡。

3. 为什么不可以写内置sd卡?

内置sd卡的挂载点情况

/dev/block/bootdevice/by-name/userdata /data ext4 rw,
/data/media /mnt/shell/emulated sdcardfs rw,seclabel,nosuid,nodev,relatime,uid=1023,gid=1023,derive=legacy,reserved=20MB 0 0

查看挂载点的文件权限情况。可以看到所有文件所属用户都是root,所属组都是sdcard_r,拥有权限都是rwx

root@a7ltectc:/mnt/shell/emulated/0 # ls -l
drwxrwx--- root     sdcard_r          2015-01-01 08:06 Alarms
drwxrwx--x root     sdcard_r          2015-01-01 08:06 Android
drwxrwx--- root     sdcard_r          2015-02-11 10:00 DCIM
drwxrwx--- root     sdcard_r          2015-01-01 08:06 Download
drwxrwx--- root     sdcard_r          2015-01-01 08:06 Movies
drwxrwx--- root     sdcard_r          2015-01-01 08:06 Music
drwxrwx--- root     sdcard_r          2015-01-01 08:06 Notifications
drwxrwx--- root     sdcard_r          2015-01-01 08:06 Pictures
drwxrwx--- root     sdcard_r          2015-01-01 08:06 Playlists
drwxrwx--- root     sdcard_r          2015-01-01 08:06 Podcasts

显然能不能读写取决于我们的APP属不属于sdcard_r用户组。根据头文件 /android/system/core/include/private/android_filesystem_config.h可以查询

#define AID_SDCARD_R      1028  /* external storage read access */

APP是否有sdcard_r用户组?

u0_a207   9009  321   1602296 72120 ffffffff b6fb3994 S com.example.myfiletest //所属用户号码是10207(u0_a207代表10207),进程pid 9009

然后查看用户组,命令cat /proc/pid/status
root@a7ltectc:/mnt/shell/emulated/0 # cat /proc/9009/status
Name:   mple.myfiletest
State:  S (sleeping)
Tgid:   9009
Pid:    9009
PPid:   321
TracerPid:      0
Uid:    10207   10207   10207   10207 用户ID
Gid:    10207   10207   10207   10207 组ID
FDSize: 256
Groups: 1015 9997 50207 所属组
VmPeak:  1602296 kB
VmSize:  1602296 kB

进程并未包含1028,访问被拒绝。如何让APP拥有1028,sdcard_r的用户组权限?

在APK安装的时候,PackageManagerService会根据应用申请的使用权限来授予不同的用户组,每个权限代表一个或者多个用户组。具体来说就是在APK的源文件AndroidManifest.xml文件里面应该要包含有申请权限的关键字
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

PackageManagerService在安装APK时会解析这些关键字,根据不同的关键字增加不同的用户组权限给APP。
那这些关键字对应什么样的权限呢?我们可以查看Android的源代码
/frameworks/base/data/etc/platform.xml
可以确认的是android.permission.WRITE_EXTERNAL_STORAGE对应的是1028用户组。

所以接下来我们给APP增加android.permission.WRITE_EXTERNAL_STORAGE的permission再来测试一遍。修改之后再检查权限

root@a7ltectc:/mnt/shell/emulated/0 # ps -t | grep myfile
u0_a207   11509 321   1584752 71316 ffffffff b6fb3994 S com.example.myfiletest
root@a7ltectc:/mnt/shell/emulated/0 # cat /proc/11509/status
cat /proc/11509/status
Name:   mple.myfiletest
State:  S (sleeping)
Tgid:   11509
Pid:    11509
PPid:   321
TracerPid:      0
Uid:    10207   10207   10207   10207
Gid:    10207   10207   10207   10207
FDSize: 256
Groups: 1015 1028 9997 50207
VmPeak:  1602436 kB

可见APP已经拿到1028组,检查测试结果
内置SD卡:
              没有内容返回,检查目录abc.txt文件已创建
外置SD卡:
03-01  06:28:56.727  D  9009  9009  test:  open failed: EACCES (Permission denied)

内置SD卡问题已解决,但外置SD卡依然无法写入。

4. 为什么不能写入外置sd卡

外置sd卡的挂载点情况

/dev/block/vold/179:64 /mnt/media_rw/extSdCard vfat rw
/mnt/media_rw/extSdCard /storage/extSdCard sdcardfs rw,seclabel,nosuid,nodev,relatime,uid=1023,gid=1023,derive=unified 0 0

查看挂载点的文件权限情况

root@a7ltectc:/storage/extSdCard # ll
drwxrwx--x root     sdcard_r          2016-07-25 13:22 Android
-rwxrwx--- root     sdcard_r   310445 2015-02-28 11:53 EncryptPhoneDemo.apk
-rwxrwx--- root     sdcard_r  3251377 2015-02-28 11:53 EncryptPhoneSDK12_21.apk
drwxrwx--- root     sdcard_r          2016-07-17 13:27 LOST.DIR

明显文件权限的情况和刚才内置SD卡的情况是一致的,增加了WRITE_EXTERNAL_STORAGE这个权限之后,APP已经拿到了sdcard_r(1028)这个所属组,按理应该可以正常操作目录才对,但是测试结果确返回失败。

问题答案需要从sdcardfs代码里面寻找,查看sdcardfs创建文件的路径

/kernel/fs/sdcardfs
const struct inode_operations sdcardfs_dir_iops = {
	.create	= sdcardfs_create,
	.lookup	= sdcardfs_lookup,
	.permission	= sdcardfs_permission,
	.unlink	= sdcardfs_unlink,
	.mkdir		= sdcardfs_mkdir,

log调试发现,sdcardfs_create返回了-EACCES

static int sdcardfs_create(struct inode *dir, struct dentry *dentry,
				umode_t mode, bool excl)
{
	...
	int has_rw = get_caller_has_rw_locked(sbi->pkgl_id, sbi->options.derive);
	if(!check_caller_access_to_name(dir, dentry->d_name.name, sbi->options.derive, 1, has_rw)) {
		printk(KERN_INFO "%s: need to check the caller's gid in packages.list\n" 
						 "  dentry: %s, task:%s\n",
						 __func__, dentry->d_name.name, current->comm);
		err = -EACCES; //此处返回了EACCES,permission denies的错误。
		goto out_eacces;
	}
	...
}

很快判断了get_caller_has_rw_locked()函数,返回了0,也就是has_rw = 0,最终导致返回了没权限的错误。

int get_caller_has_rw_locked(void *pkgl_id, derive_t derive) {
	struct packagelist_data *pkgl_dat = (struct packagelist_data *)pkgl_id;
	unsigned long appid;
	int ret;
	
	/* No additional permissions enforcement */
	if (derive == DERIVE_NONE) {//挂载参数,内置SD卡derive=legecy,外置SD卡derive=unified
		return 1; 
	}

	appid = multiuser_get_app_id(current_fsuid());
	printk("liangyi get_caller_has_rw_locked appid=%u\n", appid);
	mutex_lock(&pkgl_dat->hashtable_lock);
	ret = contain_appid_key(pkgl_dat, (void *)appid); 很明显该函数返回了0,appid就是本APP用户,参考前面内容是10207(u0_a207代表10207)
	mutex_unlock(&pkgl_dat->hashtable_lock);
	//printk(KERN_INFO "sdcardfs: %s: appid=%d, ret=%d\n", __func__, (int)appid, ret);
	return ret;
}

contain_appid_key的实现是,查询pkg1_data->appid_with_rw这个哈希表里面,是否包含有本appid,10207。

static int contain_appid_key(struct packagelist_data *pkgl_dat, void *appid) {
        struct hashtable_entry *hash_cur;

        hash_for_each_possible(pkgl_dat->appid_with_rw,	hash_cur, hlist, (unsigned long)appid)
                if (appid == hash_cur->key)
                        return 1;
	return 0;
}

因此猜测哈希表里面并没有我们这个appid,稍微理解一下,哈希表名称appid_with_rw,字面意思就是这个appid是否有读写rw的权限。所以这个哈希表里面存的肯定是有读写权限的appid。既然是哈希表,肯定代码有哈希表插入的地方。

接下来查找插入哈希表的位置。简单搜索可以找到函数insert_int_to_null,过滤掉无用代码。

static int insert_int_to_null(struct packagelist_data *pkgl_dat, void *key, int value) {
	...
	new_entry = kmem_cache_alloc(hashtable_entry_cachep, GFP_KERNEL);
	if (!new_entry)
		return -ENOMEM;
	new_entry->key = key; 这个key就是appid
	new_entry->value = value;
	hash_add(pkgl_dat->appid_with_rw, &new_entry->hlist,
			(unsigned long)new_entry->key);
	return 0;
}

哪里调用的?找到read_package_list函数,过滤无用代码之后。

static int read_package_list(struct packagelist_data *pkgl_dat) {
	...
	fd = sys_open(kpackageslist_file, O_RDONLY, 0); kpackageslist_file = "/data/system/packages.list";

	while ((read_amount = sys_read(fd, pkgl_dat->read_buf,
					sizeof(pkgl_dat->read_buf))) > 0) {
		...
		additional_read = read_amount - one_line_len;
		if (additional_read > 0)
			sys_lseek(fd, -additional_read, SEEK_CUR);	

		if (sscanf(pkgl_dat->read_buf, "%s %lu %*d %*s %*s %s",
				pkgl_dat->app_name_buf, &appid,
				pkgl_dat->gids_buf) == 3) {
			...
			while (token != NULL) {
				if (!kstrtoul(token, 10, &ret_gid) &&
						(ret_gid == pkgl_dat->write_gid)) {
					ret = insert_int_to_null(pkgl_dat, (void *)appid, 1);
					...
					break;
				}
			}
		}
	}
	...
}

简单描述代码逻辑:

  1. 打开文件/data/system/package.list
  2. 读数据,反复的读,并且解析其中的内容,把文件的内容读到pkg1_data这个数组里面去。
  3. 面对关键数据pkg1_dat->gids_buf,如果里面的值和pkg1_dat->write_gid匹配的话,把appid插入到哈希表里面去。

思路显然是不复杂的,关键是牵出来一个新的东西。/data/system/package.list是个什么东西。
这涉及到Android的PackageMangerService,这个文件是PackageMangerService根据系统安装的APP情况操作出来的,简单列下其内容:

/data/system/package.list
com.android.providers.telephony 1001 0 /data/data/com.android.providers.telephony platform 1030,3002,1023,1015,3003,3001,1007,3006
com.sec.android.app.parser 1000 0 /data/data/com.sec.android.app.parser platform 2001,3009,3002,1023,1015,3003,3001,1021,3004,3005,1000,2002,1010
com.google.android.googlequicksearchbox 10050 0 /data/data/com.google.android.googlequicksearchbox untrusted 1005,3002,3003,3001
com.android.providers.calendar 10033 0 /data/data/com.android.providers.calendar release 3003

里面描述了APP的包名,APPID,安装位置,APP签名情况,和用户组情况(也就是权限情况),这个文件在任何APP有更新(安装或卸载)的情况下都会发生改变。


sdcardfs启动了一个线程pkgld,来监控此文件的变化,并实时更新自己的哈希表内容。
packagelist_thread = kthread_run(packagelist_reader, (void *)pkgl_dat, "pkgld");
注意,每一个sdcardfs的new mount挂载都会有一个pkgld的线程出现。线程数量和挂载数量挂钩。


根据上面的代码

if (!kstrtoul(token, 10, &ret_gid) && (ret_gid == pkgl_dat->write_gid)) 
    ret = insert_int_to_null(pkgl_dat, (void *)appid, 1);

基本明白,只要APP的用户组权限里面有和pkgl_dat->write_gid匹配的,插入的到哈希表里面去。
接下来,pkgl_dat->write_gid是什么值?

void * packagelist_create(gid_t write_gid)
{
	...
	pkgl_dat->write_gid = write_gid;
}
static int sdcardfs_read_super(struct super_block *sb, const char *dev_name, 
						void *raw_data, int silent)
{
	if (sb_info->options.derive != DERIVE_NONE) 
		pkgl_id = packagelist_create(sb_info->options.write_gid);
}

很明显是挂载sdcardfs,带的挂载参数,sb_info->options.write_gid配置的。

查找挂载参数

static int parse_options(struct super_block *sb, char *options, int silent, 
				int *debug, struct sdcardfs_mount_options *opts)
{
		case Opt_wgid:
			if (match_int(&args[0], &option))
				return 0;
			opts->write_gid = option;
			break;
}
static const match_table_t sdcardfs_tokens = {
	{Opt_uid, "uid=%u"},
	{Opt_gid, "gid=%u"},
	{Opt_wgid, "wgid=%u"},
	{Opt_debug, "debug"},
	{Opt_split, "split"},
	{Opt_derive, "derive=%s"},
	{Opt_lower_fs, "lower_fs=%s"},
	{Opt_reserved_mb, "reserved_mb=%u"},
	{Opt_err, NULL}
};

解析挂载Opt_wgid这个挂载参数的时候,保存的。使用了关键字wgid=%u,参数解析是在程序/system/bin/sdcard里面完成的。

/android/system/core/sdcard/sdcard.c
static int run(const char* source_path, const char* dest_path, uid_t uid,
        gid_t gid, gid_t write_gid, int num_threads, derive_t derive,
        bool split_perms, bool support_fat, bool reserved_space) {
	res = sprintf(opts_p, "uid=%d,gid=%d,wgid=%d", uid, gid, write_gid);
}

对于内置SD卡,并没有配置-w参数,而外置SD卡,配置了-w为1023。根据sdcarfs代码没有配置采用默认值1015。

/android/device/qcom/common/rootdir/etc/init.qcom.rc
service sdcard /system/bin/sdcard -u 1023 -g 1023 -l -r /data/media /mnt/shell/emulated
    class late_start
    oneshot

service fuse_extSdCard /system/bin/sdcard -u 1023 -g 1023 -w 1023 -d -f /mnt/media_rw/extSdCard /storage/extSdCard
    class late_start
    disabled
    oneshot

回到最初进程权限列表上看
有1015,1028。但是很可惜没有1023。没有1023意味着哈希表appid_with_rw里面并没有我们这个APP id,也就是哈希表搜索会返回0,最终导致无访问权限。
那1023对应的是什么权限?
搜索下很容易发现
1023的权限是media_rw,需要有android.permission.WRITE_MEDIA_STORAGE权限。

    <permission name="android.permission.WRITE_MEDIA_STORAGE" >
        <group gid="media_rw" />
        <group gid="sdcard_rw" />
    </permission>

既然事已至此,我们在APP里面增加这个android.permission.WRITE_MEDIA_STORAGE,事情不就解决了吗?
还是很遗憾,Android4.4, 5.0 和 6.0对于这个权限的开放都仅限于system APP,也就是说第三方APP无法申请该权限,
简单的看就是如果申请该权限会得到这样一个错误

permission is only granted to system apps.
所以现在明白为什么第三方APP不能随意访问外置SD卡了吧。
 

5. 总结

APP申请android.permission.WRITE_EXTERNAL_STORAGE权限之后,可以随意读写内置sd卡,但是对于外置sd卡,android还是做了限制,需要其他途径来完成外置sd卡读写,这里不做介绍。

sdcardfs暴露出来了什么问题

1. 通过hash表来维护一个可写app list,导致每次写操作都要查询hash表才能做判断,这可能带来性能的损失。

2. 内部packagelist相关代码维护的appid_with_rw哈希表需要先被初始化好,如果sdcardfs先于package manager service初始化话好的话,始终要等待package manager service把/data/system/package.list初始化好,否则无法正常判断是否允许写入内置外置sd卡。这间接的引发了一些one time的时序问题,表现为sdcardfs一直在等待package.list的初始化。

Android8.0之后,sdcardfs被谷歌接管,后面将介绍Android8.0之后sdcardfs在控制sd卡访问权限上的差异。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐