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

[インデックス 18156] ファイルの概要

このコミットは、Go言語のsyscallパッケージ内のsyscall_bsd.goファイルに対する変更です。このファイルは、BSD系のオペレーティングシステム(FreeBSD, OpenBSD, NetBSDなど)におけるシステムコール関連の機能、特にソケット通信に関する低レベルな処理を扱っています。具体的には、Unixドメインソケットのアドレス構造体(sockaddr_un)の処理において、異なるBSDシステム間でのパス長の扱いの差異を吸収するための修正が加えられています。

コミット

commit d18057917003970322335bd1ad73a68ae6994ccd
Author: Joel Sing <jsing@google.com>
Date:   Sat Jan 4 00:29:20 2014 +1100

    syscall: handle varied path lengths for unix sockets
    
    Most BSDs include the trailing NUL character of the socket path in the
    length, however some do not (such as NetBSD 6.99). Handle this by only
    subtracting the family and length bytes from the returned length, then
    scanning the path and removing any terminating NUL bytes.
    
    Fixes #6627.
    
    R=golang-codereviews, mikioh.mikioh
    CC=golang-codereviews
    https://golang.org/cl/46420044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d18057917003970322335bd1ad73a68ae6994ccd

元コミット内容

syscall: handle varied path lengths for unix sockets

Most BSDs include the trailing NUL character of the socket path in the
length, however some do not (such as NetBSD 6.99). Handle this by only
subtracting the family and length bytes from the returned length, then
scanning the path and removing any terminating NUL bytes.

Fixes #6627.

R=golang-codereviews, mikioh.mikioh
CC=golang-codereviews
https://golang.org/cl/46420044

変更の背景

この変更の背景には、Unixドメインソケットのパス長に関するBSD系OS間の非互換性があります。Unixドメインソケットは、同じホスト上のプロセス間通信に使用されるソケットの一種で、ファイルシステム上のパス名によって識別されます。このパス名は、sockaddr_unという構造体の中に格納されます。

問題は、sockaddr_un構造体のsun_pathフィールドに格納されるパス名の「実際の長さ」を計算する際に、OSによって挙動が異なる点にありました。具体的には、多くのBSD系OS(例えばFreeBSD)では、パス名の終端を示すNUL文字(\0)もパス長に含めて計算していました。しかし、NetBSD 6.99のような一部のBSD系OSでは、NUL文字をパス長に含めない実装となっていました。

この差異により、Goのsyscallパッケージがソケットアドレス情報を受け取った際に、パス長の解釈を誤り、正しくないパス名が生成されたり、バッファオーバーフローや不正なメモリアクセスが発生する可能性がありました。特に、NUL文字がパス長に含まれないシステムで、GoがNUL文字を含むものとして処理しようとすると、余分なデータが読み込まれたり、予期せぬ動作を引き起こす可能性がありました。

この問題は、GoのIssue #6627として報告されており、このコミットはその問題を解決するために導入されました。

前提知識の解説

Unixドメインソケット (Unix Domain Sockets)

Unixドメインソケットは、同じマシン上のプロセス間で通信を行うためのメカニズムです。TCP/IPソケットがネットワークを介した通信に使用されるのに対し、Unixドメインソケットはファイルシステム上のパス名(例: /tmp/my_socket)をアドレスとして使用します。これにより、ファイルシステム権限によるアクセス制御が可能になり、ネットワークスタックを介さないため、TCP/IPソケットよりも高速なプロセス間通信が可能です。

sockaddr_un 構造体

sockaddr_unは、Unixドメインソケットのアドレス情報を格納するためのC言語の構造体です。一般的な定義は以下のようになります。

struct sockaddr_un {
    sa_family_t sun_family; // アドレスファミリー (AF_UNIX)
    char        sun_path[108]; // ソケットのパス名
};
  • sun_family: アドレスファミリーを指定します。Unixドメインソケットの場合はAF_UNIX(またはAF_LOCAL)となります。
  • sun_path: ソケットのパス名を格納する配列です。この配列のサイズはOSによって異なりますが、通常は108バイト程度です。パス名はNUL終端されます。

AF_UNIX

AF_UNIXは、アドレスファミリーの一つで、Unixドメインソケットを示す定数です。ソケットを作成する際に、このアドレスファミリーを指定することで、Unixドメインソケットが生成されます。

syscall パッケージ (Go言語)

Go言語のsyscallパッケージは、オペレーティングシステムの低レベルなシステムコールにアクセスするための機能を提供します。これにより、Goプログラムから直接OSの機能(ファイル操作、プロセス管理、ネットワーク通信など)を呼び出すことができます。このパッケージはOSに依存する部分が多く、各OSのシステムコールAPIに合わせて実装されています。

RawSockaddrAnyRawSockaddrUnix (Go言語)

Go言語のsyscallパッケージでは、C言語のsockaddr構造体に対応するGoの型が定義されています。

  • RawSockaddrAny: 任意のソケットアドレス型を表現するための汎用的な構造体です。sockaddr構造体の最初の部分(アドレスファミリーなど)を共通で保持し、残りの部分は型アサーションやunsafe.Pointerを使って特定のソケットアドレス型にキャストして利用します。
  • RawSockaddrUnix: sockaddr_un構造体に対応するGoの型です。sun_familysun_pathフィールドに相当するフィールドを持ちます。

pp.Len フィールド

RawSockaddrUnix構造体(またはその基となるC言語のsockaddr_un構造体)には、ソケットアドレス構造体全体の長さを示すフィールドが含まれることがあります。このフィールドは、recvfromなどのシステムコールがソケットアドレス情報を返す際に、実際に書き込まれたデータの長さを伝えるために使用されます。しかし、この「長さ」の解釈がOSによって異なることが、今回の問題の根源でした。

技術的詳細

このコミットが解決しようとしている技術的な問題は、Unixドメインソケットのパス名がsockaddr_un構造体内でどのように表現され、その長さがどのように計算されるかという点に集約されます。

Unixドメインソケットのパス名は、sockaddr_un.sun_pathフィールドに格納されます。このフィールドは通常、固定長のchar配列であり、パス名はC言語の文字列と同様にNUL文字で終端されます。しかし、recvfromなどのシステムコールがソケットアドレス情報を返す際に、sockaddr_un構造体全体の長さを示す値(GoのRawSockaddrUnixではpp.Lenに相当)を返します。

このpp.Lenの値の計算方法が、BSD系OS間で異なっていました。

  1. 多くのBSD系OS(例: FreeBSD): pp.Lenは、sun_familyフィールドのサイズ、sun_pathフィールドの有効なパス名の長さ、そして終端のNUL文字のサイズをすべて含んだ値として返されます。
  2. 一部のBSD系OS(例: NetBSD 6.99): pp.Lenは、sun_familyフィールドのサイズとsun_pathフィールドの有効なパス名の長さを含みますが、終端のNUL文字のサイズは含みません

Goのsyscallパッケージは、このpp.Lenの値に基づいてsun_pathからパス名を抽出していました。従来のコードでは、pp.Lenからsun_familyLenフィールド自体のサイズ、そして終端のNUL文字のサイズを引くことで、パス名の有効な長さを計算しようとしていました。

n := int(pp.Len) - 3 // subtract leading Family, Len, terminating NUL

この3というマジックナンバーは、sun_family(1バイト)、Lenフィールド(1バイト)、そして終端のNUL文字(1バイト)の合計3バイトを引くことを意図していました。

しかし、NetBSDのようにNUL文字をpp.Lenに含めないシステムでは、この3を引くと、パス名の実際の長さよりも1バイト短い長さが計算されてしまいます。これにより、Goがパス名を処理する際に、NUL文字が途中で見つからなかったり、バッファの範囲外を読み込もうとしたりする問題が発生していました。

このコミットは、このOS間の差異を吸収するために、pp.Lenからsun_familyLenフィールドのサイズ(合計2バイト)のみをまず引き、その後、sun_pathフィールドを実際にスキャンしてNUL文字を探すというロジックに変更することで、この問題を解決しています。これにより、pp.LenNUL文字を含むか含まないかにかかわらず、常に正しいパス名の長さを特定できるようになります。

コアとなるコードの変更箇所

変更はsrc/pkg/syscall/syscall_bsd.goファイルのanyToSockaddr関数内で行われています。

--- a/src/pkg/syscall/syscall_bsd.go
+++ b/src/pkg/syscall/syscall_bsd.go
@@ -221,14 +221,20 @@ func anyToSockaddr(rsa *RawSockaddrAny) (Sockaddr, error) {
 
 	case AF_UNIX:
 		pp := (*RawSockaddrUnix)(unsafe.Pointer(rsa))
-		if pp.Len < 3 || pp.Len > SizeofSockaddrUnix {
+		if pp.Len < 2 || pp.Len > SizeofSockaddrUnix {
 			return nil, EINVAL
 		}
 		sa := new(SockaddrUnix)
-		n := int(pp.Len) - 3 // subtract leading Family, Len, terminating NUL
+
+		// Some BSDs include the trailing NUL in the length, whereas
+		// others do not. Work around this by subtracting the leading
+		// family and len. The path is then scanned to see if a NUL
+		// terminator still exists within the length.
+		n := int(pp.Len) - 2 // subtract leading Family, Len
 		for i := 0; i < n; i++ {
 			if pp.Path[i] == 0 {
-				// found early NUL; assume Len is overestimating
+				// found early NUL; assume Len included the NUL
+				// or was overestimating.
 				n = i
 				break
 			}

コアとなるコードの解説

変更されたanyToSockaddr関数は、RawSockaddrAny型のソケットアドレス情報を受け取り、それをGoのSockaddrインターフェース型に変換する役割を担っています。AF_UNIXケースは、Unixドメインソケットのアドレスを処理する部分です。

  1. pp.Lenの最小値チェックの変更:

    -		if pp.Len < 3 || pp.Len > SizeofSockaddrUnix {
    +		if pp.Len < 2 || pp.Len > SizeofSockaddrUnix {
    

    以前はpp.Lenの最小値が3Family + Len + NUL)とされていましたが、NULを含まないシステムを考慮し、FamilyLenの2バイトのみを考慮した2に変更されました。これにより、NUL文字を含まないパス長でも有効なものとして扱えるようになりました。

  2. パス長nの計算ロジックの変更:

    -		n := int(pp.Len) - 3 // subtract leading Family, Len, terminating NUL
    +		n := int(pp.Len) - 2 // subtract leading Family, Len
    

    これが最も重要な変更点です。以前はpp.Lenから3Family, Len, NUL)を引いていましたが、新しいコードでは2Family, Len)のみを引くように変更されました。これにより、npp.Path配列の最大有効長(NUL文字が含まれるかどうかに関わらず)を示す値となります。

  3. NUL文字の探索ループの追加:

    		for i := 0; i < n; i++ {
    			if pp.Path[i] == 0 {
    				// found early NUL; assume Len included the NUL
    				// or was overestimating.
    				n = i
    				break
    			}
    		}
    

    nの初期計算後、pp.Path配列を0からn-1までループして、NUL文字(0)を探索します。

    • もし途中でNUL文字が見つかった場合、それはパス名の実際の終端を示します。この場合、nをそのNUL文字のインデックスiに更新し、ループを抜けます。これにより、NUL文字以降のデータはパス名として扱われなくなります。
    • このロジックは、pp.LenNUL文字を含んでいた場合(多くのBSD系OSの挙動)でも、NUL文字が途中で見つかることで、正しくパス名の長さを調整します。
    • また、pp.LenNUL文字を含まなかった場合(NetBSD 6.99の挙動)でも、ループはNUL文字を見つけずに最後まで実行され、nは初期計算された値のままとなり、結果的に正しいパス長が使用されます。

この変更により、Goのsyscallパッケージは、Unixドメインソケットのパス長に関するBSD系OS間の非互換性を透過的に吸収し、より堅牢なソケット通信処理を実現できるようになりました。

関連リンク

参考にした情報源リンク