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

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

このコミットは、Go言語のcgo機能に関連する既知の問題、具体的には「GNU/Linux環境でsetgidシステムコールがハングする(応答しなくなる)問題」(Issue 3871)を再現するためのテストケースを追加するものです。このテストは、将来的な回帰を防ぎ、問題の解決策が正しく機能することを確認するために導入されました。

コミット

commit c49af2ccafa42971a63aee0a953b82fa58285e74
Author: Ian Lance Taylor <iant@golang.org>
Date:   Thu Jul 26 23:21:41 2012 -0700

    misc/cgo/test: add test for issue 3871: cgo setgid hang on GNU/Linux
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6445049

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

https://github.com/golang/go/commit/c49af2ccafa42971a63aee0a953b82fa58285e74

元コミット内容

misc/cgo/test: add test for issue 3871: cgo setgid hang on GNU/Linux

変更の背景

このコミットは、Go言語のcgo(C言語との相互運用)機能を使用する際に、特定の条件下でsetgidシステムコールがGNU/Linux環境でハングアップするという問題(Issue 3871)に対応するために作成されました。この問題は、GoプログラムがCコードを呼び出し、そのCコード内でsetgid(またはsetuid)のような特権変更を行うシステムコールを実行しようとした際に発生しました。

当時のGoランタイムは、これらのシステムコールが特定のシグナル(特にシグナル33)を適切に処理しないことが原因で、デッドロックのような状態に陥り、プログラムが応答しなくなることがありました。このコミットは、問題の根本的な解決策を実装する前段階として、まずこのハングアップ現象を確実に再現できるテストケースを追加することで、問題の存在を明確にし、将来的な修正がこの問題を解決したことを検証できるようにすることを目的としています。

前提知識の解説

cgo

cgoは、GoプログラムからC言語のコードを呼び出したり、逆にC言語のコードからGoの関数を呼び出したりするためのGo言語の機能です。これにより、既存のCライブラリをGoプロジェクトで再利用したり、Goでは実装が難しい低レベルの操作(例: システムコールへの直接アクセス)を行ったりすることが可能になります。cgoを使用するGoファイルには、import "C"という特別なインポート文が含まれ、その前にCコードを記述するためのコメントブロックが置かれます。

setgidシステムコール

setgidは、Unix系オペレーティングシステムにおけるシステムコールの一つで、プロセスの実効グループID(effective group ID)を設定するために使用されます。実効グループIDは、プロセスがファイルやその他のシステムリソースにアクセスする際の権限を決定します。通常、特権を持つプロセス(root権限など)のみが、任意のグループIDを設定できます。セキュリティ上の理由から、setgidのような特権変更を行うシステムコールは、OSのカーネルレベルで厳密に管理されており、その呼び出しには細心の注意が必要です。

プログラムのハング(Hang)

プログラムが「ハングする」とは、プログラムが応答しなくなり、それ以上処理を進められなくなる状態を指します。これは、無限ループ、デッドロック(複数のプロセスやスレッドが互いにリソースの解放を待ち合う状態)、またはシステムコールが予期せずブロックされ続けることなど、様々な原因で発生します。今回のケースでは、cgoを介したsetgid呼び出しが、Goランタイムの内部的な問題によりブロックされ、プログラム全体が応答しなくなる現象を指しています。

GoのGoroutineとスケジューラ

Go言語は、軽量な並行処理の単位である「Goroutine」を提供します。GoroutineはOSのスレッドよりもはるかに軽量で、数百万個を同時に実行することも可能です。Goランタイムには、これらのGoroutineをOSスレッドにマッピングし、実行をスケジュールする独自のスケジューラが組み込まれています。システムコールがブロックされると、Goスケジューラは通常、そのGoroutineをブロックし、他のGoroutineを実行するように切り替えます。しかし、特定の条件下では、この切り替えがうまくいかず、システムコールが完了するまでGoroutineがブロックされ続け、結果としてプログラム全体がハングすることがあります。

技術的詳細

Issue 3871の「cgo setgid hang on GNU/Linux」問題の技術的な詳細を掘り下げると、GoランタイムとLinuxカーネルの間の相互作用、特にシグナル処理とシステムコールブロックの挙動が関係しています。

当時のGoランタイムは、cgoを介してCライブラリ関数(この場合はsetgid)を呼び出す際に、Goのスケジューラがそのシステムコールを適切に扱えないケースがありました。特に、setgidのような特権変更を行うシステムコールは、内部的にLinuxカーネルの特定のメカニズムと連携します。この連携の過程で、Goランタイムが予期しないシグナル(例えば、Goランタイムが内部的に使用するシグナルや、Cライブラリが発行するシグナル)を受け取った場合、そのシグナルハンドリングが不適切であると、システムコールが完了するのを待つGoroutineがデッドロック状態に陥ることがありました。

具体的には、Goランタイムは、システムコールがブロックされた際に、そのGoroutineをOSスレッドから切り離し、他のGoroutineを実行できるようにするメカニズムを持っています。しかし、setgidのような特定のシステムコールが、Goランタイムが想定していない方法でブロックされたり、Goランタイムが内部的に使用するシグナル(例えば、Goのプリエンプションやガベージコレクションに関連するシグナル)と競合したりすると、GoroutineがOSスレッドから切り離されずにブロックされ続け、結果としてGoスケジューラがそのGoroutineを解放できなくなり、プログラム全体がハングアップするという現象が発生しました。

この問題は、Goランタイムがシグナルを処理する方法、特にsetuid/setgid呼び出し中に発生する可能性のあるシグナル(例えば、シグナル33)を無視するように変更することで解決されました。これにより、システムコールがブロックされても、Goランタイムが適切にGoroutineを管理し、デッドロックを回避できるようになりました。このコミットで追加されたテストは、この修正が正しく機能するかどうかを検証するための重要な手段となります。

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

このコミットでは、以下の2つのファイルが変更されています。

  1. misc/cgo/test/basic.go:

    • #include <unistd.h>が追加されました。これはsetgid関数が定義されているヘッダファイルです。
    • testSetgidという新しいGo関数が追加されました。この関数がIssue 3871のハングアップをテストする主要なロジックを含んでいます。
  2. misc/cgo/test/cgo_test.go:

    • TestSetgid(t *testing.T)というテスト関数が追加されました。この関数は、basic.goで定義されたtestSetgid関数を呼び出すことで、実際のテストを実行します。

コアとなるコードの解説

misc/cgo/test/basic.go の変更

@@ -11,6 +11,7 @@ package cgotest
 #include <stdlib.h>
 #include <sys/stat.h>
 #include <errno.h>
+#include <unistd.h>
 
 #define SHIFT(x, y)  ((x)<<(y))\
 #define KILO SHIFT(1, 10)\
@@ -57,6 +58,7 @@ import "C"
 import (
 	"syscall"
 	"testing"
+	"time"
 	"unsafe"
 )
 
@@ -124,6 +126,20 @@ func testMultipleAssign(t *testing.T) {
 	C.free(unsafe.Pointer(p))\
 }\
 \n+func testSetgid(t *testing.T) {\
+\t// Issue 3871.\
+\tc := make(chan bool)\
+\tgo func() {\
+\t\tC.setgid(0)\
+\t\tc <- true\
+\t}()\
+\tselect {\
+\tcase <-c:\
+\tcase <-time.After(5 * time.Second):\
+\t\tt.Error("setgid hung")\
+\t}\
+}\
+\n var (\
 \tcuint  = (C.uint)(0)\
 \tculong C.ulong
  • #include <unistd.h>: Cgoコードブロックにunistd.hヘッダファイルがインクルードされました。これは、setgidシステムコールをC言語側で利用するために必要です。
  • func testSetgid(t *testing.T):
    • この関数は、Issue 3871のハングアップ問題をテストするために特別に設計されています。
    • c := make(chan bool): setgid呼び出しが完了したかどうかをGoroutine間で通知するためのチャネルを作成します。
    • go func() { C.setgid(0); c <- true }(): 新しいGoroutineを起動し、その中でC.setgid(0)を呼び出します。setgid(0)は、実効グループIDをroot(通常は0)に設定しようとします。この呼び出しが成功または失敗して返ってきた場合、Goroutineはチャネルctrueを送信します。
    • select { ... }: このselect文は、setgid呼び出しが完了するのを待つためのタイムアウトメカニズムを提供します。
      • case <-c:: setgidを呼び出したGoroutineがチャネルcに値を送信した場合、つまりsetgid呼び出しが完了した場合、このケースが選択されます。テストは正常に続行されます。
      • case <-time.After(5 * time.Second):: もし5秒以内にsetgid呼び出しが完了しなかった場合(つまり、チャネルcに値が送信されなかった場合)、このケースが選択されます。これは、setgidがハングアップしたことを意味します。
      • t.Error("setgid hung"): タイムアウトが発生した場合、テストはエラーとして報告され、「setgid hung」というメッセージが出力されます。これにより、ハングアップ問題が再現されたことが明確になります。

このテストの目的は、setgid(0)の呼び出しが5秒以内に完了するかどうかを検証することです。もし完了しない場合、それは以前のバグ(Issue 3871)がまだ存在するか、または回帰したことを示します。

misc/cgo/test/cgo_test.go の変更

@@ -27,5 +27,6 @@ func Test1328(t *testing.T)                { test1328(t) }\
 func TestParallelSleep(t *testing.T)       { testParallelSleep(t) }\
 func TestSetEnv(t *testing.T)              { testSetEnv(t) }\
 func TestHelpers(t *testing.T)             { testHelpers(t) }\
+func TestSetgid(t *testing.T)              { testSetgid(t) }\
 \
 func BenchmarkCgoCall(b *testing.B) { benchCgoCall(b) }\
  • func TestSetgid(t *testing.T) { testSetgid(t) }: この行は、GoのテストフレームワークがtestSetgid関数をテストとして認識し、実行するように登録します。これにより、go testコマンドを実行した際に、この新しいテストケースが自動的に実行されるようになります。

これらの変更により、Goのテストスイートに、cgosetgidの相互作用における潜在的なハングアップ問題を検出するための堅牢なテストが追加されました。

関連リンク

参考にした情報源リンク