git in fuse2fs
April 2020
1 問題
fuse2fsでマウントしたファイルシステムでは、 gitリポジトリを作成できない。
2 結論
最新のfuse2fs(e2fsprogs-1.45.6)をインストールして、 fakerootオプションをつけてマウントしろ。
3 経緯(と言うかここから覚書)
ChromebookではLinuxコンテナ(Crostini)を利用できる。しかしながら、内蔵ストレージが小さすぎる(16GBしかない)。当然、外部SDcardによる増量が必要になるが、UNIX系ファイルシステムが使えない。外部SDcardの上に作成したいgitリポジトリには、 shellやpython等のスクリプトも存在し、ファイルのアクセス権も管理したいので、 exfat(fat)ファイルシステムしか使えないのは困る。
3.1 ChromebookのSDcardでext2(ext4)を使う
- ChromebookのSDcardではexfatしか使えない。
- ext2のイメージファイルを作成してloopbackデバイスでマウントすればいいや。
- loopbackデバイスは不能。調査してないけど、多分、コンテナ(lxc)ゆえの制限。
- user-spaceのファイルシステムなら大丈夫だろう。
以上の経緯からfuse2fsの利用で解決。ここまではすぐ。
3.2 gitリポジトリをcloneできない
さぁ、gitだ。
- $ git clone ...
- ...
- unable to create temporary file
- ...
あり?他の方法も試してみる。
- $ git init
- $ git remote ...
- $ git pull
- ...
- insufficient permission for adding an object to repository database ...
- ...
これも、ダメか。
3.2.1 gitソースの調査
さて、困った。ウェブの情報を探すが、どうも同様の事例が見つからない。 fuse2fsでマウントしたファイルシステムに作ったgitリポジトリに特有の現象だ。ニッチ過ぎる。仕方ないのでソースを読む。エラーメッセージから該当部分は以下。
- static int write_loose_object(const struct object_id *oid, char *hdr,
- int hdrlen, const void *buf, unsigned long len,
- time_t mtime)
- {
- int fd, ret;
- unsigned char compressed[4096];
- git_zstream stream;
- git_hash_ctx c;
- struct object_id parano_oid;
- static struct strbuf tmp_file = STRBUF_INIT;
- static struct strbuf filename = STRBUF_INIT;
- loose_object_path(the_repository, &filename, oid);
- fd = create_tmpfile(&tmp_file, filename.buf);
- if (fd < 0) {
- if (errno == EACCES)
- return error(_("insufficient permission for adding an object to repository database %s"), get_object_directory());
- else
- return error_errno(_("unable to create temporary file"));
- }
- ...
sha1-file.c in git-2.26.0
create_tmpfileから追いかけていく。根本でエラーを返していると思われるのは以下45行目の部分。
- int git_mkstemps_mode(char *pattern, int suffix_len, int mode)
- {
- static const char letters[] =
- "abcdefghijklmnopqrstuvwxyz"
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- "0123456789";
- static const int num_letters = ARRAY_SIZE(letters) - 1;
- static const char x_pattern[] = "XXXXXX";
- static const int num_x = ARRAY_SIZE(x_pattern) - 1;
- uint64_t value;
- struct timeval tv;
- char *filename_template;
- size_t len;
- int fd, count;
- len = strlen(pattern);
- if (len < num_x + suffix_len) {
- errno = EINVAL;
- return -1;
- }
- if (strncmp(&pattern[len - num_x - suffix_len], x_pattern, num_x)) {
- errno = EINVAL;
- return -1;
- }
- /*
- * Replace pattern's XXXXXX characters with randomness.
- * Try TMP_MAX different filenames.
- */
- gettimeofday(&tv, NULL);
- value = ((uint64_t)tv.tv_usec << 16) ^ tv.tv_sec ^ getpid();
- filename_template = &pattern[len - num_x - suffix_len];
- for (count = 0; count < TMP_MAX; ++count) {
- uint64_t v = value;
- int i;
- /* Fill in the random bits. */
- for (i = 0; i < num_x; i++) {
- filename_template[i] = letters[v % num_letters];
- v /= num_letters;
- }
- fd = open(pattern, O_CREAT | O_EXCL | O_RDWR, mode);
- if (fd >= 0)
- return fd;
- /*
- * Fatal error (EPERM, ENOSPC etc).
- * It doesn't make sense to loop.
- */
- if (errno != EEXIST)
- break;
- /*
- * This is a random value. It is only necessary that
- * the next TMP_MAX values generated by adding 7777 to
- * VALUE are different with (module 2^32).
- */
- value += 7777;
- }
- /* We return the null string if we can't find a unique file name. */
- pattern[0] = '\0';
- return -1;
- }
wrapper.c in git-2.26.0
引数として、mode=0444が与えられてopenシステムコールが呼ばれているので、次のようなテストプログラムを書いてfuse2fsでマウントしたファイルシステムの上で実行。
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <errno.h>
- #include <stdio.h>
- int
- main(int argc, char *argv[])
- {
- char *fn = (1 == argc) ? "ttt" : argv[1];
- int fd = open(fn, O_RDWR | O_CREAT | O_EXCL, 0444);
- fprintf(stderr, "fd=%d, errno=%d\n", fd, errno);
- return 0;
- }
create444.c
- $ gcc -o create444 create444.c
- $ ./create444 test.txt
- fd=-1, errno=13
- $ ls
- test.txt
なんだこりゃ。 openシステムコールは確かに失敗している。そのくせ、しっかりファイルは作成されている。
3.2.2 fuse2fsソースの調査
やれやれ、仕方ない。fuse2fsのソースを読む。 EACCES(errno=13)を返す処理はどうやら次の部分のみのようだ。
- static int check_inum_access(ext2_filsys fs, ext2_ino_t ino, mode_t mask)
- {
- struct fuse_context *ctxt = fuse_get_context();
- struct fuse2fs *ff = (struct fuse2fs *)ctxt->private_data;
- struct ext2_inode inode;
- mode_t perms;
- errcode_t err;
- /* no writing to read-only or broken fs */
- if ((mask & W_OK) && !fs_writeable(fs))
- return -EROFS;
- err = ext2fs_read_inode(fs, ino, &inode);
- if (err)
- return translate_error(fs, ino, err);
- perms = inode.i_mode & 0777;
- dbg_printf("access ino=%d mask=e%s%s%s perms=0%o fuid=%d fgid=%d "
- "uid=%d gid=%d\n", ino,
- (mask & R_OK ? "r" : ""), (mask & W_OK ? "w" : ""),
- (mask & X_OK ? "x" : ""), perms, inode_uid(inode),
- inode_gid(inode), ctxt->uid, ctxt->gid);
- /* existence check */
- if (mask == 0)
- return 0;
- /* is immutable? */
- if ((mask & W_OK) &&
- (inode.i_flags & EXT2_IMMUTABLE_FL))
- return -EACCES;
- /* Figure out what root's allowed to do */
- if (ff->fakeroot || ctxt->uid == 0) {
- /* Non-file access always ok */
- if (!LINUX_S_ISREG(inode.i_mode))
- return 0;
- /* R/W access to a file always ok */
- if (!(mask & X_OK))
- return 0;
- /* X access to a file ok if a user/group/other can X */
- if (perms & 0111)
- return 0;
- /* Trying to execute a file that's not executable. BZZT! */
- return -EACCES;
- }
- /* allow owner, if perms match */
- if (inode_uid(inode) == ctxt->uid) {
- if ((mask & (perms >> 6)) == mask)
- return 0;
- return -EACCES;
- }
- /* allow group, if perms match */
- if (inode_gid(inode) == ctxt->gid) {
- if ((mask & (perms >> 3)) == mask)
- return 0;
- return -EACCES;
- }
- /* otherwise check other */
- if ((mask & perms) == mask)
- return 0;
- return -EACCES;
- }
fuse2fs.c in e2fsprogs-1.45.6
51行目から68行目で、ファイルのアクセスモード(mask:O_RDWRなど)と、パーミション(perms:0444など)を比較して不整合をチェックしている。ただし、33行目から49行目で、userがrootであるか、 fakerootオプションが指定されていれば、パーミションチェックは回避される。
3.2.3 解決(workaround)
- $ fuse2fs -o allow_other,fakeroot,uid=1000,gid=1000 image-file mount-point
3.2.4 余談
fuseモジュールなんて書いたことないです。実際にはfuse2fs.cにデバッグコードを埋め込んではコンパイルを繰り返し、 fuse2fsの挙動を確認しながら進めていたので、結構な時間がかかりました。やっぱ、Linuxのソース(特にスケジューラとファイルシステムまわり)と、メジャーなデバイスドライバくらいは読んでおく必要があると再確認した次第。
3.3 Return to Chromebook
現在のCrostini(debian buster)のfuse2fsは古いので、fakerootオプションが利用できない。 buster-backportsを使え。でも、一番良い解決法はgitにパッチをあてることだと思われる。
- *** sha1-file.c 2020-03-23 09:19:47.000000000 +0900
- --- sha1-file-patched.c 2020-04-11 02:26:44.803375581 +0900
- ***************
- *** 1803,1809 ****
- strbuf_reset(tmp);
- strbuf_add(tmp, filename, dirlen);
- strbuf_addstr(tmp, "tmp_obj_XXXXXX");
- ! fd = git_mkstemp_mode(tmp->buf, 0444);
- if (fd < 0 && dirlen && errno == ENOENT) {
- /*
- * Make sure the directory exists; note that the contents
- --- 1803,1809 ----
- strbuf_reset(tmp);
- strbuf_add(tmp, filename, dirlen);
- strbuf_addstr(tmp, "tmp_obj_XXXXXX");
- ! fd = git_mkstemp_mode(tmp->buf, 0644);
- if (fd < 0 && dirlen && errno == ENOENT) {
- /*
- * Make sure the directory exists; note that the contents
patch for git-2.26.0
作業用の一時ファイルとして実効ユーザが読み書きするのだから、普通は「mode=0644」で作成するのが当然だと思うのだが何故「mode=0444」なのであろうか。
3.4 性能が、、、
想像以上の重さだ。 SDcardでloopback(imageファイル)だ。性能は悪いだろうと思っていたが、比較的高速のSDXCでもここまでとは。全体的にもっさり。 2000ファイルもあるとlsですら10秒以上かかる。