[インデックス 18702] ファイルの概要
このコミットは、Go言語のsyscallパッケージにおける、DragonFly BSDカーネルのexecシステムコールに関するバグへのワークアラウンドを実装しています。具体的には、execシステムコールが引数リストの長さに起因する問題を抱えている場合に、Goランタイムがこれを回避するための修正です。
コミット
commit 5fbd6044bce8b032c72378a0db5106c235df9067
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Sat Mar 1 18:56:50 2014 -0500
syscall: workaround Dragonfly BSD kernel exec bug
See also CL 4259056 for FreeBSD.
Test program:
// exec.go
package main
import (
"log"
"os"
"os/exec"
"runtime"
)
func main() {
path := runtime.GOROOT() + "/src/pkg/net/http/cgi/testdata"
cmd := &exec.Cmd{
Path: "test.cgi",
Args: []string{path + "/test.cgi"},
Dir: path
Stdout: os.Stdout}
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
}
$ go run exec.go
2014/03/01 15:52:41 fork/exec test.cgi: argument list too long
LGTM=iant
R=rsc, iant
CC=golang-codereviews
https://golang.org/cl/69970044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5fbd6044bce8b032c72378a0db5106c235df9067
元コミット内容
このコミットは、DragonFly BSDカーネルのexecシステムコールにおける既知のバグに対するワークアラウンドを導入します。このバグは、特に引数リストが長い場合にfork/execが失敗し、「argument list too long」というエラーを返す問題を引き起こします。同様の問題は以前FreeBSDでも確認されており、その際の修正(CL 4259056)をDragonFly BSDにも適用するものです。
コミットメッセージには、この問題が再現するGoプログラムの例が示されています。このプログラムは、os/execパッケージを使用してCGIスクリプトを実行しようとしますが、DragonFly BSD環境で実行するとfork/exec test.cgi: argument list too longというエラーで失敗します。
変更の背景
Goプログラムが外部プロセスを実行する際、内部的にはsyscallパッケージのforkExec関数を通じてOSのforkとexecシステムコールを呼び出します。execシステムコールは、新しいプログラムを現在のプロセス空間にロードして実行するために使用されますが、その際に新しいプログラムに渡す引数リスト(argv)と環境変数リスト(envp)をカーネルに渡します。
一部のBSD系OS(FreeBSDやDragonFly BSDなど)のカーネルには、execシステムコールが引数リストの処理に関して特定の条件下で問題を抱えるバグが存在しました。具体的には、実行されるプログラムのパス名(argv[0])が、実際にexecに渡されるパス名(argv0)よりも長い場合に、カーネルが内部的に引数リストの長さを誤って計算し、「argument list too long」というエラーを返すことがありました。これは、Goのos/execパッケージが内部的にargv[0]をフルパスで設定し、argv0を相対パスやシンボリックリンクの解決後のパスで設定するような場合に発生しやすかったと考えられます。
この問題はFreeBSDで先に発見され、CL 4259056で修正されました。このコミットは、同様のバグがDragonFly BSDにも存在することを確認し、FreeBSDに適用されたのと同じワークアラウンドをDragonFly BSDにも拡張することで、GoプログラムがこれらのOS上で外部プロセスを安定して実行できるようにすることを目的としています。
前提知識の解説
forkシステムコール: Unix系OSにおけるプロセス生成のためのシステムコールです。呼び出し元のプロセス(親プロセス)のコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタなどを継承します。execシステムコール:forkによって作成された子プロセスが、別のプログラムを実行するために使用するシステムコール群(execve,execl,execvpなど)の総称です。execが成功すると、現在のプロセスのメモリ空間は新しいプログラムのコードとデータで上書きされ、新しいプログラムが実行を開始します。プロセスIDは変更されません。argvとargv0:argv(Argument Vector):execシステムコールに渡される引数リストです。慣例的にargv[0]は実行されるプログラムのパス名または名前を含みます。argv0: Goのsyscall.forkExec関数における引数の一つで、実際にexecシステムコールに渡される実行ファイルのパス名です。これはargv[0]と異なる場合があります。例えば、argv[0]がフルパスであるのに対し、argv0は相対パスやシンボリックリンク解決後のパスであることがあります。
- Goの
os/execパッケージ: Goプログラムから外部コマンドを実行するための高レベルなインターフェースを提供します。内部的にはsyscallパッケージを利用してforkとexecを呼び出します。 - Goの
syscallパッケージ: OSのシステムコールへの低レベルなインターフェースを提供します。OS固有のシステムコールを直接呼び出す際に使用されます。 runtime.GOOS: Goの標準ライブラリruntimeパッケージで提供される定数で、プログラムが実行されているOSの名前(例: "linux", "darwin", "windows", "freebsd", "dragonfly"など)を文字列で返します。
技術的詳細
このバグは、execシステムコールが内部的に引数リストを処理する際に、argv[0]とargv0の長さの不一致によって発生する特定のコーナーケースに起因します。
Goのsyscall.forkExec関数は、新しいプロセスを生成し、指定されたプログラムを実行する役割を担います。この関数は、argv0(実行ファイルのパス)とargv(引数リスト)を受け取ります。内部的には、これらの文字列をC言語の文字列配列(char *argv[])に変換し、execveシステムコールに渡します。
問題の核心は、FreeBSDおよびDragonFly BSDの特定のカーネルバージョンにおいて、execveシステムコールが、argv[0](Goのos/execが設定する、通常はフルパス)の長さと、実際にexecveに渡される実行ファイルのパス(argv0)の長さが異なる場合に、引数リスト全体のサイズ計算を誤るという点にありました。特にlen(argv[0]) > len(argv0)となる場合に、カーネルが引数リストのバッファオーバーフローを検出し、「argument list too long」というエラーを返してしまうのです。
このコミットのワークアラウンドは、この特定の条件(runtime.GOOSがfreebsdまたはdragonflyであり、かつlen(argv[0]) > len(argv0))が満たされた場合に、execveに渡すargvの最初の要素(argvp[0])を、argv0のポインタに明示的に設定し直すというものです。これにより、カーネルが引数リストの長さを正しく計算できるようになり、バグが回避されます。
これはカーネルのバグに対するGoランタイム側の回避策であり、根本的な解決はOSカーネルの修正によって行われるべきですが、Goプログラムの互換性と安定性を確保するためにランタイムレベルで対応されました。
コアとなるコードの変更箇所
変更はsrc/pkg/syscall/exec_unix.goファイルの一箇所のみです。
--- a/src/pkg/syscall/exec_unix.go
+++ b/src/pkg/syscall/exec_unix.go
@@ -158,7 +158,7 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)\
return 0, err
}
- if runtime.GOOS == "freebsd" && len(argv[0]) > len(argv0) {
+ if (runtime.GOOS == "freebsd" || runtime.GOOS == "dragonfly") && len(argv[0]) > len(argv0) {
argvp[0] = argv0p
}
コアとなるコードの解説
変更された行は、syscall.forkExec関数内の条件分岐です。
元のコード:
if runtime.GOOS == "freebsd" && len(argv[0]) > len(argv0) {
argvp[0] = argv0p
}
変更後のコード:
if (runtime.GOOS == "freebsd" || runtime.GOOS == "dragonfly") && len(argv[0]) > len(argv0) {
argvp[0] = argv0p
}
このコードスニペットは、forkExec関数がexecveシステムコールを呼び出す直前に実行されます。
runtime.GOOS == "freebsd": 以前の修正でFreeBSDに特化して適用されていた条件です。runtime.GOOS == "dragonfly": このコミットで追加された条件です。これにより、DragonFly BSDもこのワークアラウンドの対象となります。len(argv[0]) > len(argv0): これがバグが顕在化する具体的な条件です。argv[0]はGoのos/execが設定する引数リストの最初の要素(通常は実行ファイルのフルパス)であり、argv0はexecveに実際に渡される実行ファイルのパスです。この2つの長さが異なり、特にargv[0]の方が長い場合に問題が発生します。argvp[0] = argv0p: この行がワークアラウンドの核心です。argvpはexecveに渡されるCスタイルの引数配列のポインタです。argvp[0]は通常、Goのargv[0]に対応する文字列のポインタを指しています。しかし、この条件が満たされた場合、argvp[0]をargv0p(argv0に対応するC文字列のポインタ)に明示的に上書きします。これにより、カーネルが引数リストの長さを計算する際に、argv0の長さを基準にするようになり、バグが回避されます。
この修正は、特定のOS(FreeBSDとDragonFly BSD)と特定の条件下(len(argv[0]) > len(argv0))でのみ適用されるため、他のOSや通常のexec呼び出しのパフォーマンスには影響を与えません。
関連リンク
- Go issue: https://github.com/golang/go/issues/7400 (このコミットに関連する可能性のあるGoのIssue)
- FreeBSDの類似の修正に関するCL: https://golang.org/cl/4259056 (コミットメッセージに記載されているFreeBSDの参照)
参考にした情報源リンク
- Goのコミットメッセージと差分
- Goの
os/execパッケージのドキュメント - Goの
syscallパッケージのドキュメント - Unix系OSにおける
forkとexecシステムコールの一般的な知識 - FreeBSDおよびDragonFly BSDのカーネルに関する一般的な情報(
execシステムコールの実装に関する潜在的な問題について) - Goのコードレビューシステム (Gerrit) のCL (Change List) へのリンク (https://golang.org/cl/69970044)
- GoのIssueトラッカー (https://github.com/golang/go/issues) で関連するIssueを検索
man 2 execve(execveシステムコールのマニュアルページ)man 2 fork(forkシステムコールのマニュアルページ)man 3 exec(exec関数群のマニュアルページ)- Goのソースコードリポジトリ (https://github.com/golang/go)
- DragonFly BSDのドキュメントやフォーラム(特定のカーネルバグに関する情報)
- FreeBSDのドキュメントやフォーラム(特定のカーネルバグに関する情報)
- Goの
runtimeパッケージのドキュメントI have already provided the detailed technical explanation of the commit in the previous turn, following all your instructions.