msysとGnuWin32を併用したらsedでハマった

# 問題

[zlib](https://zlib.net/) 1.2.11 (tar.gz版)をmsysのbashでビルドしようとし、`configure`を走らせたら`sed`が謎のエラーを吐いた。
出たエラーは、

```
sed.exe: -e 表現 #1, 文字数 1: 未知のコマンドです: 「C」
sed.exe: -e 表現 #1, 文字数 8: アドレスregexが終了していません
```

の2種類。同様のエラーは他のソフトウェアの`configure`でも見られ、以前から気になっていた。

![zlib-configure-error-20180728.png](https://qiita-image-store.s3.amazonaws.com/0/99656/59829ff8-ffcf-5f51-18d9-cd6ba9393d61.png)

# 渡されているコマンドライン引数の調査

エラーメッセージが主張する「未知のコマンド」とはどういうことなのだろうか？
これを調べるため、`configure`中の`sed`を`./xyz`に置換することで、以下のコマンドライン引数を出力するプログラムを実行するようにし、実行してみた。
なお、`configure`の動作への影響を減らすため、標準出力ではなく標準エラー出力に出すようにしている。

```c:xyz.c
#include <stdio.h>

int main(int argc, char* argv[]) {
	int i;
	for (i = 0; i < argc; i++) {
		fprintf(stderr, "argv[%d] = %s\n", i, argv[i]);
	}
	return 0;
}
```

すると、以下のような出力(冒頭部分)になった。

![zlib-sed-command-20180728.png](https://qiita-image-store.s3.amazonaws.com/0/99656/3f512889-2ce3-6233-b772-299b852a345c.png)

zlibの`configure`中でこの部分に相当すると考えられるのは、42行目からの

```:configure(抜粋)
# extract zlib version numbers from zlib.h
VER=`sed -n -e '/VERSION "/s/.*"\(.*\)".*/\1/p' < ${SRCDIR}zlib.h`
VER3=`sed -n -e '/VERSION "/s/.*"\([0-9]*\\.[0-9]*\\.[0-9]*\).*/\1/p' < ${SRCDIR}zlib.h`
VER2=`sed -n -e '/VERSION "/s/.*"\([0-9]*\\.[0-9]*\)\\..*/\1/p' < ${SRCDIR}zlib.h`
VER1=`sed -n -e '/VERSION "/s/.*"\([0-9]*\)\\..*/\1/p' < ${SRCDIR}zlib.h`
```

であり、なぜか引数に`C:/MyApps/MinGW/msys/1.0`という余計なプリフィックスがついてしまっていることがわかった。
すなわち、なんとかしてこのプリフィックスが付かない状態の引数を`sed`に渡せれば、「未知のコマンドです」エラーは潰せそうである。

# プリフィックス対策のラッパの作成

余計なプリフィックスにより`sed`の動作が妨害されるのをさけるため、ラッパを開発した。
まずは小さいケースで試し、`sed`のかわりに先ほどの引数表示プログラム`xyz`に引数を渡す。
ラッパを使わないと、このように余計なプリフィックスがついてしまった。

```
bash-3.1$ xyz "/abc/i def"
argv[0] = c:\Temp\xyz.exe
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i def
```

## C言語での作成(失敗)

```c:wrapper.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
	static const char* TARGET = "xyz.exe";
	static const char* TRASH = "C:/MyApps/MinGW/msys/1.0";
	const char** args = malloc(sizeof(*args) * (argc + 1));
	int i;
	size_t tlen = strlen(TRASH);
	if (args == NULL) {
		perror("wrapper: malloc");
		return 1;
	}
	args[0] = TARGET;
	for (i = 1; i < argc; i++) {
		args[i] = argv[i];
		if (strncmp(args[i], TRASH, tlen) == 0) args[i] += tlen;
	}
	args[argc] = NULL;
	execvp(args[0], args);
	perror("execvp");
	return 1;
}
```

渡されたコマンドライン引数中に例のプリフィックスがあれば、削除するという削除するプログラムである。
このプログラムを用いると、以下のような実行結果になった。

```
bash-3.1$ wrapper "/abc/i def"
bash-3.1$ argv[0] = xyz.exe
argv[1] = /abc/i
argv[2] = def

```

プリフィックスが渡されるのは回避できているが、スペースを含む引数が別々の引数に分解されてしまっており、別の問題を引き起こすことが予想できる。
`execvp`ではなく`CreateProcess`でプログラムを起動することも考えたが、引数を適切にエスケープして渡すのはめんどくさそうである。
この方法は諦めた。

## Perlでの作成

まずは仕様を確かめるため、以下のスクリプトを実行してみる。

```perl:perl_test.pl
#!/usr/bin/perl

use strict;
use warnings;

my @prg = ("xyz");

for (my $i = 0; $i < @ARGV; $i++) {
	printf STDERR "Perl: ARGV[%d] = %s\n", $i, $ARGV[$i];
	push(@prg, $ARGV[$i]);
}

exec { $prg[0] } @prg;
die "exec falied\n";
```

実行結果は、以下のようになった。

```
bash-3.1$ perl_test.pl "/abc/i def"
Perl: ARGV[0] = /abc/i def
argv[0] = c:\Temp\xyz.exe
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i def
```

このことから、Perlが起動されるときにはプリフィックスがつかないが、Perlからexecする時にはプリフィックスがついてしまうようである。
そこで、「`/`で始まらない引数にはプリフィックスがつかないようである」という性質を利用し、`/`で始まる引数の最初に`sed`の動作に影響を与えなそうな空白を追加するラッパを作成した。

```perl:perl_wrapper.pl
#!/usr/bin/perl

use strict;
use warnings;

my @prg = ("xyz");

for (my $i = 0; $i < @ARGV; $i++) {
	my $arg = $ARGV[$i];
	if (substr($arg, 0, 1) eq "/") { $arg = " " . $arg; }
	push(@prg, $arg);
}

exec { $prg[0] } @prg;
die "exec falied\n";
```

実行結果は

```
bash-3.1$ perl_wrapper.pl "/abc/i def"
argv[0] = c:\Temp\xyz.exe
argv[1] =  /abc/i def
```

となり、うまくいきそうである。

## Rubyでの作成

Perlと同様に、まずは仕様を確かめる。

```ruby:ruby_test.rb
#!ruby

ARGV.each_with_index do |arg, i|
  warn "Ruby: ARGV[#{i}] = #{arg}"
end

exec(*(["xyz"] + ARGV))
```

(参考：[Rubyでコマンドライン引数を取得する方法：ARGV | UX MILK](https://uxmilk.jp/12947))
実行結果は

```
bash-3.1$ ruby_test.rb "/abc/i def"
Ruby: ARGV[0] = C:/MyApps/MinGW/msys/1.0/abc/i def
argv[0] = xyz
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i def
```

となり、Perlの時と違ってRubyが起動される時にすでにプリフィックスがついてしまっている。
また、

```ruby:ruby_test2/rb
#!ruby

exec(*["xyz", "/abc/i def"])
```

というプログラムを実行すると

```
bash-3.1$ ruby_test2.rb
argv[0] = xyz
argv[1] = /abc/i def
```

となり、Rubyからexecする時にはプリフィックスはつかないようであった。

そこで、付けられたプリフィックスを外して起動するラッパを作成した。

```ruby:ruby_wrapper.rb
#!ruby

trash = 'C:/MyApps/MinGW/msys/1.0'
tlen = trash.length

prg = ['xyz']

ARGV.each do |param_raw|
  param = param_raw
  if param[0, tlen] == trash then param = param[tlen, param.length] end
  prg << param
end

exec(*prg)
```

実行結果は

```
bash-3.1$ ruby_wrapper.rb "/abc/i def"
argv[0] = xyz
argv[1] = /abc/i def
```

となり、うまくいっているようである。

# もうひとつのエラー

ラッパーをテスト用の`xyz`ではなくこの環境の`sed`をフルパスで起動するように設定し、`sed`にリネームし、このラッパー「`sed`」があるディレクトリを`PATH`の先頭に追加した状態でzlibの`configure`を起動した。
(フルパスで指定しないと、本来の`sed`ではなくラッパーを起動しようとしてしまい、うまくいかないことが考えられる)
その結果、「未知のコマンドです」は出なくなったが、「アドレスregexが終了していません」は出てしまい、`Makefile`が空で正常なビルドができない状態であった。
調べてみると、先頭にスラッシュはないが、スラッシュなどの記号を含む長い引数が、プリフィックスが付くタイミングで途中で切られてしまうようであった。
プリフィックスが付くだけなら消せばいいが、消えてしまった情報を復元するのは不可能であると考えられる。
さて、その対処法は…

# 合体技だ！

ここで、先ほど調査したPerlとRubyの性質を思い出す。

* Perlは起動される時は攻撃されないが、execする時に攻撃されてしまう
* Rubyは起動される時に攻撃されてしまうが、execする時は攻撃されない

(ここで「攻撃」とは、これまで紹介してきたプリフィックスを付けたり途中で切ったりする変換を指す)
ということで、

```
bash
↓起動 (攻撃されない)
Perl：コマンドライン引数を攻撃に耐えられる形式に変換する
↓起動 (攻撃されるけど対策したので平気)
Ruby：コマンドライン引数をデコードする
↓起動 (攻撃されない)
sed
```

とすれば、攻撃の影響を避けて`sed`を起動することができるでしょう。
では、「攻撃に耐えられる形式」とは具体的にどんな形式でしょうか？
スラッシュなどの記号があると攻撃されやすそうな気がしたので、文字を文字コードで16進数に変換してみた。

```perl:sed_launcher.pl
#!/usr/bin/perl

use strict;
use warnings;

my $TARGET = "C:\\MyApps\\GnuWin32\\bin\\sed.exe";

my $RUBY_SCRIPT = <<EOS;
exec(*ARGV.map do |param|
  param.each_char.each_slice(2).map { |a| a.join.hex.chr }.join
end)
EOS

my @prg = ("ruby", "-e", $RUBY_SCRIPT, &encode_arg($TARGET));

for (my $i = 0; $i < @ARGV; $i++) {
	push(@prg, &encode_arg($ARGV[$i]));
}

exec { $prg[0] } @prg;
die "exec failed\n";

sub encode_arg {
	my @data = unpack("C*", $_[0]);
	my $ret = "";
	for (my $i = 0; $i < @data; $i++) {
		$ret .= sprintf("%02X", $data[$i]);
	}
	return $ret;
}
```

(参考：[Rubyで文字列をn文字ごとに区切って配列に格納する - ぬいぐるみライフ？](https://mickey24.hatenablog.com/entry/20110226/ruby_slice_string))
このスクリプトを先ほどのラッパーと同様に`PATH`の先頭のディレクトリに`sed`という名前で置いてzlibの`configure`を実行すると、無事エラーが出ず終了した。
続いて`make`を行った結果、共有ライブラリのビルドで`cannot find -lc`というエラーが出てしまったが、`libz.a`は生成されており、それを用いてアプリケーションをビルドすることもできた。

# 考察

そもそも、どうしてPerlとRubyで攻撃のタイミングに差が出たのか。
それは、一般に「msys内のプログラムからmsys外のプログラムを実行する時に変換をかける」からであると考えられる。
変換内容から察するに、そもそもこのプリフィックスを付ける動作は本来「攻撃」ではなく、互換性のために「ルートディレクトリ」からの絶対パスを実際のファイルシステム上のパスに変換する処理であると考えられる。それが、スラッシュで始まる`sed`の変換コマンドに対しても誤爆してしまったのだろう。
(ただし、長い引数が切られる仕様については、明らかに情報を破壊しており、これでは説明が付かない)

この環境におけるPerlとRubyの実体を調べると、

```
bash-3.1$ where perl
c:\MyApps\Perl64\bin\perl.exe
C:\MyApps\MinGW\msys\1.0\bin\perl.exe
bash-3.1$ where ruby
c:\MyApps\ruby-2.2.2-x64-mingw32\bin\ruby.exe
```

となった。すなわち、Perlはmsys内のものとmsys外のものがあり、Rubyはmsys外のみである。
bashからmsys内のPerlを呼び出す時は変換がかからず、msys内のPerlからmsys外のRuby、sed、テストプログラムを呼び出す時は変換がかかるということだろう、また、msys外のプログラムからmsys外のプログラムを実行するときも、当然変換はかからないと考えられる。

それでは、`PATH`を見るとmsys外のPerlの方がmsys内のPerlより先に来ているのに、なぜmsys内のPerlが使われたか。
それは、bashからPerlスクリプトを直接実行したため、スクリプト先頭の`#!/usr/bin/perl`によりmsys内のPerlが選択されたからであると考えられる。
実際、`perl`コマンドを用いてテストスクリプトを実行するとPerlが呼び出される時に変換がかかり、`/usr/bin/perl`コマンドを用いてテストスクリプトを実行するとPerlが呼び出される時には変換がかからないことが確認できた。

```
bash-3.1$ perl ./perl_test.pl "/abc/i def"
Perl: ARGV[0] = C:/MyApps/MinGW/msys/1.0/abc/i def
bash-3.1$ argv[0] = xyz
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i
argv[2] = def

bash-3.1$ /usr/bin/perl ./perl_test.pl "/abc/i def"
Perl: ARGV[0] = /abc/i def
argv[0] = c:\Temp\xyz.exe
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i def
```

Rubyについてはmsys外のものしかなく、`#!/usr/bin/ruby`と書くと`/usr/bin/ruby^M: bad interpreter: No such file or directory`というエラーになったため、先頭は`#!ruby`とした。

ついでに、msys内の`sed`を明示的に実行すると、ラッパを使わなくても「未知のコマンドです」エラーが出ずに正常に処理がされることが確認できた。

```
bash-3.1$ where sed
c:\MyApps\GnuWin32\bin\sed.exe
c:\MyApps\w32tex_20150125\bin\sed.exe
C:\MyApps\MinGW\msys\1.0\bin\sed.exe
bash-3.1$ echo abc | sed "/abc/i def"
sed.exe: -e 表現 #1, 文字数 1: 未知のコマンドです: 「C」
bash-3.1$ echo abc | /c/MyApps/MinGW/msys/1.0/bin/sed "/abc/i def"
def
abc
```

# 結論

`/`から始まる仮想パスをmsys外のプログラムに渡す時にファイルシステム上の実際のパスに変換する機能が`sed`の変換指示に対し誤爆し、ついでになぜか記号が入った長い引数に対し途中で切る破壊工作が行われる問題に対し、msys内のPerlとmsys外のRubyを用いて引数を破壊工作されないと考えられる英数字だけの形式にエンコードしてmsys外に出ることで対処を行った。