Go 1.19 で os/exec パッケージの挙動が変わった話【転載版】

（[Go強化月間](https://qiita.com/official-events/ae80b010f51f7018891a)と聞いたので（笑） この記事は[自ブログ](https://text.baldanders.info/golang/exec-package-in-go119/)からの転載です。書き捨てなのでメンテナンスしません。悪しからずご了承の程を）


[Go] 1.19 の[リリースノート](https://go.dev/doc/go1.19 "Go 1.19 Release Notes - The Go Programming Language")を眺めてみると

> [`Command`](https://go.dev/pkg/os/exec/#Command) and [`LookPath`](https://go.dev/pkg/os/exec/#LookPath) no longer allow results from a PATH search to be found relative to the current directory. This removes a [common source of security problems](https://go.dev/blog/path-security) but may also break existing programs that depend on using, say, `exec.Command("prog")` to run a binary named `prog` (or, on Windows, `prog.exe`) in the current directory. See the [`os/exec`](https://go.dev/pkg/os/exec/) package documentation for information about how best to update such programs.
*(via “[Go 1.19 Release Notes](https://go.dev/doc/go1.19)”*)

とある。さっそく試してみよう。

まず Windows 環境で拙作の [`gpgpdump.exe`](https://github.com/goark/gpgpdump "goark/gpgpdump: OpenPGP packet visualizer") コマンドを PATH で指定されたフォルダ以外，具体的には以下のソースファイルと同じフォルダに置く。

```go:sample.go
package main

import (
    "fmt"
    "os/exec"
)

func main() {
    cmd := "gpgpdump.exe"
    out, err := exec.Command(cmd, "version").CombinedOutput()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("output by %v:\n%v\n", cmd, string(out))
}
```

これを [Go] 1.19 コンパイル環境下で実行すると

```
> go run sample.go
exec: "gpgpdump.exe": cannot run executable found relative to current directory
```

「カレントディレクトリに指定の実行ファイルあるけど起動しちゃらん（←超意訳，出雲弁）」とエラーになった。

Windows ではパス指定なしでコマンドを起動する際に，カレントフォルダに同名の実行ファイルが存在すると優先的にそれを起動してしまう。[Go] 標準の [`os/exec`][`exec`] パッケージもこの挙動に合わせていたのだが，2020年の [CVE-2020-27955] で問題になった。この挙動を悪用して悪意のコマンドを実行される可能性があるというわけだ。

この脆弱性を回避するために，様々な試行錯誤が行われたが [Go] 1.19 の改修が決定打になるだろう。カレントフォルダにある同名の実行ファイルを無視するのではなく，エラーとして「起動させない」というのがポイント。

なお，今まで通りパスなしのコマンド指定時にカレントフォルダの実行ファイルを起動したいなら `exec.ErrDot` エラーを明示的に潰すことで実現できる。こんな感じ。

```go:sample2.go
package main

import (
    "errors"
    "fmt"
    "os/exec"
)

func main() {
    cmd := exec.Command("gpgpdump.exe", "version")
    if cmd.Err != nil {
        fmt.Println(cmd.Err)
        if !errors.Is(cmd.Err, exec.ErrDot) {
            return
        }
        cmd.Err = nil
    }
    out, err := cmd.CombinedOutput()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("output by %v:\n%v\n", cmd, string(out))
}
```

これを実行すると

```
> go run sample2.go
exec: "gpgpdump.exe": cannot run executable found relative to current directory
output by .\gpgpdump.exe version:
gpgpdump v0.14.0
repository: https://github.com/goark/gpgpdump
```

となる。エラーを無視してカレントディレクトリ `.` を付加した状態で実行されているのがお分かりだろうか。

ちなみに，同じコードを Windows 以外の環境で実行すると（`.exe` の拡張子は外してね）

```
$ go run sample2b.go 
exec: "gpgpdump": executable file not found in $PATH
```

と PATH 上に実行ファイルが見つからない旨の普通のエラーが表示される。これでアプリケーション側は OS ごとに処理を分ける必要がなくなったわけだ。めでたい！

ところで Windows には `NoDefaultCurrentDirectoryInExePath` なる環境変数があるそうで，これが有効になっているとパスなしのコマンド指定時にカレントフォルダの同名実行ファイルを無視するらしい。で， [`os/exec`][`exec`] パッケージは律儀にこの環境変数にも対応している。

> On Windows, `Command` and `LookPath` now respect the `NoDefaultCurrentDirectoryInExePath` environment variable, making it possible to disable the default implicit search of “`.`” in PATH lookups on Windows systems.
*(via “[Go 1.19 Release Notes](https://go.dev/doc/go1.19)”*)

少々姑息ではあるが，これを利用してカレントフォルダの同名実行ファイルを無視するよう構成することもできる。こんな感じ。

```go:sample3.go
package main

import (
    "errors"
    "fmt"
    "os"
    "os/exec"
)

func main() {
    os.Setenv("NoDefaultCurrentDirectoryInExePath", "1")
    cmd := exec.Command("gpgpdump.exe", "version")
    if cmd.Err != nil {
        fmt.Println(cmd.Err)
        if !errors.Is(cmd.Err, exec.ErrDot) {
            return
        }
        cmd.Err = nil
    }
    out, err := cmd.CombinedOutput()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("output by %v:\n%v\n", cmd, string(out))
}
```

これを実行すると

```
> go run sample3.go
exec: "gpgpdump.exe": executable file not found in %PATH%
```

となる。前のコードの実行結果で出力されるエラーメッセージの違いを確かめてほしい。Windows 以外でこの環境変数が悪さをすることはないだろうから Linux 等と挙動を合わせたいなら，おまじない的にセットしておくのもいいかもしれない。

やっぱ Windows は面倒くさいな（笑）

[Go]: https://go.dev/
[`exec`]: https://pkg.go.dev/os/exec "exec package - os/exec - Go Packages"
[CVE-2020-27955]: https://nvd.nist.gov/vuln/detail/CVE-2020-27955