プロキシ環境下でGit for Windowsが自動アップデートできないバグを直してもらった話

# 前置き
* 表題の通り、プロキシ環境下でGit for Windowsの自動アップデートがうまく動かない問題([git-for-windows/git#3818](https://github.com/git-for-windows/git/issues/3818))を発見し、修正してもらいました([git-for-windows/build-extra#413](https://github.com/git-for-windows/build-extra/pull/413))
* [この時私が気にしていた](https://github.com/git-for-windows/git/issues/3818#issuecomment-1109997125)、いつから・なぜこの問題が発現したかという点を調査したので、経緯など書いてみようと思いました。

## tl;dr
* 発見した理由は、会社のPCで[^1]自動アップデートが動かなかったため
* バグの原因は[`proxy-lookup`の初版にあった`wprintf`の書式文字列の誤り](https://github.com/git-for-windows/build-extra/blob/c6a824b528d87489061448991aaee8adba4251c6/git-extra/proxy-lookup.c#L82-L85)（`arg`,`proxy`はワイド文字列なので`%s`でなく`%ls`にすべきだった）
* 初版のあとから発現した原因は[MinGW-w64のバージョン8.0.0における仕様変更](https://www.mingw-w64.org/changelog/#v800-2020-09-18)で`USE_MINGW_ANSI_STDIO`が自動で有効になるようになり、`wprintf`の挙動が変わったため
    - [Microsoft独自拡張による`%s`は`printf`なら`char*`だが、`wprintf`なら`wchar_t*`](https://docs.microsoft.com/en-us/cpp/c-runtime-library/format-specification-syntax-printf-and-wprintf-functions?view=msvc-170#size-prefixes-for-printf-and-wprintf-format-type-specifiers)という挙動がなくなった
* IssueとPull Requestを作成し、現在はマージ済み

# バグについて
## 背景
* [Git for Windows](https://gitforwindows.org)は、Windows版のGitとして[Gitの公式サイト](https://git-scm.com)などからダウンロードできるパッケージ
    - MSYS2を使ってGitが動くために必要なPerlやTcl/Tk等のパッケージ群をまとめたもの
* 他のOSと違ってパッケージマネージャやOSによるアップデートがかからないので、自動アップデート機能がついている
* この自動アップデート機能は[curlによって更新チェック・ダウンロードを行うシェルスクリプト](https://github.com/git-for-windows/build-extra/blob/17611b0c93b9e8c294b77fead13774538647bb79/git-extra/git-update-git-for-windows)で実現されている
* curlにOSのプロキシ設定を反映するため、`proxy-lookup`というWindows APIを使用したプログラムでOSにプロキシ設定を問い合わせている

## 発見の経緯
* 会社のPCにGit for Windowsをインストールした際、自動アップデート機能があると気づいた
* 会社ネットワークはプロキシを経由しないとインターネットにアクセスできない環境なので動かないだろうと思って試したら、やはり動かなかった
* セキュリティ上自動アップデートは無効化することにこだわるべきでもないと思い、プロキシの設定をしてみようと考えた[^1]
* 設定方法がわからないのでとりあえずソースを読んでみたところ、`proxy-lookup`によってOSのプロキシ設定を取得していることがわかった
* なぜ動かないのかと思って`proxy-lookup`が何を返しているのか確認してみた
    ```bash
    $ proxy-lookup http://gitforwindows.org/
    p
    ```
    - なんかよくわからんが壊れているということはわかった

## 調査
* `proxy-lookup`のソースを探してダウンロードして読んでみたが問題に気づかなかった[^1]
MSVCでコンパイルしてみても[^2]正しく動いているようだった
    ```bash
    $ proxy-lookup http://gitforwindows.org/
    proxy.example.com:xxxx
    ```
* 気になって自宅に帰ってから調べてみた。Git for Windowsのビルドに使われているという[Git for Windows SDK](https://github.com/git-for-windows/build-extra/releases)をダウンロードしてこれでビルドしてみた
    - MSVCだと起こらないが、Git for Windows SDKでビルドするとこの問題が起こるようだった
* 挙動を見ていると、`proxy.pac`を使って特定ホストにだけ違うプロキシを返させた場合、表示がきちんと変わることが分かった
    * `proxy.pac`
        ```js
        function FindProxyForURL(url, host) {
            if (isInNet(host, "192.168.0.0", "255.255.255.0")) {
              return "PROXY 192.168.0.131:8080";
            }
            return "DIRECT";
        }
        ```
    * 実行結果
        ```bash
        $ proxy-lookup -v http://192.168.0.1/
        URL: h, proxy: 1
        $ proxy-lookup -v http://192.168.1.1/
        URL: h, proxy: 
        ```
    * ソースコード[^3]を見ると、結果の`proxy`はともかくURLは`get_proxy_for_url`にすら関係しない。
    `wprintf`がうまく動いていないことを疑った
        ```c
        int main(int argc, char **argv)
        {
        	LPWSTR *wargv;
        	int i, wargc, verbose = 0;
        
        	wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
        
        	for (i = 1; i < wargc; i++) {
        		LPCWSTR arg = wargv[i], proxy;
        
         		if (!wcscmp(L"--verbose", arg) || !wcscmp(L"-v", arg)) {
        			verbose = 1;
        			continue;
        		}
        
        		proxy = get_proxy_for_url(arg);
        		if (!proxy)
        			proxy = L"";
        
        		if (verbose)
        			wprintf(L"URL: %s, proxy: %s\n", arg, proxy);
        		else
        			wprintf(L"%s\n", proxy);
        	}
        
        	return 0;
        }
        ```
* Git for Windows SDKでデバッグビルドしてステップ実行してみると、wprintfの内部にまで入っていくことができたので、そのまま実行を進めた。すると、[次に示す処理が動いていた](https://sourceforge.net/p/mingw-w64/mingw-w64/ci/2938ef65959f0f6f56070f725a05b9d57a8920ac/tree/mingw-w64-crt/stdio/mingw_pformat.c#l2556)（`...`による省略は引用者による）[^4]
    ```c
    int
    __pformat (int flags, void *dest, int max, const APICHAR *fmt, va_list argv)
    {
      int c;
      // ...
      format_scan: while( (c = *fmt++) != 0 )
      {
        // ...
        if( c == '%' )
        {
          // ...
          __pformat_length_t length = PFORMAT_LENGTH_INT;
          // ...
          while( *fmt )
          {
            switch( c = *fmt++ )
            {
              // ...
              case 's':
                if( (length == PFORMAT_LENGTH_LONG)
                     || (length == PFORMAT_LENGTH_LLONG))
                {
                  // ...
                  __pformat_wcputs( va_arg( argv, wchar_t * ), &stream );
                }
                else
                  /* ... */
                  __pformat_puts( va_arg( argv, char * ), &stream );
                goto format_scan;
    ```
    - この関数は`APICHAR`というマクロを使って`wprintf`と`printf`で共有されているが、
    `length`の指定がついていない`%s`はその両方で（`APICHAR*`ではなく）`char *`として処理されていることが読み取れた
* `%s`の解釈について調べると、[cppreference.com](https://ja.cppreference.com/w/c/io/fwprintf#.E5.BC.95.E6.95.B0)などには`char *`として扱うと書かれていたが、Microsoftは`%s`の意味を`printf`なら`char*`だが、`wprintf`なら`wchar_t*`とする[独自拡張](https://docs.microsoft.com/en-us/cpp/c-runtime-library/format-specification-syntax-printf-and-wprintf-functions?view=msvc-170#type-conversion-specifier)を入れていることが分かった。MinGW-w64では独自拡張を反映していないのだろうと思った。

## 報告
* バグレポートを提出した（[git-for-windows/git#3818](https://github.com/git-for-windows/git/issues/3818)）
    * build-extraの方にPull Requestを出して良いのか説明文がなかったのと、なぜ今まで気づかれなかったのかわからず不安だったのでバグレポートとして出した
* [Pull Requestで出さなかったのはなぜ、と聞かれて](https://github.com/git-for-windows/git/issues/3818#issuecomment-1108799357)、[問題ないからPull Request出して、と言われた](https://github.com/git-for-windows/git/issues/3818#issuecomment-1112530020)ので提出した
    * [メールアドレスの設定をミスった](https://github.com/miimou/build-extra/commit/f56d6a62ffe570f37602fa47150e7c14b4b212da)り、[`-s`をつけ損なった](https://github.com/miimou/build-extra/commit/8dd68a888e68e378166a3c27b3206bae4ff51c1c)り、[PKGBUILDの修正漏れでチェックに怒られた](https://github.com/git-for-windows/build-extra/runs/6224431470?check_suite_focus=true)りと散々だったがなんとか終えた

# 追加の調査
* しかし、問題の分岐の仕方は[2013年](https://sourceforge.net/p/mingw-w64/mingw-w64/ci/9889e63625b20ba51391d9c94464c0e03b4e5510/tree/mingw-w64-crt/stdio/mingw_pformat.c#l2357)から変わっていない
* `proxy-lookup.c`は[2016年](https://github.com/git-for-windows/build-extra/commit/c6a824b528d87489061448991aaee8adba4251c6)に追加されたので、このプログラムが当時は動いていて途中から動かなくなった理由がわからなかった。

* 調べていくと、次のことが分かった
    * この__pformatは実は`wprintf`で常に使われるのではなく[`__USE_MINGW_ANSI_STDIO`というマクロが有効な時だけ](https://sourceforge.net/p/mingw-w64/mingw-w64/ci/75076480da2d899101edba3805db6a27cec3c009/tree/mingw-w64-headers/crt/stdio.h) [使われる](https://sourceforge.net/p/mingw-w64/mingw-w64/ci/75076480da2d899101edba3805db6a27cec3c009/tree/mingw-w64-headers/crt/stdio.h#l1083)、`__mingw_vfwprintf`という関数を通して呼ばれていること
    * この`__USE_MINGW_ANSI_STDIO`の設定条件は[2020年のコミット](https://sourceforge.net/p/mingw-w64/mingw-w64/ci/bfd33f6c0ec5e652cc9911857dd1492ece8d8383/)で[変わった](https://sourceforge.net/p/mingw-w64/mingw-w64/ci/bfd33f6c0ec5e652cc9911857dd1492ece8d8383/tree//mingw-w64-headers/crt/_mingw.h.in?diff=f183d3b82fbf17ca0ec8ea7fdaee3d82f03b66ec)こと
* この変更により、`%s`の動作はMicrosoft独自拡張からISOのものに置き換えられた
* この変更は[MinGW-w64のバージョン8.0.0のリリースノート](https://www.mingw-w64.org/changelog/#v800-2020-09-18)にも載っているように見える

# 結論
* 今回私が気にした「いつどの変更によってこの問題がトリガーされたか」は、上記の通り「[2020年のこのコミット](https://sourceforge.net/p/mingw-w64/mingw-w64/ci/bfd33f6c0ec5e652cc9911857dd1492ece8d8383/)」が答えである

[^1]: 休憩時間に行っています
[^2]: フリーソフトの使用は注意して行うという規則はありますが、Git for Windowsは社内で過去に使用したことがあるので問題ありません
[^3]: このソースコードはパブリックドメインに置かれています
[^4]: この記事の内容に必要な引用と考えています