【譯】【圖文】標準化中的 WASI:在 web 之外執行 WebAssembly 的系統介面

jywhltj發表於2019-04-04

本文原發於我的個人部落格: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 的更多文章……

相關文章