[インデックス 15459] ファイルの概要
このコミットは、Go言語のsyscall
パッケージにおけるOpenBSD上でのファイルディスクリプタ(FD)の受け渡しに関するバグを修正するものです。具体的には、Unixドメインソケットを介したSCM_RIGHTS
メッセージによるFDの送受信処理において、コントロールメッセージのデータ部分のパースが正しく行われていなかった問題を解決します。これにより、OpenBSD環境でのプロセス間FD通信の信頼性が向上します。
コミット
commit f853e9aa4ed4e3f1aa299cc9b123cda574707eaa
Author: Matthew Dempsky <mdempsky@google.com>
Date: Wed Feb 27 15:19:23 2013 +1100
syscall: Fix FD passing on OpenBSD
Fixes #3349.
R=bradfitz, dave, minux.ma
CC=golang-dev
https://golang.org/cl/7383056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f853e9aa4ed4e3f1aa299cc9b123cda574707eaa
元コミット内容
このコミットは、syscall: Fix FD passing on OpenBSD
という簡潔なメッセージで、OpenBSDにおけるファイルディスクリプタの受け渡しに関する問題を修正したことを示しています。Fixes #3349
という記述から、GoのIssueトラッカーの3349番の問題を解決したことがわかります。
変更の背景
この変更の背景には、OpenBSD環境でGoプログラムがUnixドメインソケットを介してファイルディスクリプタをプロセス間で受け渡す際に、予期せぬエラーやデータ破損が発生するという問題がありました。Goのsyscall
パッケージは、OSのシステムコールを抽象化し、低レベルな操作を可能にするためのものです。ファイルディスクリプタの受け渡しは、特にデーモンプロセスや特権分離されたプロセス間でリソースを共有する際に不可欠な機能です。
Go Issue #3349「syscall: FD passing on OpenBSD is broken」によると、OpenBSDでのSCM_RIGHTS
メッセージの処理に問題があり、ParseSocketControlMessage
関数が正しくコントロールメッセージをパースできていないことが報告されていました。これにより、UnixRights
でエンコードされたFDが、ParseUnixRights
でデコードされる際に誤った値になったり、エラーが発生したりしていました。このバグは、OpenBSD特有のCmsghdr
構造体のアライメントや、コントロールメッセージの長さの解釈の違いに起因していたと考えられます。
このコミットは、この根本的なパースの問題を修正し、OpenBSD上でのFD受け渡し機能の安定性と正確性を確保することを目的としています。
前提知識の解説
1. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系OSにおいてファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プロセスはFDを通じてこれらのリソースにアクセスします。
2. Unixドメインソケット (Unix Domain Socket)
Unixドメインソケットは、同じホスト上のプロセス間通信(IPC)に使用されるソケットの一種です。ネットワークソケット(TCP/IPなど)とは異なり、ファイルシステム上のパス名(例: /tmp/mysocket.sock
)に関連付けられ、カーネル内で直接通信が行われるため、ネットワークオーバーヘッドがなく高速です。
3. コントロールメッセージ (Control Message) と SCM_RIGHTS
ソケット通信では、通常のデータ(ペイロード)以外に、付加的な情報(メタデータ)を送信できます。これをコントロールメッセージと呼びます。sendmsg
やrecvmsg
システムコールで送受信されます。
SCM_RIGHTS
は、コントロールメッセージの一種で、プロセス間でファイルディスクリプタを安全に受け渡すために使用されます。これにより、例えば親プロセスがオープンしたファイルを子プロセスに渡したり、特権プロセスが非特権プロセスに特定のファイルアクセス権限を付与したりすることが可能になります。
4. Cmsghdr
構造体
コントロールメッセージは、Cmsghdr
(Control Message Header)構造体とそれに続くデータで構成されます。Cmsghdr
には、メッセージの長さ(cmsg_len
)、レベル(cmsg_level
、通常はSOL_SOCKET
)、タイプ(cmsg_type
、SCM_RIGHTS
など)が含まれます。
cmsg_len
は、Cmsghdr
自体とそれに続くデータ(FDのリストなど)の合計バイト長を示します。OSやアーキテクチャによっては、この長さや構造体のアライメントに特定の要件があります。
5. CmsgLen
とCmsgAlignOf
CmsgLen(datalen)
: コントロールメッセージのヘッダとデータを含む、アライメントされた合計長を計算する関数です。CmsgAlignOf(len)
: 特定の長さが、次のコントロールメッセージの開始位置として適切にアライメントされるように調整する関数です。これは、メモリ効率とOSの要件を満たすために重要です。
6. Goのsyscall
パッケージ
Go言語のsyscall
パッケージは、低レベルなOSプリミティブへのアクセスを提供します。これには、ファイル操作、プロセス管理、ネットワーク通信など、OS固有のシステムコールを直接呼び出すための関数や定数が含まれます。UnixRights
、ParseSocketControlMessage
、ParseUnixRights
などは、このパッケージ内で定義され、FDの受け渡しを抽象化しています。
技術的詳細
このコミットの技術的詳細は、主にsyscall
パッケージ内のコントロールメッセージのパースロジックの修正にあります。
問題の核心は、src/pkg/syscall/sockcmsg_unix.go
内のParseSocketControlMessage
およびsocketControlMessageHeaderAndData
関数が、受信したバイト列からコントロールメッセージのデータ部分を正確に切り出すことができていなかった点にあります。
-
socketControlMessageHeaderAndData
の修正:- この関数は、コントロールメッセージのバイト列の先頭から
Cmsghdr
をパースし、そのヘッダとデータ部分のバイトスライスを返します。 - 修正前は、データ部分のスライスを
b[cmsgAlignOf(SizeofCmsghdr):]
としていました。これは、ヘッダの直後から入力バッファの最後までをデータとして扱うことを意味します。 - 修正後は、
b[cmsgAlignOf(SizeofCmsghdr):h.Len]
と変更されました。これは、Cmsghdr
のh.Len
フィールドが示すコントロールメッセージ全体の長さに基づいて、データ部分の終端を正確に指定することを意味します。h.Len
はヘッダとデータを含む全体の長さであるため、ヘッダのサイズを考慮してデータ部分を切り出す必要があります。この修正により、データ部分が過剰に読み込まれることがなくなり、特に複数のコントロールメッセージが連結されている場合に、次のメッセージのデータが誤って現在のメッセージの一部として解釈されることを防ぎます。
- この関数は、コントロールメッセージのバイト列の先頭から
-
ParseSocketControlMessage
の修正:- この関数は、複数のコントロールメッセージを含む可能性のあるバイト列をパースし、
SocketControlMessage
のリストを返します。 - 修正前は、ループ内で
b = b[cmsgAlignOf(int(h.Len)):]
として、残りのバイト列を次のメッセージのパースに渡していました。 - 修正後は、インデックス変数
i
を導入し、i += cmsgAlignOf(int(h.Len))
でインデックスを進める方式に変更されました。そして、socketControlMessageHeaderAndData(b[i:])
のように、常に元のバッファb
の適切なオフセットからスライスを渡すようになりました。 - また、
m := SocketControlMessage{Header: *h, Data: dbuf[:int(h.Len)-cmsgAlignOf(SizeofCmsghdr)]}
からm := SocketControlMessage{Header: *h, Data: dbuf}
に変更されました。これは、socketControlMessageHeaderAndData
が既にh.Len
に基づいて正確なdbuf
を返しているため、ParseSocketControlMessage
側でさらにスライスする必要がなくなったことを示しています。
- この関数は、複数のコントロールメッセージを含む可能性のあるバイト列をパースし、
-
cmsgAlignOf
の修正:if salen == 0 { return salign }
という特殊なケースの処理が削除されました。これは、一般的なアライメント計算式(salen + salign - 1) & ^(salign - 1)
がsalen=0
の場合も正しく機能するか、あるいはsalen
が0になる状況がこのコンテキストでは問題にならないことを示唆しています。この変更はコードの簡素化に寄与します。
これらの変更により、特にOpenBSDのような特定のOS環境で、Cmsghdr
のcmsg_len
フィールドの解釈と、それに続くデータ部分の正確な抽出が保証されるようになりました。これにより、SCM_RIGHTS
メッセージに含まれるファイルディスクリプタのリストが正しくパースされ、FDの受け渡しが正常に行われるようになります。
コアとなるコードの変更箇所
src/pkg/syscall/passfd_test.go
--- a/src/pkg/syscall/passfd_test.go
+++ b/src/pkg/syscall/passfd_test.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build linux darwin freebsd netbsd
+// +build linux darwin freebsd netbsd openbsd
package syscall_test
@@ -149,3 +149,49 @@ func passFDChild() {
return
}
}\n+\n+// TestUnixRightsRoundtrip tests that UnixRights, ParseSocketControlMessage,
+// and ParseUnixRights are able to successfully round-trip lists of file descriptors.
+func TestUnixRightsRoundtrip(t *testing.T) {
+ testCases := [...][][]int{
+ {{42}},
+ {{1, 2}},
+ {{3, 4, 5}},
+ {{}},
+ {{1, 2}, {3, 4, 5}, {}, {7}},
+ }
+ for _, testCase := range testCases {
+ b := []byte{}
+ var n int
+ for _, fds := range testCase {
+ // Last assignment to n wins
+ n = len(b) + syscall.CmsgLen(4*len(fds))
+ b = append(b, syscall.UnixRights(fds...)...)
+ }
+ // Truncate b
+ b = b[:n]
+
+ scms, err := syscall.ParseSocketControlMessage(b)
+ if err != nil {
+ t.Fatalf("ParseSocketControlMessage: %v", err)
+ }
+ if len(scms) != len(testCase) {
+ t.Fatalf("expected %v SocketControlMessage; got scms = %#v", len(testCase), scms)
+ }
+ for i, scm := range scms {
+ gotFds, err := syscall.ParseUnixRights(&scm)
+ if err != nil {
+ t.Fatalf("ParseUnixRights: %v", err)
+ }
+ wantFds := testCase[i]
+ if len(gotFds) != len(wantFds) {
+ t.Fatalf("expected %v fds, got %#v", len(wantFds), gotFds)
+ }
+ for j, fd := range gotFds {
+ if fd != wantFds[j] {
+ t.Fatalf("expected fd %v, got %v", wantFds[j], fd)
+ }
+ }
+ }
+ }
+}
src/pkg/syscall/sockcmsg_unix.go
--- a/src/pkg/syscall/sockcmsg_unix.go
+++ b/src/pkg/syscall/sockcmsg_unix.go
@@ -10,7 +10,7 @@ package syscall
import "unsafe"
-// Round the length of a raw sockaddr up to align it propery.\n+// Round the length of a raw sockaddr up to align it properly.
func cmsgAlignOf(salen int) int {
salign := sizeofPtr
// NOTE: It seems like 64-bit Darwin kernel still requires 32-bit
@@ -18,9 +18,6 @@ func cmsgAlignOf(salen int) int {
if darwinAMD64 {
salign = 4
}
-\tif salen == 0 {\n-\t\treturn salign\n-\t}\n return (salen + salign - 1) & ^(salign - 1)\n }\n
@@ -50,14 +47,15 @@ type SocketControlMessage struct {
// messages.
func ParseSocketControlMessage(b []byte) ([]SocketControlMessage, error) {
var msgs []SocketControlMessage
-\tfor len(b) >= CmsgLen(0) {\n-\t\th, dbuf, err := socketControlMessageHeaderAndData(b)\n+\ti := 0\n+\tfor i+CmsgLen(0) <= len(b) {\n+\t\th, dbuf, err := socketControlMessageHeaderAndData(b[i:])\n if err != nil {
return nil, err
}
-\t\tm := SocketControlMessage{Header: *h, Data: dbuf[:int(h.Len)-cmsgAlignOf(SizeofCmsghdr)]}\n+\t\tm := SocketControlMessage{Header: *h, Data: dbuf}\n msgs = append(msgs, m)
-\t\tb = b[cmsgAlignOf(int(h.Len)):]\n+\t\ti += cmsgAlignOf(int(h.Len))\n }
return msgs, nil
}\n
@@ -67,7 +65,7 @@ func socketControlMessageHeaderAndData(b []byte) (*Cmsghdr, []byte, error) {
if h.Len < SizeofCmsghdr || int(h.Len) > len(b) {
return nil, nil, EINVAL
}
-\treturn h, b[cmsgAlignOf(SizeofCmsghdr):], nil\n+\treturn h, b[cmsgAlignOf(SizeofCmsghdr):h.Len], nil
}\n
// UnixRights encodes a set of open file descriptors into a socket
コアとなるコードの解説
src/pkg/syscall/passfd_test.go
の変更
- ビルドタグの追加:
// +build linux darwin freebsd netbsd openbsd
- これにより、
passfd_test.go
内のテストがOpenBSD環境でも実行されるようになります。これは、OpenBSD固有のFD受け渡し問題を検出・検証するために不可欠です。
- これにより、
TestUnixRightsRoundtrip
テストの追加:- この新しいテストは、
syscall.UnixRights
でファイルディスクリプタのリストをコントロールメッセージにエンコードし、それをsyscall.ParseSocketControlMessage
でパースし、さらにsyscall.ParseUnixRights
で元のFDリストにデコードするという一連の「ラウンドトリップ」処理が正しく機能するかを検証します。 - 様々なFDの組み合わせ(単一、複数、空リスト、複数のコントロールメッセージ)をテストケースとして用意し、エンコード・デコード後のFDリストが元のリストと完全に一致するかを確認します。
- このテストは、FD受け渡し機能の正確性を保証するための重要な回帰テストとなります。
- この新しいテストは、
src/pkg/syscall/sockcmsg_unix.go
の変更
-
cmsgAlignOf
関数の変更:if salen == 0 { return salign }
という条件分岐が削除されました。これは、salen
が0の場合でも、既存のアライメント計算ロジック(salen + salign - 1) & ^(salign - 1)
が正しく機能するか、あるいはこの特殊なケースが不要になったことを示します。コードの簡素化に貢献しています。
-
ParseSocketControlMessage
関数の変更:- ループ処理の変更: 以前は
for len(b) >= CmsgLen(0)
でb
自体をスライスして進めていましたが、i := 0
を導入し、for i+CmsgLen(0) <= len(b)
とi += cmsgAlignOf(int(h.Len))
でインデックスi
を管理する方式に変更されました。これにより、元のバイトスライスb
を破壊することなく、各コントロールメッセージの開始位置を正確に指定できるようになります。 socketControlMessageHeaderAndData
への引数変更:socketControlMessageHeaderAndData(b)
からsocketControlMessageHeaderAndData(b[i:])
に変更されました。これは、現在のメッセージの開始位置からスライスを渡すことで、socketControlMessageHeaderAndData
が常に正しいコンテキストで動作するようにします。SocketControlMessage
のData
フィールドの割り当て変更:m := SocketControlMessage{Header: *h, Data: dbuf[:int(h.Len)-cmsgAlignOf(SizeofCmsghdr)]}
からm := SocketControlMessage{Header: *h, Data: dbuf}
に変更されました。これは、後述のsocketControlMessageHeaderAndData
の変更により、dbuf
が既に正確な長さで返されるようになったため、ここでさらにスライスする必要がなくなったことを意味します。
- ループ処理の変更: 以前は
-
socketControlMessageHeaderAndData
関数の変更:- データ部分のスライス方法の修正:
return h, b[cmsgAlignOf(SizeofCmsghdr):], nil
からreturn h, b[cmsgAlignOf(SizeofCmsghdr):h.Len], nil
に変更されました。- これがこのコミットの最も重要な修正点です。以前は、コントロールメッセージのヘッダの直後から入力バッファの最後までをデータ部分として返していました。これは、バッファ内に複数のコントロールメッセージが連続している場合や、バッファの終端がメッセージの終端と一致しない場合に問題を引き起こす可能性がありました。
- 修正後は、
Cmsghdr
のh.Len
フィールド(コントロールメッセージ全体の長さ)を利用して、データ部分の正確な終端を計算しています。h.Len
はヘッダとデータを含む全体の長さなので、b[cmsgAlignOf(SizeofCmsghdr):h.Len]
とすることで、データ部分がh.Len
の範囲内に収まるように正確に切り出されます。 - この修正により、
ParseUnixRights
が処理するデータが常に正しい長さになり、FDのリストが正確にデコードされるようになります。OpenBSDのような特定のOS環境で、Cmsghdr
のcmsg_len
の解釈が厳密である場合に、この正確なスライスが不可欠となります。
- データ部分のスライス方法の修正:
これらの変更は、Goのsyscall
パッケージがUnixドメインソケットを介してFDを渡す際の、低レベルなバイト列のパース処理の正確性を向上させるものです。特に、Cmsghdr
のcmsg_len
フィールドの解釈と、それに基づくデータ部分の正確な抽出が、異なるOS環境間での互換性と信頼性を確保するために重要であることが示されています。
関連リンク
- Go Issue #3349: syscall: FD passing on OpenBSD is broken
- Gerrit Change-Id: I7383056 (Goのコードレビューシステム)
参考にした情報源リンク
- Unix Domain Sockets - Wikipedia
- sendmsg(2) - Linux man page
- recvmsg(2) - Linux man page
- cmsg(3) - Linux man page
- Go
syscall
package documentation (当時のバージョンに基づく) - Go
Cmsghdr
struct (当時のバージョンに基づく) - Go
UnixRights
function (当時のバージョンに基づく) - Go
ParseSocketControlMessage
function (当時のバージョンに基づく) - Go
ParseUnixRights
function (当時のバージョンに基づく)