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

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

このコミットは、Go言語のsyscallパッケージにおいて、Linux上でのSCM_CREDENTIALSおよびSO_PASSCREDのテストを追加するものです。具体的には、UNIXドメインソケットを介したプロセス間通信において、送信元プロセスの認証情報(PID, UID, GID)を補助データとして送受信する機能の検証を行います。これにより、Go言語がこれらの低レベルなシステムコールを正しく扱えることを保証します。

コミット

commit 6a0544091e04dc972069d6ce031f886a873daf32
Author: Albert Strasheim <fullung@gmail.com>
Date:   Sun Mar 18 10:03:00 2012 -0700

    syscall: Test SCM_CREDENTIALS, SO_PASSCRED on Linux.
    
    R=bradfitz, iant
    CC=golang-dev
    https://golang.org/cl/5846059

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

https://github.com/golang/go/commit/6a0544091e04dc972069d6ce031f886a873daf32

元コミット内容

syscall: Test SCM_CREDENTIALS, SO_PASSCRED on Linux.

R=bradfitz, iant
CC=golang-dev
https://golang.org/cl/5846059

変更の背景

この変更の背景には、Go言語のsyscallパッケージがLinux固有の高度なプロセス間通信(IPC)機能を適切にサポートしていることを確認する必要がありました。特に、UNIXドメインソケットを介してプロセスの認証情報(PID, UID, GID)を安全かつ効率的にやり取りするメカニズムは、特権分離やセキュリティが要求されるアプリケーションにおいて非常に重要です。

SCM_CREDENTIALSSO_PASSCREDは、UNIXドメインソケットの補助データ(ancillary data)機能を利用して、送信元プロセスの認証情報をカーネルが自動的に付加し、受信側でその情報を取得できるようにするものです。この機能がGo言語のsyscallパッケージで正しく実装され、動作することを検証するためのテストが不足していたため、このコミットで追加されました。これにより、Go言語で書かれたプログラムが、これらのLinux固有のIPC機能を信頼性高く利用できるようになります。

前提知識の解説

このコミットを理解するためには、以下のLinuxシステムプログラミングに関する概念を理解しておく必要があります。

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

UNIXドメインソケットは、同じホスト上のプロセス間で通信を行うためのIPCメカニズムです。TCP/IPソケットとは異なり、ネットワークスタックを介さずにカーネル内で直接通信が行われるため、オーバーヘッドが少なく、高速な通信が可能です。ファイルシステム上のパス(例: /tmp/mysocket)にバインドされるか、抽象名前空間(abstract namespace)を使用します。ストリーム型(SOCK_STREAM)とデータグラム型(SOCK_DGRAM)があります。

補助データ (Ancillary Data)

補助データは、通常のデータストリームとは別に、ソケットを介して送受信される制御メッセージです。sendmsg()およびrecvmsg()システムコールを使用して送受信されます。補助データは、ファイルディスクリプタ(SCM_RIGHTS)やプロセスの認証情報(SCM_CREDENTIALS)など、カーネルが提供する追加情報をユーザー空間アプリケーションに渡すために使用されます。ネットワークを介して送信されることはなく、ローカル通信に限定されます。

SCM_CREDENTIALS

SCM_CREDENTIALSは、補助データの一種で、UNIXドメインソケットを介して送信元プロセスの認証情報(PID, UID, GID)を渡すために使用されます。これは、受信側プロセスが送信元の身元を確認する、つまり認証を行う目的で利用されます。カーネルが自動的に送信元の認証情報をstruct ucred構造体に格納し、補助データとして付加します。

SO_PASSCRED

SO_PASSCREDは、ソケットオプションの一つで、UNIXドメインソケットでSCM_CREDENTIALS補助データを受信するために、受信側ソケットで明示的に有効にする必要があります。setsockopt()システムコールを使って設定します。このオプションが有効になっていない場合、受信側プロセスは送信元からの認証情報を受け取ることができません。

Ucred (struct ucred)

ucred構造体は、Linuxにおいてプロセスの認証情報を表すために使用されます。SCM_CREDENTIALS補助データが送信される際、この構造体に送信元プロセスのPID(プロセスID)、UID(ユーザーID)、およびGID(グループID)が格納されます。受信側はこの構造体を解析することで、送信元の身元を特定できます。

SO_PEERCREDとの違い

SO_PEERCREDもUNIXドメインソケットのピアの認証情報を取得するためのソケットオプションですが、SCM_CREDENTIALSとは動作が異なります。

  • SO_PEERCRED: ストリーム型UNIXドメインソケットに接続されたピアの認証情報(PID, UID, GID)を直接問い合わせる読み取り専用のオプションです。ピアが明示的に認証情報を送信する必要はありません。ソケットを「作成した」プロセスの認証情報が取得されます。
  • SCM_CREDENTIALS: 各メッセージと共に認証情報を補助データとして明示的に送信するメカニズムです。これにより、特権プロセスがより低い特権の認証情報を送信するなど、より柔軟な制御が可能になります。ただし、受信側でSO_PASSCREDを有効にする必要があります。

このコミットは、SCM_CREDENTIALSSO_PASSCREDの組み合わせに焦点を当てており、Go言語がこの明示的な認証情報交換メカニズムを正しく扱えるかを検証しています。

技術的詳細

このコミットで追加されたテストコードは、Go言語のsyscallパッケージがLinuxのSCM_CREDENTIALSSO_PASSCRED機能を正しく利用できることを検証します。テストは以下のステップで構成されます。

  1. ソケットペアの作成: syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)を呼び出して、UNIXドメインソケットのペアを作成します。これにより、2つの接続されたソケットディスクリプタ(fds[0]fds[1])が得られます。これらはそれぞれサーバー側とクライアント側として機能します。
  2. SO_PASSCREDオプションの有効化: サーバー側ソケット(fds[0])に対してsyscall.SetsockoptInt(fds[0], syscall.SOL_SOCKET, syscall.SO_PASSCRED, 1)を呼び出し、SO_PASSCREDオプションを有効にします。これにより、このソケットがSCM_CREDENTIALS補助データを受信できるようになります。
  3. net.Connへの変換: syscallパッケージのファイルディスクリプタをGoのnet.Connインターフェースに変換します。これはos.NewFilenet.FileConnを使って行われます。これにより、Goの標準ネットワークAPIを通じてソケットを操作できるようになります。
  4. 特権ユーザーでのテスト(オプション): もし現在のユーザーがroot(UID 0)でない場合、意図的にUIDを0に設定したUcred構造体を作成し、WriteMsgUnixで送信を試みます。この操作は通常、権限エラー(EPERM)となるはずであり、テストはそのエラーを期待します。これは、非特権ユーザーが任意の認証情報を偽装して送信できないことを確認するためです。
  5. 認証情報の準備と送信:
    • 現在のプロセスのPID、UID、GIDを含むsyscall.Ucred構造体を作成します。
    • syscall.UnixCredentials(&ucred)を呼び出して、このUcred構造体からSCM_CREDENTIALS補助データ(バイトスライス)を生成します。
    • クライアント側ソケット(cli)からcli.(*net.UnixConn).WriteMsgUnix(nil, oob, nil)を呼び出して、この補助データを送信します。通常のデータは送信せず(最初の引数はnil)、補助データのみを送信します。
  6. 認証情報の受信と検証:
    • サーバー側ソケット(srv)でsrv.(*net.UnixConn).ReadMsgUnix(nil, oob2)を呼び出し、補助データを受信します。
    • 受信した補助データが送信したものとバイトレベルで一致するかを確認します。
    • syscall.ParseSocketControlMessage(oob2)を呼び出して、受信した補助データからソケット制御メッセージを解析します。
    • syscall.ParseUnixCredentials(&scm[0])を呼び出して、解析されたソケット制御メッセージからUcred構造体を抽出します。
    • 抽出されたUcred構造体が、送信時に設定した元のUcred構造体と完全に一致するかを検証します。PID、UID、GIDがすべて一致すればテストは成功です。

この一連のテストにより、Go言語のsyscallパッケージがLinuxのSCM_CREDENTIALSSO_PASSCREDを介した認証情報の送受信を正確に処理できることが保証されます。

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

このコミットでは、src/pkg/syscall/creds_linux_test.goという新しいファイルが追加されています。

--- /dev/null
+++ b/src/pkg/syscall/creds_linux_test.go
@@ -0,0 +1,107 @@
+// 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.
+
+package syscall_test
+
+import (
+	"bytes"
+	"net"
+	"os"
+	"syscall"
+	"testing"
+)
+
+// TestSCMCredentials tests the sending and receiving of credentials
+// (PID, UID, GID) in an ancillary message between two UNIX
+// sockets. The SO_PASSCRED socket option is enabled on the sending
+// socket for this to work.
+func TestSCMCredentials(t *testing.T) {
+	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])
+
+	err = syscall.SetsockoptInt(fds[0], syscall.SOL_SOCKET, syscall.SO_PASSCRED, 1)
+	if err != nil {
+		t.Fatalf("SetsockoptInt: %v", err)
+	}
+
+	srv, err := net.FileConn(os.NewFile(uintptr(fds[0]), ""))
+	if err != nil {
+		t.Errorf("FileConn: %v", err)
+		return
+	}
+	defer srv.Close()
+
+	cli, err := net.FileConn(os.NewFile(uintptr(fds[1]), ""))
+	if err != nil {
+		t.Errorf("FileConn: %v", err)
+		return
+	}
+	defer cli.Close()
+
+	var ucred syscall.Ucred
+	if os.Getuid() != 0 {
+		ucred.Pid = int32(os.Getpid())
+		ucred.Uid = 0
+		ucred.Gid = 0
+		oob := syscall.UnixCredentials(&ucred)
+		_, _, err := cli.(*net.UnixConn).WriteMsgUnix(nil, oob, nil)
+		if err.(*net.OpError).Err != syscall.EPERM {
+			t.Fatalf("WriteMsgUnix failed with %v, want EPERM", err)
+		}
+	}
+
+	ucred.Pid = int32(os.Getpid())
+	ucred.Uid = uint32(os.Getuid())
+	ucred.Gid = uint32(os.Getgid())
+	oob := syscall.UnixCredentials(&ucred)
+
+	// this is going to send a dummy byte
+	n, oobn, err := cli.(*net.UnixConn).WriteMsgUnix(nil, oob, nil)
+	if err != nil {
+		t.Fatalf("WriteMsgUnix: %v", err)
+	}
+	if n != 0 {
+		t.Fatalf("WriteMsgUnix n = %d, want 0", n)
+	}
+	if oobn != len(oob) {
+		t.Fatalf("WriteMsgUnix oobn = %d, want %d", oobn, len(oob))
+	}
+
+	oob2 := make([]byte, 10*len(oob))
+	n, oobn2, flags, _, err := srv.(*net.UnixConn).ReadMsgUnix(nil, oob2)
+	if err != nil {
+		t.Fatalf("ReadMsgUnix: %v", err)
+	}
+	if flags != 0 {
+		t.Fatalf("ReadMsgUnix flags = 0x%x, want 0", flags)
+	}
+	if n != 1 {
+		t.Fatalf("ReadMsgUnix n = %d, want 1 (dummy byte)", n)
+	}
+	if oobn2 != oobn {
+		// without SO_PASSCRED set on the socket, the ReadMsgUnix will
+		// return zero oob bytes
+		t.Fatalf("ReadMsgUnix oobn = %d, want %d", oobn2, oobn)
+	}
+	oob2 = oob2[:oobn2]
+	if !bytes.Equal(oob, oob2) {
+		t.Fatal("ReadMsgUnix oob bytes don't match")
+	}
+
+	scm, err := syscall.ParseSocketControlMessage(oob2)
+	if err != nil {
+		t.Fatalf("ParseSocketControlMessage: %v", err)
+	}
+	newUcred, err := syscall.ParseUnixCredentials(&scm[0])
+	if err != nil {
+		t.Fatalf("ParseUnixCredentials: %v", err)
+	}
+	if *newUcred != ucred {
+		t.Fatalf("ParseUnixCredentials = %+v, want %+v", newUcred, ucred)
+	}
+}

コアとなるコードの解説

追加されたsrc/pkg/syscall/creds_linux_test.goファイルには、TestSCMCredentialsという単一のテスト関数が含まれています。この関数は、LinuxにおけるUNIXドメインソケットを介した認証情報(PID, UID, GID)の送受信機能を検証します。

  1. ソケットペアの初期化:

    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])
    

    syscall.Socketpairは、互いに接続された2つのソケットディスクリプタを作成します。AF_LOCALはUNIXドメインソケットを示し、SOCK_STREAMはストリーム指向のソケットであることを示します。defer syscall.Closeは、テスト終了時にソケットを確実にクローズするためのものです。

  2. SO_PASSCREDオプションの設定:

    err = syscall.SetsockoptInt(fds[0], syscall.SOL_SOCKET, syscall.SO_PASSCRED, 1)
    if err != nil {
        t.Fatalf("SetsockoptInt: %v", err)
    }
    

    サーバー側ソケット(fds[0])に対してSO_PASSCREDソケットオプションを有効にします。これにより、このソケットは補助データとして認証情報を受信できるようになります。

  3. net.Connへの変換:

    srv, err := net.FileConn(os.NewFile(uintptr(fds[0]), ""))
    // ...
    cli, err := net.FileConn(os.NewFile(uintptr(fds[1]), ""))
    // ...
    

    syscallパッケージで得られたファイルディスクリプタを、Goの標準ライブラリであるnetパッケージのnet.Connインターフェースに変換します。これにより、net.UnixConnのメソッド(WriteMsgUnix, ReadMsgUnix)を使用できるようになります。

  4. 特権ユーザーでの認証情報送信テスト(エラーケース):

    var ucred syscall.Ucred
    if os.Getuid() != 0 { // If not root
        ucred.Pid = int32(os.Getpid())
        ucred.Uid = 0 // Try to send as root
        ucred.Gid = 0
        oob := syscall.UnixCredentials(&ucred)
        _, _, err := cli.(*net.UnixConn).WriteMsgUnix(nil, oob, nil)
        if err.(*net.OpError).Err != syscall.EPERM { // Expect Permission Denied
            t.Fatalf("WriteMsgUnix failed with %v, want EPERM", err)
        }
    }
    

    現在のユーザーがrootでない場合、偽のrootユーザーの認証情報(UID=0)を含むUcred構造体を作成し、送信を試みます。非特権プロセスが他者の認証情報を偽装して送信することは許可されないため、この操作はEPERM(Permission denied)エラーを返すはずです。テストはこのエラーを期待し、Goがシステムコールのエラーを正しく伝播することを確認します。

  5. 現在のプロセスの認証情報送信:

    ucred.Pid = int32(os.Getpid())
    ucred.Uid = uint32(os.Getuid())
    ucred.Gid = uint32(os.Getgid())
    oob := syscall.UnixCredentials(&ucred)
    
    n, oobn, err := cli.(*net.UnixConn).WriteMsgUnix(nil, oob, nil)
    // ... assertions ...
    

    現在のプロセスの実際のPID、UID、GIDを含むUcred構造体を作成し、syscall.UnixCredentialsで補助データoobを生成します。cli.(*net.UnixConn).WriteMsgUnixを使って、通常のデータは送信せず(最初の引数nil)、この補助データのみを送信します。送信されたバイト数と補助データのバイト数が期待通りであることを確認します。

  6. 認証情報の受信と検証:

    oob2 := make([]byte, 10*len(oob)) // Buffer for received ancillary data
    n, oobn2, flags, _, err := srv.(*net.UnixConn).ReadMsgUnix(nil, oob2)
    // ... assertions ...
    
    scm, err := syscall.ParseSocketControlMessage(oob2)
    if err != nil {
        t.Fatalf("ParseSocketControlMessage: %v", err)
    }
    newUcred, err := syscall.ParseUnixCredentials(&scm[0])
    if err != nil {
        t.Fatalf("ParseUnixCredentials: %v", err)
    }
    if *newUcred != ucred {
        t.Fatalf("ParseUnixCredentials = %+v, want %+v", newUcred, ucred)
    }
    

    サーバー側ソケット(srv)でsrv.(*net.UnixConn).ReadMsgUnixを呼び出し、送信された補助データを受信します。受信した補助データが送信されたものと一致することを確認した後、syscall.ParseSocketControlMessageでソケット制御メッセージを解析し、さらにsyscall.ParseUnixCredentialsUcred構造体を抽出します。最後に、抽出されたnewUcredが送信時に設定した元のucredと完全に一致するかを検証します。これにより、認証情報が正しく送受信され、解析できることが確認されます。

このテストは、Go言語のsyscallパッケージがLinuxの低レベルなIPC機能を正確にラップし、開発者が安全かつ信頼性高く利用できることを保証する上で重要な役割を果たします。

関連リンク

参考にした情報源リンク