Beliebte Suchanfragen
//

Plug-in architectures with WebAssembly

3.11.2023 | 12 minutes of reading time

Plug-in architectures are an essential concept for developing customizable software. In a plug-in architecture, the application logic is split into a host (or core) system and a number of plug-in components. These plug-ins enable customers to tailor the business logic of the application to their specific use case. Plug-in architectures are widely used in end-user applications or self-hosted software products. Examples of these include integrated development environments (IDEs), web browsers, content management systems (CMS), graphic design software, and many others.

However, the use of this architecture can pose a challenge when the software system to be customized is deployed on a central server. This is typically the case in Software as a Service (SaaS) applications. In this case, implementing a plug-in architecture requires executing potentially unsafe third-party code in a host system, which raises concerns about data privacy, system stability, and unauthorized access. An attacker could inject a plug-in which looks normal from the outside, but, at runtime, might try to run malicious code. To name a few examples, it could try to encrypt the filesystem, open connections to other systems to download additional malware, or simply shut down the host.

Implementing traditional plug-in architectures in server-side applications therefore requires careful consideration of security measures to ensure the safety and integrity of the host system. The sandboxed security model of WebAssembly, together with its execution performance, makes WebAssembly a very good fit for server-side plug-in architectures.

Server-side WebAssembly

WebAssembly is a byte code format for executable code. While initially designed for use in a web browser, it has started to conquer the server-side world as well. This expansion is driven by several key factors

Efficiency. WebAssembly code is designed to be fast and compact, making it an attractive option for server applications where performance is crucial.

Portability. Developers can package server-side logic into WebAssembly modules that are easy to distribute and run consistently across different environments. This reduces compatibility and dependency issues, which are common challenges in server deployments.

Language interoperability. It can be compiled from a variety of programming languages, allowing developers to use their preferred language. Going one step further, it allows to easily integrate components written in different languages by compiling them all to WebAssembly.

Sandboxed security. WebAssembly's sandboxing features, which were initially designed for web browsers, also provide security benefits on the server-side. WebAssembly modules run in a secure, isolated environment, reducing the risk of vulnerabilities and exploits. This is particularly important for server applications that handle sensitive data and need to protect against potential security threats.

Server-side WebAssembly can be employed in a variety of different use cases, as presented in a previous article.

Here, we explore the use case of a third-party plug-in architecture. The goal of such an architecture is to allow users or customers to dynamically extend the behavior of an application. The host application provides predefined extension points. These extension points act as hooks that allow the guest WebAssembly modules to integrate seamlessly with the host application.

Users can provide their own WebAssembly modules that implement this interface. The host application invokes these guest modules at run-time, by calling the appropriate extension points.

WebAssembly plug-in architecture: Host Platform with extension point, multiple plug-ins written in different languages

WebAssembly's security model guarantees safe execution of these guest modules within the host environment. The sandboxed environment isolates the guest modules, denying access to the resources of the host application. The capability-based security model allows to grant only the system calls which are needed to fulfill the task of the plug-in. By default, no system calls are possible.

Real-world use cases

While much of the server-side WebAssembly landscape is still evolving, the plug-in architecture use case is already used in production by a few notable projects.

Web servers and reverse proxies

The Apache HTTP Server can be extended with WebAssembly modules. This enables WebAssembly modules to respond to HTTP requests. This capability is facilitated by an extension module, mod_wasm. Similarly, the Envoy proxy offers an Application Binary Interface (ABI) supporting custom HTTP filters implemented as WebAssembly plug-ins. Envoy is a reverse proxy which can both be deployed as a service proxy in a service mesh architecture or as a traditional 'front proxy' between a client and the server. In both scenarios, HTTP filters intercept and/or modify the HTTP requests. This is typically done to address cross-cutting concerns such as policy checks, authentication, and telemetry data collection.

Building on this, a talk at the recent WASMCon 2023 illustrated how the investment bank Goldman Sachs is leveraging WebAssembly for their API Platform. By creating plug-ins in higher level languages such as Go or TypeScript, they are able to provide higher flexibility for their developers and increase code reuse across their various API Gateway deployments.

SaaS providers

Shopify employs WebAssembly to allow for customization of their SaaS retail platform. Shopify offers merchants to create and manage their own online stores. However, each merchant often has very specific needs and sometimes even unique business requirements, such as offering special discounts for VIP customers, selling product bundles at discounted prices, and providing customizable shipping methods. Meeting all these diverse requirements within a single software system is not practical. Therefore, extensibility is a key quality of the Shopify platform. Shopify functions allow app developers to extend the backend logic of Shopify. Developers are required to provide a WebAssembly module containing the added logic. Additionally, they can define a GraphQL query to select the input to their plug-in. Any language that compiles to WebAssembly can be used, but Shopify provides dedicated SDKs for JavaScript and Rust to ease the development process.

Databases

In the domain of database management systems, user-defined functions (UDFs) offer a toolkit to introduce additional logic into the existing SQL or NoSQL languages. Some database systems (Singlestore, TiDB) have embraced WebAssembly to define UDFs, leveraging its near-native performance while tapping into the rich ecosystems of the various source languages. Meanwhile, the streaming data platform Redpanda is working towards enabling WebAssembly modules as stream processors directly on their platform. In comparison to the traditional architecture of separated processors, this would move the processing logic to the storage engine, the key for low latency and high throughput.

Example: Image-editing Web-Application

Let us have a look at an example application.

Most popular image editing software runs on the desktop environment. Typically, these applications come with a collection of image processing operations, such as special effects, denoising filters, or even advanced AI-based pipelines. Often, additional operations can be added through third-party plug-ins. Here, we conceptualize a minimal web-based image manipulation software. Users can upload images from their local filesystem, apply operations on these and receive the result. These image operations are not provided by the application itself, but are added by third-party developers. The image operation developers register their plug-ins by uploading a .wasm file through a web service.

Runtime view: receive image, move into sandbox, perform WebAssembly operation, extract from sandbox, send to client

Host-to-plug-in communication

The sandboxed nature of WebAssembly introduces challenges in the communication between the plug-in and the host. The WebAssembly instance has no access to the host system's memory. This security mechanism complicates the transfer and return of higher-level data structures such as strings, arrays, or objects. To address this challenge, several approaches have emerged.

The first common approach is to treat the guest plug-in as a command-line interface (CLI) application. Input and output are transferred through standard input and standard output, respectively. Shopify functions have employed this approach, but also the WebAssembly microservice framework WAGI. It is suitable when the data is already in text form or easily representable as text.

Another approach is to shift the burden onto the host platform to copy data into and out of the plug-in's memory space. The guest plug-in must implement a method to allocate a memory block and return a pointer to its location. However, this approach is prone to memory leaks because WebAssembly lacks a garbage collector. It becomes the host's responsibility to free unused memory. This approach can only work if the initial programming language supports addressing specific memory locations.

Both these approaches are rather low-level, therefore most of the aforementioned projects provide SDKs for popular programming languages to facilitate the workflow.

Extism

Extism is a framework for extending applications with plug-ins written in WebAssembly. It abstracts from the low-level details described in the previous section. By offering higher-level API, it simplifies transmitting data between the host system and guest plug-in. The project requires both the host and the guest implementations to use specific libraries - a Host SDK to integrate Extism in an application, and a Plug-in Development Kit (PDK) to write a plug-in.

Host SDKs are available for most common backend programming languages, including Python, Java, C/C++, C#, and Node. The number of PDKs is smaller, but Rust, Go, and C, the most popular programming languages targeting WebAssembly, are supported.

Here is an example on how to use the Go PDK to transfer an image as a byte array to the plug-in, apply an operation, and send the result back to the caller.

1import "github.com/extism/go-pdk"
2
3//go:export applyImageOperator
4func applyImageOperator() int32 {
5    inputBytes := pdk.Input()
6
7    result := applyOperatorOnJpeg(inputBytes)
8
9    mem := pdk.AllocateBytes(result.Bytes())
10    pdk.OutputMemory(mem)
11
12    return 0
13}

The exported function's signature does not expose any parameters. Instead, the input byte data can be retrieved pdk.Input(). Returning data is done by first allocating a chunk of memory, then returning the content with pdk.OutputMemory(mem).

The following code shows how the same plug-in can be called from a Python host.

1from extism import Plugin
2
3def apply_operator(wasm_file, image_bytes):
4    with open(wasm_file, "rb") as f:
5        wasm_bytes = f.read()
6    config = {"wasm": [{"data": wasm_bytes}]}
7    plugin = Plugin(config, wasi=True)
8    plugin.call("applyImageOperator", image_bytes)

The plug-in can be invoked by referencing the previously implemented method and passing the image as a parameter. The process of manually copying byte arrays into and out of the WebAssembly instance is abstracted away.

The two blocks of code were minimized for simplicity. A real-world application could include logging and error handling, restrict the memory of the plug-in, or validate its signature, all of which can be covered with the Extism SDKs.

The complete implementation of this project is available here.

Security considerations

WebAssembly was initially created for running within web browsers. Therefore it was designed with security in mind.

Capability-based security is a fundamental aspect of WebAssembly's design. It ensures that access to system resources and capabilities has to explicitly be granted by the host VM. Extism (like all other WebAssembly runtimes) has all system calls deactivated by default. If, in the example code above, a plug-in tried to call a remote API, the runtime would raise an exception. Extism allows granular access control, enabling HTTP and file system access on a per-host or per-file/directory basis. Additionally, more general capabilities can be enabled by injecting host functions into the plug-in.

WebAssembly also incorporates a form of call stack protection. The memory a WebAssembly application has access to is separated from the call stack, and also from the memory containing its own code. This aims to protect against stack-based buffer overflow attacks such as the ones described in a 2021 paper.

However, these security mechanisms must be implemented both in the WebAssembly VM (host runtime) and in the language-specific compiler toolchain. This brings some risk, due to the increasing number of runtimes and supported languages. A paper from 2020 examines the security of emscripten, the C++-to-WebAssembly compiler toolchain. At the time, they came to the conclusion that many compiler security features for native compilation targets were not active when compiling to WebAssembly. The paper highlights the need for improvements both in the compilers and in the runtimes to ensure the security of WebAssembly applications.

Furthermore, it's important to note that while WebAssembly itself can protect the host system, the output produced by the plug-ins should still be treated with caution. This is not an issue of WebAssembly - in any plug-in architecture, there is a risk that the output generated by a plug-in could be malicious. In the image-editing application described above, an attacker could build a plug-in that hides executable code within images. Or they could generate completely different images with inappropriate or forbidden content. The output of a third-party plug-in should be handled with the same care as user-generated data.

Alternatives

WebAssembly is not the only option for enabling plug-in architectures. There are other alternatives worth considering.

One alternative is the use of native language extensions or dynamically linked libraries. These options offer high performance due to their native code, but they lack isolation as they share memory and address space with the host application. Additionally, they have limited portability as they need to be compiled specifically for the target environment.

Another alternative is the use of scripting languages like Python, Lua, or JavaScript, which can be run embedded within a host application. These dynamically typed languages can provide reasonable performance for small scripts, particularly when a runtime with a Just-In-Time (JIT) compiler is utilized. However, they may not be suitable for larger performance-critical applications due to the nature of the JIT and garbage collection. In addition, developers may need to learn a new language, which can be a potential drawback. In contrast, WebAssembly can be targeted by multiple languages, offering more flexibility in choosing a familiar programming language.

A common approach for extending behavior in SaaS environments is through HTTP calls to external APIs. This involves registering a URL, e.g. as a webhook to be called when needed. While this approach guarantees the safety and security of the host application, it comes with the downside of high latency. Making remote API calls introduces delays due to network communication, which may not be suitable for applications requiring real-time interactions.

Java

Compiling Java to WebAssembly to run in the browser and interacting with JavaScript is facilitated by open source projects such as TeaVM and CheerpJ. However, the support for targeting the WebAssembly System Interface (WASI) in the Java ecosystem is currently limited. Notably, Extism does not provide a plug-in-SDK for Java.

While there is a TeaVM fork which aims at compiling Java to server-side-compliant WebAssembly, the support for building WebAssembly plug-ins in Java lags behind compared to other languages. For one, it is challenging to transfer data into the plug-in, due to the memory model of Java. Java lacks the ability to work with raw addresses, and the memory location of objects on the heap can change with each Garbage Collection (GC) cycle. The situation might improve with better support once native GC is built into WebAssembly. Additionally, certain parts of the Java standard library rely on native system calls that are not covered by WASI. An example of this is encoding and decoding image files.

Conclusion

Plug-in architectures offer crucial flexibility in software systems. WebAssembly presents a new and promising approach for implementing them on the server side. While still a young technology, WebAssembly plug-ins are already being used in production projects. Exploring the potential of WebAssembly for efficient and secure server-side plug-ins is an exciting opportunity in software development.

share post

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.