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

[インデックス 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プログラムが子プロセスを生成する際に、親プロセスで開かれているファイルディスクリプタが意図せず子プロセスに継承されてしまうという問題があります。特に、epollkqueue のようなイベント通知メカニズムで使用されるファイルディスクリプタが子プロセスに継承されると、以下のような問題が発生する可能性があります。

  1. リソースリーク: 子プロセスが不要なファイルディスクリプタを保持し続けることで、システムのリソースを消費し、最終的にはファイルディスクリプタの枯渇につながる可能性があります。
  2. セキュリティリスク: 意図しないファイルディスクリプタが子プロセスに継承されることで、子プロセスが親プロセスの持つリソース(例えば、ネットワークソケットやファイルハンドル)にアクセスできてしまう可能性があります。これは、特に特権を持つプロセスが子プロセスを起動する場合にセキュリティ上の脆弱性となり得ます。
  3. 予期せぬ動作: 子プロセスが継承したディスクリプタに対して予期せぬ操作を行うことで、親プロセスの動作に影響を与えたり、デッドロックなどの問題を引き起こしたりする可能性があります。

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 を使用する基本的な流れは以下の通りです。

  1. epoll_create() または epoll_create1() を呼び出して、epoll インスタンスを作成し、そのファイルディスクリプタを取得します。
  2. epoll_ctl() を呼び出して、監視したいファイルディスクリプタを epoll インスタンスに追加・削除したり、監視するイベントの種類を設定したりします。
  3. epoll_wait() を呼び出して、イベントが発生するまで待機します。イベントが発生すると、epoll_wait() はイベントが発生したファイルディスクリプタのリストを返します。

kqueue (FreeBSD, macOS, NetBSD, OpenBSDなど)

kqueue は、BSD系オペレーティングシステム(FreeBSD, macOS, NetBSD, OpenBSDなど)が提供する高性能なI/Oイベント通知メカニズムです。epoll と同様に、多数のファイルディスクリプタからのI/Oイベントを効率的に監視するために使用されます。kqueue はファイルディスクリプタだけでなく、プロセス状態の変化、タイマー、シグナルなど、より多様なイベントを監視できる点が特徴です。

kqueue を使用する基本的な流れは以下の通りです。

  1. kqueue() を呼び出して、kqueue インスタンスを作成し、そのファイルディスクリプタを取得します。
  2. 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 が設定されるようになったため、このテストブロックが再度有効化され、ファイルディスクリプタが正しく閉じられることを検証できるようになりました。具体的には、子プロセスが起動された際に、親プロセスから継承されるべきではないファイルディスクリプタが閉じられていることを確認するテストです。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

  1. 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
     }
    
  2. src/pkg/net/fd_freebsd.go, src/pkg/net/fd_netbsd.go, src/pkg/net/fd_openbsd.go (上記 fd_darwin.go と同様の変更)

  3. 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
    
  4. 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++ {
    
  5. 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
    
  6. src/pkg/syscall/zsyscall_linux_386.go, src/pkg/syscall/zsyscall_linux_amd64.go, src/pkg/syscall/zsyscall_linux_arm.goEpollCreate1 関数の追加)

    --- 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_create1ENOSYS エラー(システムコールが存在しない)を返した場合、それは古い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のネットワークパッケージは、epollkqueue を使用する際に、ファイルディスクリプタの CLOEXEC フラグを適切に設定するようになり、子プロセスへの不要なディスクリプタの継承を防ぎ、より堅牢で安全なアプリケーションの構築に貢献しています。

関連リンク

参考にした情報源リンク

  • Linux man pages (epoll_create1, fcntl)
  • FreeBSD man pages (kqueue)
  • Go言語の公式ドキュメントおよびソースコード
  • Unix/Linuxプログラミングに関する一般的な知識
  • ファイルディスクリプタとCLOEXECに関する技術記事