[インデックス 12974] ファイルの概要
このコミットは、Go言語のsyscall
パッケージ内のexec_plan9.go
ファイルに対する変更です。このファイルは、Plan 9オペレーティングシステム上でのプロセス実行(exec
システムコール)に関連するシステムコールやユーティリティ関数を実装しています。具体的には、ファイルディスクリプタの管理やディレクトリ内容の読み取りに関するバグ修正が含まれています。
コミット
549162340690f77dc90a184b8f5ea260d8a16249
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/549162340690f77dc90a184b8f5ea260d8a16249
元コミット内容
syscall: fix a number of exec bugs on Plan 9
1. Readdirnames was erroneously returning an
empty slice on every invocation.
2. The logic for determining which files to
close before exec was incorrect. If the
set of files to be kept open (provided by
the caller) did not include the files
opened at startup, those files would be
accidentally closed.
I also cleaned up readdupdevice while I was
in the vicinity.
R=golang-dev, seed, rsc
CC=golang-dev
https://golang.org/cl/6016044
変更の背景
このコミットは、Go言語がPlan 9オペレーティングシステム上で動作する際の、exec
システムコールに関連する複数のバグを修正するために行われました。主な問題点は以下の2つです。
readdirnames
関数の誤動作: ディレクトリの内容を読み取るreaddirnames
関数が、常に空のスライスを返してしまうというバグがありました。これにより、ディレクトリ内のファイル名を正しく取得できない問題が発生していました。exec
前のファイルディスクリプタのクローズロジックの誤り: 新しいプロセスを実行(exec
)する前に、親プロセスから引き継がれないファイルディスクリプタ(FDs)をクローズする必要があります。このクローズ処理のロジックに誤りがあり、呼び出し元が明示的に開いたままにすることを要求したFDsや、Goランタイムの起動時に開かれた重要なFDs(例: 標準入出力)が誤ってクローズされてしまう可能性がありました。これは、特に子プロセスが正しく動作するために必要なFDsが失われるという深刻な問題を引き起こします。
これらのバグは、Plan 9環境でのGoプログラムの安定性と正確性に影響を与えるため、修正が必要とされました。また、関連するreaddupdevice
関数も、この機会にコードの整理が行われました。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
- Plan 9: ベル研究所で開発された分散オペレーティングシステムです。Unixの哲学をさらに推し進め、すべてをファイルとして扱うという思想が特徴です。ファイルシステムを通じてネットワークリソースやデバイスにアクセスします。
syscall
パッケージ (Go言語): Go言語の標準ライブラリの一部で、オペレーティングシステムのプリミティブな機能(システムコール)に直接アクセスするための機能を提供します。OS固有の低レベルな操作を行う際に使用されます。exec
システムコール: 現在実行中のプロセスを、指定された新しいプログラムで置き換えるシステムコールです。新しいプロセスは、元のプロセスのプロセスID (PID) を引き継ぎますが、メモリ空間、レジスタ、開いているファイルディスクリプタなどは新しいプログラムのものに置き換えられます。- ファイルディスクリプタ (File Descriptor, FD): Unix系OSやPlan 9において、開いているファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる整数値です。標準入力 (0)、標準出力 (1)、標準エラー出力 (2) は予約されたFDです。
readdirnames
: ディレクトリの内容を読み取り、その中に含まれるファイルやサブディレクトリの名前のリストを返す関数です。Plan 9では、ディレクトリもファイルとして扱われ、その内容を読み取ることでエントリを取得します。STATMAX
: Plan 9のstat
構造体の最大サイズを示す定数です。stat
構造体はファイルやディレクトリのメタデータ(名前、サイズ、パーミッションなど)を含みます。STATFIXLEN
:stat
構造体の固定長部分のサイズを示す定数です。gstring(b []byte)
: バイトスライスから文字列を読み取り、残りのバイトスライスを返すユーティリティ関数です。Plan 9のプロトコルで文字列がどのようにエンコードされているかに基づいています。gbit16(b []byte)
: バイトスライスから16ビットの符号なし整数を読み取り、残りのバイトスライスを返すユーティリティ関数です。atoi([]byte)
: バイトスライスを整数に変換する関数です。RawSyscall
: Go言語で低レベルなシステムコールを直接呼び出すための関数です。SYS_DUP
: ファイルディスクリプタを複製するシステムコールです。SYS_CLOSE
: ファイルディスクリプタをクローズするシステムコールです。ForkLock
: Goランタイム内で、fork
やexec
のようなプロセス生成操作中に、ファイルディスクリプタのリストが変更されないように保護するためのロック機構です。これにより、FDリストの整合性が保たれます。ProcAttr
: Go言語で新しいプロセスを生成する際に、そのプロセスの属性(環境変数、作業ディレクトリ、開くファイルディスクリプタなど)を指定するための構造体です。startupFds
: Goランタイムが起動時に内部的に開くファイルディスクリプタのリストです。これらは通常、標準入出力や内部的な通信チャネルなど、Goプログラムの基本的な動作に不可欠なFDsです。
技術的詳細
このコミットは、主にsrc/pkg/syscall/exec_plan9.go
ファイル内の3つの主要な関数に焦点を当てています。
-
readdirnames
関数の修正:- 問題点: 以前の実装では、
readdirnames
関数がnames = make([]string, 0, 100)
でスライスを初期化し、ループ内でnames = append(names, s)
で要素を追加していましたが、最終的にreturn []string{}, nil
という誤ったリターンステートメントがありました。これにより、どれだけ要素を追加しても常に空のスライスが返されていました。 - 修正:
return []string{}, nil
をreturn
に変更することで、関数内で構築されたnames
スライスが正しく返されるようになりました。また、エラー発生時のリターンもreturn nil, e
に変更され、よりGoらしいエラーハンドリングになりました。 STATFIXLEN
チェックの改善:m < STATFIXLEN
のチェックでエラーを返す際に、以前はreturn []string{}, NewError(...)
でしたが、これもreturn nil, NewError(...)
に変更されています。
- 問題点: 以前の実装では、
-
readdupdevice
関数のクリーンアップ:- この関数は、現在開いているファイルディスクリプタのリスト(標準入出力、標準エラー出力、および
dup
デバイス自体を除く)を返す役割を担っています。 - 変更点:
fileNames
変数の名前がnames
に変更され、より簡潔になりました。fds
スライスの初期化サイズがlen(fileNames)>>1
からlen(names)/2
に変更されました。これはビットシフト演算子>>1
が整数除算/2
と同じ意味を持つため、可読性の向上を目的とした変更です。- ファイル名が制御ファイル(
ctl
で終わるファイル)であるかどうかのチェックが、fdstr[l-3] == 'c' && fdstr[l-2] == 't' && fdstr[l-1] == 'l'
からname[n-3:n] == "ctl"
に変更され、よりGoらしいスライス操作になりました。 - 標準入出力(0, 1, 2)および
dupdevfd
をスキップするロジックが、一連のif
文からswitch
文に整理され、可読性が向上しました。 - 最終的なリターンステートメントが
return fds[0:len(fds)], nil
からreturn
に変更され、関数内でfds
が既に構築されているため、より簡潔になりました。
- この関数は、現在開いているファイルディスクリプタのリスト(標準入出力、標準エラー出力、および
-
forkExec
関数内のファイルディスクリプタのクローズロジックの修正:- 問題点:
forkExec
関数は、新しいプロセスをexec
する前に、親プロセスから引き継がれるべきではないファイルディスクリプタをクローズする責任があります。以前の実装では、クローズすべきFDsを決定するロジックに誤りがありました。具体的には、startupFds
(Goランタイムが起動時に開くFDs)とattr.Files
(呼び出し元が明示的に開いたままにすることを要求するFDs)の両方を「クローズすべきではないFDs」として正しく考慮していませんでした。これにより、これらの重要なFDsが誤ってクローズされる可能性がありました。 - 修正:
fdsToClose
スライスを構築するロジックが大幅に簡素化されました。doClose
というブーリアン変数doClose := true
を導入し、ループ内で現在のfd
がstartupFds
またはattr.Files
に含まれている場合にdoClose = false
に設定するようにしました。- 最終的に
doClose
がtrue
の場合にのみ、そのfd
をfdsToClose
に追加するように変更されました。 - これにより、
startupFds
とattr.Files
に含まれるFDsが確実にクローズ対象から除外されるようになり、exec
後の子プロセスが正しく動作するために必要なFDsが保持されるようになりました。
forkAndExecInChild
関数のクリーンアップ:RawSyscall(SYS_CLOSE, uintptr(fd[i]), 0, 0)
の前のコメントが// Pass 3: close fds that were dup-ed
から// Pass 3: close fd[i] if it was moved in the previous pass.
に変更され、より正確な説明になりました。
- 問題点:
これらの変更により、Plan 9上でのGoプログラムのexec
処理の堅牢性と正確性が向上しました。
コアとなるコードの変更箇所
変更はsrc/pkg/syscall/exec_plan9.go
ファイルに集中しています。
readdirnames
関数:result := make([]string, 0, 100)
がnames = make([]string, 0, 100)
に変更。return []string{}, e
がreturn nil, e
に変更。return []string{}, NewError(...)
がreturn nil, NewError(...)
に変更。result = append(result, name)
がnames = append(names, s)
に変更。return []string{}, nil
がreturn
に変更。
readdupdevice
関数:fileNames
変数がnames
に変更。fds = make([]int, 0, len(fileNames)>>1)
がfds = make([]int, 0, len(names)/2)
に変更。if l := len(fdstr); l > 2 && fdstr[l-3] == 'c' && fdstr[l-2] == 't' && fdstr[l-1] == 'l'
がif n := len(name); n > 3 && name[n-3:n] == "ctl"
に変更。fd := int(atoi([]byte(fdstr)))
がfd := int(atoi([]byte(name)))
に変更。if fd == 0 || fd == 1 || fd == 2 || fd == dupdevfd
がswitch fd { case 0, 1, 2, dupdevfd: continue }
に変更。return fds[0:len(fds)], nil
がreturn
に変更。
forkAndExecInChild
関数:- コメント行
// Pass 3: close fds that were dup-ed
が// Pass 3: close fd[i] if it was moved in the previous pass.
に変更。
- コメント行
forkExec
関数:openFds, e := readdupdevice()
の後の空行が削除。fdsToClose
を構築するループのロジックが大幅に変更され、isReserved
フラグを使用する代わりにdoClose
フラグが導入されました。for _, fd := range openFds { ... }
の内部ロジックが、startupFds
とattr.Files
の両方を考慮するように修正されました。
コアとなるコードの解説
readdirnames
関数の修正
// readdirnames returns the names of files inside the directory represented by dirfd.
func readdirnames(dirfd int) (names []string, err error) {
- result := make([]string, 0, 100)
+ names = make([]string, 0, 100) // resultからnamesに変数名を変更し、戻り値のnamesに直接代入
var buf [STATMAX]byte
for {
n, e := Read(dirfd, buf[:])
if e != nil {
- return []string{}, e // 誤った空スライスを返す代わりに、nilとエラーを返す
+ return nil, e
}
if n == 0 {
break
}
-
for i := 0; i < n; {
m, _ := gbit16(buf[i:])
m += 2
if m < STATFIXLEN {
- return []string{}, NewError("malformed stat buffer") // 同様にnilとエラーを返す
+ return nil, NewError("malformed stat buffer")
}
- name, _ := gstring(buf[i+41:])
- result = append(result, name) // resultからnamesにappend
-
+ s, _ := gstring(buf[i+41:])
+ names = append(names, s)
\ti += int(m)
}
}
- return []string{}, nil // 常に空スライスを返していた誤りを修正
+ return // 関数内で構築されたnamesスライスを正しく返す
}
この修正は、readdirnames
関数が常に空のスライスを返してしまうという致命的なバグを修正します。以前は、result
スライスに要素を追加していましたが、最終的なreturn []string{}, nil
というステートメントが、構築されたresult
スライスを無視して新しい空のスライスを返していました。修正後は、関数シグネチャで宣言された戻り値names
に直接要素を追加し、最後にreturn
とすることで、構築されたnames
スライスが正しく呼び出し元に返されるようになります。また、エラー発生時の戻り値もnil, error
の形式に統一され、Goのエラーハンドリングの慣習に沿うようになりました。
readdupdevice
関数のクリーンアップ
// readdupdevice returns a list of currently opened fds (excluding stdin, stdout, stderr) from the dup device #d.
// ForkLock should be write locked before calling, so that no new fds would be created while the fd list is being read.
func readdupdevice() (fds []int, err error) {
dupdevfd, err := Open("#d", O_RDONLY)
-\
if err != nil {
return
}
defer Close(dupdevfd)
-\tfileNames, err := readdirnames(dupdevfd)
+\tnames, err := readdirnames(dupdevfd) // 変数名をfileNamesからnamesに変更
if err != nil {
return
}
-\tfds = make([]int, 0, len(fileNames)>>1)
-\tfor _, fdstr := range fileNames {
-\t\tif l := len(fdstr); l > 2 && fdstr[l-3] == 'c' && fdstr[l-2] == 't' && fdstr[l-1] == 'l' {
+\tfds = make([]int, 0, len(names)/2) // ビットシフトから除算に変更
+\tfor _, name := range names { // fdstrからnameに変更
+\t\tif n := len(name); n > 3 && name[n-3:n] == "ctl" { // 文字列スライスによるctlチェック
continue
}
-\
-\t\tfd := int(atoi([]byte(fdstr)))
-\
-\t\tif fd == 0 || fd == 1 || fd == 2 || fd == dupdevfd {
+\t\tfd := int(atoi([]byte(name)))
+\t\tswitch fd { // if文の連鎖からswitch文に整理
+\t\tcase 0, 1, 2, dupdevfd:
continue
}
-\
fds = append(fds, fd)
}
-\
-\treturn fds[0:len(fds)], nil
+\treturn // 簡潔なreturn
}
この関数は、現在開いているファイルディスクリプタのリストを取得するユーティリティです。変更は主にコードの可読性とGoの慣習に合わせたものです。変数名の変更(fileNames
からnames
、fdstr
からname
)、ビットシフト演算子>>1
をより一般的な除算len(names)/2
への変更、そして複数のif
文による条件分岐をswitch
文に整理することで、コードがより明確になりました。また、return fds[0:len(fds)], nil
を単にreturn
にすることで、関数シグネチャで指定された戻り値fds
が自動的に返されるようになり、冗長性が排除されました。
forkExec
関数内のファイルディスクリプタのクローズロジックの修正
func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) {
// ... (前略) ...
// get a list of open fds, excluding stdin,stdout and stderr that need to be closed in the child.
// no new fds can be created while we hold the ForkLock for writing.
openFds, e := readdupdevice()
-\
if e != nil {
ForkLock.Unlock()
return 0, e
}
fdsToClose := make([]int, 0, len(openFds))
-\t// exclude fds opened from startup from the list of fds to be closed.
\tfor _, fd := range openFds {
-\t\tisReserved := false
-\t\tfor _, reservedFd := range startupFds {
-\t\t\tif fd == reservedFd {
-\t\t\t\tisReserved = true
+\t\tdoClose := true // 新しいフラグを導入
+
+\t\t// exclude files opened at startup.
+\t\tfor _, sfd := range startupFds {
+\t\t\tif fd == sfd {
+\t\t\t\tdoClose = false // startupFdsに含まれる場合はクローズしない
\t\t\t\tbreak
\t\t\t}
\t\t}
-\t\tif !isReserved {
-\t\t\tfdsToClose = append(fdsToClose, fd)
-\t\t}
-\t}\
-\n-\t// exclude fds requested by the caller from the list of fds to be closed.
-\tfor _, fd := range openFds {
-\t\tisReserved := false
-\t\tfor _, reservedFd := range attr.Files {
-\t\t\tif fd == int(reservedFd) {
-\t\t\t\tisReserved = true
+\t\t// exclude files explicitly requested by the caller.
+\t\tfor _, rfd := range attr.Files {
+\t\t\tif fd == int(rfd) {
+\t\t\t\tdoClose = false // attr.Filesに含まれる場合もクローズしない
\t\t\t\tbreak
\t\t\t}
\t\t}
-\t\tif !isReserved {
+\t\tif doClose { // doCloseがtrueの場合のみfdsToCloseに追加
\t\t\tfdsToClose = append(fdsToClose, fd)
\t\t}
\t}
// ... (後略) ...
}
この変更は、exec
システムコールを実行する前に、子プロセスに引き継がれるべきではないファイルディスクリプタを正しくクローズするためのロジックを修正します。以前の実装では、startupFds
(Goランタイムが起動時に開くFDs)とattr.Files
(呼び出し元が明示的に開いたままにすることを要求するFDs)のいずれかに含まれるFDsが誤ってクローズ対象となる可能性がありました。
新しいロジックでは、doClose
というブーリアンフラグが導入されます。openFds
の各FDについて、まずdoClose
をtrue
に初期化します。次に、そのFDがstartupFds
に含まれるか、またはattr.Files
に含まれるかをチェックします。いずれかに含まれる場合、doClose
をfalse
に設定し、ループを抜けます。最終的に、doClose
がtrue
のまま(つまり、startupFds
にもattr.Files
にも含まれない)である場合にのみ、そのFDをfdsToClose
スライスに追加します。
この修正により、exec
後の子プロセスが正しく動作するために必要なFDs(特に標準入出力やGoランタイムが内部的に使用するFDs)が確実に保持されるようになり、Plan 9上でのGoプログラムの安定性が向上しました。
関連リンク
- Go CL 6016044: https://golang.org/cl/6016044
参考にした情報源リンク
- Go言語の
syscall
パッケージに関するドキュメント (Go公式ドキュメント): https://pkg.go.dev/syscall - Plan 9 from Bell Labs (Wikipedia): https://ja.wikipedia.org/wiki/Plan_9_from_Bell_Labs
- File descriptor (Wikipedia): https://en.wikipedia.org/wiki/File_descriptor
- exec (system call) (Wikipedia): https://en.wikipedia.org/wiki/Exec_(system_call)
- Go言語の
exec
パッケージに関するドキュメント (Go公式ドキュメント): https://pkg.go.dev/os/exec - Go言語の
os
パッケージに関するドキュメント (Go公式ドキュメント): https://pkg.go.dev/os - Go言語の
io
パッケージに関するドキュメント (Go公式ドキュメント): https://pkg.go.dev/io - Go言語の
bytes
パッケージに関するドキュメント (Go公式ドキュメント): https://pkg.go.dev/bytes - Go言語のスライスに関するドキュメント (Go公式ドキュメント): https://go.dev/blog/slices
- Go言語のエラーハンドリングに関するドキュメント (Go公式ドキュメント): https://go.dev/blog/error-handling-and-go