前置き
- 表題の通り、プロキシ環境下でGit for Windowsの自動アップデートがうまく動かない問題(git-for-windows/git#3818)を発見し、修正してもらいました(git-for-windows/build-extra#413)
- この時私が気にしていた、いつから・なぜこの問題が発現したかという点を調査したので、経緯など書いてみようと思いました。
tl;dr
- 発見した理由は、会社のPCで1自動アップデートが動かなかったため
- バグの原因は
proxy-lookup
の初版にあったwprintf
の書式文字列の誤り(arg
,proxy
はワイド文字列なので%s
でなく%ls
にすべきだった) - 初版のあとから発現した原因はMinGW-w64のバージョン8.0.0における仕様変更で
USE_MINGW_ANSI_STDIO
が自動で有効になるようになり、wprintf
の挙動が変わったため - IssueとPull Requestを作成し、現在はマージ済み
バグについて
背景
-
Git for Windowsは、Windows版のGitとしてGitの公式サイトなどからダウンロードできるパッケージ
- MSYS2を使ってGitが動くために必要なPerlやTcl/Tk等のパッケージ群をまとめたもの
- 他のOSと違ってパッケージマネージャやOSによるアップデートがかからないので、自動アップデート機能がついている
- この自動アップデート機能はcurlによって更新チェック・ダウンロードを行うシェルスクリプトで実現されている
- curlにOSのプロキシ設定を反映するため、
proxy-lookup
というWindows APIを使用したプログラムでOSにプロキシ設定を問い合わせている
発見の経緯
- 会社のPCにGit for Windowsをインストールした際、自動アップデート機能があると気づいた
- 会社ネットワークはプロキシを経由しないとインターネットにアクセスできない環境なので動かないだろうと思って試したら、やはり動かなかった
- セキュリティ上自動アップデートは無効化することにこだわるべきでもないと思い、プロキシの設定をしてみようと考えた1
- 設定方法がわからないのでとりあえずソースを読んでみたところ、
proxy-lookup
によってOSのプロキシ設定を取得していることがわかった - なぜ動かないのかと思って
proxy-lookup
が何を返しているのか確認してみた$ proxy-lookup http://gitforwindows.org/ p
- なんかよくわからんが壊れているということはわかった
調査
-
proxy-lookup
のソースを探してダウンロードして読んでみたが問題に気づかなかった1
MSVCでコンパイルしてみても2正しく動いているようだった$ proxy-lookup http://gitforwindows.org/ proxy.example.com:xxxx
- 気になって自宅に帰ってから調べてみた。Git for Windowsのビルドに使われているというGit for Windows SDKをダウンロードしてこれでビルドしてみた
- MSVCだと起こらないが、Git for Windows SDKでビルドするとこの問題が起こるようだった
- 挙動を見ていると、
proxy.pac
を使って特定ホストにだけ違うプロキシを返させた場合、表示がきちんと変わることが分かった-
proxy.pac
function FindProxyForURL(url, host) { if (isInNet(host, "192.168.0.0", "255.255.255.0")) { return "PROXY 192.168.0.131:8080"; } return "DIRECT"; }
- 実行結果
$ 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
がうまく動いていないことを疑った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の内部にまで入っていくことができたので、そのまま実行を進めた。すると、次に示す処理が動いていた(
...
による省略は引用者による)4int __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などにはchar *
として扱うと書かれていたが、Microsoftは%s
の意味をprintf
ならchar*
だが、wprintf
ならwchar_t*
とする独自拡張を入れていることが分かった。MinGW-w64では独自拡張を反映していないのだろうと思った。
報告
- バグレポートを提出した(git-for-windows/git#3818)
- build-extraの方にPull Requestを出して良いのか説明文がなかったのと、なぜ今まで気づかれなかったのかわからず不安だったのでバグレポートとして出した
-
Pull Requestで出さなかったのはなぜ、と聞かれて、問題ないからPull Request出して、と言われたので提出した
-
メールアドレスの設定をミスったり、
-s
をつけ損なったり、PKGBUILDの修正漏れでチェックに怒られたりと散々だったがなんとか終えた
-
メールアドレスの設定をミスったり、
追加の調査
-
しかし、問題の分岐の仕方は2013年から変わっていない
-
proxy-lookup.c
は2016年に追加されたので、このプログラムが当時は動いていて途中から動かなくなった理由がわからなかった。 -
調べていくと、次のことが分かった
- この__pformatは実は
wprintf
で常に使われるのではなく__USE_MINGW_ANSI_STDIO
というマクロが有効な時だけ 使われる、__mingw_vfwprintf
という関数を通して呼ばれていること - この
__USE_MINGW_ANSI_STDIO
の設定条件は2020年のコミットで変わったこと
- この__pformatは実は
-
この変更により、
%s
の動作はMicrosoft独自拡張からISOのものに置き換えられた -
この変更はMinGW-w64のバージョン8.0.0のリリースノートにも載っているように見える
結論
- 今回私が気にした「いつどの変更によってこの問題がトリガーされたか」は、上記の通り「2020年のこのコミット」が答えである