本文原發于我的個人博客:https://hltj.me/wasm/2019/04/04/standardizing-wasi.html 。本副本只用于圖靈社區,禁止第三方轉載。

本文已獲得翻譯授權,譯自 Standardizing WASI: A system interface to run WebAssembly outside the web,由作者 Lin Clark 于當地時間 2019-03-27 發布。

今天(當地時間 2019-03-27),我們宣布開始進行一項新的標準化工作——WASI,WebAssembly 系統接口(WebAssembly System Interface)。

起因(Why): 開發人員開始將 WebAssembly 向瀏覽器之外推進,因為它提供了一種快速、可擴展、安全的方式來在所有計算機上運行相同的代碼。

但是我們尚未建立一個堅實的基礎。 瀏覽器之外的代碼需要一種與系統交互的方式——系統接口。 而 WebAssembly 平臺還沒有系統接口。

事物(What): WebAssembly 是概念機的匯編語言,而不是物理機的匯編語言。 這就是它可以在各種不同計算機體系結構上運行的原因。

正因為 WebAssembly 是概念機的匯編語言,所以 WebAssembly 需要一個概念操作系統的系統接口,而不是任何單一操作系統的系統接口。 這樣,它就可以在所有不同操作系統中運行。

這就是 WASI——WebAssembly 平臺的系統接口。

我們的目標是創建一個系統接口,它會成為 WebAssembly 的真正伴侶,并經受起時間的考驗。 這意味著堅持 WebAssembly 的關鍵原則——可移植性與安全性。

人物(Who): 我們正在組建一個 WebAssembly 的一個子工作組,專注于 WASI 標準化。 我們已經聚集了一些志同道合的合作伙伴,并且還在尋找更多伙伴加入。

以下是我們、我們的合作伙伴與支持者認為這很重要的一些原因:

Sean White,Mozilla 的首席研發官

“WebAssembly 已經在改變 web 給人們帶來新的引人入勝的內容的方式,并使開發者與創作者能在 web 上盡顯身手。 到目前為止,已經通過瀏覽器實現了,不過有了 WASI,我們可以將 WebAssembly 與 web 的益處傳遞給更多用戶、更多場合、更多設備,以及作為更多體驗的一部分。”

Tyler McMullen,Fastly 的 CTO

“我們將 WebAssembly 帶到瀏覽器之外,在我們的邊緣云中作為快速、安全地執行代碼的平臺。 雖然我們的邊緣與瀏覽器之間存在環境差異,但是 WASI 意味著 WebAssembly 開發者無需將代碼移植到每個不同的平臺。”

Myles Borins,Node 技術指導委員會主任

“WebAssembly 可以解決 Node 中最大的問題之一——如何獲得接近原生速度,并像使用原生模塊一樣復用其他語言(如 C 與 C++)編寫的代碼,同時仍保持可移植性與安全性。 標準化這個系統接口是實現這一目標的第一步。”

Laurie Voss,npm 的聯合創始人

“npm 極為感興趣的是,WebAssembly 潛在具有擴展 npm 生態系統、并同時極大地簡化在服務端 JavaScript 應用程序中運行原生代碼過程的能力。 我們期待這個過程的結果。”

所以說這是個大新聞!??

WASI 目前有三份實現:

(如果你能訪問 YouTube)可以在這個視頻中看個 WASI 實戰:

https://youtu.be/ggtEJC0Jv8A

如果你希望了解關于這個系統接口應該如何工作的提案的更多信息,請繼續閱讀。

系統接口是什么?

很多人說像 C 這樣的語言可以直接訪問系統資源。 但事實并非如此。

這些語言無法直接在大多數系統上進行打開或創建文件等操作。 為什么不能呢?

因為這些系統資源(諸如文件、內存以及網絡連接)對于穩定性與安全性來說太重要了。

如果一個程序無意中攪亂了另外一個程序的資源,那么它可能會使另一個程序崩潰。 更糟的是,如果一個程序(或用戶)故意干擾另一個程序的資源,那么它可能會竊取敏感數據。

表示崩潰的皺著眉頭的終端窗口,以及表示數據泄露的帶有壞鎖的文件

因此,我們需要一種方式來控制哪些程序與用戶可以訪問哪些資源。 人們很早就發現了這一點,并想出了一個提供這種控制的方式:保護環安全。

有了保護環安全,操作系統基本上在系統資源外圍設置了保護屏障。 這就是內核。 內核是唯一可以進行創建新文件、打開文件或者打開網絡連接等操作的地方。

用戶的程序在稱之為用戶模式的內核以外運行。 如果程序想做打開文件這樣的事,它必須請求內核為它打開。

左側是一個文件目錄結構,中間有一個包含操作系統內核的保護屏障,右側是一個敲門訪問的應用程序

這就是系統調用的概念所在。 當程序需要讓內核執行這其中某一項操作時,它會使用系統調用來請求。 這讓內核有機會找出是哪個用戶在請求。 然后就可以在打開文件之前分辨該用戶是否有權訪問該文件。

在大多數設備上,這是代碼可以訪問系統資源的唯一方式——通過系統調用。

請求操作系統將數據放入已打開文件的應用程序

操作系統讓系統調用可用。 但是如果每個操作系統都有自己的系統調用,那豈不是需要為每個操作系統編寫不同版本的代碼? 幸運的是,并不用。

這個問題是如何解決的呢?——抽象。

大多數語言都提供了標準庫。 在編碼時,程序員并不需要知道他們所面向的系統。 他們只是使用相應接口。

然后在編譯時,工具鏈會根據所面向的目標系統來選擇使用該接口的哪個實現。 這個實現會使用操作系統 API 中的函數,因此它是平臺相關的。

這就是系統接口的用武之地。 例如,為 Windows 計算機編譯的 printf 可以使用 Windows API 與該計算機進行交互。 而為 Mac 或者 Linux 編譯,則會改用 POSIX。

putc 的接口會翻譯為為兩種不同的實現,一種使用 POSIX 實現,另一種使用 Windows API 實現

然而這卻給 WebAssembly 帶來了一個問題。

對于 WebAssembly 來說,即使在編譯時也無從知曉所面向的是哪種操作系統。 因此,無法在標準庫的 WebAssembly 實現中使用任何單一操作系統的系統接口。

putc 的空實現

我之前說過 WebAssembly 是一種概念機的匯編語言,而不是真實計算機的匯編語言。 同樣,WebAssembly 也需要一套概念操作系統(而不是真實操作系統)的系統接口。

不過已經存在可以在瀏覽器之外運行 WebAssembly 的運行時了,即便沒有這個系統接口。 他們是怎么做到的呢?我們來看一看。

如今 WebAssembly 是如何在瀏覽器之外運行的?

生成 WebAssembly 的第一個工具是 Emscripten。 它在 web 上模擬一個特定操作系統的系統接口 POSIX。 這意味著程序員可以使用 C 標準庫(libc)中的函數。

為此,Emscripten 創建了自己的 libc 實現。 這個實現分為兩部分——一部分編譯成 WebAssembly 模塊,另一部分用 JS 膠水代碼實現。 這個 JS 膠水層會調用瀏覽器,進而與操作系統交互。

一個 Rube Goldberg 機展示了一個調用如何從 WebAssembly 模塊進入到 Emscripten 的 JS 膠水代碼,再進入到瀏覽器,再進入到內核

大多數早期的 WebAssembly 代碼都是使用 Emscripten 編譯的。 因此,當人們開始想在沒有瀏覽器的情況下運行 WebAssembly 代碼時,他們會從讓 Emscripten 所編譯的代碼能運行入手。

于是,這些運行時需要為 JS 膠水代碼中的所有函數創建自己的實現。

不過,這里有個問題。 這個 JS 膠水代碼所提供的接口并沒有設計成標準接口,甚至并非面向公眾的接口。 這并不是它所解決的問題。

例如,對于一個在設計成公開接口的 API 中名為 read 的函數,其 JS 膠水代碼使用的是 _system3(which, varargs)

一個清晰的 read 接口,對比一個令人困惑的 system3

第一個參數 which 是一個整數,它始終與名稱中的數字相同(在本例中是 3)。

第二個參數 varargs 是用到的參數。 它之所以稱為 varargs,是因為可以有可變數量的參數。 但是 WebAssembly 并沒有提供將可變數量參數傳給函數的方式。 于是,這些參數通過線性內存傳遞。 這不是類型安全的做法,而且也比使用寄存器傳遞參數(如果可能的話)慢。

對于在瀏覽器中運行 Emscripten 來說,這沒有問題。 但是現在運行時將其視為事實上的標準,實現了自己的一版 JS 膠水代碼。 他們是在模擬 POSIX 仿真層的內部細節。

這意味著他們正在重新實現那些對于 Emscripten 的約束有意義的選擇(例如將參數作為堆中值傳遞),即便這些約束并不適于他們的環境。

一個更復雜的 Rube Goldberg 機,其中 JS 膠水與瀏覽器都是由 WebAssembly 運行時模擬的

如果我們要構建一個持續數十年的 WebAssembly 生態系統,我們就需要堅實的基礎。 這意味著我們的事實標準不能是仿真的仿真。

不過我們應該采用什么原則呢?

WebAssembly 系統接口需要堅持什么原則?

有兩項重要的原則已經融入到 WebAssembly 中:

  • 可移植性
  • 安全性

當我們轉向瀏覽器之外的應用場景時,我們需要堅持這些關鍵原則。

實際上,POSIX 與 Unix 的安全訪問控制方式并沒有幫我們達到目的。 我們來看下它們的不足之處。

可移植性

POSIX 提供了源代碼級的可移植性。 相同的源代碼可以與不同版本的 libc 一起編譯來面向不同的計算機。

一個 C 源文件編譯成多個二進制文件

但是 WebAssembly 需要超越它一步。 我們需要能夠編譯一次就能在一系列不同的計算機上運行。 我們需要可移植的二進制文件。

一個 C 源文件編譯成單個二進制文件

這種可移植性讓用戶分發代碼更容易。

例如,Node 的原生模塊如果是用 WebAssembly 編寫的,那么當用戶安裝帶有原生模塊的應用時就不需要運行 node-gyp 了,開發人員也無需配置并分發幾十個二進制文件了。

安全性

當一行代碼請求操作系統執行某些輸入或輸出時,操作系統需要確定該代碼所請求的操作是否安全。

操作系統通常使用基于所有權與組的訪問控制來處理這個問題。

例如,程序可能會請求操作系統打開一個文件。 一個用戶具有他有權訪問的特定一組文件。

當該用戶啟動程序時,該程序代表用戶運行。 如果這個用戶有權訪問某文件(要么因為他是所有者,要么因為他在有權訪問的組里),那么該程序也能訪問。

請求打開與其所執行操作相關的文件的應用程序

這在用戶之間提供了保護。 在早期操作系統開發出來時很有意義。 系統通常是多用戶的,而管理員控制安裝什么軟件。 所以最突出的威脅是其他用戶偷看你的文件。

情況已經變了。 現在系統通常是單個用戶,但是他們會運行引入了許多未知可信度的其他第三方代碼的代碼。 現在最大的威脅是你自己運行的代碼會對你不利。

例如,假設你在應用程序中使用的庫來了一個新的維護者(在開源項目中經常發生)。 該維護者可能會對你的興趣很上心……也可能是個壞人。 如果這些代碼有權在你的系統上做任何事(例如,打開你的任何文件并通過網絡發送出去),那么其代碼會造成很大的損害。

An evil application asking for access to the users bitcoin wallet and opening up a network connection

這就是為什么使用可以直接與系統交互的第三方庫可能是危險的。

WebAssembly 實現安全性的方式與此不同。 WebAssembly 采用了沙箱。

這意味著代碼不能直接與操作系統交互。 那么它是如何利用系統資源的呢? 宿主機(可能是瀏覽器,也可能是 wasm 運行時)將函數放入代碼可以使用的沙箱中。

這意味著宿主機可以逐一限制每個程序可以做什么。 它并不是僅僅讓程序代表用戶、使用該用戶的完整權限調用任何系統調用。

只是擁有沙箱機制并不會使系統本身變安全(宿主機仍然可以將所有能力都放入到沙箱中,若是這種情況則并沒有變好),不過它至少讓宿主機能夠選擇創建更安全的系統。

A runtime placing safe functions into the sandbox with an application

在我們設計的任何系統接口中,我們都需要堅持這兩項原則。 可移植性讓開發與分發軟件更容易,而為宿主機提供保護自身或用戶的工具更是絕對必需。

這個系統接口應該是什么樣的?

鑒于這兩項關鍵原則,WebAssembly 系統接口應該設計成什么樣的?

這正是我們要通過標準化過程得出的成果。 不過我們確實有個提案要啟動:

  • 創建模塊化的一組標準接口
  • 開始標準化最基本的模塊 wasi-core

Multiple modules encased in the WASI standards effort

wasi-core 里會有什么?

wasi-core 會包含所有程序都需要的基本接口。 它會覆蓋與 POSIX 近乎相同的領域,包括諸如文件、網絡連接、時鐘以及隨機數。

并且其中很多會采用與 POSIX 非常類似的方式。 例如,它會使用 POSIX 的面向文件的方式,其中有諸如 open、close、read 以及 write 這樣的系統調用,而所有其他內容基本都是在此之上提供的擴充。

不過 wasi-core 并不會覆蓋所有 POSIX 的內容。 例如,進程概念并沒有清晰映射到 WebAssembly 上。 更進一步,讓每個 WebAssembly 引擎都需要支持像 fork 這樣的進程操作并無意義。 當然我們也希望標準化 fork 成為可能。

這就模塊化方式的用武之地。 通過這種方式,我們可以獲得良好的標準化覆蓋率,同時仍然讓一些平臺能夠只使用對其有意義的 WASI 部分。

Modules filled in with possible areas for standardization, such as processes, sensors, 3D graphics, etc

像 Rust 這樣的語言會直接在其標準庫中使用 wasi-core。 例如,Rust 的 open 在編譯到 WebAssembly 時會通過調用 __wasi_path_open 來實現。

對于 C 與 C++,我們創建了一個 wasi-sysroot,它根據 wasi-core 實現了 libc。

The Rust and C implementations of openat with WASI

我們期望像 Clang 這樣的編譯器準備好與 WASI API 交互,并完成像 Rust 編譯器與 Emscripten 這樣的工具鏈,將 WASI 作為其系統實現的一部分。

用戶代碼如何調用這些 WASI 函數?

運行用戶代碼的運行時會將 wasi-core 函數作為導入傳入。

A runtime placing an imports object into the sandbox

這為我們提供了可移植性,因為每個宿主機都可以有專為其平臺(從像 Mozilla’s wasmtime 與 Fastly 的 Lucet,到 Node 乃至瀏覽器)編寫的自己的 wasi-core 實現。

它還為我們提供了沙箱,因為宿主機可以逐個程序選擇哪些 wasi-core 函數可以傳入(即允許哪些系統調用)。 這保持了安全性。

Three runtimes—wastime, Node, and the browser—passing their own implementations of wasi_fd_open into the sandbox

WASI 為我們提供了進一步擴展這種安全性的方式。 它從基于能力的安全性中引入了更多概念。

傳統方式中,如果代碼需要打開一個文件,那么它會用一個路徑名字符串調用 open。 然后操作系統檢驗該代碼是否有權限(基于啟動該程序的用戶)。

對于 WASI,調用一個需要訪問文件的函數必須傳入一個附加了權限的文件描述符。 可以是用于該文件自身的描述符,也可以是用于包含該文件的目錄的描述符。

這樣,就不會有隨機請求打開 /etc/passwd 的代碼。 相反,代碼只能對傳給它的目錄進行操作。

Two evil apps in sandboxes. The one on the left is using POSIX and succeeds at opening a file it shouldn't have access to. The other is using WASI and can't open the file.

這讓為沙箱中的代碼安全地提供更多不同系統調用的訪問控制成為可能——因為這些系統調用的能力是受限的。

并且這是發生在逐個模塊基礎上的。 默認情況下,模塊沒有對任何文件描述符的訪問權限。 但是如果一個模塊中的代碼擁有文件描述符,那么它可以選擇將該文件描述符傳給其他模塊中它所調用的函數。 也可以創建更受限版本的文件描述符來傳給其他函數。

因此運行時可以將應用可用的文件描述符傳到頂層代碼,然后這些文件描述符就可以按需傳播到系統的其余部分。

The runtime passing a directory to the app, and then then app passing a file to a function

這讓 WebAssembly 更接近最小權限原則——一個模塊只能訪問完成其工作所需的確切資源。

這些概念來自于能力導向系統(capability-oriented systems),例如 CloudABI 與 Capsicum。 能力導向系統的一個問題是通常很難向它們移植代碼。 但我們認為這個問題可以解決。

如果代碼已經使用文件相對路徑調用 openat,那么只要編譯該代碼就能用。

如果代碼使用的是 open 并且遷移到 openat 風格的前期開銷太高,WASI 可以提供一個增量解決方案。 使用 libpreopen,可以為應用程序創建一個合理需要訪問的文件路徑列表。 然后就可以使用 open 了,不過只能使用列表中的路徑。

下一步呢?

我們認為 wasi-core 是一個良好的開端。 它保持了 WebAssembly 的可移植性與安全性,為生態系統提供了堅實的基礎。

不過,在 wasi-core 完全標準化之后,我們還需要解決一些問題。這些問題包括:

  • 異步 I/O
  • 文件監視
  • 文件鎖定

這只是開始,所以如果你有解決這些問題的想法,就請加入我們吧!

關于英文原文作者 Lin Clark

Lin 在 Mozilla 的高級開發部門工作,專注于 Rust 與 WebAssembly。

Lin Clark 的更多文章……