[インデックス 13830] ファイルの概要
このコミットは、Go言語の os/exec
パッケージにおいて、ファイルディスクリプタ(FD)が不足した場合に Command.Start
メソッドがクラッシュする可能性があったバグを修正するものです。具体的には、/dev/null
を開く際にエラーが発生した場合に、nil の *os.File
オブジェクトがクリーンアップリストに追加され、その後の処理でパニックを引き起こす問題を解決しています。
コミット
commit 5c5c2c8112f774b118b9251eb15c2df529ad454c
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Sep 14 13:40:22 2012 -0700
os/exec: don't crash when out of fds
Command.Start could crash before if no fds were available
because a nil *os.File of /dev/null was added to the cleanup
list, which crashed before returning the proper error.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/6514043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5c5c2c8112f774b118b9251eb15c2df529ad454c
元コミット内容
os/exec: don't crash when out of fds
Command.Start
は、ファイルディスクリプタが利用できない場合にクラッシュする可能性がありました。これは、/dev/null
の nil の *os.File
がクリーンアップリストに追加され、適切なエラーを返す前にクラッシュしたためです。
変更の背景
Go言語の os/exec
パッケージは、外部コマンドを実行するための機能を提供します。外部コマンドを実行する際には、そのコマンドの標準入力 (stdin)、標準出力 (stdout)、標準エラー出力 (stderr) を設定する必要があります。多くの場合、これらのストリームが明示的に設定されていない場合、システムは /dev/null
(Windows では NUL
) を使用して、不要な出力を破棄したり、入力ソースを提供したりします。
このコミットが行われる前は、os/exec
パッケージ内で /dev/null
を開く処理において、ファイルディスクリプタが不足しているなどの理由で os.Open
や os.OpenFile
がエラーを返した場合に、そのエラーが適切に処理されていませんでした。具体的には、エラーが発生して *os.File
オブジェクトが nil
になったにもかかわらず、その nil
オブジェクトが c.closeAfterStart
というクリーンアップリストに追加されていました。
Command.Start
メソッドが実行され、外部コマンドの起動が試みられた後、この c.closeAfterStart
リストに登録されたファイルディスクリプタをクリーンアップ(閉じる)処理が実行されます。この際、リストに nil
の *os.File
オブジェクトが含まれていると、nil
ポインタに対するメソッド呼び出しが発生し、Goランタイムがパニック(クラッシュ)するという問題がありました。
この問題は、システムがファイルディスクリプタを使い果たしているような、リソースが枯渇した状況で発生しやすく、アプリケーション全体の安定性を損なう可能性がありました。したがって、このコミットは、このようなエッジケースにおける堅牢性を向上させるために導入されました。
前提知識の解説
1. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、プロセスが開いているファイルやソケット、パイプなどのI/Oリソースを識別するために使用される整数値です。各プロセスには利用可能なファイルディスクリプタの最大数(通常は制限がある)があり、これを使い果たすと新たなファイルを開くことができなくなります。
2. os/exec
パッケージ (Go言語)
os/exec
パッケージは、Goプログラムから外部のシステムコマンドを実行するための機能を提供します。exec.Command
関数でコマンドと引数を指定し、Cmd
構造体を作成します。Cmd
構造体には、標準入力 (Stdin
)、標準出力 (Stdout
)、標準エラー出力 (Stderr
) などのフィールドがあり、これらを io.Reader
や io.Writer
インターフェースを実装するオブジェクトに設定することで、コマンドのI/Oを制御できます。
3. os.DevNull
os.DevNull
は、Go言語の os
パッケージで提供される定数で、オペレーティングシステムが提供する「nullデバイス」のパスを表します。Unix系システムでは /dev/null
、Windowsでは NUL
に相当します。
/dev/null
に書き込まれたデータはすべて破棄されます。/dev/null
から読み取ろうとすると、即座にEOF (End Of File) が返されます。 これは、不要な出力を捨てる場合や、入力が不要な場合にダミーのI/Oストリームとして利用されます。
4. os.Open
と os.OpenFile
os.Open(name string) (*File, error)
: 指定されたパスのファイルを読み取り専用で開きます。os.OpenFile(name string, flag int, perm FileMode) (*File, error)
: 指定されたパスのファイルを、指定されたフラグ(読み書きモード、作成、追記など)とパーミッションで開きます。
これらの関数は、ファイルを開くことに成功した場合に *os.File
型のポインタと nil
エラーを返します。ファイルを開くことに失敗した場合(例: ファイルが存在しない、パーミッションがない、ファイルディスクリプタが不足しているなど)は、nil
の *os.File
ポインタと非nil
のエラーを返します。
5. Command.Start
Cmd
構造体の Start()
メソッドは、外部コマンドを非同期で起動します。コマンドが正常に起動された場合、nil
エラーを返します。起動に失敗した場合(例: コマンドが見つからない、パーミッションがない、I/O設定に問題があるなど)は、非nil
のエラーを返します。
6. Go言語のエラーハンドリング
Go言語では、関数がエラーを返す場合、通常は戻り値の最後の要素として error
型の値を返します。慣習として、エラーが発生しなかった場合は nil
を返し、エラーが発生した場合は非nil
の error
オブジェクトを返します。呼び出し元は、返されたエラーが nil
かどうかをチェックすることで、処理が成功したかどうかを判断します。
f, err := os.Open("somefile.txt")
if err != nil {
// エラー処理
return nil, err
}
// f を使用した処理
このコミットの背景にある問題は、このエラーチェックが不十分であったために発生しました。
技術的詳細
このバグは、os/exec
パッケージ内の stdin()
および writerDescriptor()
メソッドに存在していました。これらのメソッドは、Cmd
構造体の Stdin
や Stdout
/Stderr
が明示的に設定されていない場合に、デフォルトで /dev/null
を開いて *os.File
オブジェクトを生成します。
問題のコードは以下のようになっていました(修正前を想定):
// stdin() メソッド内 (修正前)
func (c *Cmd) stdin() (f *os.File, err error) {
if c.Stdin == nil {
f, err = os.Open(os.DevNull)
// ここで err が nil でない場合でも、f は nil のまま
c.closeAfterStart = append(c.closeAfterStart, f) // nil が追加される
return
}
// ...
}
// writerDescriptor() メソッド内 (修正前)
func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
if w == nil {
f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0)
// ここで err が nil でない場合でも、f は nil のまま
c.closeAfterStart = append(c.closeAfterStart, f) // nil が追加される
return
}
// ...
}
ここで重要なのは、os.Open
や os.OpenFile
がエラーを返した場合、変数 f
は nil
のままになるという点です。しかし、その後の c.closeAfterStart = append(c.closeAfterStart, f)
の行では、f
が nil
であっても、その nil
値が closeAfterStart
スライスに追加されてしまいます。
Command.Start
メソッドが外部コマンドを起動した後、内部的には closeAfterStart
リストに登録されたファイルディスクリプタを閉じる処理が実行されます。このクリーンアップ処理は、リスト内の各 *os.File
オブジェクトに対して Close()
メソッドを呼び出そうとします。
nil
ポインタに対してメソッドを呼び出すと、Goランタイムはパニックを発生させます。したがって、ファイルディスクリプタが不足しているなどの理由で /dev/null
を開くことに失敗し、nil
の *os.File
がクリーンアップリストに追加された場合、Command.Start
がエラーを返す前にアプリケーションがクラッシュするという問題が発生していました。
このコミットは、os.Open
や os.OpenFile
の呼び出し直後にエラーチェックを追加し、エラーが発生した場合はすぐに return
するように変更することで、この問題を解決しています。これにより、nil
の *os.File
オブジェクトが c.closeAfterStart
リストに追加されることを防ぎ、パニックを回避しています。
コアとなるコードの変更箇所
--- a/src/pkg/os/exec/exec.go
+++ b/src/pkg/os/exec/exec.go
@@ -143,6 +143,9 @@ func (c *Cmd) argv() []string {\n func (c *Cmd) stdin() (f *os.File, err error) {\n \tif c.Stdin == nil {\n \t\tf, err = os.Open(os.DevNull)\n+\t\tif err != nil {\n+\t\t\treturn\n+\t\t}\n \t\tc.closeAfterStart = append(c.closeAfterStart, f)\n \t\treturn\n \t}\n@@ -182,6 +185,9 @@ func (c *Cmd) stderr() (f *os.File, err error) {\n func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {\n \tif w == nil {\n \t\tf, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0)\n+\t\tif err != nil {\n+\t\t\treturn\n+\t\t}\n \t\tc.closeAfterStart = append(c.closeAfterStart, f)\n \t\treturn\n \t}\
コアとなるコードの解説
変更は src/pkg/os/exec/exec.go
ファイルの2箇所にあります。
-
stdin()
メソッド内:func (c *Cmd) stdin() (f *os.File, err error) { if c.Stdin == nil { f, err = os.Open(os.DevNull) if err != nil { // 追加された行 return // 追加された行 } // 追加された行 c.closeAfterStart = append(c.closeAfterStart, f) return } // ... }
os.Open(os.DevNull)
の呼び出し後、すぐにif err != nil { return }
というエラーチェックが追加されました。これにより、/dev/null
を開く際にエラーが発生した場合(例: ファイルディスクリプタ不足)、関数は直ちにエラーを返して終了します。この時点でf
はnil
のままであり、c.closeAfterStart
にnil
が追加されることを防ぎます。 -
writerDescriptor()
メソッド内:func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) { if w == nil { f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0) if err != nil { // 追加された行 return // 追加された行 } // 追加された行 c.closeAfterStart = append(c.closeAfterStart, f) return } // ... }
同様に、
os.OpenFile(os.DevNull, os.O_WRONLY, 0)
の呼び出し後にもif err != nil { return }
が追加されました。これにより、書き込みモードで/dev/null
を開く際にエラーが発生した場合も、同様に直ちにエラーを返して終了し、nil
の*os.File
がクリーンアップリストに追加されるのを防ぎます。
これらの変更により、ファイルディスクリプタの枯渇などのシステムリソースの問題が発生した場合でも、os/exec
パッケージが堅牢に動作し、予期せぬパニックによるアプリケーションのクラッシュを防ぐことができるようになりました。エラーは適切に呼び出し元に伝播され、呼び出し元で適切なエラーハンドリングを行うことが可能になります。
関連リンク
- Go CL 6514043: https://golang.org/cl/6514043
参考にした情報源リンク
- Go言語
os/exec
パッケージのドキュメント: https://pkg.go.dev/os/exec - Go言語
os
パッケージのドキュメント: https://pkg.go.dev/os - ファイルディスクリプタに関する一般的な情報 (Wikipediaなど)
- Go言語のエラーハンドリングに関する一般的な情報