[インデックス 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.go
src/pkg/net/sock_linux.go
src/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
listen
man page) - https://www.freebsd.org/cgi/man.cgi?query=listen&sektion=2 (FreeBSD
listen
man 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))