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

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

このコミットは、Go言語のosパッケージにおけるWindows環境でのプロセス終了処理に関するバグ修正と改善を目的としています。具体的には、TerminateProcessシステムコールを呼び出す前に、適切なアクセス権限(PROCESS_TERMINATE)を要求するように変更することで、プロセスの強制終了が失敗する問題を解決しています。また、この変更を検証するための新しいテストケースも追加されています。

コミット

commit 9b2561ef16307ad5f918e81db2521a78807280f5
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Tue Jun 11 13:06:38 2013 +1000

    os: request appropriate access rights before calling windows TerminateProcess
    
    Fixes #5615.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/9651047

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

https://github.com/golang/go/commit/9b2561ef16307ad5f918e81db2521a78807280f5

元コミット内容

os: request appropriate access rights before calling windows TerminateProcess Fixes #5615. R=golang-dev, rsc CC=golang-dev https://golang.org/cl/9651047

変更の背景

このコミットは、GoのosパッケージがWindows上でプロセスを強制終了する際に発生していた問題(Issue #5615)を修正するために導入されました。以前の実装では、syscall.TerminateProcessを直接呼び出していましたが、この関数はプロセスハンドルに対して特定のアクセス権限(PROCESS_TERMINATE)が必要とされます。しかし、既存のプロセスハンドルがこの権限を持っていない場合、TerminateProcessの呼び出しがアクセス拒否エラーで失敗することがありました。

特に、os.Process.Kill()メソッドが内部でTerminateProcessを呼び出す際に、この問題が顕在化していました。例えば、os.StartProcessos/exec.Command.Start()で起動されたプロセスは、デフォルトでPROCESS_TERMINATE権限を持たないハンドルを返すことがあり、その結果、Goプログラムからこれらのプロセスを強制終了できないという状況が発生していました。

このコミットの目的は、TerminateProcessを呼び出す前に、OpenProcessシステムコールを使用して、明示的にPROCESS_TERMINATE権限を持つ新しいプロセスハンドルを取得することで、この問題を解決することです。これにより、GoプログラムがWindows上でプロセスを確実に強制終了できるようになります。

前提知識の解説

Windowsプロセス管理とアクセス権限

Windowsオペレーティングシステムでは、プロセスやスレッドなどのオブジェクトに対して、セキュリティ記述子(Security Descriptor)とアクセス権限(Access Rights)の概念が適用されます。これにより、どのユーザーやプロセスがどの操作を実行できるかが制御されます。

  • プロセスハンドル: Windows APIでは、プロセスを操作するために「ハンドル」と呼ばれる参照を使用します。このハンドルは、特定のプロセスへのアクセス権限をカプセル化しています。
  • OpenProcess: このWindows API関数は、既存のプロセスを開き、そのプロセスへのハンドルを取得するために使用されます。この関数を呼び出す際には、取得したいハンドルのアクセス権限を明示的に指定する必要があります。
  • TerminateProcess: このWindows API関数は、指定されたプロセスを強制的に終了させます。この関数を呼び出すためには、対象プロセスのハンドルがPROCESS_TERMINATEアクセス権限を持っている必要があります。
  • PROCESS_TERMINATE: これは、プロセスを終了させるために必要な特定のアクセス権限フラグです。この権限がないハンドルを使ってTerminateProcessを呼び出すと、「アクセスが拒否されました」というエラー(ERROR_ACCESS_DENIED)が発生します。
  • PROCESS_QUERY_INFORMATION: プロセスの情報を照会するために必要なアクセス権限です。
  • CloseHandle: 開いたハンドルは、使用後に必ずこの関数で閉じる必要があります。これにより、システムリソースのリークを防ぎます。

Go言語のosパッケージとsyscallパッケージ

  • osパッケージ: Go言語の標準ライブラリの一部で、オペレーティングシステムとの基本的な相互作用を提供します。ファイル操作、プロセス管理、環境変数へのアクセスなどが含まれます。os.Process型は実行中のプロセスを表し、Kill()メソッドはプロセスを強制終了するために使用されます。
  • syscallパッケージ: Go言語の標準ライブラリの一部で、低レベルのオペレーティングシステムプリミティブへのアクセスを提供します。Windowsの場合、syscallパッケージはWindows API関数への直接的なバインディングを提供します。このコミットでは、syscall.OpenProcesssyscall.TerminateProcesssyscall.CloseHandleなどが使用されています。
  • NewSyscallError: osパッケージ内で定義されているヘルパー関数で、syscallパッケージから返されたエラーをos.SyscallError型にラップし、より詳細なエラー情報を提供します。

技術的詳細

このコミットの主要な変更点は、Windows環境におけるos.Process.signalメソッド(特にKillシグナルを処理する部分)の内部実装です。

以前は、p.signal(Kill)が呼び出された際に、syscall.TerminateProcess(syscall.Handle(p.handle), 1)が直接呼び出されていました。ここでp.handleは、プロセスが起動された際に取得されたハンドルです。しかし、このp.handleが常にPROCESS_TERMINATE権限を持っているとは限りませんでした。

新しい実装では、terminateProcessという新しいヘルパー関数が導入されました。この関数は以下のステップを実行します。

  1. syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, uint32(pid))を呼び出します。
    • PROCESS_TERMINATEフラグを指定することで、プロセスを終了させるために必要なアクセス権限を明示的に要求します。
    • falseは、ハンドルが継承可能でないことを示します。
    • uint32(pid)は、終了させたいプロセスのPID(プロセスID)です。
  2. OpenProcessが成功した場合、返されたハンドル(h)を使用してsyscall.TerminateProcess(h, uint32(exitcode))を呼び出します。これにより、適切な権限を持つハンドルを介してプロセスが終了されます。
  3. defer syscall.CloseHandle(h)を使用して、OpenProcessで取得したハンドルを確実に閉じます。これはリソースリークを防ぐために非常に重要です。
  4. エラーが発生した場合は、NewSyscallErrorを使用してos.SyscallError型のエラーを返します。

この変更により、os.Process.Kill()が呼び出された際に、常にPROCESS_TERMINATE権限を持つ新しいハンドルが取得され、それを使ってプロセスが終了されるため、アクセス拒否エラーが回避されます。

また、この変更を検証するために、os_test.goに新しいテスト関数testKillProcessと、それを利用するTestKillStartProcessおよびTestKillFindProcessが追加されました。これらのテストは、Goプログラムが生成したプロセスをKill()メソッドで正常に終了できることを確認します。

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

src/pkg/os/exec_windows.go

--- a/src/pkg/os/exec_windows.go
+++ b/src/pkg/os/exec_windows.go
@@ -42,13 +42,22 @@ func (p *Process) wait() (ps *ProcessState, err error) {
 	return &ProcessState{p.Pid, syscall.WaitStatus{ExitCode: ec}, &u}, nil
 }

+func terminateProcess(pid, exitcode int) error {
+	h, e := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, uint32(pid))
+	if e != nil {
+		return NewSyscallError("OpenProcess", e)
+	}
+	defer syscall.CloseHandle(h)
+	e = syscall.TerminateProcess(h, uint32(exitcode))
+	return NewSyscallError("TerminateProcess", e)
+}
+
 func (p *Process) signal(sig Signal) error {
 	if p.done() {
 		return errors.New("os: process already finished")
 	}
 	if sig == Kill {
-		e := syscall.TerminateProcess(syscall.Handle(p.handle), 1)
-		return NewSyscallError("TerminateProcess", e)
+		return terminateProcess(p.Pid, 1)
 	}
 	// TODO(rsc): Handle Interrupt too?
 	return syscall.Errno(syscall.EWINDOWS)

src/pkg/os/os_test.go

--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -11,11 +11,13 @@ import (
 	"io"
 	"io/ioutil"
 	. "os"
+	osexec "os/exec"
 	"path/filepath"
 	"runtime"
 	"strings"
 	"syscall"
 	"testing"
+	"text/template"
 	"time"
 )

@@ -1130,3 +1132,72 @@ func TestReadAtEOF(t *testing.T) {
 		t.Fatalf("ReadAt failed: %s", err)
 	}
 }
+
+func testKillProcess(t *testing.T, processKiller func(p *Process)) {
+	dir, err := ioutil.TempDir("", "go-build")
+	if err != nil {
+		t.Fatalf("Failed to create temp directory: %v", err)
+	}
+	defer RemoveAll(dir)
+
+	src := filepath.Join(dir, "main.go")
+	f, err := Create(src)
+	if err != nil {
+		t.Fatalf("Failed to create %v: %v", src, err)
+	}
+	st := template.Must(template.New("source").Parse(`
+package main
+import "time"
+func main() {
+	time.Sleep(time.Second)
+}
+`))
+	err = st.Execute(f, nil)
+	if err != nil {
+		f.Close()
+		t.Fatalf("Failed to execute template: %v", err)
+	}
+	f.Close()
+
+	exe := filepath.Join(dir, "main.exe")
+	output, err := osexec.Command("go", "build", "-o", exe, src).CombinedOutput()
+	if err != nil {
+		t.Fatalf("Failed to build exe %v: %v %v", exe, err, string(output))
+	}
+
+	cmd := osexec.Command(exe)
+	err = cmd.Start()
+	if err != nil {
+		t.Fatalf("Failed to start test process: %v", err)
+	}
+	go func() {
+		time.Sleep(100 * time.Millisecond)
+		processKiller(cmd.Process)
+	}()
+	err = cmd.Wait()
+	if err == nil {
+		t.Errorf("Test process succeeded, but expected to fail")
+	}
+}
+
+func TestKillStartProcess(t *testing.T) {
+	testKillProcess(t, func(p *Process) {
+		err := p.Kill()
+		if err != nil {
+			t.Fatalf("Failed to kill test process: %v", err)
+		}
+	})
+}
+
+func TestKillFindProcess(t *testing.T) {
+	testKillProcess(t, func(p *Process) {
+		p2, err := FindProcess(p.Pid)
+		if err != nil {
+			t.Fatalf("Failed to find test process: %v", err)
+		}
+		err = p2.Kill()
+		if err != nil {
+			t.Fatalf("Failed to kill test process: %v", err)
+		}
+	})
+}

src/pkg/syscall/ztypes_windows.go

--- a/src/pkg/syscall/ztypes_windows.go
+++ b/src/pkg/syscall/ztypes_windows.go
@@ -151,6 +151,7 @@ const (
 	CREATE_NEW_PROCESS_GROUP   = 0x00000200
 	CREATE_UNICODE_ENVIRONMENT = 0x00000400

+	PROCESS_TERMINATE         = 1
 	PROCESS_QUERY_INFORMATION = 0x00000400
 	SYNCHRONIZE               = 0x00100000

コアとなるコードの解説

src/pkg/os/exec_windows.goの変更

  • terminateProcess関数の追加:
    • この新しい関数は、指定されたPIDを持つプロセスを終了させるためのロジックをカプセル化します。
    • syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, uint32(pid))を呼び出すことで、PROCESS_TERMINATE権限を持つプロセスハンドルを明示的に取得しようとします。これにより、たとえ元のプロセスハンドルがこの権限を持っていなくても、終了操作に必要な権限を確保できます。
    • defer syscall.CloseHandle(h)は、関数が終了する際に確実にハンドルを閉じるための重要なパターンです。
    • syscall.TerminateProcess(h, uint32(exitcode))を呼び出し、取得したハンドルを使ってプロセスを終了させます。
    • エラーが発生した場合は、NewSyscallErrorでラップして返します。
  • Process.signalメソッドの変更:
    • sig == Killの場合の処理が変更されました。以前はsyscall.TerminateProcessを直接呼び出していましたが、新しく追加されたterminateProcess関数を呼び出すように変更されました。これにより、Killシグナルが送信された際に、常に適切なアクセス権限が確保された上でプロセス終了が試みられるようになります。

src/pkg/os/os_test.goの変更

  • testKillProcess関数の追加:
    • このヘルパー関数は、プロセスを生成し、指定されたprocessKiller関数を使ってそのプロセスを終了させ、終了が成功したかどうかを検証する汎用的なテストフレームワークを提供します。
    • 一時ディレクトリを作成し、Goのソースコード(main.go)を生成します。このソースコードは、プロセスがすぐに終了しないようにtime.Sleep(time.Second)を含んでいます。
    • go buildコマンドを使用して、生成したソースコードから実行可能ファイル(main.exe)をビルドします。
    • osexec.Command(exe).Start()でプロセスを起動します。
    • ゴルーチン内で短い遅延(100ミリ秒)の後、引数として渡されたprocessKiller関数を呼び出してプロセスを終了させます。
    • cmd.Wait()でプロセスの終了を待ち、エラーが発生した(つまり、プロセスが強制終了された)ことを確認します。
  • TestKillStartProcess関数の追加:
    • testKillProcessを呼び出し、processKillerとしてp.Kill()を渡します。これは、os.StartProcessなどで起動されたプロセスがKill()メソッドで正常に終了できることをテストします。
  • TestKillFindProcess関数の追加:
    • testKillProcessを呼び出し、processKillerとしてFindProcess(p.Pid)でプロセスを再取得し、その新しいハンドルに対してKill()を呼び出すロジックを渡します。これは、PIDからプロセスを再取得した場合でもKill()が正常に機能することをテストします。

src/pkg/syscall/ztypes_windows.goの変更

  • PROCESS_TERMINATE定数の追加:
    • Windows APIのPROCESS_TERMINATEアクセス権限に対応する定数(値は1)が追加されました。これにより、OpenProcess関数でこの権限をより明確に指定できるようになります。

これらの変更により、GoのosパッケージはWindows環境において、より堅牢で信頼性の高いプロセス強制終了機能を提供するようになりました。

関連リンク

参考にした情報源リンク