WASI の標準化: WebAssembly をウェブの外で使うためのシステムインターフェース

今日、私たちが新たな標準化の取り組みを開始したことをお伝えします。その名も WebAssembly system interface、略して WASI です。

Why: 開発者たちはブラウザという枠を超えた WebAssembly の利用を探り始めています。WebAssembly を使えば同じコードを全てのマシンで高速、スケーラブル、安全に実行できるからです。

しかし私たちにはこれを実現するための強固な地盤がありません。ブラウザの外で動くコードにはシステムと対話する仕組み、つまりシステムインターフェースが必要であり、WebAssembly プラットフォームにはこの地盤がまだないのです。

What: WebAssembly は仮想的なマシン1に対するアセンブリ言語であり、特定の物理マシンを想定しているわけではありません。これによって様々なアーキテクチャのマシンにおける実行が可能になっているわけです。

WebAssembly が仮想的なマシンに対するアセンブリ言語であるのとちょうど同じように、WebAssembly に必要となるシステムインターフェースは、単一のオペレーティングに対するシステムインターフェースではなく、仮想的なオペレーティングに対するシステムインターフェースです。これを用意すれば、異なる OS におけるプログラムの実行が実行が可能になります。

この WebAssembly プラットフォームに対するシステムインターフェースこそが、WASI です。

私たちが作ろうとしているのは、WebAssembly の真の伴侶となり、時の試練に耐えるシステムインターフェースです。つまり WebAssembly の重要な原則である、ポータビリティとセキュリティが保たれなければなりません。

Who: WASI の標準化に集中して取り組むための WebAssembly サブグループが設立されます。私たちは関心のあるパートナーを既に集めており、さらなる参加者を募集中です。

私たち、私たちのパートナー、私たちのサポーターが WASI を重要だと思う理由をいくつかあげます:

つまり、これはビッグニュースなのです!🎉

現在三つの WASI の実装があります:

次のビデオで実際に動作する WASI を見ることができます:

新しいシステムインターフェースで予定されている動作についてもっと知りたいのであれば、どうぞ読み進んでください。

システムインターフェースとは何か?

C のような言語を使うとシステムリソースに対して直接アクセスできるようになると多くの人は言いますが、正確に言うとこれは正しくありません。

そのような言語を使ったとしても、ほとんどのシステムにおいてはファイルへ直接アクセスしてファイルを作ったり開いたりできるわけではありません。どうしてでしょう?

その理由は、システムリソース (ファイル、メモリ、ネットワーク接続など) は安定性とセキュリティのためにとても重要だからです。

あるプログラムが意図せずに他のプログラムが持つリソースをめちゃくちゃにしてしまった場合、そのプログラムをクラッシュさせてしまう可能性があります。さらに悪いことに、プログラム (あるいはユーザー) が意図的に他のプログラムのリソースをめちゃくちゃにすることで、機密データの窃盗が可能になってしまいます。

「クラッシュとデータの漏洩」
「クラッシュとデータの漏洩」

そのため、どのプログラムとどのユーザーがどのリソースにアクセスできるのかを制御する仕組みが必要になります。この事実はコンピューター開発の歴史のごく初期に判明しており、このアクセス制御のための仕組みが作られました。それがリングプロテクション (protection ring) を使ったセキュリティです。

リングプロテクションがある場合、オペレーティングシステムは保有するリソース (のほとんど) に保護バリアを張ります。このバリアがカーネル (kernel) です。カーネルだけが新規ファイルの作成したり、ファイルを開いたり、ネットワーク接続を開いたりといった処理を行えます。

ユーザーによって起動されカーネルの外側で実行されるプログラムのことを、私たちはユーザーモード (user mode) と呼びます。ユーザーモードのプログラムがファイルを開いたりするときには、そのプログラムからカーネルにファイルを開くよう要求しなければなりません。

アプリケーション「コンコン」
アプリケーション「コンコン」

ここでシステムコール (system call) という概念が出てきます。プログラムがカーネルに何かして欲しいときには、システムコールを使ってその要求を伝えます。システムコールを受けてカーネルはファイルを開く処理を行いますが、その際に要求を出しているユーザーを調べ、そのユーザーがアクセスを持っているかどうかを確認できます。

ほとんどのデバイスにおいて、あなたが書くコードからシステムのリソースにアクセスする方法はこのシステムコールを使ったものしかありません。

アプリケーション「このデータを 3 番のファイルに書き込んでもらえませんか?」
アプリケーション「このデータを 3 番のファイルに書き込んでもらえませんか?」

システムコールを提供するのはオペレーティングシステムです。オペレーティングシステムごとに独自のシステムコールが存在するということは、オペレーティングシステムごとに異なるコードを書かなくてはいけないのでしょうか? 幸いにも、その必要はありません。

この問題を解決する鍵は、抽象化です。

たいていの言語には標準ライブラリがあるので、コーディングをしている間、プログラマーはターゲットのシステムを気にする必要はありません。インターフェースを使うだけで済みます。

そしてコンパイルのときに、コンパイラのツールチェインがシステムコールのインターフェースの実装をターゲットのシステムに応じて選択します。この実装はオペレーティングシステム の API に含まれる関数を使っているために、システムごとに固有となります。

システムインターフェースが登場するのはここです。例えば printf が Windows マシンをターゲットとしてコンパイルされるときには実際のマシンとのやり取りに Windows API を利用でき、Mac や Linux がターゲットのときには POSIX API を利用できます。

左: POSIX API を使った実装。 右: Windows API を使った実装。
左: POSIX API を使った実装。 右: Windows API を使った実装。

しかし WebAssembly にとっては、これが問題となります。

WebAssembly を使った場合には、ターゲットとなるオペレーティングシステムがコンパイル時に決定しません。そのため WebAssembly 内部の標準ライブラリの実装では、単一の OS のシステムインターフェースを使うことができません。

赤字「ターゲットのシステムが分からないとしたら、どのシステムインターフェースを使えばいいのだろう?」
赤字「ターゲットのシステムが分からないとしたら、どのシステムインターフェースを使えばいいのだろう?」

WebAssembly が仮想的なマシンに対するアセンブリ言語であり、現実のマシンに対するものではないことを前にお話ししました。これと同じように、WebAssembly に必要なのは仮想的なオペレーティングシステムに対するシステムインターフェースであり、現実のオペレーティングシステムに対するものではありません。

しかし WebAssembly をブラウザの外で動かすランタイムというのは既に存在しています。今説明したシステムインターフェースはまだ存在していないにもかかわらず、どうやってこれを実現しているのでしょうか? まずはこれを見ていきましょう。

ブラウザ外の WebAssembly は現在どうなっている?

WebAssembly の作成をサポートした最初のツールは Emscripten でした。Emscripten は POSIX という OS システムインターフェースをウェブ上でエミュレートしており、プログラマーは C の標準ライブラリ (libc) の関数を利用できます。

libc をエミュレートするために、Emscripten は独自に libc を実装しています。この実装は二つの部分からなっており、一つは WebAssembly モジュールにコンパイルされ、もう一つは JS グルーコード (glue code) として実装されます。このグルーコードがブラウザと対話し、ブラウザが OS との対話を行います。

初期の WebAssembly はほとんどが Emscripten を使ってコンパイルされていました。そのため WebAssembly をブラウザ外で実行しようとなったときには、Emscripten でコンパイルされたコードを実行する所から作業が開始されました。

その結果、ランタイムは Emscripten の JS グルーコードに含まれる全ての関数に対する独自の実装を作ることになりました。

しかしここで問題が生じます。JS グルーコードによって提供されるインターフェースというのは標準となるように設計されておらず、それどころかパブリックに公開されるインターフェースとしてすら設計されていません。このグルーコードが解決しているのは、私たちの考えている問題ではありません。

例として、パブリックなインターフェースとして設計された API では read のような名前が付くであろう関数を考えます。Emscripten の JS グルーコードでは、この関数に _system3(which, varargs*) という名前が付いています。

灰字「同じ値 3 が必ず渡される」
灰字「同じ値 3 が必ず渡される」

最初のパラメータは整数であり、名前についているの番号と同じです (つまり、この例では 3 です)。

二つ目のパラメータ varargs がシステムコールに渡される引数です。これが varargs と呼ばれているのは、個数が可変 (VARiable) だからです。WebAssembly には関数に引数の数を伝える仕組みが存在しないので、これらの引数はメモリ上に一列に並んだ状態で渡されます。これは型安全で無いばかりか、引数をレジスタに入れて渡せる場合には実効速度が落ちます。

ブラウザで動作する Emscripten であればこれで何の問題もありません。しかし問題は、 WebAssembly ランタイムがこの動作をデファクトスタンダードとみなし、JS グルーコードを独自に実装していることです。ランタイムは POSIX のエミュレーションレイヤーの内部の詳細をさらにエミュレートしています。

つまり、Emscripten に課された制約があるときに限って意味を持つ選択 (引数をヒープ値で渡すなど) を、その制約が及ばないような環境でわざわざ受け入れているのです。

左の人物「『これで十分』ってことにしない?」右の人物 「おー、それは思いつかなかった...」
左の人物「『これで十分』ってことにしない?」右の人物 「おー、それは思いつかなかった...」

数十年に渡って動作する WebAssembly のエコシステムを作っているのですから、その基盤は盤石なものでなくてはなりません。デファクトスタンダードがエミュレーションのエミュレーションというのはあり得ません。

ではどのような原則 (principle) を採用すればよいのでしょうか?

WASI が採用すべき原則は?

WebAssembly が当初から採用している重要な原則が二つあります。

ブラウザを超えた利用においても、この二つの原則が守られなければなりません。

POSIX と Unix が採用した、アクセス制御というセキュリティへのアプローチはここでは上手く行きません。どこが上手く行かないのかを見てみましょう。

ポータビリティ

POSIX に沿ったソースコードはポータブルになります。同じソースコードを異なるマシンをターゲットとする libc を使ってコンパイルでき、そのとき libc のバージョンが異なっていても構いません。

しかし WebAssembly はこれよりも先に行かなければなりません。つまり一度だけのコンパイルで全てのマシンで実行可能になるような、ポータブルなバイナリが必要です。

このポータビリティがあれば、ユーザーへのコードの配布がこれまでよりはるかに簡単になります。

例えば Node のネイティブモジュールが WebAssembly で書かれていれば、ネイティブモジュールを使うアプリをインストールするときに node-gyp を実行する必要がなくなります。また開発者は何十個ものバイナリを設定・配布することから解放されます。

セキュリティ

コードがオペレーティングシステムに何かを書き込んだり読み込んだりするよう要請するとき、OS はコードからの要請が安全かどうかを判定しなければなりません。

このような場合にオペレーティングシステムが行う典型的な処理は、所有者と所有グループに基づいたアクセス制御です。

例えばあるプログラムが OS にファイルを開くよう要請したとします。ユーザーにはアクセスを持つファイルの集合があらかじめ定まっています。

ユーザーがプログラムを起動すると、プログラムはそのユーザーの代理として実行されます。ユーザーがあるファイルへのアクセスを持つなら (つまりそのファイルの所有者であるか、アクセスを持つグループに属していれば)、起動されたプログラムもそのファイルに対して同じアクセスを持ちます。

アプリケーション「アリスというユーザーによって実行されているのですが、このパスにあるディレクトリを開いてもらえませんか?」カーネル「アリス? 了解!」
アプリケーション「アリスというユーザーによって実行されているのですが、このパスにあるディレクトリを開いてもらえませんか?」カーネル「アリス? 了解!」

これによりユーザーがお互いに保護されます。初期のオペレーティングシステムが開発されていたころにはこれで万事上手く行きました。当時のシステムはマルチユーザーであることが多く、管理者がインストールされるソフトウェアを管理していました。そのため最大の脅威はあなたのファイルを覗き見る他のユーザーだったわけです。

しかし状況は変わりました。現代のシステムでは通常ユーザーは一人だけであり、その代わり信用度がはっきりしないサードパーティのコードが大量に実行されます。現在のオペレーティングシステムにおける最大の脅威は、あなた自身によって実行されるコードがあなたに向かって牙を剥くことです。

例えば、あなたのアプリケーションが使っているライブラリのメンテナが新しくなったとします (オープンソースではよくあることです)。新しいメンテナは心からプロジェクトに貢献しようとしている人物かもしれません...しかしひょっとすると悪いヤツかも知れません。もしあなたがシステムで自由な処理を行う (例えばファイルを開いてネットワーク越しにどこかに送る) ためのアクセスを持っていたとすると、前述のアクセス制御を利用した場合、このコードはシステムに深刻なダメージを与えられます。

悪いアプリケーション「ボブによって実行されているんだが、彼のビットコインのウォレットを開いてくれないか?」カーネル「ボブ? お任せあれ!」悪いアプリケーション「素晴らしい! それじゃあネットワーク接続を...」
悪いアプリケーション「ボブによって実行されているんだが、彼のビットコインのウォレットを開いてくれないか?」カーネル「ボブ? お任せあれ!」悪いアプリケーション「素晴らしい! それじゃあネットワーク接続を...」

このため、システムと直接対話できるサードパーティのライブラリは危険です。

WebAssembly のセキュリティの仕組みはこれとは異なります。WebAssembly はサンドボックスで実行されます。

つまりコードは OS と直接対話ができないのですが、ならどうやってシステムのリソースを利用するかというと、ホスト (ブラウザもしくは wasm ランタイム) がサンドボックスの中で利用するための関数を用意します。

つまりホストはプログラムごとに何ができるかを制限できます。WebAssembly ではプログラムがユーザーの代理として実行されることはなく、任意のシステムコールがユーザーと同一の権限で実行されることもありません。

サンドボックスのメカニズムがあればシステムが完全にセキュアになるのかと言えばそうではありません。サンドボックスに全ての機能を入れることも可能であり、そうしてしまえばもう打つ手はありません。ただ少なくともサンドボックスによってホストの側からシステムをセキュアにすることが可能になります。

WebAssembly「ほらよ。OS と対話するときには、この安全なおもちゃを使うんだ」悪いアプリケーション「ああクソ...私のネットワーク接続はどこだ!?」
WebAssembly「ほらよ。OS と対話するときには、この安全なおもちゃを使うんだ」悪いアプリケーション「ああクソ...私のネットワーク接続はどこだ!?」

私たちが設計するシステムインターフェースにおいては、以上の二つの原則が保たれる必要があります。ポータビリティがあればソフトウェアの開発と配布が簡単になり、セキュリティのためのツールはホスト自身とそのユーザーを守るために絶対に必要です。

WASI はどうなるべき?

二つの原則が定まりましたが、さて WebAssembly システムインターフェースの設計はどうあるべきでしょうか?

これこそが標準化のプロセスを通じて私たちが見つけ出そうとしているものです。一応、私たちからの提案があります:

上「標準をモジュールの集合として作る」 下「 <code>wasi-core</code> から始める」
上「標準をモジュールの集合として作る」 下「 wasi-core から始める」

wasi-core には何が入る?

wasi-core には全てのプログラムが必要とする基礎が含まれます。ファイル、ネットワーク接続、クロック、乱数といった POSIX のかなりの部分を含むことになるでしょう。

そしてその中身も POSIX とよく似たものになるはずです。例えば POSIX のファイルを中心とした考え方が取り入れられる予定です。ファイルに関する read, close, write といったシステムコールが与えられ、その他全てはファイルを拡張したものになるでしょう。

ただし wasi-core は POSIX を完全には含まない予定です。例えば POSIX のプロセスという概念にちょうど対応するものは WebAssembly に存在しません。さらに言えば、全ての WebAssembly エンジンが fork のようなプロセスに関する操作をサポートしなければならないというのは変な話です (ただし私たちは fork の標準化も行いたいと考えています)。

モジュールを使ったアプローチがここで役に立ちます。モジュール化しておくことで、広い部分を標準化しつつ、WASI の一部しか使わないニッチなプラットフォームも存在できるようになります。

Rust のような言語では wasi-core を標準ライブラリから直接使用可能になる予定です。例えば Rust の open を WebAssembly へとコンパイルすると __wasi_path_open の呼び出しとして処理されます。

C と C++ に対しては、 wasi-core の関数を使って libc を実装した wasi-sysroot が用意されています。

Clang などのコンパイラは WASI API を通じたインターフェースをサポートし、Rust コンパイラや Emscripten のような完全なツールチェインは WASI をシステム実装の一部として利用することを期待しています。

ユーザーのコードはどう WASI 関数を呼ぶ?

コードを実行しているランタイムが wasi-core の関数を呼ぶときには、関数をインポートする処理が最初に行われます。

赤字「ランタイムがモジュールをインスタンス化するために使うコード...そしてそのコードが実際に行うこと」
赤字「ランタイムがモジュールをインスタンス化するために使うコード...そしてそのコードが実際に行うこと」

これによってプラットフォームごとに独自の wasi-core 実装を作れるようになり、ポータビリティが向上します。ここで言うプラットフォームとは、Mozilla の wasmtime や Fastly の Lucet などの WebAssembly ランタイム、Node、あるいはブラウザだって指します。

またこのメカニズムによってサンドボックス化も可能になります。ホストがどの wasi-core 関数を渡すか ──つまり、どのシステムコールを許可するか── をプログラムごとに選べるようになるためです。これによってセキュリティが保たれます。

WASI にはセキュリティをさらに堅牢にする仕組みがあり、capability-based security の一種が使われています。

伝統的な方法では、ファイルを開くときにはそのファイルのパスを表す文字列を引数として open を呼びます。OS はそれを受けてコードが (つまりプログラムを起動したユーザーが) 権限を持っているかどうかを調べます。

一方 WASI がファイルにアクセスする関数を読ぶときには、ファイルディスクリプタを渡します。このファイルディスクリプタにはアクセス権限に関する情報が付いており、ファイルそのものあるいは含まれるディレクトリに対するアクセス権限を表します。

このようにすることで、突然 /etc/passwd にアクセスするコードは書けなくなります。コードが操作できるのは渡されたディレクトリだけだからです。

赤字「与えられたディレクトリでだけ行動できる」
赤字「与えられたディレクトリでだけ行動できる」

この機能により、より多いシステムコールへのアクセスをサンドボックスのコードに対して安全に許可できるようになります。システムコールを使用を許可した上でできることを制限できるからです。

この制限はモジュールごとに設定できます。デフォルトではモジュールはファイルディスクリプタに一切アクセスできません。しかしあるモジュールがファイルディスクリプタを持つなら、そのファイルディスクリプタを関数に渡すことができ、そのときファイルに対する制限をきつくすることもできます。

つまりアプリケーションが使うファイルディスクリプタはトップレベルのコードでランタイムから取得され、そのファイルディスクリプタが必要に応じてシステム中を駆け巡るということです。

WebAssembly「君が使っていいディレクトリはこれだ。読み込み、書き込み、実行権限があるぞ」アプリケーション「じゃあ君には読み込み権限を付けたこのファイルを渡そう」
WebAssembly「君が使っていいディレクトリはこれだ。読み込み、書き込み、実行権限があるぞ」アプリケーション「じゃあ君には読み込み権限を付けたこのファイルを渡そう」

これによって WebAssembly のモジュールは処理に必要なモジュールだけにアクセスするようになり、最小権限の原則に近づきます。

以上のコンセプトは CloudABI や Capsicum などの capability-oriented なシステムを参考にしています。capability-oriented システムはコードの移植が難しいことが多いのですが、これも解決できると私たちは考えています。

コードが相対ファイルパスと共に openat を使っている場合には、コードをコンパイルするだけで capability-based security が有効になります。

コードが open を使っていて、 openat を使うスタイルに切り替えるだけのリソースが割けない場合でも、WASI にはインクリメンタルな解決法が用意されています。libpreopen を使うとアプリケーションが合法的にアクセスできるディレクトリを制限でき、 open を使いつつパスを制限できます。

次は?

wasi-core は良いスタートであると考えています。WebAssembly のポータビリティとセキュリティを保ちつつも、エコシステムに対する強固な地盤となっているからです。

しかし wasi-core の標準化が完了した後にも考えなければならない問題があります。例えば:

以上はまだ始まりにすぎません。もしこれらの問題を解決するアイデアを持っているなら、ぜひ私たちに加わってください。

Lin Clark について

Lin は Mozilla の Advanced Development で働いており、Rust と WebAssembly にフォーカスしています。Twitter は @linclark です。


  1. 訳注:「仮想的なマシン」は原文では "conceptual machine" であり、「仮想マシン (virtual machine, VM)」とは異なります。 ↩︎

広告