Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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つです。

  1. readdirnames関数の誤動作: ディレクトリの内容を読み取るreaddirnames関数が、常に空のスライスを返してしまうというバグがありました。これにより、ディレクトリ内のファイル名を正しく取得できない問題が発生していました。
  2. 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ランタイム内で、forkexecのようなプロセス生成操作中に、ファイルディスクリプタのリストが変更されないように保護するためのロック機構です。これにより、FDリストの整合性が保たれます。
  • ProcAttr: Go言語で新しいプロセスを生成する際に、そのプロセスの属性(環境変数、作業ディレクトリ、開くファイルディスクリプタなど)を指定するための構造体です。
  • startupFds: Goランタイムが起動時に内部的に開くファイルディスクリプタのリストです。これらは通常、標準入出力や内部的な通信チャネルなど、Goプログラムの基本的な動作に不可欠なFDsです。

技術的詳細

このコミットは、主にsrc/pkg/syscall/exec_plan9.goファイル内の3つの主要な関数に焦点を当てています。

  1. readdirnames関数の修正:

    • 問題点: 以前の実装では、readdirnames関数がnames = make([]string, 0, 100)でスライスを初期化し、ループ内でnames = append(names, s)で要素を追加していましたが、最終的にreturn []string{}, nilという誤ったリターンステートメントがありました。これにより、どれだけ要素を追加しても常に空のスライスが返されていました。
    • 修正: return []string{}, nilreturnに変更することで、関数内で構築されたnamesスライスが正しく返されるようになりました。また、エラー発生時のリターンもreturn nil, eに変更され、よりGoらしいエラーハンドリングになりました。
    • STATFIXLENチェックの改善: m < STATFIXLENのチェックでエラーを返す際に、以前はreturn []string{}, NewError(...)でしたが、これもreturn nil, NewError(...)に変更されています。
  2. 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が既に構築されているため、より簡潔になりました。
  3. forkExec関数内のファイルディスクリプタのクローズロジックの修正:

    • 問題点: forkExec関数は、新しいプロセスをexecする前に、親プロセスから引き継がれるべきではないファイルディスクリプタをクローズする責任があります。以前の実装では、クローズすべきFDsを決定するロジックに誤りがありました。具体的には、startupFds(Goランタイムが起動時に開くFDs)とattr.Files(呼び出し元が明示的に開いたままにすることを要求するFDs)の両方を「クローズすべきではないFDs」として正しく考慮していませんでした。これにより、これらの重要なFDsが誤ってクローズされる可能性がありました。
    • 修正:
      • fdsToCloseスライスを構築するロジックが大幅に簡素化されました。
      • doCloseというブーリアン変数doClose := trueを導入し、ループ内で現在のfdstartupFdsまたはattr.Filesに含まれている場合にdoClose = falseに設定するようにしました。
      • 最終的にdoClosetrueの場合にのみ、そのfdfdsToCloseに追加するように変更されました。
      • これにより、startupFdsattr.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{}, ereturn nil, e に変更。
    • return []string{}, NewError(...)return nil, NewError(...) に変更。
    • result = append(result, name)names = append(names, s) に変更。
    • return []string{}, nilreturn に変更。
  • 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 == dupdevfdswitch fd { case 0, 1, 2, dupdevfd: continue } に変更。
    • return fds[0:len(fds)], nilreturn に変更。
  • 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 { ... } の内部ロジックが、startupFdsattr.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からnamesfdstrから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について、まずdoClosetrueに初期化します。次に、そのFDがstartupFdsに含まれるか、またはattr.Filesに含まれるかをチェックします。いずれかに含まれる場合、doClosefalseに設定し、ループを抜けます。最終的に、doClosetrueのまま(つまり、startupFdsにもattr.Filesにも含まれない)である場合にのみ、そのFDをfdsToCloseスライスに追加します。

この修正により、exec後の子プロセスが正しく動作するために必要なFDs(特に標準入出力やGoランタイムが内部的に使用するFDs)が確実に保持されるようになり、Plan 9上でのGoプログラムの安定性が向上しました。

関連リンク

参考にした情報源リンク