[インデックス 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_CREDENTIALS
とSO_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_CREDENTIALS
とSO_PASSCRED
の組み合わせに焦点を当てており、Go言語がこの明示的な認証情報交換メカニズムを正しく扱えるかを検証しています。
技術的詳細
このコミットで追加されたテストコードは、Go言語のsyscall
パッケージがLinuxのSCM_CREDENTIALS
とSO_PASSCRED
機能を正しく利用できることを検証します。テストは以下のステップで構成されます。
- ソケットペアの作成:
syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
を呼び出して、UNIXドメインソケットのペアを作成します。これにより、2つの接続されたソケットディスクリプタ(fds[0]
とfds[1]
)が得られます。これらはそれぞれサーバー側とクライアント側として機能します。 SO_PASSCRED
オプションの有効化: サーバー側ソケット(fds[0]
)に対してsyscall.SetsockoptInt(fds[0], syscall.SOL_SOCKET, syscall.SO_PASSCRED, 1)
を呼び出し、SO_PASSCRED
オプションを有効にします。これにより、このソケットがSCM_CREDENTIALS
補助データを受信できるようになります。net.Conn
への変換:syscall
パッケージのファイルディスクリプタをGoのnet.Conn
インターフェースに変換します。これはos.NewFile
とnet.FileConn
を使って行われます。これにより、Goの標準ネットワークAPIを通じてソケットを操作できるようになります。- 特権ユーザーでのテスト(オプション): もし現在のユーザーがroot(UID 0)でない場合、意図的にUIDを0に設定した
Ucred
構造体を作成し、WriteMsgUnix
で送信を試みます。この操作は通常、権限エラー(EPERM
)となるはずであり、テストはそのエラーを期待します。これは、非特権ユーザーが任意の認証情報を偽装して送信できないことを確認するためです。 - 認証情報の準備と送信:
- 現在のプロセスのPID、UID、GIDを含む
syscall.Ucred
構造体を作成します。 syscall.UnixCredentials(&ucred)
を呼び出して、このUcred
構造体からSCM_CREDENTIALS
補助データ(バイトスライス)を生成します。- クライアント側ソケット(
cli
)からcli.(*net.UnixConn).WriteMsgUnix(nil, oob, nil)
を呼び出して、この補助データを送信します。通常のデータは送信せず(最初の引数はnil
)、補助データのみを送信します。
- 現在のプロセスのPID、UID、GIDを含む
- 認証情報の受信と検証:
- サーバー側ソケット(
srv
)でsrv.(*net.UnixConn).ReadMsgUnix(nil, oob2)
を呼び出し、補助データを受信します。 - 受信した補助データが送信したものとバイトレベルで一致するかを確認します。
syscall.ParseSocketControlMessage(oob2)
を呼び出して、受信した補助データからソケット制御メッセージを解析します。syscall.ParseUnixCredentials(&scm[0])
を呼び出して、解析されたソケット制御メッセージからUcred
構造体を抽出します。- 抽出された
Ucred
構造体が、送信時に設定した元のUcred
構造体と完全に一致するかを検証します。PID、UID、GIDがすべて一致すればテストは成功です。
- サーバー側ソケット(
この一連のテストにより、Go言語のsyscall
パッケージがLinuxのSCM_CREDENTIALS
とSO_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)の送受信機能を検証します。
-
ソケットペアの初期化:
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
は、テスト終了時にソケットを確実にクローズするためのものです。 -
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
ソケットオプションを有効にします。これにより、このソケットは補助データとして認証情報を受信できるようになります。 -
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
)を使用できるようになります。 -
特権ユーザーでの認証情報送信テスト(エラーケース):
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がシステムコールのエラーを正しく伝播することを確認します。 -
現在のプロセスの認証情報送信:
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
)、この補助データのみを送信します。送信されたバイト数と補助データのバイト数が期待通りであることを確認します。 -
認証情報の受信と検証:
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.ParseUnixCredentials
でUcred
構造体を抽出します。最後に、抽出されたnewUcred
が送信時に設定した元のucred
と完全に一致するかを検証します。これにより、認証情報が正しく送受信され、解析できることが確認されます。
このテストは、Go言語のsyscall
パッケージがLinuxの低レベルなIPC機能を正確にラップし、開発者が安全かつ信頼性高く利用できることを保証する上で重要な役割を果たします。
関連リンク
- Go CL: https://golang.org/cl/5846059
- GitHubコミットページ: https://github.com/golang/go/commit/6a0544091e04dc972069d6ce031f886a873daf32
参考にした情報源リンク
- man7.org - unix(7): https://man7.org/linux/man-pages/man7/unix.7.html
- man7.org - cmsg(3): https://man7.org/linux/man-pages/man3/cmsg.3.html
- man7.org - recvmsg(2): https://man7.org/linux/man-pages/man2/recvmsg.2.html
- Stack Overflow - What is the difference between SO_PEERCRED and SCM_CREDENTIALS?: https://stackoverflow.com/questions/17000830/what-is-the-difference-between-so-peercred-and-scm-credentials
- Arch Linux Wiki - Unix domain socket: https://wiki.archlinux.org/title/Unix_domain_socket
- Medium - Unix Domain Sockets in Linux: https://medium.com/@ahmed.t.abdelaziz/unix-domain-sockets-in-linux-a71212121212
- litux.nl - Unix Domain Sockets: https://www.litux.nl/unix_domain_sockets.html