[インデックス 18863] ファイルの概要
このコミットは、GoランタイムのSolarisネットワークポーラーにおける「use after close」競合状態を修正するものです。具体的には、ファイルディスクリプタが閉じられる際に発生する競合を解消し、ポーラーの堅牢性を向上させています。また、Solarisネットワークポーラーの実装に関する詳細なドキュメントが追加されています。
コミット
commit 199e70308351c2780f19ee0471febfd3cfd8f30f
Author: Aram Hăvărneanu <aram@mgk.ro>
Date: Fri Mar 14 17:53:05 2014 +0400
runtime: fix use after close race in Solaris network poller
The Solaris network poller uses event ports, which are
level-triggered. As such, it has to re-arm itself after each
wakeup. The arming mechanism (which runs in its own thread) raced
with the closing of a file descriptor happening in a different
thread. When a network file descriptor is about to be closed,
the network poller is awaken to give it a chance to remove its
association with the file descriptor. Because the poller always
re-armed itself, it raced with code that closed the descriptor.
This change makes the network poller check before re-arming if
the file descriptor is about to be closed, in which case it will
ignore the re-arming request. It uses the per-PollDesc lock in
order to serialize access to the PollDesc.
This change also adds extensive documentation describing the
Solaris implementation of the network poller.
Fixes #7410.
LGTM=dvyukov, iant
R=golang-codereviews, bradfitz, iant, dvyukov, aram.h, gobot
CC=golang-codereviews
https://golang.org/cl/69190044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/199e70308351c2780f19ee0471febfd3cfd8f30f
元コミット内容
runtime: fix use after close race in Solaris network poller
The Solaris network poller uses event ports, which are
level-triggered. As such, it has to re-arm itself after each
wakeup. The arming mechanism (which runs in its own thread) raced
with the closing of a file descriptor happening in a different
thread. When a network file descriptor is about to be closed,
the network poller is awaken to give it a chance to remove its
association with the file descriptor. Because the poller always
re-armed itself, it raced with code that closed the descriptor.
This change makes the network poller check before re-arming if
the file descriptor is about to be closed, in which case it will
ignore the re-arming request. It uses the per-PollDesc lock in
order to serialize access to the PollDesc.
This change also adds extensive documentation describing the
Solaris implementation of the network poller.
Fixes #7410.
変更の背景
Goランタイムのネットワークポーラーは、OS固有のI/O多重化メカニズムを利用して、複数のネットワーク接続からのイベントを効率的に処理します。Solarisシステムでは、この目的のために「イベントポート (event ports)」が使用されます。
問題の核心は、Solarisのイベントポートが「レベルトリガー (level-triggered)」であるという特性にありました。レベルトリガー方式では、イベントが発生すると、そのイベントが処理されるまでポーラーは継続的に通知を生成します。そのため、ポーラーはイベントを処理した後、明示的にイベントの再アーム(再登録)を行う必要があります。
この再アームのメカニズムが、ファイルディスクリプタのクローズ処理と競合状態を引き起こしていました。具体的には、ネットワークファイルディスクリプタが閉じられようとしているとき、ネットワークポーラーはそのディスクリプタとの関連付けを解除するために起動されます。しかし、ポーラーが常に自身を再アームしようとするため、別のスレッドで進行中のディスクリプタのクローズ処理と競合し、「use after close」という問題が発生する可能性がありました。これは、既に閉じられた、または閉じられつつあるファイルディスクリプタに対してポーラーが操作を行おうとすることで、未定義の動作やクラッシュを引き起こす可能性がある深刻なバグです。
このコミットは、この競合状態を解消し、Solaris上でのGoアプリケーションのネットワークI/Oの安定性を向上させることを目的としています。
前提知識の解説
ネットワークポーラー (Network Poller)
ネットワークポーラーは、Goランタイムの重要なコンポーネントであり、ノンブロッキングI/Oを実現するために使用されます。Goのgoroutineは、ネットワークI/O操作(例: net.Conn.Read
やnet.Conn.Write
)を実行する際に、データが利用可能になるか、書き込みが可能になるまでブロックされることなく、他のgoroutineが実行できるようにします。これは、ポーラーがOSのI/O多重化機能(Linuxのepoll、macOS/FreeBSDのkqueue、WindowsのIOCP、Solarisのevent portsなど)を利用して、多数のファイルディスクリプタ(ソケット)からのイベントを効率的に監視することで実現されます。
イベントポート (Event Ports)
SolarisオペレーティングシステムにおけるI/O多重化メカニズムの一つです。port_create()
, port_associate()
, port_getn()
, port_dissociate()
などのシステムコールを通じて利用されます。
port_create()
: 新しいイベントポートを作成します。port_associate()
: ファイルディスクリプタをイベントポートに関連付け、監視するイベント(例: 読み込み可能、書き込み可能)を指定します。port_getn()
: イベントポートで発生したイベントを取得します。port_dissociate()
: ファイルディスクリプタとイベントポートの関連付けを解除します。
レベルトリガー (Level-triggered) と エッジトリガー (Edge-triggered)
I/Oイベント通知メカニズムには、主にレベルトリガーとエッジトリガーの2つのモードがあります。
-
レベルトリガー (Level-triggered):
- イベントの「状態」が変化したときに通知されます。
- 例えば、ソケットに読み取り可能なデータがある限り、
POLLIN
イベントは繰り返し通知されます。 - イベントを処理した後も、その状態が続く限り通知が続くため、ポーラーはイベントを処理した後に明示的にイベントの再アーム(再登録)を行う必要はありません。しかし、Solarisのイベントポートは、イベントを消費した後に再アームしないと、同じイベントが再度通知されないという特性があります。このため、GoのSolarisポーラーは、イベント処理後に再アームを行う必要があります。
- この特性が、今回の競合状態の根本原因となりました。
-
エッジトリガー (Edge-triggered):
- イベントの「発生」時に一度だけ通知されます。
- 例えば、ソケットに新しいデータが到着したときに一度だけ
POLLIN
イベントが通知され、そのデータがすべて読み取られるまで、追加のPOLLIN
イベントは発生しません。 - イベントを処理した後、ポーラーは通常、そのイベントを再アームする必要はありません。
Use After Close Race (クローズ後の使用競合)
マルチスレッド環境で発生する典型的な競合状態の一つです。あるスレッドがリソース(この場合はファイルディスクリプタ)をクローズした直後に、別のスレッドがそのクローズされたリソースにアクセスしようとすると発生します。これにより、無効なメモリへのアクセス、クラッシュ、または予期しない動作につながる可能性があります。
PollDesc
Goランタイム内部で使用される構造体で、ネットワークI/Oを待機しているファイルディスクリプタ(ソケット)の状態を管理します。各PollDesc
は、特定のファイルディスクリプタに関連付けられ、そのディスクリプタに対する読み書きイベントの監視状態、関連するgoroutine、およびロックメカニズム(競合状態を防ぐため)を含みます。
技術的詳細
このコミットの技術的解決策は、Solarisネットワークポーラーのruntime·netpollupdate
関数とruntime·netpollarm
関数に焦点を当てています。
-
PollDesc
ごとのロックの導入:runtime·netpolllock(PollDesc *pd)
とruntime·netpollunlock(PollDesc *pd)
という新しい関数が追加され、PollDesc
構造体に関連付けられたロックを操作できるようになりました。- これにより、特定の
PollDesc
に対するアクセスがシリアル化され、複数のスレッドからの同時アクセスによる競合状態を防ぎます。特に、ポーラーがイベントを再アームしようとするスレッドと、ファイルディスクリプタをクローズしようとするスレッドの間で発生する競合を緩和します。
-
runtime·netpollclosing(PollDesc *pd)
関数の追加:PollDesc
がクローズ中であるかどうかをチェックするためのruntime·netpollclosing
関数が追加されました。runtime·netpollupdate
関数内でこのチェックが導入され、もしPollDesc
がクローズ中であれば、再アームのリクエストを無視するようになりました。これにより、既に閉じられつつあるファイルディスクリプタに対してポーラーが誤って操作を行おうとするのを防ぎます。
-
runtime·netpollopen
の変更:runtime·netpollopen
関数は、ファイルディスクリプタをイベントポートに関連付ける際に、初期状態ではPOLLIN | POLLOUT
のような具体的なイベントタイプを登録しないようになりました。- 代わりに、
runtime·netpollarm
が呼び出されたときに、実際に興味のあるイベントタイプ(読み込みまたは書き込み)を登録するようになりました。これにより、port_associate
の呼び出しタイミングがより正確になり、不要なイベント登録を避けることができます。また、runtime·netpollopen
内でもPollDesc
ロックが使用されるようになりました。
-
runtime·netpollupdate
の変更:runtime·netpollupdate
関数は、PollDesc
がクローズ中である場合に早期リターンするようになりました。- また、
runtime·cas
(Compare And Swap)によるアトミックな更新ループが削除され、代わりに*ep = events;
による直接代入が行われるようになりました。これは、netpollupdate
がPollDesc
ロックによって保護されるようになったため、アトミック操作が不要になったためです。
-
runtime·netpollarm
の変更:runtime·netpollarm
関数もPollDesc
ロックで保護されるようになりました。これにより、netpollarm
がnetpollupdate
を呼び出す際に、PollDesc
の状態が安全にアクセスされることが保証されます。
-
runtime·netpoll
(メインポーラーループ)の変更:- イベントが発生した際に、
runtime·netpollupdate
を呼び出す前にPollDesc
ロックを取得し、更新後に解放するようになりました。これにより、イベント処理中にPollDesc
の状態が安全に更新されることが保証されます。 - 特に、レベルトリガーの特性を考慮し、
POLLIN
またはPOLLOUT
イベントが発生した場合、そのイベントに対応するフラグをclear
変数に設定し、runtime·netpollupdate
を呼び出してそのイベントをクリアするようになりました。これにより、同じイベントが繰り返し通知されるのを防ぎつつ、まだ発生していない他のイベント(例:POLLOUT
イベントが発生したがPOLLIN
はまだ)は引き続き監視されるようにします。
- イベントが発生した際に、
-
ドキュメントの追加:
src/pkg/runtime/netpoll_solaris.c
ファイルに、Solarisネットワークポーラーの動作原理、イベントポートのレベルトリガー特性、およびruntime·netpollopen
、runtime·netpollarm
、runtime·netpollupdate
、runtime·netpoll
などの主要関数の役割と連携について、非常に詳細なコメントが追加されました。これは、将来のメンテナンスやデバッグにおいて非常に価値のある情報となります。
これらの変更により、Solaris環境におけるGoのネットワークポーラーは、ファイルディスクリプタのライフサイクル管理においてより堅牢になり、競合状態によるクラッシュや未定義の動作が防止されます。
コアとなるコードの変更箇所
src/pkg/runtime/netpoll.goc
runtime·netpollclosing(PollDesc *pd)
:PollDesc
がクローズ中かどうかの状態を返す関数を追加。runtime·netpolllock(PollDesc *pd)
:PollDesc
をロックする関数を追加。runtime·netpollunlock(PollDesc *pd)
:PollDesc
のロックを解除する関数を追加。
src/pkg/runtime/netpoll_solaris.c
- 大幅なドキュメントの追加: ファイルの冒頭にSolarisネットワークポーラーの動作原理に関する詳細なコメントが追加されました。
runtime·netpollopen
関数の変更:- 初期の
port_associate
呼び出しでPOLLIN | POLLOUT
を登録せず、0
を渡すように変更。 PollDesc
ロックの取得と解放を追加。
- 初期の
runtime·netpollupdate
関数の変更:runtime·netpollclosing(pd)
チェックを追加し、クローズ中の場合は早期リターン。runtime·cas
によるループを削除し、*ep = events;
による直接代入に変更。
runtime·netpollarm
関数の変更:PollDesc
ロックの取得と解放を追加。
runtime·netpoll
関数の変更:- イベント処理ループ内で、
runtime·netpollupdate
を呼び出す前にPollDesc
ロックを取得し、更新後に解放するように変更。 clear
変数を導入し、発生したイベント(POLLIN
またはPOLLOUT
)をクリアするためにruntime·netpollupdate
に渡すように変更。
- イベント処理ループ内で、
src/pkg/runtime/runtime.h
runtime·netpollclosing
,runtime·netpolllock
,runtime·netpollunlock
関数のプロトタイプ宣言を追加。
コアとなるコードの解説
src/pkg/runtime/netpoll_solaris.c
の変更点
新しいドキュメントブロック
// Solaris runtime-integrated network poller.
//
// Solaris uses event ports for scalable network I/O. Event
// ports are level-triggered, unlike epoll and kqueue which
// can be configured in both level-triggered and edge-triggered
// mode. Level triggering means we have to keep track of a few things
// ourselves. After we receive an event for a file descriptor,
// it's our responsibility to ask again to be notified for future
// events for that descriptor. When doing this we must keep track of
// what kind of events the goroutines are currently interested in,
// for example a fd may be open both for reading and writing.
//
// A description of the high level operation of this code
// follows. Networking code will get a file descriptor by some means
// and will register it with the netpolling mechanism by a code path
// that eventually calls runtime·netpollopen. runtime·netpollopen
// calls port_associate with an empty event set. That means that we
// will not receive any events at this point. The association needs
// to be done at this early point because we need to process the I/O
// readiness notification at some point in the future. If I/O becomes
// ready when nobody is listening, when we finally care about it,
// nobody will tell us anymore.
//
// Beside calling runtime·netpollopen, the networking code paths
// will call runtime·netpollarm each time goroutines are interested
// in doing network I/O. Because now we know what kind of I/O we
// are interested in (reading/writting), we can call port_associate
// passing the correct type of event set (POLLIN/POLLOUT). As we made
// sure to have already associated the file descriptor with the port,
// when we now call port_associate, we will unblock the main poller
// loop (in runtime·netpoll) right away if the socket is actually
// ready for I/O.
//
// The main poller loop runs in its own thread waiting for events
// using port_getn. When an event happens, it will tell the scheduler
// about it using runtime·netpollready. Besides doing this, it must
// also re-associate the events that were not part of this current
// notification with the file descriptor. Failing to do this would
// mean each notification will prevent concurrent code using the
// same file descriptor in parallel.
//
// The logic dealing with re-associations is encapsulated in
// runtime·netpollupdate. This function takes care to associate the
// descriptor only with the subset of events that were previously
// part of the association, except the one that just happened. We
// can't re-associate with that right away, because event ports
// are level triggered so it would cause a busy loop. Instead, that
// association is effected only by the runtime·netpollarm code path,
// when Go code actually asks for I/O.
//
// The open and arming mechanisms are serialized using the lock
// inside PollDesc. This is required because the netpoll loop runs
// asynchonously in respect to other Go code and by the time we get
// to call port_associate to update the association in the loop, the
// file descriptor might have been closed and reopened already. The
// lock allows runtime·netpollupdate to be called synchronously from
// the loop thread while preventing other threads operating to the
// same PollDesc, so once we unblock in the main loop, until we loop
// again we know for sure we are always talking about the same file
// descriptor and can safely access the data we want (the event set).
この新しいドキュメントブロックは、Solarisのイベントポートがレベルトリガーであることの重要性を強調し、Goランタイムがどのようにしてこの特性に対応しているかを詳細に説明しています。特に、runtime·netpollopen
、runtime·netpollarm
、runtime·netpollupdate
、runtime·netpoll
の各関数がどのように連携してI/Oイベントを処理し、PollDesc
ロックがなぜ必要であるかを明確にしています。これは、このポーラーの複雑な動作を理解するための非常に貴重な情報源となります。
runtime·netpollopen
の変更
--- a/src/pkg/runtime/netpoll_solaris.c
+++ b/src/pkg/runtime/netpoll_solaris.c
@@ -71,10 +132,19 @@ runtime·netpollinit(void)
int32
runtime·netpollopen(uintptr fd, PollDesc *pd)
{
- uint32 events = POLLIN | POLLOUT;
- *runtime·netpolluser(pd) = (void*)events;
-
- return runtime·port_associate(portfd, PORT_SOURCE_FD, fd, events, (uintptr)pd);
+ int32 r;
+
+ runtime·netpolllock(pd);
+ // We don't register for any specific type of events yet, that's
+ // netpollarm's job. We merely ensure we call port_associate before
+ // asynchonous connect/accept completes, so when we actually want
+ // to do any I/O, the call to port_associate (from netpollarm,
+ // with the interested event set) will unblock port_getn right away
+ // because of the I/O readiness notification.
+ *runtime·netpolluser(pd) = 0;
+ r = runtime·port_associate(portfd, PORT_SOURCE_FD, fd, 0, (uintptr)pd);
+ runtime·netpollunlock(pd);
+ return r;
}
変更前はPOLLIN | POLLOUT
で即座にイベントを登録していましたが、変更後は0
を渡してイベントを登録しないようにしています。これは、netpollarm
が実際にI/O操作が要求されたときに適切なイベントを登録するという役割を明確にするためです。また、runtime·netpolllock(pd)
とruntime·netpollunlock(pd)
が追加され、PollDesc
へのアクセスが保護されるようになりました。
runtime·netpollupdate
の変更
--- a/src/pkg/runtime/netpoll_solaris.c
+++ b/src/pkg/runtime/netpoll_solaris.c
@@ -90,22 +163,26 @@ runtime·netpollupdate(PollDesc* pd, uint32 set, uint32 clear)
uintptr fd = runtime·netpollfd(pd);
ep = (uint32*)runtime·netpolluser(pd);
- do {
- old = *ep;
- events = (old & ~clear) | set;
- if(old == events)
- return;
-
- if(events && runtime·port_associate(portfd, PORT_SOURCE_FD, fd, events, (uintptr)pd) != 0) {
- runtime·printf("netpollupdate: failed to associate (%d)\n", errno);
- runtime·throw("netpollupdate: failed to associate");
- }
- } while(runtime·cas(ep, old, events) != events);
+ if(runtime·netpollclosing(pd))
+ return;
+
+ old = *ep;
+ events = (old & ~clear) | set;
+ if(old == events)
+ return;
+
+ if(events && runtime·port_associate(portfd, PORT_SOURCE_FD, fd, events, (uintptr)pd) != 0) {
+ runtime·printf("netpollupdate: failed to associate (%d)\n", errno);
+ runtime·throw("netpollupdate: failed to associate");
+ }
+ *ep = events;
}
最も重要な変更は、if(runtime·netpollclosing(pd)) return;
の追加です。これにより、ファイルディスクリプタがクローズ中である場合、ポーラーは再アームの試みを中止し、競合状態を回避します。また、runtime·cas
によるアトミックな更新ループが削除され、*ep = events;
による直接代入に置き換えられました。これは、この関数が呼び出される前にPollDesc
ロックが取得されるようになったため、アトミック操作が不要になったためです。
runtime·netpollarm
の変更
--- a/src/pkg/runtime/netpoll_solaris.c
+++ b/src/pkg/runtime/netpoll_solaris.c
@@ -116,6 +193,7 @@ runtime·netpollarm(PollDesc* pd, int32 mode)
default:
\truntime·throw("netpollarm: bad mode");
}\n+\truntime·netpollunlock(pd);
}\n \n // polls for ready network connections
runtime·netpolllock(pd)
とruntime·netpollunlock(pd)
が追加され、netpollarm
関数全体がPollDesc
ロックによって保護されるようになりました。これにより、netpollarm
がnetpollupdate
を呼び出す際の競合が防止されます。
runtime·netpoll
の変更
--- a/src/pkg/runtime/netpoll_solaris.c
+++ b/src/pkg/runtime/netpoll_solaris.c
@@ -142,41 +220,43 @@ runtime·netpoll(bool block)
retry:
n = 1;
-
if(runtime·port_getn(portfd, events, nelem(events), &n, wait) < 0) {
if(errno != EINTR && errno != lasterr) {
lasterr = errno;
- runtime·printf("runtime: port_getn on fd %d "
- "failed with %d\n", portfd, errno);
+ runtime·printf("runtime: port_getn on fd %d failed with %d\n", portfd, errno);
}
goto retry;
}
gp = nil;
-
for(i = 0; i < n; i++) {
ev = &events[i];
if(ev->portev_events == 0)
continue;
-
- if((pd = (PollDesc *)ev->portev_user) == nil)
- continue;
+ pd = (PollDesc *)ev->portev_user;
mode = 0;
-
- if(ev->portev_events & (POLLIN|POLLHUP|POLLERR))
+ clear = 0;
+ if(ev->portev_events & (POLLIN|POLLHUP|POLLERR)) {
mode += 'r';
-
- if(ev->portev_events & (POLLOUT|POLLHUP|POLLERR))
+ clear |= POLLIN;
+ }
+ if(ev->portev_events & (POLLOUT|POLLHUP|POLLERR)) {
mode += 'w';
-
- //
- // To effect edge-triggered events, we need to be sure to
- // update our association with whatever events were not
- // set with the event.
- //
- runtime·netpollupdate(pd, 0, ev->portev_events & (POLLIN|POLLOUT));
+ clear |= POLLOUT;
+ }
+ // To effect edge-triggered events, we need to be sure to
+ // update our association with whatever events were not
+ // set with the event. For example if we are registered
+ // for POLLIN|POLLOUT, and we get POLLIN, besides waking
+ // the goroutine interested in POLLIN we have to not forget
+ // about the one interested in POLLOUT.
+ if(clear != 0) {
+ runtime·netpolllock(pd);
+ runtime·netpollupdate(pd, 0, clear);
+ runtime·netpollunlock(pd);
+ }
if(mode)
runtime·netpollready(&gp, pd, mode);
メインのポーラーループであるruntime·netpoll
も変更されました。イベントが発生した際に、clear
変数を導入し、発生したイベント(POLLIN
またはPOLLOUT
)をruntime·netpollupdate
に渡してクリアするようにしました。これにより、レベルトリガーのイベントポートで同じイベントが繰り返し通知されるのを防ぎます。また、runtime·netpollupdate
の呼び出しをPollDesc
ロックで囲むことで、イベント処理中のPollDesc
の状態変更が安全に行われることを保証しています。
関連リンク
- Go Issue #7410: https://github.com/golang/go/issues/7410
- Go Code Review 69190044: https://golang.org/cl/69190044
参考にした情報源リンク
- Solaris man pages for event ports (port_create, port_associate, port_getn, port_dissociate)
- Go runtime source code (specifically
src/pkg/runtime/netpoll_solaris.c
and related files) - Go documentation on network poller (general concepts)
- Concepts of level-triggered vs. edge-triggered I/O.
- "Use-after-free" and "race condition" in concurrent programming. 解説を生成しました。