[インデックス 15711] ファイルの概要
このコミットは、Go言語のnetパッケージにおけるソケットリスナーのバックログサイズに関する潜在的な問題を修正するものです。具体的には、システムコールが受け取るバックログの最大値が、カーネル内部でuint16(符号なし16ビット整数)として扱われることによって発生するオーバーフロー(ラップアラウンド)を防ぐための変更が加えられています。これにより、非常に大きなバックログ値を設定しようとした際に、意図せずバックログが0として解釈されてしまう問題を回避します。
コミット
commit e64f3f211ac03f270a2db476e01b34fd1d1c5301
Author: Russ Cox <rsc@golang.org>
Date: Tue Mar 12 01:48:48 2013 -0400
net: never use backlog > 65535
The system call takes an int, but the kernel stores it in a uint16.
At least one Linux system sets /proc/sys/net/core/somaxconn
to 262144, which ends up being 0 in the uint16. Avoid being tricked.
FreeBSD sources also store the backlog in a uint16.
Assume the problem is systemic and fix it everywhere.
Fixes #5030.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7480046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e64f3f211ac03f270a2db476e01b34fd1d1c5301
元コミット内容
net: never use backlog > 65535
システムコールはint型のバックログ値を受け取りますが、カーネルはそれをuint16として保存します。少なくとも一つのLinuxシステムでは/proc/sys/net/core/somaxconnが262144に設定されており、これはuint16では0になってしまいます。この誤った解釈を避けます。
FreeBSDのソースコードもバックログをuint16で保存しています。この問題はシステム全体にわたるものと仮定し、すべての箇所で修正します。
Issue #5030 を修正します。
変更の背景
この変更の背景には、ネットワークプログラミングにおけるソケットのlistenシステムコールが受け入れる「バックログ」引数の解釈に関する、オペレーティングシステム(OS)カーネルの実装上の特性があります。
listenシステムコールは、サーバーソケットが同時に処理できる保留中の接続要求(まだacceptされていない接続)の最大数を指定するために使用されます。この最大数を「バックログ」と呼びます。多くのOSでは、このバックログ値を内部的にuint16(符号なし16ビット整数)として扱っています。uint16で表現できる最大値は65535(2^16 - 1)です。
しかし、一部のOS(特にLinux)では、ユーザーが設定できるバックログの最大値(例えば/proc/sys/net/core/somaxconnで設定される値)が65535を超える場合があります。コミットメッセージにあるように、262144という値が設定されているシステムも存在しました。
問題は、システムコールがint型(通常32ビットまたは64ビット)でバックログ値を受け取るにもかかわらず、カーネルがその値をuint16に切り詰めて保存することにありました。この切り詰めが発生すると、例えば262144という値はuint16の範囲を超えているため、モジュロ演算(262144 % 65536)によって0として解釈されてしまいます。結果として、意図したよりもはるかに小さい(または全くない)バックログが設定され、サーバーが大量の同時接続要求を適切に処理できなくなる可能性がありました。これは、サービス拒否(DoS)攻撃につながる脆弱性となる可能性もあります。
この問題はGo言語のnetパッケージがOSのシステムコールをラップしているため、Goアプリケーションにも影響を及ぼします。GoのランタイムがOSから取得した最大バックログ値(例えばSOMAXCONN)をそのまま使用すると、カーネル内部での切り詰めによって予期せぬ動作を引き起こす可能性がありました。
このコミットは、このようなOSカーネルの実装に起因する問題を回避し、Goアプリケーションが常に期待通りのバックログ動作をするようにするための修正です。
前提知識の解説
1. ソケットとlistenシステムコール
- ソケット: ネットワーク通信のエンドポイントです。サーバーアプリケーションは、クライアントからの接続を受け入れるために「リスニングソケット」を作成します。
listenシステムコール: サーバーソケットをリスニング状態にし、クライアントからの接続要求を受け入れる準備をします。この関数は通常、2つの引数を取ります。sockfd: リスニングするソケットのファイルディスクリプタ。backlog: 保留中の接続要求(まだacceptされていない接続)の最大数。これは、カーネルがキューに保持できる接続の数を制限します。
2. バックログ (Backlog)
バックログは、サーバーがacceptシステムコールを呼び出す前に、OSカーネルがキューに保持できる未処理の接続要求の最大数です。
- クライアントがサーバーに接続しようとすると、まずTCPの3ウェイハンドシェイクが行われます。
- ハンドシェイクが完了すると、その接続はカーネルの「完了済み接続キュー」(または「受け入れ待ちキュー」)に入れられます。
- サーバーアプリケーションが
acceptを呼び出すと、このキューから接続が取り出され、処理されます。 - バックログは、この完了済み接続キューの最大サイズを決定します。バックログが満杯の状態でさらに接続要求が来ると、その接続要求は拒否されるか、タイムアウトする可能性があります。
3. SOMAXCONN
SOMAXCONNは、listenシステムコールで指定できるバックログのデフォルトの最大値を示す定数です。これはOSによって定義されており、通常はシステムヘッダーファイル(例: Linuxのsys/socket.h)で定義されています。
- Linuxでは、
/proc/sys/net/core/somaxconnというカーネルパラメータでこの値を調整できます。このパラメータは、システム全体で許可されるlistenバックログの最大値を設定します。
4. uint16 (符号なし16ビット整数)
- データ型:
uint16は、0から65535までの整数値を表現できるデータ型です。16ビット(2バイト)のメモリを使用します。 - オーバーフロー:
uint16の最大値である65535を超える値を格納しようとすると、値は「ラップアラウンド」します。例えば、65536は0に、65537は1に、といった具合に、最大値を超えた分が最小値から数え直されます。これはモジュロ演算(値 % (2^16))と同じ結果になります。
5. システムコール (System Call)
- 定義: アプリケーションプログラムがOSカーネルのサービスを要求するためのインターフェースです。ファイルI/O、ネットワーク通信、メモリ管理など、OSが提供する低レベルな機能にアクセスするために使用されます。
- Go言語とシステムコール: Go言語の標準ライブラリ(特に
syscallパッケージやnetパッケージ)は、OSのシステムコールを抽象化して、Goプログラムから利用できるようにしています。
技術的詳細
このコミットの技術的な核心は、OSカーネルがlistenシステムコールのバックログ引数をどのように内部的に処理するかという点にあります。
-
問題の特定:
- Goの
netパッケージは、listenシステムコールを呼び出す際に、OSから取得した推奨バックログ値(例えばSOMAXCONNや/proc/sys/net/core/somaxconnから読み取った値)を使用します。 - しかし、一部のOS(LinuxやFreeBSD)のカーネル実装では、
listenシステムコールに渡されたint型のバックログ値を、内部的にuint16型の変数に格納していました。 - この
intからuint16への暗黙的な型変換(または切り詰め)により、もしint型のバックログ値が65535(uint16の最大値)を超えていた場合、値がラップアラウンドしてしまい、カーネルが認識するバックログ値が意図せず非常に小さい値(または0)になってしまう問題が発生しました。 - コミットメッセージの例では、Linuxの
/proc/sys/net/core/somaxconnが262144に設定されている場合、これがuint16に切り詰められると262144 % 65536 = 0となり、バックログが0として扱われてしまうことが指摘されています。
- Goの
-
修正アプローチ:
- この問題を解決するために、Goの
netパッケージは、OSから取得したバックログ値がuint16の最大値(1<<16 - 1、つまり65535)を超える場合、その値を強制的に65535に切り詰めるように変更されました。 - これにより、カーネル内部でのラップアラウンドを防ぎ、Goアプリケーションが設定しようとしたバックログ値が、少なくともOSがサポートする範囲内で最大の有効な値として解釈されるようになります。
- この修正は、Linuxだけでなく、FreeBSDや他のBSD系OS、そしてWindowsにも適用されました。これは、同様の
uint16によるバックログの内部処理が他のOSでも行われている可能性を考慮した、予防的な措置です。
- この問題を解決するために、Goの
-
影響:
- この修正により、Goアプリケーションは、OSが設定可能な最大バックログ値が65535を超える場合でも、意図しないバックログの縮小や0へのリセットを心配することなく、安定して動作するようになります。
- サーバーアプリケーションの堅牢性が向上し、高負荷時でもより多くの保留中の接続を適切に処理できるようになります。
コアとなるコードの変更箇所
このコミットでは、src/pkg/netディレクトリ内の以下の3つのファイルが変更されています。
src/pkg/net/sock_bsd.gosrc/pkg/net/sock_linux.gosrc/pkg/net/sock_windows.go
これらのファイルは、それぞれBSD系OS(FreeBSDを含む)、Linux、Windowsにおけるソケット関連のシステムコールをラップするGoのコードを含んでいます。
具体的な変更は、各ファイル内のmaxListenerBacklog()関数に、バックログ値が1<<16 - 1(65535)を超える場合にその値を切り詰めるロジックが追加された点です。
src/pkg/net/sock_bsd.go の変更
--- a/src/pkg/net/sock_bsd.go
+++ b/src/pkg/net/sock_bsd.go
@@ -27,5 +27,11 @@ func maxListenerBacklog() int {
if n == 0 || err != nil {
return syscall.SOMAXCONN
}
+ // FreeBSD stores the backlog in a uint16, as does Linux.
+ // Assume the other BSDs do too. Truncate number to avoid wrapping.
+ // See issue 5030.
+ if n > 1<<16-1 {
+ n = 1<<16 - 1
+ }
return int(n)
}
src/pkg/net/sock_linux.go の変更
--- a/src/pkg/net/sock_linux.go
+++ b/src/pkg/net/sock_linux.go
@@ -21,5 +21,11 @@ func maxListenerBacklog() int {
if n == 0 || !ok {
return syscall.SOMAXCONN
}
+ // Linux stores the backlog in a uint16.
+ // Truncate number to avoid wrapping.
+ // See issue 5030.
+ if n > 1<<16-1 {
+ n = 1<<16 - 1
+ }
return n
}
src/pkg/net/sock_windows.go の変更
--- a/src/pkg/net/sock_windows.go
+++ b/src/pkg/net/sock_windows.go
@@ -8,6 +8,7 @@ import "syscall"
func maxListenerBacklog() int {
// TODO: Implement this
+ // NOTE: Never return a number bigger than 1<<16 - 1. See issue 5030.
return syscall.SOMAXCONN
}
Windows版では、maxListenerBacklog()関数自体はまだ実装されていませんが、将来の実装に向けたコメントが追加され、同様の制約が適用されるべきであることが示されています。
コアとなるコードの解説
変更のコアとなるのは、maxListenerBacklog()関数内に挿入された以下の条件分岐です。
if n > 1<<16-1 {
n = 1<<16 - 1
}
このコードスニペットは、maxListenerBacklog()関数がOSから取得した推奨バックログ値nを返す直前に実行されます。
n: OSから取得した、リスナーのバックログとして使用できる最大値。Linuxでは/proc/sys/net/core/somaxconnから読み取られた値、BSD系OSではsysctlなどから取得された値がこれに該当します。1<<16-1: これは2^16 - 1をビットシフト演算で表現したもので、数値としては65535に相当します。これはuint16型で表現できる最大値です。
このif文の目的は以下の通りです。
- チェック:
nがuint16の最大値(65535)を超えているかどうかをチェックします。 - 切り詰め: もし
nが65535を超えていた場合、nの値を強制的に65535に設定し直します。
これにより、Goのnetパッケージがlistenシステムコールに渡すバックログ値は、OSカーネルがuint16として内部的に処理する際にラップアラウンドするのを確実に防ぐことができます。たとえOSが非常に大きなバックログ値を推奨していたとしても、Goはカーネルの制約を考慮して、安全な最大値に制限してシステムコールを呼び出すようになります。
Windows版のsock_windows.goでは、maxListenerBacklog()関数がまだsyscall.SOMAXCONNを直接返しているため、具体的な切り詰めロジックは追加されていませんが、コメントで「1<<16 - 1より大きな値を返してはならない」という注意書きが追加されており、将来の実装で同様の考慮が必要であることが示されています。
関連リンク
- Go Issue #5030: https://go.dev/issue/5030
- Go Change List (CL) 7480046: https://go.googlesource.com/go/+/7480046
参考にした情報源リンク
- https://go.dev/issue/5030 (Go Issue Tracker)
- https://go.googlesource.com/go/+/7480046 (Go Git Repository - Change List)
- https://man7.org/linux/man-pages/man2/listen.2.html (Linux
listenman page) - https://www.freebsd.org/cgi/man.cgi?query=listen&sektion=2 (FreeBSD
listenman page) - https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html (Linux Kernel Documentation -
net.core.somaxconn) - https://en.wikipedia.org/wiki/Integer_overflow (Wikipedia - Integer overflow)
- https://en.wikipedia.org/wiki/Socket_(network) (Wikipedia - Socket (network))