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

[インデックス 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_familyAF_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

ここで、2sa.raw.Familyuint16、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--
}

この変更により、以下の点が改善されました。

  1. アドレス名が空の場合の修正: n > 0の条件が追加され、アドレス名が空の場合(n=0)にsl2Familyのサイズのみ)となるように修正されました。これにより、空のアドレス(自動バインドを意図する場合など)が正しく処理されるようになりました。
  2. 抽象名前空間の正確なアドレス長計算: 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(ソケットアドレス長)の計算ロジックの修正が中心です。

  1. if n >= len(sa.raw.Path) || n == 0 から if n >= len(sa.raw.Path) への変更:

    • 元のコードでは、n == 0(アドレス名が空文字列)の場合もEINVALエラーを返していました。これは、自動バインドを意図して空文字列を渡すシナリオを妨げていました。
    • 修正後は、n == 0の場合でもエラーを返さなくなり、後続のロジックで適切に処理されるようになりました。これにより、GoのnetパッケージがUnixドメインソケットの自動バインド機能を利用できるようになります。
  2. sl の計算ロジックの変更:

    • 修正前: sl := 2 + _Socklen(n) + 1
      • 2: sa.raw.Family (AF_UNIX) のサイズ(uint16なので2バイト)。
      • _Socklen(n): アドレス名 name の長さ。
      • 1: 末尾のNULLバイトのサイズ。
      • この計算は、ファイルシステムパスにバインドされるソケットには適切ですが、抽象名前空間のアドレスには不適切でした。特に、n=0の場合にsl3となり、正しくありませんでした。
    • 修正後:
      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--
      }
      
      • まず、slFamilyのサイズである2で初期化します。
      • if n > 0の条件が追加され、アドレス名nameが空でない場合にのみ、その長さと末尾のNULLバイトのサイズ(+ 1)がslに追加されます。これにより、n=0の場合にslが正しく2Familyのサイズのみ)となります。
      • 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ソケットの自動バインド機能の動作を検証します。

  1. OSチェック:

    if runtime.GOOS != "linux" {
        t.Skip("skipping: autobind is linux only")
    }
    

    このテストはLinux固有の機能に依存するため、Linux以外のOSではスキップされます。

  2. 自動バインドによるリスニング:

    laddr := &UnixAddr{Name: "", Net: "unixgram"}
    c1, err := ListenUnixgram("unixgram", laddr)
    

    ListenUnixgram関数にName: ""(空文字列)を持つUnixAddrを渡すことで、システムに自動的にアドレスを割り当てさせ、Unixデータグラムソケットをリッスンします。

  3. 自動バインドされたアドレスの取得と検証:

    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バイト)短い文字列になることが期待されます。テストは、アドレスが有効な長さであり、かつ@で始まっていることを確認します。

  4. 自動バインドされたアドレスへの接続:

    c2, err := DialUnix("unixgram", nil, autoAddr)
    

    取得したautoAddrを使用して、別のUnixデータグラムソケットc2DialUnixで接続します。これにより、自動バインドされたソケットが外部から到達可能であることを確認します。

  5. 接続の検証:

    if !reflect.DeepEqual(c1.LocalAddr(), c2.RemoteAddr()) {
        t.Fatalf("Expected autobind address %v, got %v", c1.LocalAddr(), c2.RemoteAddr())
    }
    

    c1のローカルアドレスとc2のリモートアドレスが一致することを確認します。これは、c2c1に正しく接続できたことを意味し、自動バインド機能がエンドツーエンドで機能していることを証明します。

このテストの追加により、LinuxにおけるUnixソケットの自動バインド機能の正確な動作が保証され、将来的な回帰バグを防ぐための安全網が提供されました。

関連リンク

参考にした情報源リンク

  • Linux man page for unix(7): Unix domain sockets. (特に「Abstract sockets」セクション)
  • Go言語のnetパッケージとsyscallパッケージのソースコード。
  • Go言語のコミット履歴と関連するコードレビュー。
  • Stack Overflowや技術ブログにおけるUnixドメインソケット、特にLinux抽象名前空間に関する議論。
  • GoのIssueトラッカー(該当するバグ報告があった場合)。
  • Goのメーリングリスト(golang-dev)。