GNU coreutils へのバグ報告

本ページでは、 拙著『lsを読まずにプログラマを名乗るな!』 の執筆過程で見つけた GNU coreutils のバグに関して、 バグの詳細と、 バグ報告の顛末について説明します。

GNU coreutils へのバグ報告時の注意点

GNU coreutils のトップページには、 バグ報告用メーリングリスト (bug-coreutils@gnu.org) への投函には、 ユーザ登録等が必要ない旨の記載があります。

You do not need to be subscribed in order to post messages to any GNU mailing list.

〜 from "Mailing Lists"

その一方で、 ユーザ登録無しでメーリングリストにメール投函した場合は、 人手による投稿のチェックが発生するため、 メーリングリストによる配信にはタイムラグが発生する、 という旨の記述もあります。

However non-subscribers are moderated by humans so please be patient when waiting for your email to arrive.

〜 from "Mailing Lists"

『たまたま見つけたバグ報告のためにユーザ登録』 するのが面倒であることを理解できる一方で、 『非登録ユーザからの投函を無条件で受理』する事が、 容易にスパムメールの踏み台にされててしまうという事情も理解できます。 ですので、 『人手による判定』という手順を挟むのは、 妥当な落としどころと言えるでしょう。

It has been necessary to moderate the Coreutils mailing lists to prevent the flood of spam. Postings to the lists are held for release by the list moderator.

〜 from "Mailing Lists"

しかし、 『予想に反して』なのか、 『心配した通り』なのか表現が悩ましいところですが(笑)、 未登録ユーザからのメール投函に対する『人手による判定』は、 bug-coreutils@gnu.org においては機能していないと言って良いでしょう。

私自身、 メーリングリストへのユーザ登録前に、 バグ報告をメール投函してみたのですが、 結局一か月程度待っても、 メールが受理される様子がありませんでした。

最終的には、 バグ報告用メーリングリストにユーザ登録した上で、 バグ報告メールを投函したのですが、 その場合も、 『ユーザ登録直後は投函したメールが受理されない』 ように思われました。

登録から一か月ほど経過してから、 再度投函したメールは受理されましたので、 登録から一定時間経過しないユーザからの投函は、 破棄されるか、 あるいは『人手による判定』+『判定作業の放棄』によって、 実質的に破棄されている可能性があります。

bug-coreutils@gnu.org へのバグ報告の際には、 『メーリングリストへのユーザ登録』+ 『登録後しばらくしてからの投函』をお勧めします。

再帰表示時のエラーコード不正 (ID:15249)

以下で説明するバグの詳細は、 本書の『第2章 ディレクトリ要素の表示』および 『第4章 ディレクトリ要素の再帰的な表示』 における説明を理解していることを前提とします。

問題の詳細/修正内容

print_dir() での gobble_file() 呼び出し (2593行) では、 command_line_arg 引数には常に false が指定されます。

2571|      next = readdir (dirp);
〜〜〜〜
2593|              total_blocks += gobble_file (next->d_name, type,
2594|                                           RELIABLE_D_INO (next),
2595|                                           false, name);
ソースコード: 1-1

これは、 gobble_file() 呼び出しの対象が、 readdir(3) によって得られた 『指定ディレクトリ直下の要素』であることから、 これらが『コマンドラインで指定されたもの』 ではないことが自明であるためです。

その一方で、 再帰的な表示 (-R/--recursive オプション指定時) を行う際の、 print_dir() での extract_dirs_from_files() 呼び出し (2641行) における command_line_arg 引数には、 print_dir() 呼び出しにおける command_line_arg 引数が使用されます。

2640|  if (recursive)
2641|    extract_dirs_from_files (name, command_line_arg);
ソースコード: 1-2

extract_dirs_from_files() 呼び出しにおける command_line_arg 引数は、 queue_directory() 呼び出しによって、 単方向リスト pending_dirs の要素に記録されます。

2640|  if (recursive)
2641|    extract_dirs_from_files (name, command_line_arg);
3239|static void 3240|extract_dirs_from_files (char const *dirname, | bool command_line_arg) 3241|{ 〜〜〜〜 3264| if (!dirname || f->name[0] == '/') 3265| queue_directory (f->name, f->linkname, | command_line_arg); 3266| else 3267| { 〜〜〜〜 3269| queue_directory (name, f->linkname, | command_line_arg);
2486|static void 2487|queue_directory (char const *name, char const *realname, | bool command_line_arg) 2488|{ 2489| struct pending *new = xmalloc (sizeof *new); 〜〜〜〜 2492| new->command_line_arg = command_line_arg; 2493| new->next = pending_dirs; 2494| pending_dirs = new; 2495|}
ソースコード: 1-3

ここで記録された command_line_arg 引数値は、 回り回って、 main() における print_dir() 呼び出しの際の command_line_arg 引数値として現れます。

1421|  while (pending_dirs)
1422|    {
1423|      thispend = pending_dirs;
〜〜〜〜
1444|      print_dir (thispend->name, thispend->realname,
1445|                 thispend->command_line_arg);
ソースコード: 1-4

さて、 main() での extract_dirs_from_files() 呼び出しでは、 対象が『コマンドラインで指定されたもの』ですから、 command_line_arg 引数には常に true が指定されます。

1400|  if (cwd_n_used)
1401|    {
1402|      sort_files ();
1403|      if (!immediate_dirs)
1404|        extract_dirs_from_files (NULL, true);
ソースコード: 1-5

main() における extract_dirs_from_files() 呼び出しで、 command_line_arg 引数が true であることと、 これまで説明した処理の流れを組み合わせると、 以下のような呼び出しの連鎖において、 『コマンドラインで指定されたもの』 ではない要素に対しても、 command_line_argtrue になってしまうことがわかります。

  1. 『コマンドラインで指定されたもの』に対する extract_dirs_from_files() 呼び出し (@main())
  2. 『コマンドラインで指定されたもの』に対する queue_directory() 呼び出し (@extract_dirs_from_files())
  3. 『コマンドラインで指定されたもの』に対する 単方向リスト pending_dirs 要素の記録 (@queue_directory())
  4. 単方向リスト pending_dirs 要素を使った print_dir() 呼び出し (@main())
  5. readdir(3) で得られた要素に対する extract_dirs_from_files() 呼び出し (@print_dir())
  6. readdir(3) で得られた要素に対する queue_directory() 呼び出し (@extract_dirs_from_files())
  7. readdir(3) で得られた要素に対する 単方向リスト pending_dirs 要素の記録 (@queue_directory())

さて、 間違った command_line_arg 引数値での extract_dirs_from_files() 呼び出しは、 どのような影響を及ぼすのでしょうか?

print_dir() 呼び出しにおける command_line_arg 引数は、 opendir(3)readdir(3)closedir(3) 等がエラー終了した場合の、 file_failure() 呼び出しにおける serious 引数としても使用されます。

2502|static void
2503|print_dir (char const *name, char const *realname,
    |           bool command_line_arg)
2504|{
〜〜〜〜
2511|  dirp = opendir (name);
2512|  if (!dirp)
2513|    {
2514|      file_failure (command_line_arg,
    |                    _("cannot open directory %s"), name);
2469|static void 2470|file_failure (bool serious, char const *message, | char const *file) 2471|{ 2472| error (0, errno, message, quotearg_colon (file)); 2473| set_exit_status (serious);
2456|static void 2457|set_exit_status (bool serious) 2458|{ 2459| if (serious) 2460| exit_status = LS_FAILURE; 2461| else if (exit_status == EXIT_SUCCESS) 2462| exit_status = LS_MINOR_PROBLEM; 2463|}
ソースコード: 1-6

本来 false であるべき command_line_arg 引数値が true になっていることで、 file_failure()serious 引数も true になってしまいます。 その結果、 本来であれば終了コード 1 (=LS_MINOR_PROBLEM) で終了すべきところが、 終了コード 2 (= LS_FAILURE) で終了することになります。

764|/* Exit statuses.  */
765|enum
766|  {
767|    /* "ls" had a minor problem.  E.g., while processing a directory,
768|       ls obtained the name of an entry via readdir, yet was later
769|       unable to stat that name.  This happens when listing a directory
770|       in which entries are actively being removed or renamed.  */
771|    LS_MINOR_PROBLEM = 1,
772|
773|    /* "ls" had more serious trouble (e.g., memory exhausted, invalid
774|       option or failure to stat a command line argument.  */
775|    LS_FAILURE = 2
776|  };
ソースコード: 1-7

print_dir() での extract_dirs_from_files() 呼び出しにおける command_line_arg 引数には、 明示的に false を指定すべきですから、 以下の修正が妥当でしょう。

diff --git a/src/ls.c b/src/ls.c
index e341c67..08e86ce 100644
--- a/src/ls.c
+++ b/src/ls.c
@@ -2647,7 +2647,7 @@ print_dir (char const *name, char const *realname, bool command_line_arg)
      contents listed rather than being mentioned here as files.  */

   if (recursive)
-    extract_dirs_from_files (name, command_line_arg);
+    extract_dirs_from_files (name, false);

   if (format == long_format || print_block_size)
     {
修正案

報告顛末

このバグは、 バグID 15249 として登録されています。

保守担当者からの、 『妥当な修正に見えるので、詳細確認後、修正内容を反映する』旨の返信の後、 私名義の修正が、 保守担当者によってコミットされた模様です。

patch コマンドのバージョン間互換性 (ID:15255)

問題の詳細/修正内容

Git 経由で入手したソースコードに対する ./bootstrap スクリプトを実行した場合、 バージョン 2.6 よりも古い patch コマンドを使用している環境で、 以下の様なエラーが発生してしまいます。

gnulib/gnulib-tool: *** patch file gl/modules/tempname.diff didn't apply cleanly
gnulib/gnulib-tool: *** Stop.
missing header for unified diff at line 12 of patch
The text leading up to this was:
--------------------------
|
| Files:
| lib/tempname.c
--------------------------
File to patch: EOF ※ Ctrl-D による『EOF』入力
Skip this patch? [y] ※ Enter 入力
1 out of 1 hunk ignored
gnulib/gnulib-tool: *** patch file gl/modules/tempname.diff didn't apply cleanly
gnulib/gnulib-tool: *** Stop.
./bootstrap[348]: build-aux/prefix-gnulib-mk: not found [No such file or directory]
./bootstrap: bootstrap_post_import_hook failed
./bootstrap 実行時のエラー

私の確認した範囲では、 2.6 以降の patch コマンドを使用した場合、 上記のエラーは発生しません。

bootstrap コマンドの実行可否が、 patch コマンドのバージョンに、 明らかに依存していますので、 他のユーティリティコマンドと同様に、 事前にバージョン確認を実施すべきと思われます。

bootstrap コマンド実行時における、 各種コマンドの利用可否/バージョン確認は、 設定ファイル bootstrap.conf で記述されていますので、 以下の様な修正が妥当と思われます。

diff --git a/bootstrap.conf b/bootstrap.conf
index 0863590..2535b20 100644
--- a/bootstrap.conf
+++ b/bootstrap.conf
@@ -324,7 +324,7 @@ git        1.4.4
 gperf      -
 gzip       -
 makeinfo   4.13
-patch      -
+patch      2.6
 perl       5.5
 rsync      -
 tar        -
修正案

報告顛末

このバグは、 バグID 15255 として登録されています。

保守担当者によると、 『パッチファイルを修正することで、2.6 より前の patch コマンドにも対応可能』 とのことで、 バグ報告の際に提案した『patch コマンドのバージョン確認の実施』ではなく、 パッチファイル自体の修正により、 patch コマンドバージョン間での非互換を回避する方向で、 対処するものと思われます。 具体的な対処方法に関しては、 ビルド時の留意点における 『patch コマンドのバージョン依存性』を参照してください。

なお、 保守担当者からの返信にも書かれているように、 実はこの障害は、 『2.6 より前の patch コマンドでパッチ適用が失敗』する問題ではなく、 『2.6 以降の patch コマンドは、適用できないハンクがあるのに正常終了してしまう』 という問題だった模様です。

保守担当者による、 patch コマンドのメーリングリストへの 『2.6 以降の patch コマンドの問題なのか?』 というメール投函がありましたが、 最終的には以下の様な結論に落ち着いた模様です。