[インデックス 10877] ファイルの概要
このコミットは、Go言語のネットワークおよびシステムコール関連パッケージにおいて、epoll
および kqueue
ディスクリプタに CLOEXEC
フラグを設定する変更を導入しています。これにより、ファイルディスクリプタのリークを防ぎ、セキュリティと堅牢性を向上させています。
コミット
commit 384329592a72e8ce7cfdacb1f3cf2d05af07562a
Author: Ian Lance Taylor <iant@golang.org>
Date: Mon Dec 19 12:57:49 2011 -0800
net, syscall, os: set CLOEXEC flag on epoll/kqueue descriptor
Enable new test in os.
R=dave, iant, rsc
CC=golang-dev
https://golang.org/cl/5494061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/384329592a72e8ce7cfdacb1f3cf2d05af07562a
元コミット内容
net, syscall, os: set CLOEXEC flag on epoll/kqueue descriptor
Enable new test in os.
変更の背景
この変更の背景には、Goプログラムが子プロセスを生成する際に、親プロセスで開かれているファイルディスクリプタが意図せず子プロセスに継承されてしまうという問題があります。特に、epoll
や kqueue
のようなイベント通知メカニズムで使用されるファイルディスクリプタが子プロセスに継承されると、以下のような問題が発生する可能性があります。
- リソースリーク: 子プロセスが不要なファイルディスクリプタを保持し続けることで、システムのリソースを消費し、最終的にはファイルディスクリプタの枯渇につながる可能性があります。
- セキュリティリスク: 意図しないファイルディスクリプタが子プロセスに継承されることで、子プロセスが親プロセスの持つリソース(例えば、ネットワークソケットやファイルハンドル)にアクセスできてしまう可能性があります。これは、特に特権を持つプロセスが子プロセスを起動する場合にセキュリティ上の脆弱性となり得ます。
- 予期せぬ動作: 子プロセスが継承したディスクリプタに対して予期せぬ操作を行うことで、親プロセスの動作に影響を与えたり、デッドロックなどの問題を引き起こしたりする可能性があります。
CLOEXEC
(Close-on-exec) フラグは、このような問題を解決するために設計されたメカニズムです。このフラグが設定されたファイルディスクリプタは、exec
ファミリーのシステムコール(新しいプログラムを実行する際に使用される)が呼び出されたときに自動的に閉じられます。これにより、子プロセスには必要なディスクリプタのみが継承され、不要なディスクリソースの継承やそれに伴う問題が回避されます。
このコミットは、Goのネットワークパッケージが内部的に使用する epoll
(Linux) および kqueue
(BSD系OS) のファイルディスクリプタに対して CLOEXEC
フラグを明示的に設定することで、これらの問題を解決し、Goプログラムの堅牢性とセキュリティを向上させることを目的としています。また、この変更によって、関連するテストケースが有効化されています。
前提知識の解説
ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、プロセスが開いているファイルやI/Oリソース(ソケット、パイプ、デバイスなど)を識別するために使用される整数値です。プロセスがファイルを開いたり、ソケットを作成したりすると、カーネルは対応するファイルディスクリプタをプロセスに返します。プロセスはこのディスクリプタを使って、そのリソースに対する読み書きなどの操作を行います。
CLOEXEC
フラグ (Close-on-exec)
CLOEXEC
は、ファイルディスクリプタに設定できるフラグの一つです。このフラグが設定されたファイルディスクリプタは、execve()
などの exec
ファミリーのシステムコールが呼び出され、新しいプログラムが実行される際に、自動的に閉じられます。
通常、Unix系OSでは、fork()
システムコールによって子プロセスが作成されると、親プロセスのファイルディスクリプタはすべて子プロセスに継承されます。しかし、子プロセスが親プロセスとは異なるプログラムを実行する場合、親プロセスのファイルディスクリプタの多くは子プロセスにとって不要であり、むしろ問題を引き起こす可能性があります。CLOEXEC
フラグは、このような不要なディスクリプタの継承を防ぐために使用されます。
epoll
(Linux)
epoll
は、Linuxカーネルが提供する高性能なI/Oイベント通知メカニズムです。多数のファイルディスクリプタ(ソケットなど)からのI/Oイベント(読み込み可能、書き込み可能など)を効率的に監視するために使用されます。select()
や poll()
と比較して、監視対象のディスクリプタ数が増えてもパフォーマンスが劣化しにくいという特徴があります。
epoll
を使用する基本的な流れは以下の通りです。
epoll_create()
またはepoll_create1()
を呼び出して、epoll
インスタンスを作成し、そのファイルディスクリプタを取得します。epoll_ctl()
を呼び出して、監視したいファイルディスクリプタをepoll
インスタンスに追加・削除したり、監視するイベントの種類を設定したりします。epoll_wait()
を呼び出して、イベントが発生するまで待機します。イベントが発生すると、epoll_wait()
はイベントが発生したファイルディスクリプタのリストを返します。
kqueue
(FreeBSD, macOS, NetBSD, OpenBSDなど)
kqueue
は、BSD系オペレーティングシステム(FreeBSD, macOS, NetBSD, OpenBSDなど)が提供する高性能なI/Oイベント通知メカニズムです。epoll
と同様に、多数のファイルディスクリプタからのI/Oイベントを効率的に監視するために使用されます。kqueue
はファイルディスクリプタだけでなく、プロセス状態の変化、タイマー、シグナルなど、より多様なイベントを監視できる点が特徴です。
kqueue
を使用する基本的な流れは以下の通りです。
kqueue()
を呼び出して、kqueue
インスタンスを作成し、そのファイルディスクリプタを取得します。kevent()
を呼び出して、監視したいイベント(ファイルディスクリプタのI/Oイベント、プロセスの終了など)を登録・変更・削除したり、発生したイベントを取得したりします。
syscall
パッケージ (Go言語)
Go言語の syscall
パッケージは、オペレーティングシステムの低レベルなシステムコールへのアクセスを提供します。これにより、Goプログラムから直接カーネルの機能を利用することができます。このパッケージは、OS固有の機能やパフォーマンスが重要な場面で利用されますが、通常はより高レベルな標準ライブラリ(os
, net
など)を使用することが推奨されます。
技術的詳細
このコミットの主要な技術的変更は、epoll
および kqueue
のファイルディスクリプタが作成される際に、CLOEXEC
フラグを自動的に設定するようにした点です。
Linuxにおける epoll
の変更
Linuxでは、epoll_create1()
システムコールが導入されており、このシステムコールは EPOLL_CLOEXEC
フラグを引数として受け取ることができます。このフラグを指定することで、epoll
インスタンスのファイルディスクリプタが作成されると同時に CLOEXEC
フラグが設定されます。これは、epoll_create()
でディスクリプタを作成した後に fcntl(fd, F_SETFD, FD_CLOEXEC)
を呼び出すよりもアトミックで効率的です。
このコミットでは、src/pkg/net/fd_linux.go
内の newpollster()
関数において、まず syscall.EpollCreate1(syscall.EPOLL_CLOEXEC)
を試みるように変更されています。もし epoll_create1()
が利用できない場合(例えば、古いカーネルバージョンなど)、フォールバックとして従来の syscall.EpollCreate(16)
を使用し、その後に syscall.CloseOnExec(p.epfd)
を呼び出して明示的に CLOEXEC
フラグを設定しています。
syscall
パッケージには、EpollCreate1
システムコールをラップする新しい関数が追加されています。これは、src/pkg/syscall/syscall_linux.go
およびアーキテクチャ固有の zsyscall_linux_*.go
ファイル(386
, amd64
, arm
)に反映されています。
BSD系OSにおける kqueue
の変更
Darwin (macOS), FreeBSD, NetBSD, OpenBSD などのBSD系OSでは、kqueue()
システムコールで作成されたディスクリプタに対して、明示的に CLOEXEC
フラグを設定する必要があります。
このコミットでは、src/pkg/net/fd_darwin.go
, src/pkg/net/fd_freebsd.go
, src/pkg/net/fd_netbsd.go
, src/pkg/net/fd_openbsd.go
内の newpollster()
関数において、syscall.Kqueue()
で kqueue
ディスクリプタが作成された直後に syscall.CloseOnExec(p.kq)
を呼び出すように変更されています。syscall.CloseOnExec
は、指定されたファイルディスクリプタに FD_CLOEXEC
フラグを設定するGoのヘルパー関数です。
テストの変更
src/pkg/os/exec/exec_test.go
内のテストコードが変更されています。以前は、CLOEXEC
の問題が未解決であったため、特定のテストブロックが無効化されていました。このコミットにより、epoll
/kqueue
ディスクリプタに CLOEXEC
が設定されるようになったため、このテストブロックが再度有効化され、ファイルディスクリプタが正しく閉じられることを検証できるようになりました。具体的には、子プロセスが起動された際に、親プロセスから継承されるべきではないファイルディスクリプタが閉じられていることを確認するテストです。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードスニペットは以下の通りです。
-
src/pkg/net/fd_darwin.go
--- a/src/pkg/net/fd_darwin.go +++ b/src/pkg/net/fd_darwin.go @@ -27,6 +27,7 @@ func newpollster() (p *pollster, err error) { if p.kq, err = syscall.Kqueue(); err != nil { return nil, os.NewSyscallError("kqueue", err) } + syscall.CloseOnExec(p.kq) p.events = p.eventbuf[0:0] return p, nil }
-
src/pkg/net/fd_freebsd.go
,src/pkg/net/fd_netbsd.go
,src/pkg/net/fd_openbsd.go
(上記fd_darwin.go
と同様の変更) -
src/pkg/net/fd_linux.go
--- a/src/pkg/net/fd_linux.go +++ b/src/pkg/net/fd_linux.go @@ -37,11 +37,17 @@ func newpollster() (p *pollster, err error) { p = new(pollster) var e error - // The arg to epoll_create is a hint to the kernel - // about the number of FDs we will care about. - // We don't know, and since 2.6.8 the kernel ignores it anyhow. - if p.epfd, e = syscall.EpollCreate(16); e != nil { - return nil, os.NewSyscallError("epoll_create", e) + if p.epfd, e = syscall.EpollCreate1(syscall.EPOLL_CLOEXEC); e != nil { + if e != syscall.ENOSYS { + return nil, os.NewSyscallError("epoll_create1", e) + } + // The arg to epoll_create is a hint to the kernel + // about the number of FDs we will care about. + // We don't know, and since 2.6.8 the kernel ignores it anyhow. + if p.epfd, e = syscall.EpollCreate(16); e != nil { + return nil, os.NewSyscallError("epoll_create", e) + } + syscall.CloseOnExec(p.epfd) } p.events = make(map[int]uint32) return p, nil
-
src/pkg/os/exec/exec_test.go
--- a/src/pkg/os/exec/exec_test.go +++ b/src/pkg/os/exec/exec_test.go @@ -256,12 +256,6 @@ func TestHelperProcess(*testing.T) { fmt.Printf("ReadAll from fd 3: %v", err) os.Exit(1) } - // TODO(bradfitz,iant): the rest of this test is disabled - // for now. remove this block once 5494061 is in. - { - os.Stderr.Write(bs) - os.Exit(0) - } // Now verify that there are no other open fds. var files []*os.File for wantfd := os.Stderr.Fd() + 2; wantfd <= 100; wantfd++ {
-
src/pkg/syscall/syscall_linux.go
--- a/src/pkg/syscall/syscall_linux.go +++ b/src/pkg/syscall/syscall_linux.go @@ -806,6 +806,7 @@ func Mount(source string, target string, fstype string, flags uintptr, data stri //sysnb Dup(oldfd int) (fd int, err error) //sysnb Dup2(oldfd int, newfd int) (fd int, err error) //sysnb EpollCreate(size int) (fd int, err error) +//sysnb EpollCreate1(flag int) (fd int, err error) //sysnb EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) //sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) //sys Exit(code int) = SYS_EXIT_GROUP
-
src/pkg/syscall/zsyscall_linux_386.go
,src/pkg/syscall/zsyscall_linux_amd64.go
,src/pkg/syscall/zsyscall_linux_arm.go
(EpollCreate1
関数の追加)--- a/src/pkg/syscall/zsyscall_linux_386.go +++ b/src/pkg/syscall/zsyscall_linux_386.go @@ -232,6 +232,17 @@ func EpollCreate(size int) (fd int, err error) { // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func EpollCreate1(flag int) (fd int, err error) { + r0, _, e1 := RawSyscall(SYS_EPOLL_CREATE1, uintptr(flag), 0, 0) + fd = int(r0) + if e1 != 0 { + err = e1 + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) { _, _, e1 := RawSyscall6(SYS_EPOLL_CTL, uintptr(epfd), uintptr(op), uintptr(fd), uintptr(unsafe.Pointer(event)), 0, 0) if e1 != 0 {
コアとなるコードの解説
src/pkg/net/fd_*.go
ファイル群
これらのファイルは、GoのネットワークパッケージがOS固有のファイルディスクリプタ操作を抽象化するために使用されます。newpollster()
関数は、epoll
または kqueue
のインスタンスを作成し、そのファイルディスクリプタを返します。
-
BSD系OS (
fd_darwin.go
,fd_freebsd.go
,fd_netbsd.go
,fd_openbsd.go
):syscall.Kqueue()
でkqueue
ディスクリプタが作成された直後にsyscall.CloseOnExec(p.kq)
が追加されています。これは、kqueue
ディスクリプタにCLOEXEC
フラグを設定し、子プロセスに継承されないようにします。 -
Linux (
fd_linux.go
):newpollster()
関数内で、まずsyscall.EpollCreate1(syscall.EPOLL_CLOEXEC)
を呼び出してepoll
ディスクリプタを作成しようとします。syscall.EPOLL_CLOEXEC
は、epoll_create1
システムコールに渡すフラグで、作成されるディスクリプタにCLOEXEC
フラグを自動的に設定するようカーネルに指示します。- もし
epoll_create1
がENOSYS
エラー(システムコールが存在しない)を返した場合、それは古いLinuxカーネルで実行されていることを意味します。この場合、従来のsyscall.EpollCreate(16)
を使用してepoll
ディスクリプタを作成し、その後syscall.CloseOnExec(p.epfd)
を呼び出して明示的にCLOEXEC
フラグを設定します。これにより、新旧のカーネルバージョン両方に対応できる互換性が確保されています。
src/pkg/os/exec/exec_test.go
このファイルは、Goの os/exec
パッケージのテストケースを含んでいます。変更点としては、以前コメントアウトされていたテストブロックが削除され、テストが有効化されています。このテストは、子プロセスが起動された際に、親プロセスから継承されるべきではないファイルディスクリプタが正しく閉じられていることを検証するものです。CLOEXEC
フラグが正しく機能していることを確認するために重要です。
src/pkg/syscall/syscall_linux.go
および src/pkg/syscall/zsyscall_linux_*.go
-
src/pkg/syscall/syscall_linux.go
://sysnb EpollCreate1(flag int) (fd int, err error)
というコメントが追加されています。これは、go generate
コマンドによってzsyscall_linux_*.go
ファイルにEpollCreate1
システムコールのGoラッパー関数が自動生成されることを示しています。 -
src/pkg/syscall/zsyscall_linux_*.go
(386, amd64, arm): これらのファイルは、各アーキテクチャ向けのシステムコールラッパーを自動生成するものです。EpollCreate1
関数が追加されており、これはSYS_EPOLL_CREATE1
システムコールをRawSyscall
を使って呼び出すGoの関数です。これにより、Goプログラムからepoll_create1
システムコールを直接呼び出すことが可能になります。
これらの変更により、Goのネットワークパッケージは、epoll
や kqueue
を使用する際に、ファイルディスクリプタの CLOEXEC
フラグを適切に設定するようになり、子プロセスへの不要なディスクリプタの継承を防ぎ、より堅牢で安全なアプリケーションの構築に貢献しています。
関連リンク
- Go Issue: https://golang.org/cl/5494061 (元のGoのコードレビューシステムへのリンク)
epoll_create1(2)
man page: https://man7.org/linux/man-pages/man2/epoll_create1.2.htmlkqueue(2)
man page: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2fcntl(2)
man page (forFD_CLOEXEC
): https://man7.org/linux/man-pages/man2/fcntl.2.html
参考にした情報源リンク
- Linux man pages (epoll_create1, fcntl)
- FreeBSD man pages (kqueue)
- Go言語の公式ドキュメントおよびソースコード
- Unix/Linuxプログラミングに関する一般的な知識
- ファイルディスクリプタとCLOEXECに関する技術記事