[インデックス 15152] ファイルの概要
このコミットは、Go言語のsyscall
パッケージとnet
パッケージにおけるUnixソケットの自動バインド(autobind)機能に関するバグ修正と、それに関連するテストの追加を目的としています。特にLinux環境下でのUnixドメインソケットの挙動に焦点を当てています。
コミット
commit 309d88e28cf91e417d8f92bb6c85c08ed43e8304
Author: Albert Strasheim <fullung@gmail.com>
Date: Wed Feb 6 06:45:57 2013 -0800
syscall, net: Fix unix socket autobind on Linux.
R=rsc, iant, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/7300047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/309d88e28cf91e417d8f92bb6c85c08ed43e8304
元コミット内容
このコミットの元のメッセージは以下の通りです。
syscall, net: Fix unix socket autobind on Linux.
R=rsc, iant, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/7300047
これは、syscall
パッケージとnet
パッケージにおけるLinux上でのUnixソケットの自動バインドに関する修正であることを簡潔に示しています。
変更の背景
このコミットの背景には、LinuxにおけるUnixドメインソケットの「抽象名前空間(abstract namespace)」を利用した自動バインド機能の不具合がありました。
通常のUnixドメインソケットはファイルシステム上のパスにバインドされますが、Linuxではファイルシステム上にエントリを作成せずにソケットをバインドできる「抽象名前空間」という機能があります。この機能は、ソケットアドレスの先頭にNULLバイト(\0
)または@
文字を置くことで利用されます。これにより、ソケットファイルが残存する問題(ソケットが閉じられた後もファイルが残ってしまう)を回避できます。
Goのnet
パッケージでUnixドメインソケットを使用する際、ListenUnixgram
などの関数でアドレスを指定しない(空文字列""
を指定する)と、システムが自動的に一時的なアドレスを割り当ててバインドする「自動バインド」が行われます。Linuxの場合、この自動バインドは抽象名前空間を利用して行われることが期待されます。
しかし、このコミット以前の実装では、syscall.SockaddrUnix
構造体のアドレス長(sl
)の計算に誤りがあり、特に抽象名前空間のアドレス(先頭が@
またはNULLバイト)の場合に、アドレス長が正しく計算されず、結果としてソケットのバインドや接続が正常に行われない、あるいは予期せぬエラーが発生する可能性がありました。具体的には、抽象名前空間のアドレスでは末尾のNULLバイトをアドレス長に含めないというLinuxの慣習が考慮されていませんでした。
このバグにより、GoプログラムがLinux上でUnixドメインソケットの自動バインド機能を利用しようとした際に、接続が確立できない、またはソケットアドレスの取得が正しく行えないといった問題が発生していました。このコミットは、この自動バインドの挙動を修正し、Linuxの抽象名前空間の仕様に正しく準拠させることを目的としています。
前提知識の解説
Unixドメインソケット (Unix Domain Socket, UDS)
Unixドメインソケットは、同じホストマシン上で動作するプロセス間通信(IPC: Inter-Process Communication)のためのメカニズムです。TCP/IPソケットがネットワークを介した通信に使用されるのに対し、Unixドメインソケットはカーネルを介して直接データをやり取りするため、ネットワークオーバーヘッドがなく、非常に高速です。
Unixドメインソケットには主に以下の2種類があります。
- ストリームソケット (SOCK_STREAM): TCPと同様に、信頼性のある接続指向のバイトストリーム通信を提供します。
- データグラムソケット (SOCK_DGRAM): UDPと同様に、コネクションレスで信頼性のないデータグラム通信を提供します。
Unixドメインソケットは通常、ファイルシステム上のパス(例: /tmp/my_socket
)にバインドされます。クライアントはこのパスを使用してサーバーソケットに接続します。
LinuxのUnixドメインソケット抽象名前空間 (Abstract Namespace)
Linuxカーネルは、標準的なファイルシステムパスにバインドするUnixドメインソケットに加えて、「抽象名前空間」と呼ばれる特別な機能を提供します。
- 特徴:
- ファイルシステム上にエントリを作成しません。これにより、ソケットが閉じられた後にファイルが残存する「ソケットファイル残存問題」を回避できます。
- アドレスはNULLバイト(
\0
)で始まる文字列として指定されます。Go言語のnet
パッケージでは、慣習的に@
文字で始まるアドレスが抽象名前空間のアドレスとして扱われます(内部的には@
がNULLバイトに変換されます)。 - アドレスの長さの計算において、末尾のNULLバイトは含まれません。これはファイルシステムパスにバインドされるソケットとは異なる重要な点です。
- 利点:
- ソケットファイルのクリーンアップが不要になります。
- ファイルシステム権限の問題を回避できます。
- 一時的なソケットや、特定のアプリケーション内でのみ使用されるソケットに適しています。
ソケットアドレス構造体 (sockaddr_un
)
Unixドメインソケットのアドレスは、C言語のstruct sockaddr_un
という構造体で表現されます。この構造体は通常、ソケットのファミリー(sa_family
、AF_UNIX
またはAF_LOCAL
)と、ソケットのパス(sun_path
)を含みます。
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX
char sun_path[108]; // Pathname
};
sun_path
のサイズはシステムによって異なりますが、一般的に108バイト程度です。抽象名前空間のアドレスの場合、sun_path
の最初のバイトはNULLバイト(\0
)になります。
socklen_t
(ソケットアドレス長)
ソケット関連のシステムコール(bind
, connect
, accept
など)では、ソケットアドレス構造体へのポインタと共に、そのアドレス構造体の実際の長さを示すsocklen_t
型の引数が必要です。この長さの正確な計算は非常に重要であり、誤りがあるとシステムコールが失敗したり、予期せぬ動作を引き起こしたりします。
抽象名前空間のアドレスの場合、sockaddr_un
構造体のsun_path
フィールドの先頭にNULLバイトを配置し、そのNULLバイトを含まない形でアドレス長を計算する必要があります。
技術的詳細
このコミットは、主にGoのsyscall
パッケージ内のSockaddrUnix
構造体のsockaddr()
メソッドにおけるアドレス長(sl
)の計算ロジックを修正しています。
Goのsyscall
パッケージは、OSのシステムコールを直接呼び出すための低レベルなインターフェースを提供します。SockaddrUnix
はUnixドメインソケットのアドレスを表現するためのGoの構造体であり、そのsockaddr()
メソッドは、このGoの構造体をC言語のsockaddr_un
構造体に対応する形式に変換し、システムコールに渡すためのポインタと長さを返します。
修正前のコードでは、sockaddr()
メソッド内でアドレス長sl
を計算する際に、以下のロジックが使われていました。
// 修正前
sl := 2 + _Socklen(n) + 1
ここで、2
はsa.raw.Family
(uint16
、2バイト)のサイズ、_Socklen(n)
はアドレス名name
の長さ、1
は末尾のNULLバイトのサイズをそれぞれ表しています。この計算は、ファイルシステムパスにバインドされる通常のUnixドメインソケットには適切ですが、Linuxの抽象名前空間のアドレスには不適切でした。
抽象名前空間のアドレスは、name
の先頭が@
(内部的にはNULLバイトに変換される)で始まる場合に適用されます。この場合、Linuxの慣習では、アドレス長に末尾のNULLバイトを含めません。また、アドレス名が空文字列の場合(n == 0
)、sl
の計算が正しくありませんでした。
修正後のコードでは、このsl
の計算ロジックが以下のように変更されました。
// 修正後
sl := _Socklen(2) // Familyのサイズ (2バイト)
if n > 0 {
sl += _Socklen(n) + 1 // アドレス名がある場合、その長さ + 末尾のNULLバイト
}
if sa.raw.Path[0] == '@' {
sa.raw.Path[0] = 0 // @をNULLバイトに変換
// Don't count trailing NUL for abstract address.
// 抽象アドレスの場合、末尾のNULLバイトはカウントしない
sl--
}
この変更により、以下の点が改善されました。
- アドレス名が空の場合の修正:
n > 0
の条件が追加され、アドレス名が空の場合(n=0
)にsl
が2
(Family
のサイズのみ)となるように修正されました。これにより、空のアドレス(自動バインドを意図する場合など)が正しく処理されるようになりました。 - 抽象名前空間の正確なアドレス長計算:
sa.raw.Path[0] == '@'
(抽象名前空間のアドレス)の場合にsl--
が実行され、末尾のNULLバイトがアドレス長から除外されるようになりました。これにより、Linuxの抽象名前空間の仕様に完全に準拠するようになりました。
また、src/pkg/net/unix_test.go
には、この自動バインド機能がLinuxで正しく動作するかを確認するための新しいテストケースTestUnixAutobind
が追加されました。このテストは、ListenUnixgram
で空のアドレスを指定して自動バインドを行い、その後にDialUnix
で自動バインドされたアドレスに接続できるかを確認します。これにより、修正が正しく機能していることを検証しています。
コアとなるコードの変更箇所
src/pkg/syscall/syscall_linux.go
SockaddrUnix
構造体のsockaddr()
メソッド内のsl
(ソケットアドレス長)の計算ロジックが変更されました。
--- a/src/pkg/syscall/syscall_linux.go
+++ b/src/pkg/syscall/syscall_linux.go
@@ -279,7 +279,7 @@ type SockaddrUnix struct {
func (sa *SockaddrUnix) sockaddr() (uintptr, _Socklen, error) {
name := sa.Name
n := len(name)
- if n >= len(sa.raw.Path) || n == 0 {
+ if n >= len(sa.raw.Path) {
return 0, 0, EINVAL
}
sa.raw.Family = AF_UNIX
@@ -287,7 +287,10 @@ func (sa *SockaddrUnix) sockaddr() (uintptr, _Socklen, error) {
for i := 0; i < n; i++ {
sa.raw.Path[i] = int8(name[i])
}
// length is family (uint16), name, NUL.
- sl := 2 + _Socklen(n) + 1
+ sl := _Socklen(2)
+ if n > 0 {
+ sl += _Socklen(n) + 1
+ }
if sa.raw.Path[0] == '@' {
sa.raw.Path[0] = 0
// Don't count trailing NUL for abstract address.
src/pkg/net/unix_test.go
Linux上でのUnixソケットの自動バインド機能をテストするための新しいテストケースTestUnixAutobind
が追加されました。
--- a/src/pkg/net/unix_test.go
+++ b/src/pkg/net/unix_test.go
@@ -10,6 +10,8 @@ import (
"bytes"
"io/ioutil"
"os"
+ "reflect"
+ "runtime"
"syscall"
"testing"
"time"
@@ -121,3 +123,35 @@ func TestReadUnixgramWithZeroBytesBuffer(t *testing.T) {
t.Errorf("peer adddress is %v", peer)
}
}
+
+func TestUnixAutobind(t *testing.T) {
+ if runtime.GOOS != "linux" {
+ t.Skip("skipping: autobind is linux only")
+ }
+
+ laddr := &UnixAddr{Name: "", Net: "unixgram"}
+ c1, err := ListenUnixgram("unixgram", laddr)
+ if err != nil {
+ t.Fatalf("ListenUnixgram failed: %v", err)
+ }
+ defer c1.Close()
+
+ // retrieve the autobind address
+ autoAddr := c1.LocalAddr().(*UnixAddr)
+ if len(autoAddr.Name) <= 1 {
+ t.Fatalf("Invalid autobind address: %v", autoAddr)
+ }
+ if autoAddr.Name[0] != '@' {
+ t.Fatalf("Invalid autobind address: %v", autoAddr)
+ }
+
+ c2, err := DialUnix("unixgram", nil, autoAddr)
+ if err != nil {
+ t.Fatalf("DialUnix failed: %v", err)
+ }
+ defer c2.Close()
+
+ if !reflect.DeepEqual(c1.LocalAddr(), c2.RemoteAddr()) {
+ t.Fatalf("Expected autobind address %v, got %v", c1.LocalAddr(), c2.RemoteAddr())
+ }
+}
コアとなるコードの解説
syscall_linux.go
の変更点
syscall_linux.go
の変更は、SockaddrUnix
構造体のsockaddr()
メソッドにおけるsl
(ソケットアドレス長)の計算ロジックの修正が中心です。
-
if n >= len(sa.raw.Path) || n == 0
からif n >= len(sa.raw.Path)
への変更:- 元のコードでは、
n == 0
(アドレス名が空文字列)の場合もEINVAL
エラーを返していました。これは、自動バインドを意図して空文字列を渡すシナリオを妨げていました。 - 修正後は、
n == 0
の場合でもエラーを返さなくなり、後続のロジックで適切に処理されるようになりました。これにより、Goのnet
パッケージがUnixドメインソケットの自動バインド機能を利用できるようになります。
- 元のコードでは、
-
sl
の計算ロジックの変更:- 修正前:
sl := 2 + _Socklen(n) + 1
2
:sa.raw.Family
(AF_UNIX) のサイズ(uint16
なので2バイト)。_Socklen(n)
: アドレス名name
の長さ。1
: 末尾のNULLバイトのサイズ。- この計算は、ファイルシステムパスにバインドされるソケットには適切ですが、抽象名前空間のアドレスには不適切でした。特に、
n=0
の場合にsl
が3
となり、正しくありませんでした。
- 修正後:
sl := _Socklen(2) // Familyのサイズ (2バイト) if n > 0 { sl += _Socklen(n) + 1 // アドレス名がある場合、その長さ + 末尾のNULLバイト } if sa.raw.Path[0] == '@' { sa.raw.Path[0] = 0 // @をNULLバイトに変換 // Don't count trailing NUL for abstract address. sl-- }
- まず、
sl
をFamily
のサイズである2
で初期化します。 if n > 0
の条件が追加され、アドレス名name
が空でない場合にのみ、その長さと末尾のNULLバイトのサイズ(+ 1
)がsl
に追加されます。これにより、n=0
の場合にsl
が正しく2
(Family
のサイズのみ)となります。if sa.raw.Path[0] == '@'
のブロックは、抽象名前空間のアドレスを処理します。sa.raw.Path[0] = 0
: Goのnet
パッケージで抽象名前空間のアドレスを示すために使われる@
文字を、Linuxカーネルが認識するNULLバイトに変換します。sl--
: 抽象名前空間のアドレスでは、末尾のNULLバイトをアドレス長に含めないというLinuxの慣習に従い、sl
から1
を減算します。
- まず、
- 修正前:
この修正により、Goのsyscall
パッケージがLinuxのUnixドメインソケットの抽象名前空間の仕様に正確に準拠し、自動バインド機能が正しく動作するようになりました。
unix_test.go
の変更点
unix_test.go
に追加されたTestUnixAutobind
テスト関数は、Linux上でのUnixソケットの自動バインド機能の動作を検証します。
-
OSチェック:
if runtime.GOOS != "linux" { t.Skip("skipping: autobind is linux only") }
このテストはLinux固有の機能に依存するため、Linux以外のOSではスキップされます。
-
自動バインドによるリスニング:
laddr := &UnixAddr{Name: "", Net: "unixgram"} c1, err := ListenUnixgram("unixgram", laddr)
ListenUnixgram
関数にName: ""
(空文字列)を持つUnixAddr
を渡すことで、システムに自動的にアドレスを割り当てさせ、Unixデータグラムソケットをリッスンします。 -
自動バインドされたアドレスの取得と検証:
autoAddr := c1.LocalAddr().(*UnixAddr) if len(autoAddr.Name) <= 1 { t.Fatalf("Invalid autobind address: %v", autoAddr) } if autoAddr.Name[0] != '@' { t.Fatalf("Invalid autobind address: %v", autoAddr) }
c1.LocalAddr()
を呼び出して、自動的に割り当てられたローカルアドレスを取得します。Linuxの抽象名前空間では、このアドレスは@
で始まる(内部的にはNULLバイト)短い文字列になることが期待されます。テストは、アドレスが有効な長さであり、かつ@
で始まっていることを確認します。 -
自動バインドされたアドレスへの接続:
c2, err := DialUnix("unixgram", nil, autoAddr)
取得した
autoAddr
を使用して、別のUnixデータグラムソケットc2
をDialUnix
で接続します。これにより、自動バインドされたソケットが外部から到達可能であることを確認します。 -
接続の検証:
if !reflect.DeepEqual(c1.LocalAddr(), c2.RemoteAddr()) { t.Fatalf("Expected autobind address %v, got %v", c1.LocalAddr(), c2.RemoteAddr()) }
c1
のローカルアドレスとc2
のリモートアドレスが一致することを確認します。これは、c2
がc1
に正しく接続できたことを意味し、自動バインド機能がエンドツーエンドで機能していることを証明します。
このテストの追加により、LinuxにおけるUnixソケットの自動バインド機能の正確な動作が保証され、将来的な回帰バグを防ぐための安全網が提供されました。
関連リンク
- Go言語の
net
パッケージドキュメント: https://pkg.go.dev/net - Go言語の
syscall
パッケージドキュメント: https://pkg.go.dev/syscall - Unixドメインソケット (man 7 unix): https://man7.org/linux/man-pages/man7/unix.7.html
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージにある
https://golang.org/cl/7300047
は、このGerritの変更リストへのリンクです)
参考にした情報源リンク
- Linux man page for
unix(7)
: Unix domain sockets. (特に「Abstract sockets」セクション) - Go言語の
net
パッケージとsyscall
パッケージのソースコード。 - Go言語のコミット履歴と関連するコードレビュー。
- Stack Overflowや技術ブログにおけるUnixドメインソケット、特にLinux抽象名前空間に関する議論。
- GoのIssueトラッカー(該当するバグ報告があった場合)。
- Goのメーリングリスト(golang-dev)。