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

[インデックス 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.Readnet.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関数に焦点を当てています。

  1. PollDescごとのロックの導入:

    • runtime·netpolllock(PollDesc *pd)runtime·netpollunlock(PollDesc *pd)という新しい関数が追加され、PollDesc構造体に関連付けられたロックを操作できるようになりました。
    • これにより、特定のPollDescに対するアクセスがシリアル化され、複数のスレッドからの同時アクセスによる競合状態を防ぎます。特に、ポーラーがイベントを再アームしようとするスレッドと、ファイルディスクリプタをクローズしようとするスレッドの間で発生する競合を緩和します。
  2. runtime·netpollclosing(PollDesc *pd)関数の追加:

    • PollDescがクローズ中であるかどうかをチェックするためのruntime·netpollclosing関数が追加されました。
    • runtime·netpollupdate関数内でこのチェックが導入され、もしPollDescがクローズ中であれば、再アームのリクエストを無視するようになりました。これにより、既に閉じられつつあるファイルディスクリプタに対してポーラーが誤って操作を行おうとするのを防ぎます。
  3. runtime·netpollopenの変更:

    • runtime·netpollopen関数は、ファイルディスクリプタをイベントポートに関連付ける際に、初期状態ではPOLLIN | POLLOUTのような具体的なイベントタイプを登録しないようになりました。
    • 代わりに、runtime·netpollarmが呼び出されたときに、実際に興味のあるイベントタイプ(読み込みまたは書き込み)を登録するようになりました。これにより、port_associateの呼び出しタイミングがより正確になり、不要なイベント登録を避けることができます。また、runtime·netpollopen内でもPollDescロックが使用されるようになりました。
  4. runtime·netpollupdateの変更:

    • runtime·netpollupdate関数は、PollDescがクローズ中である場合に早期リターンするようになりました。
    • また、runtime·cas(Compare And Swap)によるアトミックな更新ループが削除され、代わりに*ep = events;による直接代入が行われるようになりました。これは、netpollupdatePollDescロックによって保護されるようになったため、アトミック操作が不要になったためです。
  5. runtime·netpollarmの変更:

    • runtime·netpollarm関数もPollDescロックで保護されるようになりました。これにより、netpollarmnetpollupdateを呼び出す際に、PollDescの状態が安全にアクセスされることが保証されます。
  6. runtime·netpoll(メインポーラーループ)の変更:

    • イベントが発生した際に、runtime·netpollupdateを呼び出す前にPollDescロックを取得し、更新後に解放するようになりました。これにより、イベント処理中にPollDescの状態が安全に更新されることが保証されます。
    • 特に、レベルトリガーの特性を考慮し、POLLINまたはPOLLOUTイベントが発生した場合、そのイベントに対応するフラグをclear変数に設定し、runtime·netpollupdateを呼び出してそのイベントをクリアするようになりました。これにより、同じイベントが繰り返し通知されるのを防ぎつつ、まだ発生していない他のイベント(例: POLLOUTイベントが発生したがPOLLINはまだ)は引き続き監視されるようにします。
  7. ドキュメントの追加:

    • src/pkg/runtime/netpoll_solaris.cファイルに、Solarisネットワークポーラーの動作原理、イベントポートのレベルトリガー特性、およびruntime·netpollopenruntime·netpollarmruntime·netpollupdateruntime·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·netpollopenruntime·netpollarmruntime·netpollupdateruntime·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ロックによって保護されるようになりました。これにより、netpollarmnetpollupdateを呼び出す際の競合が防止されます。

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の状態変更が安全に行われることを保証しています。

関連リンク

参考にした情報源リンク

  • 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. 解説を生成しました。