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

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

このコミットは、Go言語のsyscallパッケージ内のexec_bsd.goおよびexec_plan9.goファイルに対する修正です。具体的には、forkAndExecInChild関数におけるファイルディスクリプタ(FD)のハンドリングを改善し、execシステムコール実行時のFDの衝突や上書きを防ぐことを目的としています。この修正は、exec_linux.goに加えられた変更に合わせたものであり、異なるOS(BSD系、Plan 9)においても同様の安全性を確保します。

コミット

  • コミットハッシュ: 479b1241b5c7451d367d55a4afa9f071f9beb4f6
  • Author: Rob Pike r@golang.org
  • Date: Tue Apr 30 11:52:15 2013 -0700

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

https://github.com/golang/go/commit/479b1241b5c7451d367d55a4afa9f071f9beb4f6

元コミット内容

syscall: fix exec_bsd.go to accompany exec_linux.go changes
exec_plan9.go too.
Those are in CL 8334044

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/9055043

変更の背景

このコミットの背景には、Go言語のsyscallパッケージにおけるexec(実行)関連の処理の堅牢性向上が挙げられます。特に、新しいプロセスを起動する際に、親プロセスから子プロセスへファイルディスクリプタを渡す(または再配置する)処理において、予期せぬFDの衝突や上書きが発生する可能性がありました。

元のコミットメッセージにある「exec_linux.go changes」は、Linux環境での同様の問題に対する修正が行われたことを示唆しています。このコミットは、そのLinuxでの修正と整合性を保つために、BSD系OS(exec_bsd.go)およびPlan 9(exec_plan9.go)においても同様のFDハンドリングの改善を適用するものです。これにより、異なるプラットフォーム間でのexec処理の挙動の一貫性と安全性が確保されます。

具体的には、execシステムコールが新しいプログラムを実行する際、既存のファイルディスクリプタを再配置したり、新しいFDを割り当てたりすることがあります。このとき、もし新しいFDの割り当てが既存の重要なFDと衝突すると、プログラムの動作が不安定になったり、セキュリティ上の問題を引き起こしたりする可能性があります。このコミットは、このような衝突を未然に防ぐためのメカニズムを導入しています。

前提知識の解説

1. syscallパッケージ

Go言語のsyscallパッケージは、オペレーティングシステム(OS)の低レベルなシステムコールにアクセスするための機能を提供します。これにより、ファイル操作、プロセス管理、ネットワーク通信など、OSカーネルが提供する基本的なサービスを直接利用できます。exec関連の関数もこのパッケージに含まれており、新しいプログラムを実行するために使用されます。

2. execシステムコール

execは、現在のプロセスイメージを新しいプログラムイメージで置き換えるシステムコール群の総称です。これにより、新しいプロセスを生成することなく、現在のプロセスが別のプログラムを実行できます。Go言語では、os.StartProcessos.Execなどの高レベルな関数が内部でsyscallパッケージのexec関連の機能を利用しています。

3. ファイルディスクリプタ(File Descriptor, FD)

ファイルディスクリプタは、Unix系OSにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルが割り当てる非負の整数です。プログラムはFDを通じてこれらのリソースにアクセスします。標準入力(stdin)はFD 0、標準出力(stdout)はFD 1、標準エラー出力(stderr)はFD 2として予約されています。

4. dup2システムコール

dup2(oldfd, newfd)は、oldfdが参照するファイルディスクリプタをnewfdに複製するシステムコールです。もしnewfdが既に開かれている場合、dup2はまずnewfdを閉じ、その後oldfdnewfdに複製します。これは、子プロセスで標準入出力のリダイレクトなどを行う際によく使用されます。

5. forkAndExecInChild関数

Go言語のsyscallパッケージにおいて、forkAndExecInChild関数は、新しいプロセスをフォーク(複製)し、その子プロセス内で指定されたプログラムを実行(exec)するための内部的なヘルパー関数です。この関数は、親プロセスから子プロセスへファイルディスクリプタを安全に引き継ぎ、必要に応じて再配置する責任を負います。

6. nextfd変数

nextfdは、この文脈では、execシステムコールが新しいファイルディスクリプタを割り当てる際に使用できる、安全な(既存のFDと衝突しない)最小のファイルディスクリプタ番号を追跡するための変数として機能します。exec処理中にFDのシャッフルや複製が行われる際、このnextfdを適切に管理することで、重要なFDが誤って上書きされることを防ぎます。

技術的詳細

このコミットの核心は、forkAndExecInChild関数におけるnextfdの計算ロジックの改善です。execシステムコールが実行される際、子プロセスに引き継がれるファイルディスクリプタは、親プロセスで開かれているFDのリストattr.Filesに基づいて処理されます。

以前の実装では、nextfdの初期値がlen(attr.Files)に設定されていました。これは、attr.Filesに含まれるFDの数よりも大きいFD番号から新しいFDの割り当てを開始するという意図があったと考えられます。しかし、このアプローチには潜在的な問題がありました。もしattr.Filesに含まれるFDの中に、len(attr.Files)よりも大きな番号のFDが存在する場合、nextfdがその大きなFD番号を考慮せずに設定されてしまい、後続のFD操作(特にdup2など)で既存の重要なFDを上書きしてしまうリスクがありました。

このコミットでは、この問題を解決するためにnextfdの計算方法が変更されました。

  1. nextfdの初期化: nextfdはまずlen(attr.Files)で初期化されます。これは、attr.Filesの要素数分のFDが少なくとも存在することを考慮するためです。
  2. 既存FDの最大値の考慮: その後、attr.Files内の各ufd(ユーザーが指定したファイルディスクリプタ)についてループ処理が行われます。このループ内で、nextfdが現在のufdよりも小さい場合、nextfdufdの値に更新されます。これにより、nextfdattr.Filesに含まれるすべてのFDの中で最も大きな番号よりも少なくとも大きくなることが保証されます。
  3. インクリメント: 最後に、nextfdは1インクリメントされます(nextfd++)。これにより、nextfdattr.Filesに含まれるどのFDとも衝突しない、利用可能な最小のFD番号となります。

この修正により、exec処理中にdup2などの操作で一時的に使用されるFD番号が、既存の重要なFDと衝突する可能性が大幅に低減されます。特に、パイプなどの一時的なFDを高い番号に複製する際に、既存のFDを誤って閉じてしまうようなサイドエフェクトを防ぐことができます。

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

src/pkg/syscall/exec_bsd.go

--- a/src/pkg/syscall/exec_bsd.go
+++ b/src/pkg/syscall/exec_bsd.go
@@ -39,10 +39,18 @@ func forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr
 		i      int
 	)
 
+	// guard against side effects of shuffling fds below.
+	// Make sure that nextfd is beyond any currently open files so
+	// that we can't run the risk of overwriting any of them.
 	fd := make([]int, len(attr.Files))
+	nextfd = len(attr.Files)
 	for i, ufd := range attr.Files {
+		if nextfd < int(ufd) {
+			nextfd = int(ufd)
+		}
 		fd[i] = int(ufd)
 	}
+	nextfd++
 
 	darwin := runtime.GOOS == "darwin"
 
@@ -131,7 +139,6 @@ func forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr
 
 	// Pass 1: look for fd[i] < i and move those up above len(fd)
 	// so that pass 2 won't stomp on an fd it needs later.
-	nextfd = int(len(fd))
 	if pipe < nextfd {
 		_, _, err1 = RawSyscall(SYS_DUP2, uintptr(pipe), uintptr(nextfd), 0)
 		if err1 != 0 {

src/pkg/syscall/exec_plan9.go

--- a/src/pkg/syscall/exec_plan9.go
+++ b/src/pkg/syscall/exec_plan9.go
@@ -183,11 +183,18 @@ func forkAndExecInChild(argv0 *byte, argv []*byte, envv []envItem, dir *byte, at
 		errbuf   [ERRMAX]byte
 	)
 
-	// guard against side effects of shuffling fds below.
+	// Guard against side effects of shuffling fds below.
+	// Make sure that nextfd is beyond any currently open files so
+	// that we can't run the risk of overwriting any of them.
 	fd := make([]int, len(attr.Files))
+	nextfd = len(attr.Files)
 	for i, ufd := range attr.Files {
+		if nextfd < int(ufd) {
+			nextfd = int(ufd)
+		}
 		fd[i] = int(ufd)
 	}
+	nextfd++
 
 	if envv != nil {
 		clearenv = RFCENVG
@@ -251,7 +258,6 @@ func forkAndExecInChild(argv0 *byte, argv []*byte, envv []envItem, dir *byte, at
 
 	// Pass 1: look for fd[i] < i and move those up above len(fd)
 	// so that pass 2 won't stomp on an fd it needs later.
-	nextfd = int(len(fd))
 	if pipe < nextfd {
 		r1, _, _ = RawSyscall(SYS_DUP, uintptr(pipe), uintptr(nextfd), 0)
 		if int32(r1) == -1 {

コアとなるコードの解説

両ファイルにおける変更はほぼ同じであり、forkAndExecInChild関数内のnextfdの計算ロジックを修正しています。

  1. コメントの追加/修正: // guard against side effects of shuffling fds below. // Make sure that nextfd is beyond any currently open files so // that we can't run the risk of overwriting any of them. これらのコメントは、このコードブロックの目的を明確にしています。FDのシャッフルによる副作用を防ぎ、既存の開いているファイルを上書きするリスクを排除するために、nextfdが現在開いているどのファイルよりも大きな値になるように保証することを示しています。

  2. nextfdの初期化と最大値の計算:

    nextfd = len(attr.Files)
    for i, ufd := range attr.Files {
        if nextfd < int(ufd) {
            nextfd = int(ufd)
        }
        fd[i] = int(ufd)
    }
    nextfd++
    
    • nextfd = len(attr.Files): まず、nextfdattr.Filesスライスの長さで初期化します。これは、ユーザーが指定したファイルディスクリプタの数に基づいて、少なくともその数以上のFDが使用される可能性があることを考慮しています。
    • for i, ufd := range attr.Files { ... }: attr.Files内の各ファイルディスクリプタufdを反復処理します。
    • if nextfd < int(ufd) { nextfd = int(ufd) }: この行が最も重要な変更点です。もし現在のnextfdの値が、ループ中のufdの値よりも小さい場合、nextfdufdの値に更新します。これにより、nextfdattr.Filesに含まれるすべてのFDの中で最も大きな番号を確実に含むようになります。
    • nextfd++: ループが終了した後、nextfdを1インクリメントします。これにより、nextfdattr.Filesに含まれるどのFDとも衝突しない、利用可能な最小のファイルディスクリプタ番号となります。この番号は、exec処理中に一時的なFD(例えばパイプ)を複製する際に安全に使用できます。
  3. 不要な行の削除: nextfd = int(len(fd)) この行は、新しいnextfdの計算ロジックが導入されたため、不要となり削除されました。以前はここでnextfdが再設定されていましたが、新しいロジックではループ内で動的に最大値が計算されるため、この再設定は必要ありません。

これらの変更により、forkAndExecInChild関数は、子プロセスに引き継がれるファイルディスクリプタの番号をより安全に管理できるようになり、execシステムコール実行時のFDの衝突や上書きのリスクが排除されました。

関連リンク

  • Go言語のsyscallパッケージに関する公式ドキュメント: https://pkg.go.dev/syscall
  • Go言語のos/execパッケージに関する公式ドキュメント: https://pkg.go.dev/os/exec
  • Unix系OSにおけるファイルディスクリプタの概念(一般的な情報源)

参考にした情報源リンク

  • Go言語のソースコード(上記コミットの差分)
  • Go言語の公式ドキュメント
  • Unix系OSのシステムコールに関する一般的な知識
  • ファイルディスクリプタとdup2に関する一般的なプログラミング知識