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

[インデックス 12900] ファイルの概要

このコミットは、Go言語のsyscallパッケージにおけるPlan 9オペレーティングシステム向けのforkAndExecInChild関数に存在していた、ファイルディスクリプタ (fd) の重複に関するバグを修正するものです。具体的には、親プロセスから子プロセスへファイルディスクリプタが複製 (dup) された際に、子プロセス側でそれらのディスクリプタが正しく管理されず、結果として「fork/exec: fd out of range or not open」というエラーが発生する問題を解決します。

コミット

commit 4cf577edf98fbb642840b55b474d9fd19b2f6606
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date:   Mon Apr 16 17:35:15 2012 -0700

    syscall: fix duplicate fd bug for Plan 9
    
    This change comes from CL 5536043,
    created by Andrey Mirtchovski. His
    description follows:
    
    "The plan9 exec child handler does not manage
    dup-ed fds from the parent correctly: when a
    dup-ed file descriptor appears in the child's fd
    list it is closed when first encountered and then
    subsequent attempt to dup it later in Pass 2 fails,
    resulting in 'fork/exec: fd out of range or not
    open'."
    
    R=golang-dev, rminnich, ality
    CC=golang-dev, mirtchovski, rsc
    https://golang.org/cl/6009046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/4cf577edf98fbb642840b55b474d9fd19b2f6606

元コミット内容

syscall: fix duplicate fd bug for Plan 9

This change comes from CL 5536043,
created by Andrey Mirtchovski. His
description follows:

"The plan9 exec child handler does not manage
dup-ed fds from the parent correctly: when a
dup-ed file descriptor appears in the child's fd
list it is closed when first encountered and then
subsequent attempt to dup it later in Pass 2 fails,
resulting in 'fork/exec: fd out of range or not
open'."

R=golang-dev, rminnich, ality
CC=golang-dev, mirtchovski, rsc
https://golang.org/cl/6009046

変更の背景

この変更は、Go言語がPlan 9オペレーティングシステム上でプロセスを生成(fork/exec)する際の、ファイルディスクリプタの取り扱いに関するバグを修正するために行われました。

従来のforkAndExecInChild関数(子プロセスで実行される部分)では、親プロセスから複製(dup)されたファイルディスクリプタが正しく処理されていませんでした。具体的には、複製されたファイルディスクリプタが子プロセスのファイルディスクリプタリストに複数回出現する場合、最初の出現時に閉じられてしまい、その後の処理(特に「Pass 2」と呼ばれる段階での再度のdup操作)でそのディスクリプタを使用しようとすると、「fd out of range or not open」というエラーが発生していました。

この問題は、子プロセスが期待通りに起動できない、または予期せぬファイル操作エラーを引き起こす可能性があり、Goプログラムの安定性と信頼性に影響を与えていました。このコミットは、この特定のシナリオにおけるファイルディスクリプタのライフサイクル管理を改善し、堅牢なプロセス生成を保証することを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  • ファイルディスクリプタ (File Descriptor, FD): Unix系オペレーティングシステム(Plan 9も含む)において、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プロセスはファイルディスクリプタを通じてこれらのリソースにアクセスします。標準入力 (stdin) は0、標準出力 (stdout) は1、標準エラー出力 (stderr) は2というように、特定のディスクリプタは予約されています。

  • dup (Duplicate File Descriptor): dupシステムコールは、既存のファイルディスクリプタを複製し、新しいファイルディスクリプタを返します。新しいディスクリプタは元のディスクリプタと同じファイル記述エントリを参照するため、両方のディスクリプタが同じファイルオフセット、ファイルステータスフラグ、およびアクセスモードを共有します。これは、例えば標準出力とファイルの両方に同じ出力を書き込む場合や、子プロセスに親プロセスのファイルディスクリプタを引き継がせる場合などに使用されます。

  • fork/exec プロセス: Unix系システムで新しいプロセスを生成する際の典型的なパターンです。

    • fork: 既存のプロセス(親プロセス)のコピーとして新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタ、その他のリソースのコピーを受け取ります。
    • exec: 現在のプロセスイメージを、指定された新しいプログラムイメージで置き換えます。execが成功すると、現在のプロセスのコード、データ、スタックは新しいプログラムのものに置き換えられ、新しいプログラムが実行を開始します。ファイルディスクリプタは通常、exec後も開いたまま引き継がれます。
  • Plan 9: ベル研究所で開発された分散オペレーティングシステムです。Unixの概念をさらに推し進め、すべてのリソースをファイルとして表現するという思想を持っています。Go言語は、Unix系OSだけでなく、Plan 9もサポート対象としていました。Plan 9のシステムコールやプロセス管理のメカニズムは、Unixと類似している点も多いですが、細部で異なる挙動を示すことがあります。このコミットのバグは、まさにその細部の違いに起因していました。

  • RawSyscall: Go言語のsyscallパッケージで提供される関数の一つで、低レベルなシステムコールを直接呼び出すためのものです。OS固有のシステムコール番号と引数を受け取り、そのシステムコールを実行します。

技術的詳細

このバグは、src/pkg/syscall/exec_plan9.go内のforkAndExecInChild関数におけるファイルディスクリプタの処理ロジックにありました。この関数は、子プロセスがexecシステムコールを呼び出す前に、親プロセスから引き継がれたファイルディスクリプタを適切に設定する役割を担っています。

問題の核心は、forkAndExecInChild関数がファイルディスクリプタを処理する「Pass 2」と「Pass 3」の順序とロジックにありました。

元の問題点: コミットメッセージによると、Plan 9のexec子ハンドラは、親からdupされたファイルディスクリプタを正しく管理していませんでした。

  1. 子プロセスのファイルディスクリプタリストに、dupによって複製された同じファイルディスクリプタが複数回出現する可能性がありました。
  2. 従来のコードでは、これらのファイルディスクリプタを処理する際に、最初にそのディスクリプタに遭遇した時点でSYS_CLOSE(ファイルディスクリプタを閉じるシステムコール)を呼び出してしまっていました。
  3. これにより、同じファイルディスクリプタがリストの後半で再度処理される「Pass 2」の段階で、すでに閉じられたディスクリプタに対してdup操作を試みることになり、結果として「fork/exec: fd out of range or not open」というエラーが発生していました。

修正内容: このコミットでは、ファイルディスクリプタを閉じるタイミングとロジックが変更されました。

  • 変更前: Pass 2のループ内で、dup操作を行った直後に、元のファイルディスクリプタを無条件に閉じていました (RawSyscall(SYS_CLOSE, uintptr(fd[i]), 0, 0))。これは、fd[i]が複製元であり、複製が成功した後は不要になるという前提に基づいています。しかし、fd[i]がリスト内で複数回出現する(つまり、同じファイルディスクリプタが複数回dupされている)場合、この早期のクローズが問題を引き起こしていました。

  • 変更後: ファイルディスクリプタを閉じる処理が、Pass 2のループから分離され、新たに「Pass 3: close fds that were dup-ed」という独立したループとして追加されました。 この新しいPass 3では、fd[i] >= 0 && fd[i] != int(i)という条件が追加されています。

    • fd[i] >= 0: 有効なファイルディスクリプタであることを確認します。
    • fd[i] != int(i): これは、fd[i]がそのインデックスiにマップされたファイルディスクリプタではないことを意味します。つまり、dup操作によって元のディスクリプタが新しい位置に複製された場合、元のディスクリプタ(fd[i])は、そのインデックスiとは異なる値を持つことになります。この条件は、元のディスクリプタが複製された後にのみ閉じられるべきであることを保証します。

この変更により、すべてのdup操作が完了し、新しいファイルディスクリプタが適切に設定された後に、元の(複製元の)ファイルディスクリプタがまとめて閉じられるようになりました。これにより、dup操作の途中で誤ってファイルディスクリプタが閉じられてしまうことがなくなり、バグが解消されました。

コアとなるコードの変更箇所

diff --git a/src/pkg/syscall/exec_plan9.go b/src/pkg/syscall/exec_plan9.go
index 7e4e180fa1..46131bb0cd 100644
--- a/src/pkg/syscall/exec_plan9.go
+++ b/src/pkg/syscall/exec_plan9.go
@@ -287,7 +287,13 @@ func forkAndExecInChild(argv0 *byte, argv []*byte, envv []envItem, dir *byte, at
 		if int(r1) == -1 {
 			goto childerror
 		}
-		RawSyscall(SYS_CLOSE, uintptr(fd[i]), 0, 0)
+	}
+
+	// Pass 3: close fds that were dup-ed
+	for i = 0; i < len(fd); i++ {
+		if fd[i] >= 0 && fd[i] != int(i) {
+			RawSyscall(SYS_CLOSE, uintptr(fd[i]), 0, 0)
+		}
 	}
 
 	// Time to exec.

コアとなるコードの解説

変更はsrc/pkg/syscall/exec_plan9.goファイルのforkAndExecInChild関数内で行われています。

変更前:

	// ... (Pass 2 のループの続き)
		if int(r1) == -1 {
			goto childerror
		}
		RawSyscall(SYS_CLOSE, uintptr(fd[i]), 0, 0) // ここで fd[i] が閉じられていた
	}

このコードは、Pass 2のループ内で、dupシステムコール(またはそれに相当する操作)が成功した直後に、元のファイルディスクリプタfd[i]を無条件に閉じていました。もしfd[i]が他の場所でも参照されている(つまり、複数回dupされている)場合、この早期のクローズが問題を引き起こしていました。

変更後:

	// ... (Pass 2 のループの続き)
		if int(r1) == -1 {
			goto childerror
		}
	} // Pass 2 のループがここで終了

	// Pass 3: close fds that were dup-ed
	for i = 0; i < len(fd); i++ {
		if fd[i] >= 0 && fd[i] != int(i) {
			RawSyscall(SYS_CLOSE, uintptr(fd[i]), 0, 0)
		}
	}

変更後では、ファイルディスクリプタを閉じる処理がPass 2のループから完全に切り離され、独立した新しいループ「Pass 3」として追加されました。

新しいPass 3のループでは、fdスライス内のすべてのファイルディスクリプタを再度イテレートし、以下の条件を満たす場合にのみSYS_CLOSEシステムコールを呼び出します。

  • fd[i] >= 0: ファイルディスクリプタが有効な値であることを確認します。無効なディスクリプタ(例えば-1)を閉じようとしないためのガードです。
  • fd[i] != int(i): この条件が重要です。これは、fd[i]がそのインデックスiにマップされたファイルディスクリプタではないことを意味します。
    • forkAndExecInChild関数は、子プロセスでファイルディスクリプタを再配置する際に、fdスライスのi番目の要素に、最終的にi番目のファイルディスクリプタとして開かれるべき元のファイルディスクリプタの値を格納します。
    • もしfd[i]iと異なる値であれば、それはdup操作によってi番目の位置に別のファイルディスクリプタが複製されたことを意味します。この場合、元のfd[i](複製元)はもはや必要ないため、閉じることができます。
    • 逆に、もしfd[i]iと同じ値であれば、それはi番目のファイルディスクリプタがそのままi番目の位置に保持されることを意味し、閉じる必要はありません。

この修正により、すべてのdup操作が完了し、ファイルディスクリプタの再配置が確定した後に、不要になった元のファイルディスクリプタのみが安全に閉じられるようになりました。これにより、dup操作の途中で誤ってファイルディスクリプタが閉じられてしまうという問題が解消され、Plan 9上でのfork/execの信頼性が向上しました。

関連リンク

  • Go Change-Id: https://golang.org/cl/6009046
  • 元の変更リスト (CL 5536043): コミットメッセージに記載されている元の変更リストですが、直接アクセスできるURLは提供されていません。

参考にした情報源リンク

  • コミットメッセージ自体
  • Go言語のsyscallパッケージのドキュメント(一般的なシステムコールとファイルディスクリプタの概念理解のため)
  • Unix/Linuxのfork, exec, dupシステムコールに関する一般的なドキュメント(概念理解のため)
  • Plan 9オペレーティングシステムに関する一般的な情報(Plan 9の特性理解のため)