2019/10/23(Wed)
○[オレオレN6] 続・pkg_add(1) won't work with libarchive 3.4.0
前回は原因をarchive_write_data_blockと断定してしまったけど、ワイのgrep(1)ミスであり同じエラーメッセージを表示するarchive_write_header側の問題であった、メンゴメンゴ(死語)。
683 static int
684 extract_files(struct pkg_task *pkg)
685 {
...
779 r = archive_write_header(writer, pkg->entry);
780 if (r != ARCHIVE_OK) {
781 warnx("Failed to write %s for %s: %s",
782 archive_entry_pathname(pkg->entry),
783 pkg->pkgname,
784 archive_error_string(writer));
785 goto out;
786 }
archive_write_data_blockは3.3.3→3.4.0でコードに変更はないので変だなと思って再度コード検索し直して気づいた。
最初はarchive_write_data_blockには ドキュメントと実装の相違があって、バイナリ互換性壊すし将来リリースで直すわという結論になっとるのでその関係かなと思ったんだけど無駄な時間だったね(およそ5分)。
ちなみにその部分の実装を軽く読んでみると
[libarchive/archive.h]
191 #define ARCHIVE_EOF 1 /* Found end of archive. */
192 #define ARCHIVE_OK 0 /* Operation was successful. */
193 #define ARCHIVE_RETRY (-10) /* Retry might succeed. */
194 #define ARCHIVE_WARN (-20) /* Partial success. */
195 /* For example, if write_header "fails", then you can't push data. */
196 #define ARCHIVE_FAILED (-25) /* Current operation cannot complete. */
197 /* But if write_header is "fatal," then this archive is dead and useless. */
198 #define ARCHIVE_FATAL (-30) /* No more operations are possible. */
[libarchive/archive_write_disk_posix.c]
1545 static ssize_t
1546 _archive_write_disk_data_block(struct archive *_a,
1547 const void *buff, size_t size, int64_t offset)
1548 {
1549 struct archive_write_disk *a = (struct archive_write_disk *)_a;
1550 ssize_t r;
1551
1552 archive_check_magic(&a->archive, ARCHIVE_WRITE_DISK_MAGIC,
1553 ARCHIVE_STATE_DATA, "archive_write_data_block");
1554
1555 a->offset = offset;
1556 if (a->todo & TODO_HFS_COMPRESSION)
1557 r = hfs_write_data_block(a, buff, size);
1558 else
1559 r = write_data_block(a, buff, size);
1560 if (r < ARCHIVE_OK)
1561 return (r);
1562 if ((size_t)r < size) {
1563 archive_set_error(&a->archive, 0,
1564 "Too much data: Truncating file at %ju bytes",
1565 (uintmax_t)a->filesize);
1566 return (ARCHIVE_WARN);
1567 }
1568 #if ARCHIVE_VERSION_NUMBER < 3999000
1569 return (ARCHIVE_OK);
1570 #else
1571 return (size);
1572 #endif
1573 }
と将来的に修正する準備としてARCHIVE_VERSION_NUMBERでifdefしとる、でもこれ設計的にはこれプロトタイプ変更して
- 戻り値はエラーコードだけを返すようにする
- 第5引数を追加し書き込んだサイズはそいつで返す
- あるいはbuff/sizeをポインタにして書き込んだサイズ分buffを進めsizeを減らす
ようにすべきよね、まぁ人は愚かなので滅びるまで設計ミスは無くならんしこれ以上つっこまんとこ。
ということで仕切り直してarchive_write_headerの3.3.3→3.4.0への変更点をざっと眺めてみる。
--- libarchive.orig/files/libarchive/archive_write_disk_posix.c 2019-04-10 17:24:05.000000000 +0900
+++ libarchive/files/libarchive/archive_write_disk_posix.c 2019-10-04 23:49:49.000000000 +0900
@@ -165,6 +165,10 @@
#define O_NOFOLLOW 0
#endif
+#ifndef AT_FDCWD
+#define AT_FDCWD -100
+#endif
+
struct fixup_entry {
struct fixup_entry *next;
struct archive_acl acl;
@@ -348,6 +352,8 @@
#define HFS_BLOCKS(s) ((s) >> 12)
+
+static int la_opendirat(int, const char *);
static void fsobj_error(int *, struct archive_string *, int, const char *,
const char *);
static int check_symlinks_fsobj(char *, int *, struct archive_string *,
@@ -401,6 +407,37 @@
size_t, int64_t);
static int
+la_opendirat(int fd, const char *path) {
+ const int flags = O_CLOEXEC
+#if defined(O_BINARY)
+ | O_BINARY
+#endif
+#if defined(O_DIRECTORY)
+ | O_DIRECTORY
+#endif
+#if defined(O_PATH)
+ | O_PATH
+#elif defined(O_SEARCH)
+ | O_SEARCH
+#elif defined(O_EXEC)
+ | O_EXEC
+#else
+ | O_RDONLY
+#endif
+ ;
+
+#if !defined(HAVE_OPENAT)
+ if (fd != AT_FDCWD) {
+ errno = ENOTSUP;
+ return (-1);
+ } else
+ return (open(fd, path, flags));
+#else
+ return (openat(fd, path, flags));
+#endif
+}
+
+static int
はい、POSIX:2008で新規に追加されたopenat(2)のラッパーが追加されとるわね。
このopenat(2)ってsyscallは以前にも 解説したTickTokなのかTOCTTTTTTTTOOOOOUUUUUUなのか毎回綴りを思い出せないファイルシステムの競合状態に関する脆弱性への対策として導入されたもの。 この問題は古くより存在するopen(2)が相対パスを処理する際、起点となる現在の作業ディレクトリはプロセス単位でグローバルだからスレッドセーフではないことで起きる。 なので明示的に起点をファイルディスクリプタとして引数に渡すもの。
もしこのAPIはじめて知ったなんて人おったら、もう追加されて干支一回りしとるので勉強してどうぞ( プレゼンでかるく触れた記憶があるぞ)。
ともかくN6の時点ではこのopenat(2)は未実装だったので、ラッパー中のエミュレーションコードが使われ結果としてバグっとるちゅーことなんやな。
なのでN6をとっくに切り捨てた現在のpkgsrcでは問題は起きないという事、もちろんマルチプラットホームを標榜するpkgsrcのサポートするOSの中でopenat(2)がまだ未実装のものものもあるだろうし、そっちでは当然発生すると思われる。 まあでもそんな環境でpkgsrc使う人なぞ世界で3人くらいしかおらんだろうから見過ごされとるんだろう。
んで再度さっきの差分を読むと
- 引数にAT_FDCWDが指定された場合はopen(2)と完全に同じ動作が保証されるのでopen(2)を呼びだす
- それ以外はエミュレーション不能という事でENOTSUP(未サポート)を返す
という破綻間違いなしの実装になっとるので、ここで引っかかっとるんだろうなと想像がつく。
ということでlibarchive 3.4.0に対してすべき作業は、完全なopenat(2)エミュレーションとして
- mutexかける
- getcwd(3)の戻り値より現在の作業ディレクトリを覚えておく
- 現在の作業ディレクトリを引数で渡されたファイルディスクリプタへfchdir(2)使って変更
- これまでどおりopen(2)を呼ぶ
- 覚えておいた現在の作業ディレクトリにchdir(2)で戻る
- mutexはずす
を実装するかなんだけど、そもそもlibarchiveのAPIでこのエミュレーションを呼びだす箇所は
1948 /* Try to record our starting dir. */
1949 a->restore_pwd = la_opendirat(AT_FDCWD, ".");
...
2642 chdir_fd = la_opendirat(AT_FDCWD, ".");
2643 __archive_ensure_cloexec_flag(chdir_fd);
...
2705 } else if (S_ISDIR(st.st_mode)) {
2706 if (!last) {
2707 #if defined(HAVE_OPENAT) && defined(HAVE_FSTATAT) && defined(HAVE_UNLINKAT)
2708 fd = la_opendirat(chdir_fd, head);
2709 if (fd < 0)
2710 r = -1;
2711 else {
2712 r = 0;
2713 close(chdir_fd);
2714 chdir_fd = fd;
2715 }
2716 #else
2717 r = chdir(head);
2718 #endif
...
2811 #if defined(HAVE_OPENAT) && defined(HAVE_FSTATAT) && defined(HAVE_UNLINKAT)
2812 fd = la_opendirat(chdir_fd, head);
2813 if (fd < 0)
2814 r = -1;
2815 else {
2816 r = 0;
2817 close(chdir_fd);
2818 chdir_fd = fd;
2819 }
2820 #else
2821 r = chdir(head);
2822 #endif
のように、HAVE_OPENATが定義されてない限りはAT_FDCWDつきで呼ばれとるんだよね…なので完全なエミュレーションは不要なはずなんだけど。
ということでもうちょっとコードを読み進めないとならんのだけど、いつ認知症老人による失火で焼け死んでも不思議でない明日は無い状態なので次回は未定。