[インデックス 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.StartProcess
やos/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.OpenProcess
、syscall.TerminateProcess
、syscall.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
という新しいヘルパー関数が導入されました。この関数は以下のステップを実行します。
syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, uint32(pid))
を呼び出します。PROCESS_TERMINATE
フラグを指定することで、プロセスを終了させるために必要なアクセス権限を明示的に要求します。false
は、ハンドルが継承可能でないことを示します。uint32(pid)
は、終了させたいプロセスのPID(プロセスID)です。
OpenProcess
が成功した場合、返されたハンドル(h
)を使用してsyscall.TerminateProcess(h, uint32(exitcode))
を呼び出します。これにより、適切な権限を持つハンドルを介してプロセスが終了されます。defer syscall.CloseHandle(h)
を使用して、OpenProcess
で取得したハンドルを確実に閉じます。これはリソースリークを防ぐために非常に重要です。- エラーが発生した場合は、
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
関数でこの権限をより明確に指定できるようになります。
- Windows APIの
これらの変更により、Goのos
パッケージはWindows環境において、より堅牢で信頼性の高いプロセス強制終了機能を提供するようになりました。
関連リンク
- Go Issue #5615: https://github.com/golang/go/issues/5615
- Go CL 9651047: https://golang.org/cl/9651047
参考にした情報源リンク
- Microsoft Docs - OpenProcess function: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
- Microsoft Docs - TerminateProcess function: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess
- Microsoft Docs - Process Security and Access Rights: https://learn.microsoft.com/en-us/windows/win32/secauthz/process-security-and-access-rights
- Go言語の
os
パッケージドキュメント: https://pkg.go.dev/os - Go言語の
syscall
パッケージドキュメント: https://pkg.go.dev/syscall