[インデックス 11910] ファイルの概要
このコミットは、Go言語の net/http
パッケージにおける fs_test.go
内の TestLinuxSendfile
テストをより堅牢にするための変更です。特に、strace
を用いたテストにおいて、一部のLinuxディストリビューションで自身のプロセスに ptrace
でアタッチできないという問題に対処しています。
コミット
commit c210ddfe2c881835a283346e8fa498a777ea8af7
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 15 11:05:51 2012 +1100
net/http: make strace test more robust
Apparently some distros don't let you ptrace attach
to your own existing processes.
Run strace on the child directly, instead, which
reportedly is more often allowed, and makes the
code simpler too.
R=golang-dev, n13m3y3r
CC=golang-dev
https://golang.org/cl/5675050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c210ddfe2c881835a283346e8fa498a777ea8af7
元コミット内容
commit c210ddfe2c881835a283346e8fa498a777ea8af7
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 15 11:05:51 2012 +1100
net/http: make strace test more robust
Apparently some distros don't let you ptrace attach
to your own existing processes.
Run strace on the child directly, instead, which
reportedly is more often allowed, and makes the
code simpler too.
R=golang-dev, n13m3y3r
CC=golang-dev
https://golang.org/cl/5675050
---
src/pkg/net/http/fs_test.go | 21 +++++----------------
1 file changed, 5 insertions(+), 16 deletions(-)\n
diff --git a/src/pkg/net/http/fs_test.go b/src/pkg/net/http/fs_test.go
index 11ca786fce..143617e95f 100644
--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -18,7 +18,6 @@ import (
"path/filepath"
"regexp"
"runtime"
-"strconv"
"strings"
"testing"
"time"
@@ -387,24 +386,15 @@ func TestLinuxSendfile(t *testing.T) {
}\n \tdefer ln.Close()\n \n-"child := exec.Command(os.Args[0], "-test.run=TestLinuxSendfileChild")"
+" var buf bytes.Buffer"
+" child := exec.Command("strace", "-f", os.Args[0], "-test.run=TestLinuxSendfileChild")"
\tchild.ExtraFiles = append(child.ExtraFiles, lnf)\n \tchild.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)\n-\n+\tchild.Stdout = &buf\n+\tchild.Stderr = &buf\n \terr = child.Start()\n \tif err != nil {\n-"\t\tt.Fatal(err)"
-"\t}"
-"\n-"\tpid := child.Process.Pid"
-"\n-"\tvar buf bytes.Buffer"
-"\tstrace := exec.Command("strace", "-f", "-p", strconv.Itoa(pid))"
-"\tstrace.Stdout = &buf"
-"\tstrace.Stderr = &buf"
-"\terr = strace.Start()"
-"\tif err != nil {"
-"\t\tt.Logf("skipping; failed to start strace: %v", err)"
+" \tt.Logf("skipping; failed to start straced child: %v", err)"
\t\treturn\n \t}\n \n@@ -417,7 +407,6 @@ func TestLinuxSendfile(t *testing.T) {\n \t// Force child to exit cleanly.\n \tGet(fmt.Sprintf("http://%s/quit", ln.Addr()))\n \tchild.Wait()\n-"\tstrace.Wait()"
\n \trx := regexp.MustCompile(`sendfile(64)?\\(\\d+,\\s*\\d+,\\s*NULL,\\s*\\d+\\)\\s*=\\s*\\d+\\s*\\n`)\n \trxResume := regexp.MustCompile(`<\\.\\.\\. sendfile(64)? resumed> \\)\\s*=\\s*\\d+\\s*\\n`)\n```
## 変更の背景
このコミットの主な背景は、`net/http` パッケージの `TestLinuxSendfile` テストが、特定のLinuxディストリビューションで失敗するという問題に直面していたことです。このテストは、`sendfile` システムコールの動作を検証するために `strace` を利用していました。
従来のテストでは、まず子プロセスを起動し、その子プロセスのPID(プロセスID)を取得した後、別の `strace` コマンドを使ってそのPIDにアタッチしていました。しかし、一部のLinuxディストリビューション(特にセキュリティが強化された環境や特定のカーネル設定を持つシステム)では、**自身のプロセス(またはその子プロセス)に対して `ptrace` システムコールでアタッチすることが許可されていない**という制約がありました。これは、セキュリティ上の理由から、プロセスが自身のメモリ空間や実行状態をデバッグツールなどで操作することを制限するためです。
この制約により、テスト環境によっては `strace` が子プロセスにアタッチできず、テストがスキップされたり、失敗したりする問題が発生していました。開発者は、このテストの信頼性と移植性を向上させる必要がありました。
## 前提知識の解説
### `strace` コマンド
`strace` はLinuxおよびUnix系OSで利用される強力なデバッグツールです。プログラムが実行中に発行するシステムコール(syscall)や、受け取るシグナルをリアルタイムで追跡し、その詳細な情報を標準エラー出力に表示します。これにより、プログラムがファイルシステム、ネットワーク、メモリ管理など、OSカーネルとどのようにやり取りしているかを詳細に把握できます。
* **`-f` オプション**: `strace` が子プロセスも追跡するように指示します。これにより、親プロセスだけでなく、そのプロセスが `fork` や `exec` などで生成した子プロセスのシステムコールも追跡できます。
* **`-p <pid>` オプション**: 既に実行中のプロセスに `strace` をアタッチし、そのプロセスのシステムコールを追跡します。
### `ptrace` システムコール
`ptrace` は、Linuxカーネルが提供するシステムコールで、あるプロセス(トレーサー)が別のプロセス(トレース対象)の実行を制御・監視するために使用されます。デバッガ(例: GDB)や、`strace` のようなツールは、この `ptrace` を利用してプロセスのレジスタ、メモリ、システムコールなどを操作・検査します。
`ptrace` のセキュリティモデルは複雑で、通常は親プロセスが子プロセスをトレースすることは許可されますが、無関係なプロセスや、特定の条件下では自身のプロセスへのアタッチが制限されることがあります。これは、悪意のあるプログラムが他のプロセスの状態を改ざんしたり、情報を盗んだりするのを防ぐためです。
### `sendfile` システムコール
`sendfile` はLinuxカーネルが提供するシステムコールで、ファイルディスクリプタから別のファイルディスクリプタへデータを直接転送するために使用されます。特に、ファイルからネットワークソケットへデータを転送する際に非常に効率的です。通常、ファイルの内容をネットワークに送信する場合、データはカーネル空間からユーザー空間へコピーされ、その後再びユーザー空間からカーネル空間へコピーされてソケットに送信されます。`sendfile` を使用すると、このユーザー空間へのコピーが不要になり、データがカーネル空間内で直接転送されるため、CPUオーバーヘッドとメモリコピー回数が削減され、パフォーマンスが向上します。Webサーバーなどで静的ファイルを配信する際によく利用されます。
### Go言語の `os/exec` パッケージ
Go言語の `os/exec` パッケージは、外部コマンドを実行するための機能を提供します。
* **`exec.Command(name string, arg ...string) *Cmd`**: 指定されたコマンド名と引数で `Cmd` 構造体を作成します。この時点ではコマンドは実行されません。
* **`Cmd.Start() error`**: コマンドを新しいプロセスとして非同期に実行します。
* **`Cmd.Wait() error`**: `Start` で開始されたコマンドの終了を待ち、その終了ステータスを返します。
* **`Cmd.Stdout` / `Cmd.Stderr`**: 実行されるコマンドの標準出力/標準エラー出力をリダイレクトするための `io.Writer` インターフェースです。ここに `bytes.Buffer` などを設定することで、コマンドの出力をプログラム内でキャプチャできます。
* **`Cmd.ExtraFiles`**: 子プロセスに渡す追加のファイルディスクリプタを指定します。
* **`Cmd.Env`**: 子プロセスに渡す環境変数を指定します。
## 技術的詳細
このコミットの技術的な核心は、`strace` を用いたテストの実行方法を変更することで、`ptrace` の制限を回避し、テストの堅牢性を高める点にあります。
**変更前の問題点**:
変更前は、`TestLinuxSendfile` テスト内で以下のような手順を踏んでいました。
1. `os.Args[0]` (現在のテストバイナリ自身) を `TestLinuxSendfileChild` というテスト関数を実行する子プロセスとして起動。
2. 起動した子プロセスのPIDを取得。
3. 別の `exec.Command` で `strace -f -p <child_pid>` を実行し、子プロセスにアタッチしてシステムコールを追跡。
この方法では、`strace` が既に実行中の子プロセスに `ptrace` システムコールを使ってアタッチしようとします。しかし、前述の通り、一部のLinuxディストリビューションでは、セキュリティ上の理由から、プロセスが自身の生成した子プロセスであっても、実行中のプロセスに `ptrace` でアタッチすることを制限している場合があります。これにより、`strace` コマンドの `Start()` が失敗し、テストがスキップされる原因となっていました。
**変更後の解決策**:
このコミットでは、`strace` の実行方法を根本的に変更しました。
1. 子プロセスを直接 `strace` コマンドの引数として実行するように変更。具体的には、`exec.Command("strace", "-f", os.Args[0], "-test.run=TestLinuxSendfileChild")` とします。
この変更により、`strace` は子プロセスを起動する際に、最初からその子プロセスの親プロセスとして振る舞います。`ptrace` のセキュリティモデルでは、親プロセスが子プロセスをトレースすることは一般的に許可されています。したがって、この方法であれば、自身のプロセスに後からアタッチするという問題が回避され、より多くの環境でテストが正常に実行されるようになります。
また、この変更はコードの複雑性も軽減しています。
* 子プロセスのPIDを明示的に取得する必要がなくなりました。
* `strconv` パッケージを使ってPIDを文字列に変換する必要がなくなりました。
* `strace` コマンドを別個に起動し、その終了を待つ必要がなくなりました。
これにより、テストコードがよりシンプルになり、可読性も向上しています。
## コアとなるコードの変更箇所
```diff
--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -18,7 +18,6 @@ import (
"path/filepath"
"regexp"
"runtime"
-"strconv"
"strings"
"testing"
"time"
@@ -387,24 +386,15 @@ func TestLinuxSendfile(t *testing.T) {
}\n \tdefer ln.Close()\n \n-"child := exec.Command(os.Args[0], "-test.run=TestLinuxSendfileChild")"
+" var buf bytes.Buffer"
+" child := exec.Command("strace", "-f", os.Args[0], "-test.run=TestLinuxSendfileChild")"
\tchild.ExtraFiles = append(child.ExtraFiles, lnf)\n \tchild.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)\n-\n+\tchild.Stdout = &buf\n+\tchild.Stderr = &buf\n \terr = child.Start()\n \tif err != nil {\n-"\t\tt.Fatal(err)"
-"\t}"
-"\n-"\tpid := child.Process.Pid"
-"\n-"\tvar buf bytes.Buffer"
-"\tstrace := exec.Command("strace", "-f", "-p", strconv.Itoa(pid))"
-"\tstrace.Stdout = &buf"
-"\tstrace.Stderr = &buf"
-"\terr = strace.Start()"
-"\tif err != nil {"
-"\t\tt.Logf("skipping; failed to start strace: %v", err)"
+" \tt.Logf("skipping; failed to start straced child: %v", err)"
\t\treturn\n \t}\n \n@@ -417,7 +407,6 @@ func TestLinuxSendfile(t *testing.T) {\n \t// Force child to exit cleanly.\n \tGet(fmt.Sprintf("http://%s/quit", ln.Addr()))\n \tchild.Wait()\n-"\tstrace.Wait()"
\n \trx := regexp.MustCompile(`sendfile(64)?\\(\\d+,\\s*\\d+,\\s*NULL,\\s*\\d+\\)\\s*=\\s*\\d+\\s*\\n`)\n \trxResume := regexp.MustCompile(`<\\.\\.\\. sendfile(64)? resumed> \\)\\s*=\\s*\\d+\\s*\\n`)\n```
## コアとなるコードの解説
変更の核心は、`TestLinuxSendfile` 関数内の `exec.Command` の呼び出し方です。
**変更前**:
```go
child := exec.Command(os.Args[0], "-test.run=TestLinuxSendfileChild")
// ... child.Start() ...
pid := child.Process.Pid
var buf bytes.Buffer
strace := exec.Command("strace", "-f", "-p", strconv.Itoa(pid))
strace.Stdout = &buf
strace.Stderr = &buf
err = strace.Start()
if err != nil {
t.Logf("skipping; failed to start strace: %v", err)
return
}
// ...
strace.Wait() // straceプロセスの終了を待つ
- まず、
os.Args[0]
(現在のテストバイナリ) を引数-test.run=TestLinuxSendfileChild
で実行し、テストヘルパープロセスを起動していました。 - その子プロセスのPID (
child.Process.Pid
) を取得し、strconv.Itoa
で文字列に変換していました。 - 次に、別の
exec.Command
でstrace -f -p <pid>
を実行し、このstrace
プロセスが既に起動している子プロセスにアタッチしようとしていました。 strace.Wait()
でstrace
プロセスの終了を待っていました。
変更後:
var buf bytes.Buffer
child := exec.Command("strace", "-f", os.Args[0], "-test.run=TestLinuxSendfileChild")
child.ExtraFiles = append(child.ExtraFiles, lnf)
child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
child.Stdout = &buf
child.Stderr = &buf
err = child.Start()
if err != nil {
t.Logf("skipping; failed to start straced child: %v", err)
return
}
// ...
child.Wait() // straceが親となっている子プロセスの終了を待つ
exec.Command
の呼び出しがexec.Command("strace", "-f", os.Args[0], "-test.run=TestLinuxSendfileChild")
に変更されました。- これにより、
strace
コマンド自体が、os.Args[0]
(テストバイナリ) を引数として直接実行する形になります。つまり、strace
がテストヘルパープロセスの親プロセスとして機能し、最初からそのシステムコールを追跡します。 -f
オプションは、strace
が子プロセスも追跡することを保証します。
- これにより、
strconv
パッケージのインポートが不要になったため、削除されています。- 子プロセスのPIDを取得したり、そのPIDにアタッチするための別の
strace
コマンドを起動したりするコードがすべて削除されました。これにより、コードが大幅に簡素化されています。 - エラーメッセージも
skipping; failed to start straced child: %v
に変更され、より状況を正確に反映しています。 child.Wait()
のみが残り、strace
が親となっている子プロセスの終了を待つ形になりました。
この変更により、ptrace
のアタッチに関するセキュリティ上の制約を回避し、テストの信頼性と移植性が向上しました。
関連リンク
- Go CL 5675050: https://golang.org/cl/5675050
参考にした情報源リンク
strace
man page: https://man7.org/linux/man-pages/man1/strace.1.htmlptrace
man page: https://man7.org/linux/man-pages/man2/ptrace.2.htmlsendfile
man page: https://man7.org/linux/man-pages/man2/sendfile.2.html- Go
os/exec
package documentation: https://pkg.go.dev/os/exec - Linux
ptrace
security implications (general information): https://lwn.net/Articles/600000/ (LWN.netの記事など、ptrace
のセキュリティに関する一般的な情報源) - "ptrace attach to own process linux" などのキーワードで検索した結果 (具体的なURLは特定せず、一般的な知識として参照)