"/sdcard"の呪い
初期のAndroid端末の多くにはSDカードを挿すスロットがついていました. SDカード中のファイルへは, "/sdcard"からアクセスできました. アプリは"/sdcard"の下にファイルを作り, 読み書きすることで端末内部の﹁小さい領域﹂を節約して, ﹁外部デバイスの広い領域﹂を活用することができました. 結局, 多くのAndroid端末がSDカードスロットを持たなくなったようです*1. そのような端末でも, "/sdcard"は存在しています. この領域はSDカードを指す代わりに, 端末内部のストレージを指しています. こうしておけば, 外部ストレージを使いたいアプリが, SDカードの有無にかかわらず同じように動作することができますね. 一般にSDカードはVFATでフォーマットされています. 一方, Android (Linux)で用いられるファイルシステムはExt4(時々F2FS)です. VFATとExt4とは機能に違いがあります. 特に2つの大きな違いがあります. 1つ目は, ファイル名の大文字・小文字の扱いです. VFATではファイル名中の大文字・小文字を区別しませんが, Ext4では大文字・小文字が厳密に区別されます. たとえば, "cat.JPG"という猫の画像ファイルを保存したとします. VFATであれば"JPG"を小文字にして, "cat.jpg"でアクセスしてもファイルを読むことができます. 一方Ext4では, "cat.jpg"でアクセスすると"JPG"部分の違いから, そんなファイルはない!と言われてしまいます. 2つ目の違いは, アクセス権限の取扱いです. Linuxのファイルシステムでは伝統的なUNIXのファイルアクセス制御が使われています. すなわち, 各ファイルにその所有者, 所有グループがあり, 所有者・所有グループ・その他のユーザがそれぞれ読みこみ・書きこみ・実行の何ができるかがファイルシステムに記録されています. 一方で, もともとMS-DOSのファイルシステムであるVFATには, そのようなアクセス権限情報は記録することができません. 1つ目の大文字・小文字の取扱いの違いは, Ext4上でアプリの互換性に影響をおよぼします. これまで﹁全部小文字でもアクセスできてた﹂アプリが, "/sdcard"が内部フラッシュを指す端末では急に動かなくなります. アプリの互換性はなるべく守らなければなりません. 2つ目のアクセス権限の違いは, VFAT上でAndroidにおけるセキュリティ機能の制限を生んでしまいます. Androidのセキュリティの詳細は割愛しますが,WRITE_EXTERNAL_
STORAGE
という権限を持ったアプリは"/sdcard"下のデータを読み書きすることができます. VFATの場合, ファイルの所有者がどのアプリかを記録できないので, OSレべルではアプリは"/sdcard"下のどんなファイルでも読めてしまいます. すなわち, 他のアプリの"/sdcard"下のデータを﹁盗み見る﹂ことも可能となってしまいます. とは言え, ﹁広いSDカード領域﹂を活用するためには, この権限が必要となるわけでアプリとしてはこの権限を要求しないわけにもいきません.
ここでジレンマに陥ります. 互換性を保とうと思えば, Ext4(などLinux固有のファイルシステム)を使うわけにはいきません. 一方でVFATではセキュリティ機能がうまくいきません. はてさて, どうしましょう.
ファイルシステムの﹁ラッピング﹂
VFATがアクセス権限を保存してくれない?なら, 動的に﹁アクセス権限を合成﹂してやればいいんじゃよ. VFATでLinuxのセキュリティチェックをうまく適用できないのは, VFATがファイルの所有者情報を付けてくれないせいです. ならば, VFATをラップして, 所有者情報を付けてあげれば, OSのセキュリティチェックが適切に動きます. アイデアはこうです. ●"/sdcard"下に, アプリごとにディレクトリを作る ●アプリ固有のディレクトリ下のファイルに, アプリの所有者情報を合成する ●アプリは自分のディレクトリの下なら自由に読み書きできるが, 他のアプリのファイルにはアクセスできなくなる (OSが保証)![f:id:meech:20170121053222p:plain f:id:meech:20170121053222p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/m/meech/20170121/20170121053222.png)
sdcardfs: FUSEからの脱出
これで話はめでたしめでたし…で終わらず, ここからが本題です. FUSEを使うことで, アプリからはこれまで通りにファイルアクセスするだけで, ファイルシステムを拡張することができます. ここで通常のファイルアクセスと, FUSE上のファイルアクセスとを比較してみましょう. 通常のファイルシステムのケースを見てみましょう. アプリのファイルアクセスは, まずVFSにハンドルされます. VFSはExt4など, そのディレクトリを担当するファイルシステムのコードを呼びだします. 一方FUSEでは, アプリがファイルにアクセスすると, その呼び出しがVFSを通ってFUSEドライバに行きます. ここでアプリがアクセスしようとしたファイルの情報が, カーネルからユーザランドのFUSEプログラムに伝えられます. FUSEプログラムを受け取った情報をもとに, ssh先からファイルを取ってくるなどして, その情報をカーネルに送りかえします. カーネルは受け取った結果を, もとのアプリに渡してファイルアクセスが完了します. ここでFUSEが入っていることで, 一度カーネルからユーザランドに戻っていることがわかります. 今回のような既存のファイルシステムをラップしている場合には, FUSE以外は完全に元のままで, FUSEの部分が純粋にオーバヘッドとなっています.![f:id:meech:20170121044423p:plain f:id:meech:20170121044423p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/m/meech/20170121/20170121044423.png)
![f:id:meech:20170121044510p:plain f:id:meech:20170121044510p:plain](https://cdn-ak.f.st-hatena.com/images/fotolife/m/meech/20170121/20170121044510.png)
VFAT | Ext4/F2FS | FUSE | SDCardFS | |
---|---|---|---|---|
互換性 | o | x | o | o |
アクセス権限 | x | o | o | o |
オーバヘッド | なし | なし | 大きい | 小さい |
結果として, FUSEを使っていたオーバヘッドが小さくなる分だけ性能が向上します. しかしながら, 「SD Card FS」はSDカード専用のファイルシステム, というものではありません. SDカードの特性をうまく使って高速化とか, そういうものではありません. F2FSと比べるとかそういうものではありません. SDカードがいた領域を, あたかもそのままSDカードであったかのように見せる, それを速くしました. そういうものです.
宣伝
ね, ファイルシステムおもしろいですよね. Linuxでは, いろんなファイルシステムが実装されています. そんなファイルシステムたちがどのようなデータ構造で, どのようにデータをディスクに保存しているか気になってきましたよね? ちょうど現在, 発売中のSoftware Design 2017年2月号に「Linuxファイルシステムの教科書」と題して, Ext3, Ext4, XFS, F2FS, Btrfsの実装について特集記事を書きました. ファイルシステムの中身をもっと知りたくなったあなたはぜひ購入してみてください.
![ソフトウェアデザイン 2017年 02 月号 [雑誌] ソフトウェアデザイン 2017年 02 月号 [雑誌]](https://images-fe.ssl-images-amazon.com/images/I/513zzp82e7L._SL160_.jpg)
- 出版社/メーカー: 技術評論社
- 発売日: 2017/01/18
- メディア: 雑誌
- この商品を含むブログを見る
参考文献
Androidの"/sdcard"については, 知識がなかったので以下を参考にしています
Linuxのストレージ・メモリ管理に関する会議の議題として, sdcardfsでやっていることさらにVFSに統合していこうという話が提案されている. case-insensitveな探索ができるようにしようとか, アプリケーションレベルでpermissionのハンドルを行うLSM(Linux Security Module)が実装できないかなどが話されるようだ.
'[LSF/MM TOPIC] Getting rid of Android's FUSE/wrapfs hackery' - MARC
で, おれはコードを読みたいんだ. コードはどこだ
(ここから先は上級向けで飛んでいきます)
よっしゃまかせろ. コードはここだ. 読んでくぞ. (以下ほとんど読む過程のdumpで整理されていない)
fs/sdcardfs - kernel/common.git - Git at Google
まず, ファイルシステムに限らずLinux kernel moduleを読む時の鉄則として"__init"の付いた関数を見つけよう. こいつがmoduleの初期化をする. moduleの初期化で, だいたい大事なデータ構造を作ったりしてるので眺めておくと理解がスムーズに行く.
こいつの場合, main.cのinit_sdcardfs_fs
がそれ.
fs/sdcardfs/main.c - kernel/common.git - Git at Google
err = sdcardfs_init_inode_cache(); if (err) goto out; err = sdcardfs_init_dentry_cache(); if (err) goto out; err = packagelist_init(); if (err) goto out; err = register_filesystem(&sdcardfs_fs_type);
ざっと見ると, 以下の3つのデータ構造が使われてそう. package listってなんだろね.
- inode cache
- dentry cache
- packagelist
ファイルシステムの場合, 次に見ていきたいのは, struct file_operations
. こいつらがファイルシステム上のファイルの挙動を決める. (mount処理は, だいたいoptionをなめていろいろ初期化してるだけなので, 頭から読むようなものではない, と思う)
const struct file_operations sdcardfs_main_fops = { .llseek = generic_file_llseek, .read = sdcardfs_read, .write = sdcardfs_write, .unlocked_ioctl = sdcardfs_unlocked_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = sdcardfs_compat_ioctl, #endif .mmap = sdcardfs_mmap, .open = sdcardfs_open, .flush = sdcardfs_flush, .release = sdcardfs_file_release, .fsync = sdcardfs_fsync, .fasync = sdcardfs_fasync, }; /* trimmed directory options */ const struct file_operations sdcardfs_dir_fops = { .llseek = generic_file_llseek, .read = generic_read_dir, .iterate = sdcardfs_readdir, .unlocked_ioctl = sdcardfs_unlocked_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = sdcardfs_compat_ioctl, #endif .open = sdcardfs_open, .release = sdcardfs_file_release, .flush = sdcardfs_flush, .fsync = sdcardfs_fsync, .fasync = sdcardfs_fasync, };
変数名から, それぞれファイルとディレクトリに対してのoperationsだとわかる. case-insensitve searchしてくれるんだよな, という知識で眺めると, .iterate
のsdcardfs_readdir
が気になる.
static int sdcardfs_readdir(struct file *file, struct dir_context *ctx) { int err; struct file *lower_file = NULL; struct dentry *dentry = file->f_path.dentry; lower_file = sdcardfs_lower_file(file); lower_file->f_pos = file->f_pos; err = iterate_dir(lower_file, ctx); file->f_pos = lower_file->f_pos; if (err >= 0) /* copy the atime */ fsstack_copy_attr_atime(d_inode(dentry), file_inode(lower_file)); return err; }
意外と面白いところはない. lower_file
で下位のFSのファイルをとってきて, そいつをiterate_dir()
でなめる. atimeを上のディレクトリにcopy upする. それだけっぽい
じゃあ, 多分open
だな, ということで, sdcardfs_open
に行く.
if(!check_caller_access_to_name(parent->d_inode, dentry->d_name.name)) { 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; goto out_err; } /* save current_cred and override it */ OVERRIDE_CRED(sbi, saved_cred);
check_caller_access_to_name()
に親ディレクトリと, アクセスしようとしているファイル名を渡してアクセス可能かチェックし, OVERRIDE_CRED
で権限を書きかえるという流れだろう.
/* 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. */ int check_caller_access_to_name(struct inode *parent_node, const char* name) { /* Always block security-sensitive files at root */ if (parent_node && SDCARDFS_I(parent_node)->perm == PERM_ROOT) { if (!strcasecmp(name, "autorun.inf") || !strcasecmp(name, ".android_secure") || !strcasecmp(name, "android_secure")) { return 0; } } /* Root always has access; access for any other UIDs should always * be controlled through packages.list. */ if (from_kuid(&init_user_ns, current_fsuid()) == 0) { return 1; } /* No extra permissions to enforce */ return 1; }
ところがcheck_caller_access_to_name
はあまり大したことをしていない. "autorun.inf"とかのやばそうなファイルへのアクセスを消り, rootにアクセス権を与え, それ以外は弾いているだけ. ふ〜ん…コメントが気になるけど, 一度OVERRIDE_CRED
を見る.
OVERRIDE_CRED
は結局以下の関数でstruct cred
を書きかえる.
const struct cred * override_fsids(struct sdcardfs_sb_info* sbi) { struct cred * cred; const struct cred * old_cred; cred = prepare_creds(); if (!cred) return NULL; cred->fsuid = make_kuid(&init_user_ns, sbi->options.fs_low_uid); cred->fsgid = make_kgid(&init_user_ns, sbi->options.fs_low_gid); old_cred = override_creds(cred); return old_cred; }
cred->fsuid
というのがVFSでのUID, fsgidは言うまでもなくGID. FUSE版の方はファイルのattributeをアプリの所有に書きかえる感じだったのが, kernel版では実行主体のcredentialを書きかえてるのが面白い.
コメントで気になったderive_permissions_locked()
を見ていく……と思ったけど, そんな関数はない. 似た名前のget_derive_permissions()
というのがあやしい. 多分移植の時にコメント書き変えてないんでしょ. よくある. よくする.
結局get_derived_permission_new
にたどりつく. 親ディレクトリparent
と, アクセスするファイルdentry
で呼ばれる. newdentry
はrenameの時のため.
void get_derived_permission_new(struct dentry *parent, struct dentry *dentry, struct dentry *newdentry) { … inherit_derived_state(parent->d_inode, dentry->d_inode); /* Derive custom permissions based on parent and current node */ switch (parent_info->perm) { case PERM_INHERIT: /* Already inherited above */ break; … case PERM_ANDROID_DATA: case PERM_ANDROID_OBB: case PERM_ANDROID_MEDIA: appid = get_appid(sbi->pkgl_id, newdentry->d_name.name); if (appid != 0) { info->d_uid = multiuser_get_uid(parent_info->userid, appid); } break; } }
inherit_derived_state
で, 親からなんか引き継ぐ. ほとんどそのままだが, 親がPERM_ANDROID_DATA
などだとなんかしている.
/* This node is "/Android/data" */ PERM_ANDROID_DATA, /* This node is "/Android/obb" */ PERM_ANDROID_OBB, /* This node is "/Android/media" */ PERM_ANDROID_MEDIA,
なるほどね〜, "/Android/data"とかの直下のファイルだった場合ね. この時, ファイル名から"appid"をとってきて, それをinfo_uid
に設定している. multiuser_get_uid
は, Androidのマルチユーザ対応っぽい. ここでアプリ個別のディレクトリの所有者が記録されるのかな.
inherit_derived_state()
に行く
static void inherit_derived_state(struct inode *parent, struct inode *child) { struct sdcardfs_inode_info *pi = SDCARDFS_I(parent); struct sdcardfs_inode_info *ci = SDCARDFS_I(child); ci->perm = PERM_INHERIT; ci->userid = pi->userid; ci->d_uid = pi->d_uid; ci->under_android = pi->under_android; }
いかにもsdcardfs専用構造っぽいstruct sdcardfs_inode_info
が出てきた. SDCARDFS_I
は, struct inode*
からstruct sdcardfs_inode_info*
に変換するマクロだろ. FS, だいたいこんな書き方する.
perm
をPERM_INHERIT
, userdid, d_uid, under_androidは上から引き継ぎ. すなわち, こいつの下もこいつの親からの情報をひきついでいくことになる. それっぽい.
ここまでは, struct sdcardfs_inode_info
しかいじられていない. これではアクセス制御になってないな.
get_derived_permission
の呼びだしあたりがあやしいのでgrepする.
struct dentry *sdcardfs_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags) { … /* save current_cred and override it */ OVERRIDE_CRED_PTR(SDCARDFS_SB(dir->i_sb), saved_cred); … ret = __sdcardfs_lookup(dentry, flags, &lower_parent_path, SDCARDFS_I(dir)->userid); if (IS_ERR(ret)) { goto out; } if (ret) dentry = ret; if (dentry->d_inode) { fsstack_copy_attr_times(dentry->d_inode, sdcardfs_lower_inode(dentry->d_inode)); /* get drived permission */ mutex_lock(&dentry->d_inode->i_mutex); get_derived_permission(parent, dentry); fix_derived_permission(dentry->d_inode); mutex_unlock(&dentry->d_inode->i_mutex); }
ここでもcredを変えて, __sdcardfs_lookup
して, attributeとかをcopy upして, get_derived_permission
してる. fix_derived_permission
かなあ
#define fix_derived_permission(x) \ do { \ (x)->i_uid = make_kuid(&init_user_ns, SDCARDFS_I(x)->d_uid); \ (x)->i_gid = make_kgid(&init_user_ns, get_gid(SDCARDFS_I(x))); \ (x)->i_mode = ((x)->i_mode & S_IFMT) | get_mode(SDCARDFS_I(x));\ } while (0)
あぁ〜確かに, inodeのuidとかgidが, struct sdcardfs_inode_info
に入ってたものに書きかえられてる. さっき, こっちではcredを書きかえるのか〜と思ったけれど, あれはlower FSを読む権限を獲得してるだけで, やっぱりファイルの所有情報書き変えてんのね.
アクセス権限のとこはわかったけど, case-insensitiveのとこはどうなってんだろ. やっぱdentry.cかな.
const struct dentry_operations sdcardfs_ci_dops = { .d_revalidate = sdcardfs_d_revalidate, .d_release = sdcardfs_d_release, .d_hash = sdcardfs_hash_ci, .d_compare = sdcardfs_cmp_ci, .d_canonical_path = sdcardfs_canonical_path, };
専用の dentry_operationsがある. .d_compare = sdcardfs_cmp_ci
とかそれっぽい.
static int sdcardfs_cmp_ci(const struct dentry *parent, const struct dentry *dentry, unsigned int len, const char *str, const struct qstr *name) { if (name->len == len) { if (strncasecmp(name->name, str, len) == 0) return 0; } return 1; }
なるほど見たまんま.
最後に, get_appid
を見ておくか…
appid_t get_appid(void *pkgl_id, const char *app_name) { struct packagelist_data *pkgl_dat = pkgl_data_all; struct hashtable_entry *hash_cur; unsigned int hash = str_hash(app_name); appid_t ret_id; mutex_lock(&pkgl_dat->hashtable_lock); hash_for_each_possible(pkgl_dat->package_to_appid, hash_cur, hlist, hash) { if (!strcasecmp(app_name, hash_cur->key)) { ret_id = (appid_t)hash_cur->value; mutex_unlock(&pkgl_dat->hashtable_lock); return ret_id; } } mutex_unlock(&pkgl_dat->hashtable_lock); return 0; }
まあそりゃhashtableひくだけだよね. tableはpackage_to_appid
ってやつ. insert_str_to_int_lock()
が追加している.
このpackagelist.cを見ているとどうやらconfigfsを使ってアプリ名->appidのマッピングが入っているっぽい.
static struct configfs_subsystem sdcardfs_packages_subsys = { .su_group = { .cg_item = { .ci_namebuf = "sdcardfs", .ci_type = &sdcardfs_packages_type, }, }, }; static int configfs_sdcardfs_init(void) { int ret; struct configfs_subsystem *subsys = &sdcardfs_packages_subsys; config_group_init(&subsys->su_group); mutex_init(&subsys->su_mutex); ret = configfs_register_subsystem(subsys);
configfs中のrootは"sdcardfs"
static struct configfs_group_operations sdcardfs_packages_group_ops = { .make_item = sdcardfs_packages_make_item, }; static struct config_item_type sdcardfs_packages_type = { .ct_item_ops = &sdcardfs_packages_item_ops, .ct_group_ops = &sdcardfs_packages_group_ops, .ct_attrs = sdcardfs_packages_attrs, .ct_owner = THIS_MODULE, };
その中にディレクトリを作ることができる (ct_group_ops
から)
static struct configfs_attribute package_appid_attr_add_pid = { .ca_owner = THIS_MODULE, .ca_name = "appid", .ca_mode = S_IRUGO | S_IWUGO, .show = package_appid_attr_show, .store = package_appid_attr_store, }; static struct configfs_attribute *package_appid_attrs[] = { &package_appid_attr_add_pid, NULL, };
その中に"appid"というファイルがある. ここに読み書きできる.
まとめると, "/sys/kernel/config/sdcardfs/app.name/appid"というファイルに数字を書くと, "app.name"->42といったマッピングがさっきのhashtableに登録される.
てことは, 誰かがユーザランド側でこれをやってくれるんだね. なるほどね.
ここまでコードがんがんはしてないけれど、データ構造がわかる感じになるので, もいちどよろしく.
![ソフトウェアデザイン 2017年 02 月号 [雑誌] ソフトウェアデザイン 2017年 02 月号 [雑誌]](https://images-fe.ssl-images-amazon.com/images/I/513zzp82e7L._SL160_.jpg)
- 出版社/メーカー: 技術評論社
- 発売日: 2017/01/18
- メディア: 雑誌
- この商品を含むブログを見る
*1:実際どの程度かわからぬですが, 確かに手元のNexus7にはSDカードスロットがない