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

[インデックス 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ドメインソケットを介してファイルディスクリプタを安全かつ正確に受け渡せることを検証します。テストは以下のステップで構成されます。

  1. 一時ディレクトリの作成: テストで使用する一時ファイルを格納するための一時ディレクトリを作成します。これはテスト終了時にクリーンアップされます。
  2. ソケットペアの作成: syscall.Socketpairを使用して、相互に接続されたUnixドメインソケットのペアを作成します。これにより、親プロセスと子プロセスが通信するためのチャネルが確立されます。
    • fds[0]は親プロセスが子プロセスに渡すソケットの書き込み側として、fds[1]は親プロセスが子プロセスから読み取るソケットの読み取り側として使用されます。
  3. 子プロセスの起動:
    • os.Args[0](現在の実行可能ファイル自身)をexec.Commandで実行し、-test.run=TestPassFDChildフラグを付けて、テストヘルパー関数TestPassFDChildを子プロセスとして実行させます。
    • GO_WANT_HELPER_PROCESS=1という環境変数を設定し、子プロセスがヘルパーモードで実行されていることを識別できるようにします。
    • cmd.ExtraFiles = []*os.File{writeFile}を使って、親プロセスが持つソケットの書き込み側(writeFile)を子プロセスに渡します。これにより、子プロセスは親プロセスとの通信チャネルを持つことになります。
  4. 子プロセスでの操作 (TestPassFDChild):
    • 子プロセスは、親プロセスから渡されたソケット(uc)を見つけます。
    • 一時ディレクトリ内に新しい一時ファイルを作成し、そのファイルに「Hello from child process!\n」という文字列を書き込みます。
    • syscall.UnixRights(int(f.Fd()))を使って、作成した一時ファイルのファイルディスクリプタを補助データとして準備します。
    • uc.WriteMsgUnix(dummyByte, rights, nil)を呼び出し、ダミーの1バイトデータと共に、一時ファイルのFDを親プロセスに送信します。
  5. 親プロセスでの操作 (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パッケージが提供する低レベルな機能が、異なるプロセス間でファイルディスクリプタを安全かつ確実に受け渡すために正しく機能することを示しています。

関連リンク

参考にした情報源リンク