WebAssembly – Webanwendungen auf der Überholspur

Sicherheit in WebAssembly

Der Browser führt WebAssembly-Module innerhalb der JavaScript-VM aus. Damit sind sie zunächst normalem JavaScript-Code gleichgestellt und haben die gleichen Zugriffsrechte. Zumindest im Vergleich zu JavaScript verändert sich im Hinblick auf Sicherheit nichts: Wasm-Module können nicht beliebig auf Hardware oder Dateisysteme zugreifen, es sei denn, der Nutzer gibt das im Browserinterface explizit frei.

Besonders kritisch sind Speicherzugriffe. Viele Sprachen, die nach Wasm kompiliert werden können, erlauben Zugriffe auf beliebige Speicheradressen. Wenn man weiß, wo man hinschauen muss, könnte man sicherheitsrelevante Daten wie Passwörter oder Authentifizierungs-Tokens auslesen. Das lässt sich verhindern, indem man WebAssembly-Modulen ein separates Stück Speicher zuweist. Das heißt, die Module können nur auf eine dedizierte Untermenge des Heap der JavaScript-VM zugreifen. Der spezielle Heap ist durch ein JavaScript-ArrayBuffer realisiert. Damit unterliegt der gesamte Speicher eines Wasm-Moduls den gleichen Beschränkungen wie normale JavaScript-Objekte. Dementsprechend trackt die Garbage Collection das Modul und gibt es frei. Memory Leaks sind damit in genau den gleichen Grenzen möglich wie bei normalen JavaScript-Programmen. Über die ArrayBuffer hat JavaScript-Code Zugriff auf den gesamten Speicher des WebAssembly-Moduls.

Da der komplette Heap eines WebAssembly-Moduls in einem ArrayBuffer steckt, weiß die JavaScript-Umgebung, wie groß der Heap des Moduls ist und wo genau er im Heap liegt. Damit kann die JavaScript-Umgebung bei jedem Speicherzugriff des WebAssembly-Codes zum einen exakt nachprüfen, ob sich der Zugriff innerhalb des ArrayBuffers bewegt, und zum anderen unerlaubte Zugriffe unterbinden. Das macht die Ausführung geringfügig langsamer, aber verhindert unerlaubte Speicherzugriffe in die JavaScript-VM oder gar in den Adressraum des Browser-Prozesses.

Ebenfalls kritisch sind Zugriffe auf den Execution Stack. Neben dem Aufrufkontext liegt insbesondere die Rücksprungadresse an die aufrufende Funktion auf dem Stack. Gelingt es, die Rücksprungadresse zu manipulieren, kann ein Angreifer sie nutzen, um beliebigen Code auszuführen. Wasm unterbindet einen Schreibzugriff auf den Execution Stack komplett, da Wasm ihn außerhalb des Speichers des Wasm-Moduls verwaltet.

Ebenfalls kritisch sind üblicherweise Instruktionen, die an eine andere Stelle im Code springen. WebAssembly springt nur bei Funktionsaufrufen Speicheradressen an. Teilweise berechnet Wasm die Speicheradressen erst dynamisch zur Laufzeit. Um einen Missbrauch zu verhindern, kommen Tabellen zum Einsatz: Anstatt eine Adresse direkt in die call-Anweisung zu codieren, arbeiten call-Instruktionen mit zwei Parametern: einem Index in einer Tabelle und einer Funktionssignatur. Die beiden Parameter muss man nicht unbedingt statisch ablegen; sie können auch erst zur Laufzeit dynamisch ermittelt werden, zum Beispiel um Aufrufe an virtuelle Methoden in C++ oder Traits-Implementierungen in Rust zu realisieren. Der Index zeigt in eine Tabelle mit Funktions-Pointer. Sie liegt, genau wie der Execution Stack, außerhalb des Wasm-Moduls. Ein Überschreiben vom Wasm-Modul aus ist nicht möglich.

Wann immer eine solche call-Instruktion auftritt, schlägt die Wasm-Laufzeitumgebung zunächst die Funktion in der Tabelle nach und vergleicht die an die call-Instruktion übergebene Funktionssignatur mit der in der Tabelle hinterlegten. Gibt es unter dem Index keinen Eintrag in der Tabelle oder stimmen die beiden Signaturen nicht überein, stoppt die Ausführung des Wasm-Moduls sofort. Existiert der Eintrag und stimmen die Signaturen überein, ruft die Laufzeitumgebung die in der Tabelle hinterlegte Funktion auf.

Wie gut performt Wasm?

Einige der Sicherheitsfeatures gehen auf Kosten von Geschwindigkeit, sind aber notwendig, um Sicherheitslücken in Wasm-Modulen zu verhindern. WebAssembly ist überdies noch relativ jung und das Performancepotenzial sicherlich noch lange nicht ausgeschöpft. Aber schon heute ist Wasm in einigen Szenarien deutlich schneller als JavaScript. Mehrere Untersuchungen haben die Geschwindigkeit von WebAssembly genauer geprüft.

Samsung hat die WebAssembly-Performance untersucht. Dabei haben sie verschiedene Matrix-Matrix-Multiplikationsalgorithmen in JavaScript und C implementiert und miteinander verglichen. Im Ergebnis ist bei diesen Untersuchungen JavaScript bei kleinen Berechnungen schneller und WebAssembly bei rechenintensiven.

PSPDFKit ist eine Bibliothek zum Anzeigen und annotieren von PDFs. Erste Untersuchungen
brachten ernüchternde Ergebnisse. In Zusammenarbeit mit den Browser-Herstellern konnte das Team die Wasm-Implementierung beschleunigen. Wasm ist nun entweder gleichauf mit JavaScript oder schneller. Das Google-V8 Team hat diese Benchmark Ende August in der Ankündigung von Liftoff, einem neuen Wasm-Compiler in V8, aufgegriffen und demonstriert enorme Verbesserungen.

Die beiden Projekte Gutenberg-Parser und HeapViz berichten von enormen Geschwindigkeitsvorteilen bei WebAssembly. Der Gutenberg-Parser ist mit wasm im Schnitt 159-mal schneller als die äquivalente JavaScript-Implementierung. HeapViz ist bis zu 30-mal schneller. Wer in seinem eigenen Browser JavaScript gegen WebAssembly antreten lassen möchte, kann dies mit dem Wasm-Raytracer
tun.

Mit Wasm ist es ähnlich wie mit Multithreading: Man sollte nicht erwarten, einfach durch die Verwendung von WebAssembly auf magische Weise massive Performanceverbesserungen zu erhalten. WebAssembly gibt Entwicklern die Möglichkeit, performante Applikationen zu schreiben, macht sie aber nicht automatisch performant.