how to execute the binary in Windows host from the process in WSL2 guest VM

背景

最近久々に視聴者が操作を注入できるFall Guysを配信*1 でやろうとしていたのだが、久々にやろうとしたらbotのコードを一切変えてないのに操作を注入するpowershell scriptがLinux側から実行できなくなる現象が起きた。 結論としてはWindows 10 ビルド19042.782でのバグ(あるいはデフォルト設定の変更?)っぽいことが分かり、Windows Updateを適用したところ解決した。

この記事ではせっかくなのでWSL2 VMからのWindowsホストBinaryの実行について掘り下げたいと思う。

その際に調査した内容はあまり有益ではないので興味ある人だけ読んで欲しい。

調査

まず、問題のWSL2からWindows binaryが実行できない状況について以下の事実が確認できた。ただし実行ログなどは残っていないためあまり信用しないでほしい。

Build 19042.782 環境

  • powershell から wsl コマンドで bash を実行する際
    • /mnt/c/Users/MysticDoll 以下では powershell.exe が実行可能。
    • $HOME 上では powershell.exe を実行した際、以下の現象が起きた
      • ^C がttyから送信できない、またttyの応答が消える(仮想端末のキー入力が無視される)
  • dbus-daemonを起動しそこから起動したgnome-terminalをX転送しWindows上のX serverで利用している際
    • powershell.exe 等Windows側のバイナリを起動しようとした場合 ^C などが効かない(上記 $HOME での実行時と同じ状態)
      • この時 $PWD については関係なく同様の結果が得られた

また、Build 19042.789 においては上記は再現せず、問題なくWindows Binaryの実行が可能であった

WSL2からWindows Binaryを実行できる仕組み

docs.microsoft.com

こちらのドキュメントにWIndows/Linuxの相互運用性として纏められている、こちらの 相互運用性の無効化 の項目を見ると

ユーザーは、ルートとして次のコマンドを実行することで、1 つの WSL セッションに対して Windows ツールを実行する機能を無効にできます。

echo 0 > /proc/sys/fs/binfmt_misc/WSLInterop

Windows バイナリを再び有効にするには、すべての WSL セッションを終了して bash.exe を再実行するか、ルートとして次のコマンドを実行します。

echo 1 > /proc/sys/fs/binfmt_misc/WSLInterop

相互運用の無効化は、WSL セッション間で保持されません。新しいセッションが開始されると、相互運用は再び有効になります。

何やら /proc/sys/fs/binfmt_misc/WSLInterop という procfs のなにかで管理しているらしい。

binfmt_misc

binfmt_miscというのは好きなフォーマットのバイナリを好きなインタプリタで解釈して実行できるようにするためにLinuxに用意されたカーネルの機能らしい www.kernel.org

とりあえずWSL2を起動して当該のbinfmt_miscを見てみると以下の出力が得られる

mysticdoll@Himalayan:~$ cat /proc/sys/fs/binfmt_misc/WSLInterop 
enabled
interpreter /tools/init
flags: F
offset 0
magic 4d5a

とりあえず分かることは

  • 現在有効である
  • /tools/init というインタプリタで評価される
  • F フラグであるということ(他namespace/chroot環境下でも動くためにlazyにbinaryをロードせず、configuration timeにロードする? あまり分かっていない)
  • 対象となるbinaryは 0x4d5a から始まっている *2

ということである。

つまりWindows binaryを実行している本体は /tools/init であり、これがどういうものかというのが知りたいわけである。

/tools/init を探す

「探す」と言っている通り、WSL上で ls /tools/init して見つからないというわけである。よってなんらかの検索をする前に思いつく方法で探すだけ探してみようと思う。

find

古典的なfindをする、当然こんな方法で見つかるとは思っていないが一応やってみることにした。

結果を載せても文字数の無駄なので割愛します

適当なWindows binaryを実行し、 /proc/{pid}/exe を見てみる

とりあえず仮想端末の別のタブで PowerShellを起動してみる。するとそれっぽいのがいるのが分かる。

mysticdoll@Himalayan:~$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   1396   784 ?        Sl   Feb07   0:00 /init
mysticd+  1145  0.0  0.0  10000  5128 pts/2    Ss   16:57   0:00 bash
mysticd+  1156  0.0  0.0    804     4 pts/2    S+   16:57   0:00 /tools/init /mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe

とりあえず /tools/init を使ってるのは 1156 らしいので見てみる

mysticdoll@Himalayan:~$ ls -al /proc/1156/exe 
lrwxrwxrwx 1 mysticdoll mysticdoll 0 Feb  8 16:59 /proc/1156/exe -> /tools/init

どうやらリンク先ファイルはユーザランドからは見れない様子なので諦めるしかないっぽい。

stracebash経由から動作を見る

$ sudo strace -p {pid} -f

で対象となるbashをtraceし、powershell.exeを実行する。trace結果は以下。

[pid  1297] execve("/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe", ["powershell.exe"], 0x55b487b67ae0 /* 25 vars */) = 0
[pid  1297] arch_prctl(ARCH_SET_FS, 0x29c800) = 0
[pid  1297] set_tid_address(0x29c838)   = 1297
[pid  1297] brk(NULL)                   = 0xf55000
[pid  1297] brk(0xf56000)               = 0xf56000
[pid  1297] sched_getaffinity(0, 128, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) = 32
[pid  1297] getpid()                    = 1297
[pid  1297] getcwd("/home/mysticdoll", 4096) = 17
[pid  1297] uname({sysname="Linux", nodename="Himalayan", ...}) = 0
[pid  1297] getcwd("/home/mysticdoll", 4096) = 17
[pid  1297] open("/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_PATH) = 3
[pid  1297] readlink("/proc/self/fd/3", "/mnt/c/WINDOWS/System32/WindowsP"..., 4095) = 61
[pid  1297] fstat(3, {st_mode=S_IFREG|0555, st_size=452608, ...}) = 0
[pid  1297] stat("/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe", {st_mode=S_IFREG|0555, st_size=452608, ...}) = 0
[pid  1297] close(3)                    = 0
[pid  1297] open("/proc/self/mountinfo", O_RDONLY) = 3
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="33 24 8:16 / / rw,relatime - ext"..., iov_len=1024}], 2) = 1024
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="de=755\n46 45 0:28 / /sys/fs/cgro"..., iov_len=1024}], 2) = 1024
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="group cgroup rw,net_prio\n57 45 0"..., iov_len=1024}], 2) = 461
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="", iov_len=1024}], 2) = 0
[pid  1297] close(3)                    = 0
[pid  1297] getcwd("/home/mysticdoll", 4096) = 17
[pid  1297] open("/proc/self/mountinfo", O_RDONLY) = 3
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="33 24 8:16 / / rw,relatime - ext"..., iov_len=1024}], 2) = 1024
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="de=755\n46 45 0:28 / /sys/fs/cgro"..., iov_len=1024}], 2) = 1024
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="group cgroup rw,net_prio\n57 45 0"..., iov_len=1024}], 2) = 461
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="", iov_len=1024}], 2) = 0
[pid  1297] close(3)                    = 0
[pid  1297] ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
[pid  1297] ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
[pid  1297] ioctl(2, TCGETS, {B38400 opost isig icanon echo ...}) = 0
[pid  1297] ioctl(0, TIOCGPGRP, [1297]) = 0
[pid  1297] getpgid(0)                  = 1297
[pid  1297] fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
[pid  1297] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
[pid  1297] fstat(2, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
[pid  1297] ioctl(0, TIOCGWINSZ, {ws_row=55, ws_col=166, ws_xpixel=0, ws_ypixel=0}) = 0
[pid  1297] ioctl(0, SNDCTL_TMR_START or TCSETS, {B38400 -opost -isig -icanon -echo ...}) = 0
[pid  1297] dup(0)                      = 3
[pid  1297] socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0) = 4
[pid  1297] bind(4, {sa_family=AF_VSOCK, sa_data="\0\0\377\377\377\377\377\377\377\377\0\0\0\0"}, 16) = 0
[pid  1297] getsockname(4, {sa_family=AF_VSOCK, sa_data="\0\0\322\22z\211\377\377\377\377\0\0\0\0"}, [16]) = 0
[pid  1297] listen(4, 4)                = 0
[pid  1297] socket(AF_UNIX, SOCK_SEQPACKET, 0) = 5
[pid  1297] connect(5, {sa_family=AF_UNIX, sun_path="/run/WSL/948_interop"}, 110) = 0
[pid  1297] write(5, "\6\0\0\0\356\0\0\0\322\22z\211\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 238) = 238
[pid  1297] accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0]p\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 6
[pid  1297] accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0^p\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 7
[pid  1297] accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0_p\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 8
[pid  1297] accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0`p\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 9
[pid  1297] close(4)                    = 0
[pid  1297] rt_sigprocmask(SIG_BLOCK, [INT WINCH], NULL, 8) = 0
[pid  1297] signalfd4(-1, [INT WINCH], 8, 0) = 4
[pid  1297] poll([{fd=0, events=POLLIN}, {fd=7, events=POLLIN}, {fd=8, events=POLLIN}, {fd=9, events=POLLIN}, {fd=4, events=POLLIN}], 5, -1) = 1 ([{fd=9, revents=POLL
IN}])
[pid  1297] recvfrom(9, "\t\0\0\0 \0\0\0", 8, MSG_WAITALL, NULL, NULL) = 8
[pid  1297] brk(0xf58000)               = 0xf58000
[pid  1297] recvfrom(9, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 24, 0, NULL, NULL) = 24
[pid  1297] poll([{fd=0, events=POLLIN}, {fd=7, events=POLLIN}, {fd=8, events=POLLIN}, {fd=9, events=POLLIN}, {fd=4, events=POLLIN}], 5, -1) = 1 ([{fd=7, revents=POLL
IN}])

このあたりがもの凄く怪しい

[pid  1297] connect(5, {sa_family=AF_UNIX, sun_path="/run/WSL/948_interop"}, 110) = 0

ここから推測するに、socketとして生えている /run/WSL/{id?}_interop を経由してプロセス生成関連のやり取りをしていそう。

このbashExploroer.exe を起動する場合以下のstrace出力が得られる。

[pid  1443] execve("/mnt/c/WINDOWS/Explorer.exe", ["Explorer.exe", "."], 0x55b487b67ae0 /* 25 vars */) = 0
(中略)
[pid  1443] socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0) = 4
[pid  1443] bind(4, {sa_family=AF_VSOCK, sa_data="\0\0\377\377\377\377\377\377\377\377\0\0\0\0"}, 16) = 0
[pid  1443] getsockname(4, {sa_family=AF_VSOCK, sa_data="\0\0\327\22z\211\377\377\377\377\0\0\0\0"}, [16]) = 0
[pid  1443] listen(4, 4)                = 0
[pid  1443] socket(AF_UNIX, SOCK_SEQPACKET, 0) = 5
[pid  1443] connect(5, {sa_family=AF_UNIX, sun_path="/run/WSL/948_interop"}, 110) = 0
[pid  1443] write(5, "\6\0\0\0\254\0\0\0\327\22z\211\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\09\0\0\0Q\0\0\0~\0\0\0\2\0\0\0u\0\0\0L\0000\1\1C:\\WINDOWS\\Explorer.exe\0\\\\wsl$\\Ubuntu-20.04\\home\\mysticdoll\0WSLENV=\0\0Explorer.exe\0.\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 172)
 = 172
(中略)
[pid  1443] exit_group(1)               = ?
[pid  1443] +++ exited with 1 +++

ここからも /run/WSL/948_interop がなにやらプロセスに対して引数他を渡していることが分かる。

また、PowerShellが出力した文字をLinux側に出力している部分を見ると

read(7, "Windows PowerShell\33[63X\r\nCopyright (C) Microsoft Corporation. All rights reserved.\33[24X\r\n\33[81X\r\n\346\226\260\343\201\227\343\201\204\343\202\257\343\203\255\343\202\271\343\203\227\343\203\251\343\203\203\343\203\210\343\203\225\343\202\251\343\203\274\343\203\240\343\20
1\256 PowerShell \343\202\222\343\201\212\350\251\246\343\201\227\343\201\217\343\201\240\343\201\225\343\201\204 https://aka.ms/pscore6", 4096) = 200

とあり、よって fd が7のsocketを探すと

socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0) = 4
(中略)
accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0&q\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 7

となっているため、対話部分の実装については vsock(7) *3を立てることでホストマシン上のプロセスとやりとりしていることが分かった。

まとめ

これらから以下が推測できる

  • WSL2上からのプロセス実行は binfmt_misc によって実現されている
  • binfmt_misc のinterpretor /tools/init は以下の事をしている
    • /run/WSL/{id?}_interop を経由してホストマシンのWindowsプロセスの生成をする
    • vsock(7) を利用してホストマシン上のプロセスのttyをLinuxプロセスに転送している

細かいところは適当だけどひとまずこのあたりで終わりにします。