[インデックス 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.StartProcess
やos.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
を閉じ、その後oldfd
をnewfd
に複製します。これは、子プロセスで標準入出力のリダイレクトなどを行う際によく使用されます。
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
の計算方法が変更されました。
nextfd
の初期化:nextfd
はまずlen(attr.Files)
で初期化されます。これは、attr.Files
の要素数分のFDが少なくとも存在することを考慮するためです。- 既存FDの最大値の考慮: その後、
attr.Files
内の各ufd
(ユーザーが指定したファイルディスクリプタ)についてループ処理が行われます。このループ内で、nextfd
が現在のufd
よりも小さい場合、nextfd
はufd
の値に更新されます。これにより、nextfd
はattr.Files
に含まれるすべてのFDの中で最も大きな番号よりも少なくとも大きくなることが保証されます。 - インクリメント: 最後に、
nextfd
は1インクリメントされます(nextfd++
)。これにより、nextfd
はattr.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
の計算ロジックを修正しています。
-
コメントの追加/修正:
// 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
が現在開いているどのファイルよりも大きな値になるように保証することを示しています。 -
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)
: まず、nextfd
をattr.Files
スライスの長さで初期化します。これは、ユーザーが指定したファイルディスクリプタの数に基づいて、少なくともその数以上のFDが使用される可能性があることを考慮しています。for i, ufd := range attr.Files { ... }
:attr.Files
内の各ファイルディスクリプタufd
を反復処理します。if nextfd < int(ufd) { nextfd = int(ufd) }
: この行が最も重要な変更点です。もし現在のnextfd
の値が、ループ中のufd
の値よりも小さい場合、nextfd
をufd
の値に更新します。これにより、nextfd
はattr.Files
に含まれるすべてのFDの中で最も大きな番号を確実に含むようになります。nextfd++
: ループが終了した後、nextfd
を1インクリメントします。これにより、nextfd
はattr.Files
に含まれるどのFDとも衝突しない、利用可能な最小のファイルディスクリプタ番号となります。この番号は、exec
処理中に一時的なFD(例えばパイプ)を複製する際に安全に使用できます。
-
不要な行の削除:
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
に関する一般的なプログラミング知識