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

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

ソケット通信では、通常のデータ(ペイロード)以外に、付加的な情報(メタデータ)を送信できます。これをコントロールメッセージと呼びます。sendmsgrecvmsgシステムコールで送受信されます。 SCM_RIGHTSは、コントロールメッセージの一種で、プロセス間でファイルディスクリプタを安全に受け渡すために使用されます。これにより、例えば親プロセスがオープンしたファイルを子プロセスに渡したり、特権プロセスが非特権プロセスに特定のファイルアクセス権限を付与したりすることが可能になります。

4. Cmsghdr構造体

コントロールメッセージは、Cmsghdr(Control Message Header)構造体とそれに続くデータで構成されます。Cmsghdrには、メッセージの長さ(cmsg_len)、レベル(cmsg_level、通常はSOL_SOCKET)、タイプ(cmsg_typeSCM_RIGHTSなど)が含まれます。 cmsg_lenは、Cmsghdr自体とそれに続くデータ(FDのリストなど)の合計バイト長を示します。OSやアーキテクチャによっては、この長さや構造体のアライメントに特定の要件があります。

5. CmsgLenCmsgAlignOf

  • CmsgLen(datalen): コントロールメッセージのヘッダとデータを含む、アライメントされた合計長を計算する関数です。
  • CmsgAlignOf(len): 特定の長さが、次のコントロールメッセージの開始位置として適切にアライメントされるように調整する関数です。これは、メモリ効率とOSの要件を満たすために重要です。

6. Goのsyscallパッケージ

Go言語のsyscallパッケージは、低レベルなOSプリミティブへのアクセスを提供します。これには、ファイル操作、プロセス管理、ネットワーク通信など、OS固有のシステムコールを直接呼び出すための関数や定数が含まれます。UnixRightsParseSocketControlMessageParseUnixRightsなどは、このパッケージ内で定義され、FDの受け渡しを抽象化しています。

技術的詳細

このコミットの技術的詳細は、主にsyscallパッケージ内のコントロールメッセージのパースロジックの修正にあります。

問題の核心は、src/pkg/syscall/sockcmsg_unix.go内のParseSocketControlMessageおよびsocketControlMessageHeaderAndData関数が、受信したバイト列からコントロールメッセージのデータ部分を正確に切り出すことができていなかった点にあります。

  1. socketControlMessageHeaderAndDataの修正:

    • この関数は、コントロールメッセージのバイト列の先頭からCmsghdrをパースし、そのヘッダとデータ部分のバイトスライスを返します。
    • 修正前は、データ部分のスライスをb[cmsgAlignOf(SizeofCmsghdr):]としていました。これは、ヘッダの直後から入力バッファの最後までをデータとして扱うことを意味します。
    • 修正後は、b[cmsgAlignOf(SizeofCmsghdr):h.Len]と変更されました。これは、Cmsghdrh.Lenフィールドが示すコントロールメッセージ全体の長さに基づいて、データ部分の終端を正確に指定することを意味します。h.Lenはヘッダとデータを含む全体の長さであるため、ヘッダのサイズを考慮してデータ部分を切り出す必要があります。この修正により、データ部分が過剰に読み込まれることがなくなり、特に複数のコントロールメッセージが連結されている場合に、次のメッセージのデータが誤って現在のメッセージの一部として解釈されることを防ぎます。
  2. 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側でさらにスライスする必要がなくなったことを示しています。
  3. cmsgAlignOfの修正:

    • if salen == 0 { return salign }という特殊なケースの処理が削除されました。これは、一般的なアライメント計算式(salen + salign - 1) & ^(salign - 1)salen=0の場合も正しく機能するか、あるいはsalenが0になる状況がこのコンテキストでは問題にならないことを示唆しています。この変更はコードの簡素化に寄与します。

これらの変更により、特にOpenBSDのような特定のOS環境で、Cmsghdrcmsg_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が常に正しいコンテキストで動作するようにします。
    • SocketControlMessageDataフィールドの割り当て変更: 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に変更されました。
      • これがこのコミットの最も重要な修正点です。以前は、コントロールメッセージのヘッダの直後から入力バッファの最後までをデータ部分として返していました。これは、バッファ内に複数のコントロールメッセージが連続している場合や、バッファの終端がメッセージの終端と一致しない場合に問題を引き起こす可能性がありました。
      • 修正後は、Cmsghdrh.Lenフィールド(コントロールメッセージ全体の長さ)を利用して、データ部分の正確な終端を計算しています。h.Lenはヘッダとデータを含む全体の長さなので、b[cmsgAlignOf(SizeofCmsghdr):h.Len]とすることで、データ部分がh.Lenの範囲内に収まるように正確に切り出されます。
      • この修正により、ParseUnixRightsが処理するデータが常に正しい長さになり、FDのリストが正確にデコードされるようになります。OpenBSDのような特定のOS環境で、Cmsghdrcmsg_lenの解釈が厳密である場合に、この正確なスライスが不可欠となります。

これらの変更は、GoのsyscallパッケージがUnixドメインソケットを介してFDを渡す際の、低レベルなバイト列のパース処理の正確性を向上させるものです。特に、Cmsghdrcmsg_lenフィールドの解釈と、それに基づくデータ部分の正確な抽出が、異なるOS環境間での互換性と信頼性を確保するために重要であることが示されています。

関連リンク

参考にした情報源リンク