[インデックス 12678] ファイルの概要
このコミットは、Go言語のsyscall
パッケージに、Unixドメインソケットを介したファイルディスクリプタ(FD)の受け渡しをテストする新しいファイルsrc/pkg/syscall/passfd_test.go
を追加します。このテストは、子プロセスが作成したファイルディスクリプタを親プロセスに渡し、親プロセスがそのFDを使ってファイルの内容を読み取れることを検証します。
コミット
commit c97cf055d91f25e25c3d576ab483586eff224e0b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sat Mar 17 22:19:57 2012 -0700
syscall: add a test for passing an fd over a unix socket
Updates #1101
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/5849057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c97cf055d91f25e25c3d576ab483586eff224e0b
元コミット内容
syscall: add a test for passing an fd over a unix socket
このコミットは、Unixソケットを介してファイルディスクリプタを渡すためのテストを追加します。これはIssue #1101に関連する更新です。
変更の背景
このコミットは、Go言語のIssue #1101「x/net/unix: support credentials on unix sockets」の更新として行われました。Issue #1101は、Unixドメインソケットを介して資格情報(credentials)を渡す機能のサポートに関するものでしたが、ファイルディスクリプタの受け渡しは、Unixドメインソケットの高度な機能の一つであり、プロセス間通信(IPC)において非常に強力なメカニズムです。
ファイルディスクリプタの受け渡しは、異なるプロセス間で開かれたファイルやソケットなどのリソースを共有するために使用されます。これにより、例えば、特権プロセスがファイルをオープンし、そのファイルディスクリプタを非特権プロセスに渡して、非特権プロセスがそのファイルにアクセスできるようにするといった、セキュリティと効率性を両立させた設計が可能になります。
このコミットは、Go言語のsyscall
パッケージがUnixドメインソケットを介したファイルディスクリプタの受け渡しを正しく処理できることを保証するためのテストを追加することで、この重要な機能の堅牢性を高めることを目的としています。
前提知識の解説
Unixドメインソケット (Unix Domain Sockets, UDS)
Unixドメインソケットは、同じホストマシン上のプロセス間で通信を行うためのプロセス間通信(IPC)メカニズムの一種です。TCP/IPソケットがネットワークを介した通信に使用されるのに対し、Unixドメインソケットはファイルシステム上のパス名(例: /tmp/mysocket
)によって識別され、カーネル内で直接データがやり取りされるため、ネットワークソケットよりも高速で効率的な通信が可能です。ファイルシステム上のパーミッションによってアクセス制御が行える点も特徴です。
ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、開かれたファイル、ソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数値です。プロセスは、このFDを使って対応するリソースに対して読み書きなどの操作を行います。標準入力(0)、標準出力(1)、標準エラー出力(2)は予約されたFDです。
ファイルディスクリプタの受け渡し (File Descriptor Passing)
ファイルディスクリプタの受け渡しは、Unixドメインソケットの特別な機能の一つで、あるプロセスがオープンしているファイルディスクリプタを、Unixドメインソケットを介して別のプロセスに送信するメカニズムです。受信側のプロセスは、送信されたFDをあたかも自身でオープンしたかのように利用できます。これは、sendmsg
およびrecvmsg
システムコール(Go言語ではsyscall.WriteMsgUnix
およびsyscall.ReadMsgUnix
に対応)の補助データ(ancillary data)機能を用いて実現されます。この機能は、以下のようなシナリオで非常に有用です。
- 特権の分離: 特権を持つプロセスが機密性の高いリソース(例: ネットワークポート、設定ファイル)をオープンし、そのFDを非特権プロセスに渡すことで、非特権プロセスは特権なしにそのリソースにアクセスできるようになります。
- リソースの共有: 複数のプロセス間で単一のファイルやソケットを効率的に共有できます。
- パフォーマンスの向上: ファイルの内容をコピーする代わりに、FDを渡すことで、データ転送のオーバーヘッドを削減できます。
Go言語のsyscall
パッケージ
syscall
パッケージは、Goプログラムから低レベルのオペレーティングシステム(OS)のシステムコールに直接アクセスするための機能を提供します。これにより、ファイル操作、ネットワーク通信、プロセス管理など、OSカーネルが提供するプリミティブな機能を利用できます。ファイルディスクリプタの受け渡しのような高度なIPC機能は、このパッケージを通じて実現されます。
Socketpair
システムコール
syscall.Socketpair
は、相互に接続されたソケットのペアを作成するシステムコールです。これにより、同じプロセス内または親子プロセス間で、双方向の通信チャネルを確立できます。このコミットのテストでは、親プロセスと子プロセス間の通信チャネルとして使用されます。
ReadMsgUnix
/ WriteMsgUnix
これらは、Unixドメインソケットでデータと補助データ(ancillary data)を送受信するためのGo言語の関数です。ファイルディスクリプタの受け渡しは、この補助データ機能を利用して行われます。
WriteMsgUnix(data, rights, dest)
:data
を送信し、rights
(ファイルディスクリプタの配列)を補助データとして送信します。ReadMsgUnix(data, oob)
:data
を受信し、補助データ(out-of-band data,oob
)を受信します。
ParseSocketControlMessage
/ ParseUnixRights
syscall.ParseSocketControlMessage
は、ReadMsgUnix
で受信した補助データ(oob
バイトスライス)からソケット制御メッセージ(SocketControlMessage
)を解析します。
syscall.ParseUnixRights
は、解析されたSocketControlMessage
の中から、渡されたファイルディスクリプタのリストを抽出します。
技術的詳細
このコミットで追加されたテストTestPassFD
は、Go言語でUnixドメインソケットを介してファイルディスクリプタを安全かつ正確に受け渡せることを検証します。テストは以下のステップで構成されます。
- 一時ディレクトリの作成: テストで使用する一時ファイルを格納するための一時ディレクトリを作成します。これはテスト終了時にクリーンアップされます。
- ソケットペアの作成:
syscall.Socketpair
を使用して、相互に接続されたUnixドメインソケットのペアを作成します。これにより、親プロセスと子プロセスが通信するためのチャネルが確立されます。fds[0]
は親プロセスが子プロセスに渡すソケットの書き込み側として、fds[1]
は親プロセスが子プロセスから読み取るソケットの読み取り側として使用されます。
- 子プロセスの起動:
os.Args[0]
(現在の実行可能ファイル自身)をexec.Command
で実行し、-test.run=TestPassFDChild
フラグを付けて、テストヘルパー関数TestPassFDChild
を子プロセスとして実行させます。GO_WANT_HELPER_PROCESS=1
という環境変数を設定し、子プロセスがヘルパーモードで実行されていることを識別できるようにします。cmd.ExtraFiles = []*os.File{writeFile}
を使って、親プロセスが持つソケットの書き込み側(writeFile
)を子プロセスに渡します。これにより、子プロセスは親プロセスとの通信チャネルを持つことになります。
- 子プロセスでの操作 (
TestPassFDChild
):- 子プロセスは、親プロセスから渡されたソケット(
uc
)を見つけます。 - 一時ディレクトリ内に新しい一時ファイルを作成し、そのファイルに「Hello from child process!\n」という文字列を書き込みます。
syscall.UnixRights(int(f.Fd()))
を使って、作成した一時ファイルのファイルディスクリプタを補助データとして準備します。uc.WriteMsgUnix(dummyByte, rights, nil)
を呼び出し、ダミーの1バイトデータと共に、一時ファイルのFDを親プロセスに送信します。
- 子プロセスは、親プロセスから渡されたソケット(
- 親プロセスでの操作 (
TestPassFD
):- 親プロセスは、子プロセスからのメッセージを受信するために、
uc.ReadMsgUnix(buf, oob)
を呼び出します。ここで、oob
(out-of-band data)には、子プロセスから送信されたファイルディスクリプタが含まれます。 syscall.ParseSocketControlMessage(oob[:oobn])
を使って、受信した補助データからソケット制御メッセージを解析します。syscall.ParseUnixRights(&scm)
を使って、解析されたソケット制御メッセージからファイルディスクリプタを抽出します。- 抽出されたファイルディスクリプタ(
gotFds[0]
)を使ってos.NewFile
で新しい*os.File
オブジェクトを作成します。 - この新しいファイルオブジェクトから
ioutil.ReadAll
で内容を読み取り、それが子プロセスが書き込んだ「Hello from child process!\n」と一致するかを検証します。
- 親プロセスは、子プロセスからのメッセージを受信するために、
この一連のプロセスを通じて、ファイルディスクリプタがプロセス間で正しく受け渡され、そのFDが有効に機能することが確認されます。
コアとなるコードの変更箇所
src/pkg/syscall/passfd_test.go
が新規追加され、150行のコードが挿入されました。
--- /dev/null
+++ b/src/pkg/syscall/passfd_test.go
@@ -0,0 +1,150 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build linux darwin probablyfreebsd probablyopenbsd
+
+package syscall_test
+
+import (
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "os/exec"
+ "syscall"
+ "testing"
+ "time"
+)
+
+// TestPassFD tests passing a file descriptor over a Unix socket.
+func TestPassFD(t *testing.T) {
+ tempDir, err := ioutil.TempDir("", "TestPassFD")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
+ if err != nil {
+ t.Fatalf("Socketpair: %v", err)
+ }
+ defer syscall.Close(fds[0])
+ defer syscall.Close(fds[1])
+ writeFile := os.NewFile(uintptr(fds[0]), "child-writes")
+ readFile := os.NewFile(uintptr(fds[1]), "parent-reads")
+ defer writeFile.Close()
+ defer readFile.Close()
+
+ cmd := exec.Command(os.Args[0], "-test.run=TestPassFDChild", "--", tempDir)
+ cmd.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
+ cmd.ExtraFiles = []*os.File{writeFile}
+
+ out, err := cmd.CombinedOutput()
+ if len(out) > 0 || err != nil {
+ t.Errorf("child process: %q, %v", out, err)
+ return // not fatalf, so defers above run.
+ }
+
+ c, err := net.FileConn(readFile)
+ if err != nil {
+ t.Errorf("FileConn: %v", err)
+ return
+ }
+ defer c.Close()
+
+ uc, ok := c.(*net.UnixConn)
+ if !ok {
+ t.Errorf("unexpected FileConn type; expected UnixConn, got %T", c)
+ return
+ }
+
+ buf := make([]byte, 32) // expect 1 byte
+ oob := make([]byte, 32) // expect 24 bytes
+ closeUnix := time.AfterFunc(5*time.Second, func() {
+ t.Logf("timeout reading from unix socket")
+ uc.Close()
+ })
+ _, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
+ closeUnix.Stop()
+
+ scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
+ if err != nil {
+ t.Errorf("ParseSocketControlMessage: %v", err)
+ return
+ }
+ if len(scms) != 1 {
+ t.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms)
+ return
+ }
+ scm := scms[0]
+ gotFds, err := syscall.ParseUnixRights(&scm)
+ if err != nil {
+ t.Errorf("syscall.ParseUnixRights: %v", err)
+ return
+ }
+ if len(gotFds) != 1 {
+ t.Errorf("wanted 1 fd; got %#v", gotFds)
+ return
+ }\n
+ f := os.NewFile(uintptr(gotFds[0]), "fd-from-child")
+ defer f.Close()
+
+ got, err := ioutil.ReadAll(f)
+ want := "Hello from child process!\\n"
+ if string(got) != want {
+ t.Errorf("child process ReadAll: %q, %v; want %q", got, err, want)
+ }
+}
+
+// Not a real test. This is the helper child process for TestPassFD.
+func TestPassFDChild(*testing.T) {
+ if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
+ return
+ }
+ defer os.Exit(0)
+
+ // Look for our fd. I<t should be fd 3, but we work around an fd leak
+ // bug here (http://golang.org/issue/2603) to let it be elsewhere.
+ var uc *net.UnixConn
+ for fd := uintptr(3); fd <= 10; fd++ {
+ f := os.NewFile(fd, "unix-conn")
+ var ok bool
+ netc, _ := net.FileConn(f)
+ uc, ok = netc.(*net.UnixConn)
+ if ok {
+ break
+ }
+ }
+ if uc == nil {
+ fmt.Println("failed to find unix fd")
+ return
+ }
+
+ // Make a file f to send to our parent process on uc.
+ // We make it in tempDir, which our parent will clean up.
+ flag.Parse()
+ tempDir := flag.Arg(0)
+ f, err := ioutil.TempFile(tempDir, "")
+ if err != nil {
+ fmt.Printf("TempFile: %v", err)
+ return
+ }
+
+ f.Write([]byte("Hello from child process!\\n"))
+ f.Seek(0, 0)
+
+ rights := syscall.UnixRights(int(f.Fd()))
+ dummyByte := []byte("x")
+ n, oobn, err := uc.WriteMsgUnix(dummyByte, rights, nil)
+ if err != nil {
+ fmt.Printf("WriteMsgUnix: %v", err)
+ return
+ }
+ if n != 1 || oobn != len(rights) {
+ fmt.Printf("WriteMsgUnix = %d, %d; want 1, %d", n, oobn, len(rights))
+ return
+ }
+}
コアとなるコードの解説
TestPassFD
関数 (親プロセス側)
syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
: Unixドメインソケットのペアを作成します。AF_LOCAL
はローカル通信、SOCK_STREAM
はストリーム指向のソケットを示します。cmd.ExtraFiles = []*os.File{writeFile}
: 子プロセスにソケットの書き込み側(writeFile
)を渡します。これにより、子プロセスはこのソケットを通じて親プロセスにデータを送信できます。uc.ReadMsgUnix(buf, oob)
: 子プロセスからデータと補助データ(oob
)を受信します。oob
には渡されたファイルディスクリプタが含まれます。syscall.ParseSocketControlMessage(oob[:oobn])
: 受信した補助データからソケット制御メッセージを解析します。syscall.ParseUnixRights(&scm)
: 解析されたソケット制御メッセージから、実際に渡されたファイルディスクリプタのリスト(gotFds
)を抽出します。os.NewFile(uintptr(gotFds[0]), "fd-from-child")
: 抽出されたファイルディスクリプタから*os.File
オブジェクトを再構築します。ioutil.ReadAll(f)
: 再構築されたファイルオブジェクトから内容を読み取り、子プロセスが書き込んだ内容と一致するかを検証します。
TestPassFDChild
関数 (子プロセス側)
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1"
: この環境変数が設定されている場合のみ、ヘルパープロセスとして動作します。- ファイルディスクリプタの探索: 親プロセスから渡されたソケットのファイルディスクリプタを、既知のFD範囲(3から10)を探索して見つけます。これは、GoのIssue #2603(FDリークバグ)へのワークアラウンドとして実装されています。
ioutil.TempFile(tempDir, "")
: 一時ディレクトリ内に新しい一時ファイルを作成します。f.Write([]byte("Hello from child process!\\n"))
: 作成した一時ファイルにテスト文字列を書き込みます。syscall.UnixRights(int(f.Fd()))
: 一時ファイルのファイルディスクリプタをUnixRights
構造体に変換し、補助データとして送信できるように準備します。uc.WriteMsgUnix(dummyByte, rights, nil)
: ダミーの1バイトデータと共に、rights
(一時ファイルのFD)を親プロセスに送信します。
このテストは、Go言語のsyscall
パッケージが提供する低レベルな機能が、異なるプロセス間でファイルディスクリプタを安全かつ確実に受け渡すために正しく機能することを示しています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/c97cf055d91f25e25c3d576ab483586eff224e0b
- Go Issue #1101: https://github.com/golang/go/issues/1101
- Go CL 5849057: https://golang.org/cl/5849057
参考にした情報源リンク
- Go Issue #1101: "x/net/unix: support credentials on unix sockets" - GitHub: https://github.com/golang/go/issues/1101
- Go Issue #2603: "os/exec: ExtraFiles can leak fds" - GitHub: https://github.com/golang/go/issues/2603
- Unix domain socket - Wikipedia: https://en.wikipedia.org/wiki/Unix_domain_socket
- File descriptor - Wikipedia: https://en.wikipedia.org/wiki/File_descriptor
sendmsg(2)
man page (Linux): https://man7.org/linux/man-pages/man2/sendmsg.2.htmlrecvmsg(2)
man page (Linux): https://man7.org/linux/man-pages/man2/recvmsg.2.htmlsocketpair(2)
man page (Linux): https://man7.org/linux/man-pages/man2/socketpair.2.html- Go
syscall
package documentation: https://pkg.go.dev/syscall - Go
net
package documentation: https://pkg.go.dev/net - Go
os/exec
package documentation: https://pkg.go.dev/os/exec