[インデックス 19208] ファイルの概要
このコミットは、Go言語のリンカ (cmd/ld
) における出力バイナリの削除ロジックに関するバグ修正です。特に、出力ファイルが通常のファイル(regular file)ではない場合に誤って削除されることを防ぎ、Windows環境でのファイル操作の信頼性を向上させることを目的としています。
コミット
commit 7c7aaa4156a280960749467dfd3651b8798d420e
Author: Mike Andrews <mra@xoba.com>
Date: Fri Apr 18 15:37:55 2014 -0700
cmd/ld: don't delete output binary if not "ordinary" file (redux).
following on CL https://golang.org/cl/76810045 and
issue 7563, i now see there's another "remove(outfile)" a few
dozen lines down that also needs fixing.
LGTM=iant
R=golang-codereviews, iant
CC=0intro, golang-codereviews, r
https://golang.org/cl/89030043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7c7aaa4156a280960749467dfd3651b8798d420e
元コミット内容
このコミットは、以前の変更(CL 76810045)と関連する問題(issue 7563)に続くものです。元々の問題は、Goリンカが新しいバイナリを書き込む前に既存の出力バイナリを削除する際に、そのファイルが通常のファイルではない場合(例えば、シンボリックリンクやデバイスファイルなど)でも削除を試みてしまうことにありました。さらに、以前の修正では見落とされていた、コード内の別の remove(outfile)
呼び出しも修正する必要があることが判明しました。
変更の背景
Goリンカは、新しい実行可能ファイルを生成する際に、既存の同名ファイルを上書きする前に一度削除する挙動を持っています。これは、特にUnix系システムにおいて、実行中のバイナリや最近実行されたバイナリに直接書き込むことを避けるための一般的なプラクティスです。しかし、この削除処理が、出力先が通常のファイルではない場合(例: /dev/null
へのリダイレクト、シンボリックリンク、名前付きパイプなど)にも適用されてしまうと問題が発生します。
具体的には、以下の問題が背景にあります。
- 非通常ファイルの誤削除: 出力先がシンボリックリンクやデバイスファイルなどの「通常のファイル」ではない場合、
remove()
関数が意図しない動作を引き起こす可能性があります。例えば、シンボリックリンクを削除すると、リンク先のファイルではなくリンク自体が削除されることが期待されますが、リンカの意図としてはリンク先のファイルを上書きしたい場合があります。また、デバイスファイルを削除しようとするとエラーになるか、システムに予期せぬ影響を与える可能性があります。 - Windows環境での信頼性の問題: コミットメッセージとコード内のコメントにあるように、Windows 7では
remove()
(ファイルを削除する)の直後にcreate()
(ファイルを新規作成する)を行うと、ファイルロックやパーミッションの問題によりcreate()
が失敗することがあります。これは、remove()
がファイルを完全に解放する前にcreate()
が実行されることや、Windowsのファイルシステムがファイルハンドルをすぐに解放しない特性に起因することが多いです。この問題は、特にビルドプロセスにおいて、リンカが生成したバイナリをすぐに実行しようとする場合に顕著になります。 - 既存の修正の不完全性: 以前のCL (76810045) で同様の問題に対処しようとしましたが、コードベース内に複数の
remove(outfile)
呼び出しが存在し、そのうちの1つが見落とされていたため、問題が完全に解決されていませんでした。このコミットは、その見落とされた箇所を修正し、より堅牢なファイル削除ロジックを導入することを目的としています。
前提知識の解説
- Goリンカ (
cmd/ld
): Go言語のビルドツールチェーンの一部であり、コンパイルされたGoパッケージやライブラリを結合して、実行可能なバイナリファイルを生成する役割を担います。go build
コマンドの内部で自動的に呼び出されます。 remove()
関数 (C言語): C標準ライブラリ関数で、指定されたパスのファイルまたは空のディレクトリを削除します。lstat()
関数 (Unix系): Unix系システムコールで、指定されたパスのファイルに関する情報を取得します。stat()
と異なり、パスがシンボリックリンクである場合、リンク先のファイルではなくシンボリックリンク自体の情報を取得します。struct stat
:lstat()
やstat()
関数によって返されるファイル情報を格納するための構造体です。ファイルの種類(通常ファイル、ディレクトリ、シンボリックリンクなど)、パーミッション、サイズ、タイムスタンプなどが含まれます。S_ISREG()
マクロ (Unix系):struct stat
のst_mode
メンバー(ファイルモード)を引数に取り、そのファイルが通常のファイル(regular file)であるかどうかを判定するマクロです。通常のファイルとは、テキストファイルや実行可能ファイルなど、ディスク上に連続したデータとして格納されるファイルを指します。シンボリックリンク、ディレクトリ、デバイスファイルなどは通常のファイルとは見なされません。_WIN32
およびPLAN9
マクロ: C/C++のプリプロセッサマクロで、それぞれコンパイル対象がWindows環境であるか、Plan 9環境であるかを判定するために使用されます。これにより、OS固有のコードを条件付きでコンパイルすることができます。- ファイルロックとパーミッション (Windows): Windowsでは、ファイルが他のプロセスによって開かれている場合、そのファイルを削除したり上書きしたりすることが困難な場合があります。また、ユーザーアカウントに適切なパーミッションがない場合もファイル操作が拒否されます。
技術的詳細
このコミットの主要な技術的変更は、出力バイナリを削除するロジックを mayberemoveoutfile()
という新しい静的関数にカプセル化し、その関数内でファイルの種類をチェックする条件を追加したことです。
変更前は、libinit()
関数内と errorexit()
関数内の2箇所で remove(outfile)
が直接呼び出されていました。これらの呼び出しは、出力ファイルが通常のファイルであるかどうかをチェックするロジックを部分的に含んでいましたが、そのチェックが不完全であったり、特定のOS(WindowsやPlan 9)では適用されていなかったりしました。
新しい mayberemoveoutfile()
関数は以下のロジックを含んでいます。
- OS固有の条件分岐:
_WIN32
またはPLAN9
マクロが定義されている場合(つまり、WindowsまたはPlan 9環境の場合)は、ファイルの種類チェックを行わずに直接remove(outfile)
を呼び出します。これは、これらのOSではlstat()
やS_ISREG()
が利用できないか、あるいはUnix系システムとは異なるファイルセマンティクスを持つためです。特にWindowsでは、remove()
の直後のcreate()
の失敗を避けるために、ファイルの種類に関わらず削除を試みる必要があるという判断が背景にあります。- それ以外のUnix系システムでは、以下のファイル種類チェックを行います。
- ファイル種類チェック (Unix系):
lstat(outfile, &st)
を呼び出して、出力ファイル (outfile
) の情報を取得します。lstat()
を使用することで、outfile
がシンボリックリンクであっても、リンク先のファイルではなくシンボリックリンク自体の情報を取得します。lstat()
が成功し(== 0
)、かつ!S_ISREG(st.st_mode)
が真である場合、つまりファイルが存在し、かつそれが通常のファイルではない場合、関数はreturn
してremove(outfile)
を実行しません。これにより、シンボリックリンクやデバイスファイルなどの非通常ファイルが誤って削除されることを防ぎます。- ファイルが存在しない場合、または通常のファイルである場合は、
remove(outfile)
が実行されます。
この変更により、リンカは出力先が非通常ファイルである場合に不必要または危険な remove()
呼び出しを避け、ビルドプロセスの堅牢性が向上します。また、remove()
呼び出しを共通の関数にまとめることで、コードの重複が解消され、将来的なメンテナンスが容易になります。
コアとなるコードの変更箇所
src/cmd/ld/lib.c
ファイルが変更されています。
--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -83,6 +83,23 @@ Lflag(char *arg)
ctxt->libdir[ctxt->nlibdir++] = arg;
}
+/*
+ * Unix doesn't like it when we write to a running (or, sometimes,
+ * recently run) binary, so remove the output file before writing it.
+ * On Windows 7, remove() can force a subsequent create() to fail.
+ * S_ISREG() does not exist on Plan 9.
+ */
+static void
+mayberemoveoutfile(void)
+{
+#if !(defined(_WIN32) || defined(PLAN9))
+ struct stat st;
+ if(lstat(outfile, &st) == 0 && !S_ISREG(st.st_mode))
+ return;
+#endif
+ remove(outfile);
+}
+
void
libinit(void)
{
@@ -106,17 +123,7 @@ libinit(void)
}
Lflag(smprint("%s/pkg/%s_%s%s%s", goroot, goos, goarch, suffixsep, suffix));
-\t// Unix doesn't like it when we write to a running (or, sometimes,\n-\t// recently run) binary, so remove the output file before writing it.\n-\t// On Windows 7, remove() can force the following create() to fail.\n-\t// S_ISREG() does not exist on Plan 9.\n-#if !(defined(_WIN32) || defined(PLAN9))\n-\t{\n-\t\tstruct stat st;\n-\t\tif(lstat(outfile, &st) == 0 && S_ISREG(st.st_mode))\n-\t\t\tremove(outfile);\n-\t}\n-#endif
+\tmayberemoveoutfile();
cout = create(outfile, 1, 0775);\n if(cout < 0) {\n \t\tdiag("cannot create %s: %r", outfile);\n@@ -139,7 +146,7 @@ errorexit(void)
{\n \tif(nerrors) {\n \t\tif(cout >= 0)\n-\t\t\tremove(outfile);\n+\t\t\tmayberemoveoutfile();\n \t\texits("error");\n \t}\n \texits(0);\n```
## コアとなるコードの解説
1. **`mayberemoveoutfile()` 関数の追加**:
* この新しい静的関数は、出力ファイル (`outfile`) を削除するロジックをカプセル化します。
* `#if !(defined(_WIN32) || defined(PLAN9))` プリプロセッサディレクティブにより、WindowsとPlan 9以外のOS(主にUnix系)でのみ以下のファイル種類チェックが有効になります。
* `lstat(outfile, &st) == 0 && !S_ISREG(st.st_mode)`:
* `lstat(outfile, &st)` は、`outfile` のファイル情報を `st` 構造体に格納します。成功すると0を返します。
* `!S_ISREG(st.st_mode)` は、`st.st_mode` が示すファイルが通常のファイルではない場合に真となります。
* この条件が真の場合(ファイルが存在し、かつ通常のファイルではない場合)、関数は `return` し、`remove(outfile)` は実行されません。これにより、シンボリックリンクやデバイスファイルなどの非通常ファイルが保護されます。
* 上記の条件に合致しない場合(Windows/Plan 9環境、またはUnix系でファイルが存在しないか通常のファイルである場合)は、`remove(outfile)` が実行されます。
2. **`libinit()` 内の変更**:
* 以前は `libinit()` 関数内に直接記述されていた、出力ファイルの削除に関する条件付きロジック(`#if !(defined(_WIN32) || defined(PLAN9))` ブロック)が削除されました。
* 代わりに、新しく定義された `mayberemoveoutfile()` 関数が呼び出されるようになりました。これにより、コードの重複が解消され、ロジックが一元化されました。
3. **`errorexit()` 内の変更**:
* エラー終了時に出力ファイルを削除する `remove(outfile)` の呼び出しも、`mayberemoveoutfile()` 関数に置き換えられました。
* これにより、エラー発生時にも、非通常ファイルの保護ロジックが適用されるようになり、リンカの堅牢性がさらに向上しました。
この変更により、Goリンカは、出力先が通常のファイルである場合にのみ既存のファイルを削除し、それ以外の特殊なファイルタイプ(シンボリックリンク、デバイスファイルなど)に対しては削除操作を行わないようになりました。これは、特にクロスプラットフォームでのビルドの信頼性を高める上で重要な改善です。
## 関連リンク
* Go言語の公式リポジトリ: [https://github.com/golang/go](https://github.com/golang/go)
* Go言語のリンカ (`cmd/ld`) のソースコード: [https://github.com/golang/go/tree/master/src/cmd/link](https://github.com/golang/go/tree/master/src/cmd/link) (現在のリンカのコードは `src/cmd/link/internal/ld` にあります)
## 参考にした情報源リンク
* `cmd/ld` Go linkerに関する情報:
* [https://go.dev/doc/articles/go-build-process](https://go.dev/doc/articles/go-build-process)
* [https://medium.com/@shijuvar/understanding-go-build-process-and-linker-flags-c712217221d](https://medium.com/@shijuvar/understanding-go-build-process-and-linker-flags-c712217221d)
* `S_ISREG` および `stat` 関数に関する情報:
* [https://linuxjm.osdn.jp/html/LDP_man-pages/man2/stat.2.html](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/stat.2.html) (Linux man page for stat)
* [https://stackoverflow.com/questions/10323060/what-is-s-isreg-in-c](https://stackoverflow.com/questions/10323060/what-is-s-isreg-in-c)
* Windows 7における `remove()` および `create()` の失敗に関する情報:
* [https://learn.microsoft.com/ja-jp/windows/win32/api/fileapi/nf-fileapi-createfilew](https://learn.microsoft.com/ja-jp/windows/win32/api/fileapi/nf-fileapi-createfilew) (CreateFileW function)
* [https://stackoverflow.com/questions/100003/why-does-deletefile-fail-on-windows-when-the-file-is-not-in-use](https://stackoverflow.com/questions/100003/why-does-deletefile-fail-on-windows-when-the-file-is-not-in-use)
* [https://www.ionos.com/digitalguide/server/configuration/windows-file-in-use/](https://www.ionos.com/digitalguide/server/configuration/windows-file-in-use/)
* Go CLs (Change Lists) は通常、Googleの内部Gerritシステムで管理されており、公開されているGitHubのコミットメッセージに記載されているURL (`https://golang.org/cl/XXXX`) は、そのGerritの変更セットへのリンクです。これらのCLは、Goの公式リポジトリにマージされるとGitHubのコミットとして反映されます。
* Go issue 7563については、公開されているGoのissueトラッカーでは直接見つかりませんでしたが、コミットメッセージの文脈から、出力バイナリの削除に関する問題であったと推測されます。