Introduction
wslplugins-rs is a Rust framework for building DLL plugins loaded by Windows Subsystem for Linux.
It wraps the raw WSL plugin API with Rust types and provides a procedural macro that generates the
exported entry points and hook registration code expected by WSL.
Use it when your plugin needs to observe WSL lifecycle events, inspect distribution metadata, or run commands inside a WSL session from plugin code.
This book is written for plugin authors using the library in their own crate. It focuses on the path from an empty Rust plugin project to a signed and registered DLL on Windows. For item-by-item API reference, use the generated Rust documentation on docs.rs.
Design Goals
The framework keeps the plugin authoring model small:
- implement
WSLPluginV1for a Rust type that owns the plugin state; - annotate the implementation with
#[wsl_plugin_v1]or a specific API requirement; - use typed wrappers for WSL sessions, distributions, versions, and command execution;
- return errors instead of panicking inside the WSL service process.
WSL loads plugins into a host service process. Reliability and conservative error handling matter
more than convenience shortcuts. The Error Handling chapter explains how to
turn plugin failures into WinResult or PluginResult values.
What You Build
A plugin built with wslplugins-rs is a Rust library crate compiled as a Windows DLL:
[lib]
crate-type = ["cdylib"]
The DLL is then signed, registered under the WSL plugins registry key, and loaded by the WSL service.
The examples in the repository are useful starting points, but the same model applies to any plugin
crate that depends on wslplugins-rs.
Getting Started
This chapter assumes you are creating a plugin crate outside this repository and want to consume
wslplugins-rs as a dependency.
If you want the shortest copy-and-run path first, use the End-to-End Quickstart, then return here for the background details.
Prerequisites
Install these tools on Windows:
- Rust stable and Cargo.
- PowerShell.
SignTool.exefrom the Windows SDK.- WSL installed and able to run at least one distribution.
- A Windows environment that can build native MSVC Rust targets.
SignTool.exe is usually easiest to access from a Visual Studio Developer Command Prompt or from a
shell where the Windows SDK tools are on PATH.
Packaging and host validation also require administrator access because local testing can touch the
machine certificate store, the HKLM registry hive, and the WSL service.
Create a Plugin Crate
Create a Rust library crate and configure it to produce a DLL:
cargo new my-wsl-plugin --lib
cd my-wsl-plugin
[lib]
crate-type = ["cdylib"]
The crate can contain normal Rust modules and dependencies. The important requirement is that the final artifact is a native Windows DLL that WSL can load.
Add the Framework
Most plugins should enable the macro feature so the crate generates the required WSL exports:
[dependencies]
wslplugins-rs = { version = "0.1.0-beta.4", features = ["macro"] }
Import the prelude in your plugin code:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
}
Implement the Plugin
A minimal plugin is a type plus a WSLPluginV1 implementation:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct Plugin {
context: &'static WSLContext,
}
#[wsl_plugin_v1]
impl WSLPluginV1 for Plugin {
fn try_new(context: &'static WSLContext) -> WinResult<Self> {
Ok(Self { context })
}
}
}
Add hook methods only for the events your plugin handles. The default implementation for each hook does nothing and returns success.
If a hook or API call is mandatory for the plugin, declare that requirement with
#[wsl_plugin_v1(...)]. Prefer named capabilities where they exist; see
Version Capabilities.
Validate During Development
Run ordinary Rust checks while developing:
cargo test
cargo clippy --all-targets --all-features
cargo fmt -- --check
When you are ready to load the plugin into WSL, build a release DLL:
cargo build --release
The DLL is written under target\release\ using your crate name.
Use the Examples
The repository examples show complete plugins for common scenarios:
minimal: lifecycle hooks and command execution inside a WSL session.dist-info: distribution metadata access.unpackaged-distro-blacklist-policy: a policy-style plugin.
Use them as references for plugin behavior, not as required workspace structure.
Next: follow the End-to-End Quickstart for a complete deployment loop, or read Your First Plugin to add hook behavior.
End-to-End Quickstart
This chapter shows the shortest complete path from a new Rust crate to a WSL plugin DLL that is signed, registered, loaded, tested, and removed again.
Run the deployment commands from an elevated PowerShell session because signing trust, registry registration, and WSL service restarts affect the host machine.
Create the Crate
cargo new my-wsl-plugin --lib
cd my-wsl-plugin
Configure the crate as a Windows DLL and add the framework dependency:
[package]
name = "my-wsl-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wslplugins-rs = { version = "0.1.0-beta.4", features = ["macro"] }
Use this minimal src/lib.rs:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct Plugin {
_context: &'static WSLContext,
}
#[wsl_plugin_v1]
impl WSLPluginV1 for Plugin {
fn try_new(context: &'static WSLContext) -> WinResult<Self> {
Ok(Self { _context: context })
}
}
}
Run normal Rust validation first:
cargo test
cargo clippy --all-targets --all-features
cargo fmt -- --check
Then build the DLL:
cargo build --release
The DLL path uses underscores even when the crate name uses hyphens:
target\release\my_wsl_plugin.dll
Sign and Register
The repository contains a development signing helper. Set variables for the plugin you are testing:
$PluginName = "my-wsl-plugin"
$DllPath = "C:\path\to\my-wsl-plugin\target\release\my_wsl_plugin.dll"
$RepoPath = "C:\path\to\wslplugins-rs"
Sign the DLL:
& "$RepoPath\sign-plugin.ps1" -PluginPath $DllPath
For local development, you may also trust the generated certificate on the test machine:
& "$RepoPath\sign-plugin.ps1" -PluginPath $DllPath -Trust
Register the signed DLL:
Set-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Plugins" `
-Name $PluginName `
-Value $DllPath `
-Force
Restart WSL and trigger plugin loading:
Stop-Service -Name "wslservice" -Force
wsl.exe echo "plugin load test"
Verify and Clean Up
Check that the registry value points to the DLL you built:
Get-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Plugins" `
-Name $PluginName
If WSL reports a plugin load error, use the troubleshooting chapter before changing code.
When finished testing, unregister the plugin and restart WSL:
Remove-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Plugins" `
-Name $PluginName `
-Force
Stop-Service -Name "wslservice" -Force
Next: read Your First Plugin to add hooks and behavior.
Your First Plugin
The minimal shape of a plugin is a Rust cdylib crate containing a type plus an implementation of
WSLPluginV1.
The #[wsl_plugin_v1] macro generates the exported functions and hook wiring expected by WSL.
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct MyPlugin {
context: &'static WSLContext,
}
#[wsl_plugin_v1]
impl WSLPluginV1 for MyPlugin {
fn try_new(context: &'static WSLContext) -> WinResult<Self> {
Ok(Self { context })
}
}
}
Use a requirement in #[wsl_plugin_v1(...)] only when the whole plugin depends on a specific WSL
Plugin API version or named capability. See Version Capabilities for
the supported forms and feature gates.
The context gives access to the framework API wrapper. Store it if the plugin needs to call WSL
APIs later from hook methods.
Add Hooks
Hooks are regular trait methods. Override only the lifecycle events the plugin needs:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use std::ffi::OsString;
use wslplugins_rs::prelude::*;
use wslplugins_rs::windows_core::HRESULT;
const E_FAIL: HRESULT = HRESULT(0x80004005u32 as i32);
pub(crate) struct MyPlugin {
context: &'static WSLContext,
}
#[wsl_plugin_v1]
impl WSLPluginV1 for MyPlugin {
fn try_new(context: &'static WSLContext) -> WinResult<Self> {
Ok(Self { context })
}
fn on_vm_started(
&self,
session: &WSLSessionInformation,
user_settings: &WSLVmCreationSettings,
) -> PluginResult<()> {
let mut stream = self
.context
.api
.new_command(session, "/bin/cat")
.with_arg("/proc/version")
.execute()?;
let mut kernel_version = String::new();
std::io::Read::read_to_string(&mut stream, &mut kernel_version).map_err(|error| {
PluginError::with_message(E_FAIL, OsString::from(error.to_string()))
})?;
Ok(())
}
}
}
Command paths are Linux paths such as /bin/cat. execute() returns a TcpStream connected to the
process stdin and stdout; stderr is forwarded to Linux dmesg. This example runs in the VM root
namespace; see Command Execution for distribution-scoped execution.
Start from an Example
If you want a complete working reference, start with the minimal example from the repository. It
demonstrates:
- creating plugin state in
try_new; - logging VM and distribution lifecycle events;
- running
/bin/cat /proc/versionwhen the VM starts; - using the macro to declare plugin-wide requirements when a hook or API capability is mandatory. New plugins should prefer named capabilities where available; see Version Capabilities.
In your own plugin crate, the equivalent release build is:
cargo build --release
After that, sign and register the DLL as described in the packaging chapter.
Next: read WSL Plugin Model for the host model and available events, or jump to Packaging and Deployment when you are ready to load the DLL into WSL.
WSL Plugin Model
WSL plugins are native DLLs loaded by the WSL service. The WSL plugin API calls exported functions from the DLL and sends synchronous notifications for lifecycle events.
wslplugins-rs turns that model into a Rust trait:
try_newcreates the plugin state.on_vm_startedruns after a VM starts.on_vm_stoppingruns before a VM stops.on_distribution_startedruns after a distribution starts.on_distribution_stoppingruns before a distribution stops.on_distribution_registeredruns when a distribution is registered.on_distribution_unregisteredruns when a distribution is unregistered.
The default hook implementations do nothing and return success, so a plugin only implements the events it needs.
What WSL Loads
At the ABI level, a plugin exposes the WSLPluginAPIV1_EntryPoint entry point. WSL passes that
entry point a WSLPluginAPIV1 table and expects the plugin to fill a WSLPluginHooksV1 table with
the callbacks it wants to handle.
The #[wsl_plugin_v1(...)] macro generates this entry point and hook table wiring for an
implementation of WSLPluginV1. Most plugin crates should use the macro instead of writing the
entry point manually.
The API table contains:
- the WSL plugin API version currently offered by the host;
MountFolder, for creating a Plan 9 mount between Windows and Linux;ExecuteBinary, for running a program in the root namespace of the WSL2 VM;PluginError, for passing a user-facing failure message back to WSL;ExecuteBinaryInDistribution, for running a program inside a user distribution when the host API supports that capability.
The hook table contains VM lifecycle hooks, distribution lifecycle hooks, and distribution registration hooks. Registration and unregistration hooks are version-gated capabilities; see Version Capabilities.
Plugin State
The plugin type owns state that must live for the loaded plugin lifetime. The WSL context gives access to the API wrapper:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct Plugin {
context: &'static WSLContext,
}
}
The trait requires Sync because the host may call plugin hooks from service-owned execution paths.
Shared state should use synchronization primitives that are appropriate for service code and should
avoid long blocking critical sections.
The raw WSL structs passed to hooks are borrowed from WSL. The Microsoft header documents those
pointers as valid only while the hook call is in progress. wslplugins-rs exposes typed wrapper
references for those values, but the same lifetime rule still matters: copy out the data you need if
it must outlive the hook.
API Wrappers
The public crate provides typed wrappers for common WSL concepts:
WSLContext: plugin context and access toApiV1.WSLSessionInformation: current WSL session metadata.WSLDistributionInformation: online distribution metadata.WSLOfflineDistributionInformation: offline distribution metadata used by registration hooks.WSLVmCreationSettings: VM creation settings.SessionID,DistributionID, andUserDistributionID: typed identifiers.WSLVersion: parsed WSL version values.WSLVersionCapability: named feature gates for version-sensitive API surface.
Prefer these wrappers over raw FFI types in plugin code. Raw access is available behind the sys
feature for cases where a wrapper does not yet expose the needed API.
Online and offline distribution wrappers both implement CoreWSLDistributionInformation. That gives
common access to the distribution ID, name, optional package family name, flavor, and version. The
crate checks the runtime API capability for fields that were added later. The capability names and
minimum versions are listed in Version Capabilities.
Versioned Hooks
Some hooks are only available in newer WSL plugin API versions. Distribution registration and
unregistration are represented by
WSLVersionCapability::DistributionRegisteredHook
(2.1.2) and
WSLVersionCapability::DistributionUnregisteredHook
(2.1.2).
Choose the requirement passed to #[wsl_plugin_v1(...)] according to the hooks and API calls that
are mandatory for the plugin. A plugin that only handles basic VM lifecycle events can use
#[wsl_plugin_v1]. A plugin whose core behavior depends on registration events should declare the
matching capability.
If WSL offers an older API version than the requirement requested by the plugin macro,
initialization fails with WSL_E_PLUGIN_REQUIRES_UPDATE. This is preferable when the plugin cannot
operate correctly without that hook or API call.
Version Checks
wslplugins-rs uses two complementary version checks.
The first check happens when WSL loads the DLL. The requirement passed to #[wsl_plugin_v1(...)]
is the minimum API support required by the plugin as a whole. It can be omitted, expressed as an
explicit version, or expressed as one or more capabilities. During entry point initialization, the
generated code compares that requirement with context.api.version(). If the runtime API is too
old, plugin creation stops and WSL receives WSL_E_PLUGIN_REQUIRES_UPDATE.
That means WSL does not call try_new and the hook table is not registered for that plugin. A hook
that requires a newer API version will therefore never be called if you declare that version in the
macro and the host runtime is older. This is the right choice for features that are central to the
plugin’s behavior.
Hook availability is decided when the plugin is registered with WSL, not lazily when an event
happens. If the current WSL plugin API version does not provide a hook slot, the method associated
with that event is never called. For example, without the distribution registration capabilities,
on_distribution_registered and on_distribution_unregistered cannot run.
The second check happens at runtime on individual APIs or fields. Some wrappers return a Result
instead of silently reading a field that may not exist on the current WSL API version:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
fn describe_distribution(distribution: &WSLDistributionInformation) -> PluginResult<String> {
let init_pid = distribution.init_pid()?;
Ok(format!(
"{} is running with init PID {init_pid}",
distribution.name().to_string_lossy()
))
}
}
If the runtime API is too old for init_pid(), the call returns a RequiresUpdate error that maps
to WSL_E_PLUGIN_REQUIRES_UPDATE. The same pattern is used by distribution-scoped command
execution: with_distribution_id(DistributionID::User(...)).execute() requires
WSLVersionCapability::ExecuteBinaryInDistribution
(2.1.2) and can return an API error on an older runtime.
Use runtime Result checks when the feature is optional:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
fn optional_flavor(distribution: &WSLDistributionInformation) -> Option<String> {
distribution
.flavor()
.ok()
.flatten()
.map(|value| value.to_string_lossy().into_owned())
}
}
Use the macro version requirement when the plugin cannot behave correctly without the newer hook,
field, or API call. For example, a plugin whose main purpose is to run commands inside a specific
user distribution should require
WSLVersionCapability::ExecuteBinaryInDistribution
(2.1.2); a plugin that only uses flavor() as a nicer log detail can keep a lower macro
requirement and treat that field as optional. See Version Capabilities
for the full capability list.
Hook Failure Semantics
Most hook failures are treated as lifecycle failures by WSL. For example, an error from
OnVMStarted or OnDistributionStarted can prevent the VM or distribution from starting.
Distribution registration notifications are different in the current Microsoft header: failures from
OnDistributionRegistered and OnDistributionUnregistered do not cause the register or unregister
operation itself to fail. Use those hooks for observation, cleanup, indexing, or best-effort policy
work. If you need to block startup, enforce that policy in a startup hook instead.
External References
- Microsoft WSL plugin documentation: https://learn.microsoft.com/en-us/windows/wsl/wsl-plugins
- Microsoft WSL Plugin API NuGet package: https://www.nuget.org/packages/Microsoft.WSL.PluginApi
- Microsoft WSL plugin sample: https://github.com/microsoft/wsl-plugin-sample
Version Capabilities
WSL Plugin API features are introduced over time. wslplugins-rs models those feature gates with
WSLVersionCapability, so plugin code can name the API feature it depends on instead of hard-coding
the version number everywhere.
Use capabilities when the crate exposes one for the feature you need. Use an explicit version only when the plugin depends on an API surface that does not yet have a named capability. The capability matrix below is the canonical list for this book; other chapters link here instead of repeating it.
Capability Reference
For command execution details, see Command Execution. For the lifecycle
and registration events exposed by WSLPluginV1, see WSL Plugin Model.
Plugin Requirements
The #[wsl_plugin_v1] macro checks the plugin-wide API requirement before try_new runs:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct Plugin {
context: &'static WSLContext,
}
#[wsl_plugin_v1]
impl WSLPluginV1 for Plugin {
fn try_new(context: &'static WSLContext) -> WinResult<Self> {
Ok(Self { context })
}
}
}
No argument means the plugin only requires the base entry point. If the whole plugin needs a known minimum API version, pass that version explicitly:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct Plugin;
#[wsl_plugin_v1(2, 1)]
impl WSLPluginV1 for Plugin {
fn try_new(_context: &'static WSLContext) -> WinResult<Self> {
Ok(Self)
}
}
}
The revision component is optional and defaults to 0:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct Plugin;
#[wsl_plugin_v1(2, 1, 2)]
impl WSLPluginV1 for Plugin {
fn try_new(_context: &'static WSLContext) -> WinResult<Self> {
Ok(Self)
}
}
}
When a named capability exists, prefer it:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct Plugin;
#[wsl_plugin_v1(WSLVersionCapability::ExecuteBinaryInDistribution)]
impl WSLPluginV1 for Plugin {
fn try_new(_context: &'static WSLContext) -> WinResult<Self> {
Ok(Self)
}
}
}
Capabilities can be combined with |. The macro uses the highest minimum API version required by
the listed capabilities:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
pub(crate) struct Plugin;
#[wsl_plugin_v1(
WSLVersionCapability::DistributionRegisteredHook
| WSLVersionCapability::DistributionUnregisteredHook
)]
impl WSLPluginV1 for Plugin {
fn try_new(_context: &'static WSLContext) -> WinResult<Self> {
Ok(Self)
}
}
}
If the host WSL Plugin API is too old for the plugin-wide requirement, initialization fails with
WSL_E_PLUGIN_REQUIRES_UPDATE. WSL does not call try_new, and the hook table is not registered
for that plugin.
Runtime Checks
Plugin-wide requirements are the right choice when the plugin cannot behave correctly without the
feature. Optional features should stay as runtime Result checks:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::prelude::*;
fn optional_flavor(distribution: &WSLDistributionInformation) -> Option<String> {
distribution
.flavor()
.ok()
.flatten()
.map(|value| value.to_string_lossy().into_owned())
}
}
The same rule applies to command execution. If distribution-scoped command execution is central to
the plugin, declare
WSLVersionCapability::ExecuteBinaryInDistribution
(2.1.2) in #[wsl_plugin_v1(...)].
If it is optional, handle the API error returned by execute().
Inspecting Versions
WSLVersionCapability::required_version() returns the minimum API version for one capability.
WSLVersion::supports(capability) checks whether a runtime API version supports a capability, and
WSLVersion::capabilities() iterates over the capabilities supported by that version.
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use wslplugins_rs::{WSLVersion, WSLVersionCapability};
let version = WSLVersion::new(2, 1, 2);
assert!(version.supports(WSLVersionCapability::ExecuteBinaryInDistribution));
}
Error Handling
WSL plugins run inside the WSL service process, so failures should be explicit errors instead of process-level failures. Return errors instead of panicking:
- use
WinResult<T>for Windows API style errors; - use
PluginResult<T>for plugin hook errors; - map I/O errors into Windows errors where required by the hook signature;
- avoid
panic!,unwrap, andexpectin plugin code.
Panics are a poor fit for WSL plugins because the plugin is loaded inside a WSL service process, not inside a short-lived CLI that only affects its own execution. A panic in a hook can unwind through an FFI boundary, abort the process, poison shared state, or leave WSL with only a generic failure code. The host is also allowed to call hooks during lifecycle operations such as VM startup, distribution registration, or shutdown; failing predictably is more useful than terminating the service path.
Avoiding unwrap and expect is part of the same rule. A missing field, inaccessible file, invalid
distribution name, or failed command execution should become an explicit error that WSL can report
or handle. It should not become an accidental process failure.
Plugin Errors and Windows Errors
wslplugins-rs exposes two result styles because WSL has two different error channels:
WinResult<T>returns a normal WindowsHRESULTto the host.PluginResult<T>returnsPluginError, which contains anHRESULTand an optional message.
Use WinResult<T> when the hook signature is purely Windows-style or when only an error code is
expected. Use PluginResult<T> where the framework hook supports a plugin-level diagnostic message.
When a PluginError has a message, the framework sends that message to WSL through the plugin API
before converting the error back into a Windows error code. That gives WSL a human-readable plugin
diagnostic instead of only an HRESULT. In practice, this is useful for policy-style failures where
the user should see why an operation was denied:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use std::ffi::OsString;
use wslplugins_rs::prelude::*;
use wslplugins_rs::windows_core::HRESULT;
const E_ACCESSDENIED: HRESULT = HRESULT(0x80070005u32 as i32);
fn reject_distribution(name: &str) -> PluginResult<()> {
Err(PluginError::with_message(
E_ACCESSDENIED,
OsString::from(format!(
"Distribution '{name}' is blocked by local policy"
)),
))
}
}
If you convert everything to windows_core::Error too early, WSL still receives the failure code,
but it can lose the plugin-specific explanation. Prefer PluginResult for hooks where the message
is part of the user experience.
The raw PluginError API is intended for synchronous use while WSL is processing VM or distribution
creation. In practice, use PluginResult from on_vm_started and on_distribution_started when a
message should be shown to the user. Do not store a delayed error message and try to report it after
the hook has returned.
Command Execution
ApiV1::new_command builds a Linux command to run in a WSL session. The program path and arguments
must be valid for the target Linux environment:
#![allow(unused)]
fn main() {
extern crate wslplugins_rs;
use std::ffi::OsString;
use wslplugins_rs::prelude::*;
use wslplugins_rs::windows_core::HRESULT;
const E_FAIL: HRESULT = HRESULT(0x80004005u32 as i32);
fn read_kernel_version(
context: &WSLContext,
session: &WSLSessionInformation,
) -> PluginResult<String> {
let mut stream = context
.api
.new_command(session, "/bin/cat")
.with_arg("/proc/version")
.execute()?;
let mut kernel_version = String::new();
std::io::Read::read_to_string(&mut stream, &mut kernel_version).map_err(|error| {
PluginError::with_message(E_FAIL, OsString::from(error.to_string()))
})?;
Ok(kernel_version)
}
}
The returned stream is connected to stdin and stdout. Drop it when no more interaction is needed.
By default, WSLCommand targets the system/root namespace through the WSL API ExecuteBinary. This
namespace is not the same as a user distribution. Microsoft documents it as a minimal Mariner-based
root filesystem backed by writable tmpfs, so changes made there disappear when the WSL2 VM shuts
down.
Use a user-distribution target when the command must run inside a distribution rather than the root
namespace. That path uses ExecuteBinaryInDistribution, which is represented by
WSLVersionCapability::ExecuteBinaryInDistribution
(2.1.2). If that command path is central to the plugin, declare the capability in
#[wsl_plugin_v1(...)]. If it is optional, handle the error returned by execute().
The returned TcpStream is connected to the command’s stdin and stdout. It does not carry stderr;
WSL sends stderr output to Linux dmesg.
FFI and Safety
WSL plugins run inside the WSL service process. A crash, panic across an FFI boundary, invalid pointer, or blocking hook can affect the host service. Treat plugin code like systems code even when using safe Rust wrappers.
Because hooks cross an FFI boundary, plugin code should return typed errors instead of panicking.
See Error Handling for the WinResult and PluginResult conventions.
Unsafe Code
Most plugin code should not need raw FFI access. When unsafe code is necessary:
- keep unsafe blocks as small as possible;
- document each block with a
SAFETY:comment; - validate pointer lifetimes and ownership before wrapping raw values;
- avoid storing borrowed FFI data beyond the lifetime guaranteed by WSL.
The wslplugins-rs crate centralizes raw API handling so plugin authors can usually work with typed
Rust values instead.
Hook Behavior
Hooks are synchronous notifications. Keep them predictable:
- do the minimum required work in the hook;
- avoid unbounded waits;
- avoid holding locks while calling into WSL APIs;
- make logging best-effort and failure-aware;
- assume hooks may be called multiple times for lifecycle retries.
If a plugin needs heavier work, prefer moving it to a controlled background path with clear shutdown behavior.
Microsoft’s WSL plugin documentation calls out three operational consequences of this model:
- WSL waits for hook callbacks before continuing the lifecycle operation.
- Most plugin errors are fatal to the VM or distribution startup path.
- Plugin code runs in the WSL service process, so a plugin crash can crash the service.
That is why the framework examples keep hooks small and use typed errors instead of process-level failure mechanisms.
Advanced FFI Notes
The sys feature re-exports the raw wslpluginapi-sys bindings for cases where the safe wrapper
does not yet expose a field or function. Prefer the safe wrapper first. If raw access is necessary,
keep these header-level rules in mind:
- hook input pointers are valid only during the callback;
- string pointers from WSL may be null where the header allows it, such as
PackageFamilyName; ExecuteBinaryargument arrays are null-terminated at the ABI boundary;- some fields and function pointers are available only when the host API supports the matching
WSLVersionCapability; see Version Capabilities.
These details are useful when debugging ABI mismatches, but ordinary plugin logic should stay on the typed Rust side.
Testing
Testing a WSL plugin happens at two levels: ordinary Rust validation for your crate and host validation on Windows after the DLL is signed and registered.
Rust Validation
Run normal Rust checks while developing your plugin:
cargo test
cargo clippy --all-targets --all-features
cargo fmt -- --check
Unit tests are useful for your own parsing, policy, configuration, and decision logic. Keep direct WSL service behavior behind small boundaries so most of the plugin can be tested without loading a DLL into the host service.
Hook-Oriented Tests
Where possible, separate hook code from business logic:
#![allow(unused)]
fn main() {
fn should_block_distribution(name: &str) -> bool {
name.eq_ignore_ascii_case("blocked")
}
}
Then call that logic from on_distribution_registered, on_vm_started, or whichever hook applies.
This keeps hook implementations small and makes failures easier to reproduce in ordinary Rust tests.
Windows Host Validation
Use host validation when you need to prove the DLL works with the real WSL service:
- Build the target example in release mode.
- Sign the DLL.
- Register the DLL under the WSL plugins registry key.
- Restart the WSL service.
- Run a WSL command that triggers the plugin.
- Inspect plugin output and any expected side effects.
For the minimal example, the observable output is written to C:\wsl-plugin-demo.txt.
For your own plugin, choose an observable result that fits the behavior: a log file, an allowed or blocked operation, a command output, or another side effect that confirms the expected hook ran.
Use commands that trigger the hook you are validating:
wsl.exe echo "vm startup test"
wsl.exe -d Ubuntu -- echo "distribution startup test"
For more WSL command examples, see Microsoft’s basic WSL commands documentation.
After rebuilding or changing the registry value, restart the WSL service before testing again:
Stop-Service -Name "wslservice" -Force
If the plugin does not load or the expected hook does not run, use the Troubleshooting chapter before changing the plugin code.
Packaging and Deployment
WSL loads plugin DLLs from Windows. A development deployment usually has four steps:
- Build the plugin DLL.
- Sign the DLL.
- Register the DLL path in the WSL plugins registry key.
- Restart the WSL service and trigger plugin loading.
The underlying Microsoft package for the native API is Microsoft.WSL.PluginApi on NuGet. Version
2.4.4 was the current package version checked on 2026-05-27, and it contains the WslPluginApi.h
header used to define the native ABI. Check NuGet for newer versions when updating bindings or
comparing against the C header. Rust users normally consume that ABI through wslplugins-rs and
wslpluginapi-sys; you do not need to add the NuGet package to a Rust plugin crate unless you are
comparing against the C header or writing C/C++ interop code.
Build a DLL
Build your plugin in release mode when you are ready to deploy it locally:
cargo build --release
The resulting DLL is written to target\release\<crate-name>.dll.
Sign the DLL
WSL requires a signed DLL. In a real deployment, sign with the certificate and process used for your
environment. During local development, you can use a test certificate and SignTool.exe.
The repository includes sign-plugin.ps1 as a development helper. From an elevated PowerShell
session in a checkout of this repository:
.\sign-plugin.ps1 -PluginPath C:\path\to\my-wsl-plugin\target\release\my_wsl_plugin.dll
To also trust the generated certificate locally:
.\sign-plugin.ps1 -PluginPath C:\path\to\my-wsl-plugin\target\release\my_wsl_plugin.dll -Trust
The -Trust option imports the generated certificate into the local machine trusted root store.
Do not commit generated certificates, private keys, or signed DLLs.
If WSL rejects a locally signed plugin with TRUST_E_NOSIGNATURE, Microsoft documentation may
require Windows test signing on the test machine:
Bcdedit.exe -set TESTSIGNING ON
A reboot may be required after changing test-signing mode.
If Bcdedit.exe -set TESTSIGNING ON fails because the value is protected by Secure Boot policy,
Microsoft’s WSL plugin documentation recommends disabling Secure Boot from firmware settings before
trying again. Consider the security impact before doing that on a daily-use machine.
Register the Plugin
Register the signed DLL under the WSL plugins registry key:
$PluginName = "my-wsl-plugin"
$DllPath = "C:\path\to\my-wsl-plugin\target\release\my_wsl_plugin.dll"
Set-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Plugins" `
-Name $PluginName `
-Value $DllPath `
-Force
Use a value name and DLL path that match the plugin you are testing.
To unregister the plugin after testing, remove the registry value and restart WSL again:
Remove-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Plugins" `
-Name $PluginName `
-Force
Stop-Service -Name "wslservice" -Force
Reload WSL
Restart the WSL service, then run a WSL command:
Stop-Service -Name "wslservice" -Force
wsl.exe echo "test"
After loading the plugin, verify:
- the DLL path in the registry;
- the signature status;
- the expected plugin logs or side effects;
- Windows validation notes relevant to the change.
Common WSL plugin load failures include:
Wsl/Service/CreateInstance/CreateVm/Plugin/ERROR_MOD_NOT_FOUND: WSL could not load the DLL. Check the registered path and any native dependencies.Wsl/Service/CreateInstance/CreateVm/Plugin/TRUST_E_NOSIGNATURE: the DLL is unsigned or the signing certificate is not trusted by the machine.Wsl/Service/CreateInstance/CreateVm/Plugin/*: the plugin returned an error from the entry point orOnVMStarted.Wsl/Service/CreateInstance/Plugin/*: the plugin returned an error fromOnDistributionStarted.
Because plugins run in the WSL service process, treat host validation as a machine-affecting test: keep a note of the registry value you added, the certificate you trusted, and the service restart commands you ran.
For symptom-by-symptom checks, see Troubleshooting.
Sources
- Microsoft WSL plugin documentation: https://learn.microsoft.com/en-us/windows/wsl/wsl-plugins
- Microsoft WSL Plugin API NuGet package: https://www.nuget.org/packages/Microsoft.WSL.PluginApi
- Microsoft WSL plugin sample: https://github.com/microsoft/wsl-plugin-sample
Troubleshooting
This page maps common WSL plugin failures to the first checks to run. Start with the registered DLL path, signature, WSL service restart, and API version before changing plugin logic.
Plugin Does Not Load
Error:
Wsl/Service/CreateInstance/CreateVm/Plugin/ERROR_MOD_NOT_FOUND
Likely causes:
- the registry value points to the wrong path;
- the DLL was rebuilt under a different crate name;
- a native dependency of the DLL is missing.
Check the registered path:
Get-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Plugins"
Check that the DLL exists:
Test-Path "C:\path\to\plugin.dll"
Remember that Cargo converts hyphens to underscores for library artifact names. A crate named
my-wsl-plugin produces my_wsl_plugin.dll.
Signature Is Rejected
Error:
Wsl/Service/CreateInstance/CreateVm/Plugin/TRUST_E_NOSIGNATURE
Likely causes:
- the DLL is unsigned;
- the signing certificate is not trusted by the machine;
- Windows test signing is required for the development scenario.
Check the signature:
Get-AuthenticodeSignature "C:\path\to\plugin.dll"
For local development, sign with a test certificate and trust it on the test machine. If WSL still rejects the DLL, Microsoft documentation may require test signing:
Bcdedit.exe -set TESTSIGNING ON
A reboot may be required. If Secure Boot blocks the setting, decide whether disabling Secure Boot is acceptable for that machine before continuing.
Hook Does Not Run
Likely causes:
- the DLL was registered after WSL had already loaded plugins;
- the event you are testing does not trigger the hook you implemented;
- the hook requires a newer WSL plugin API version than the host provides.
Restart the service after registration or rebuilds:
Stop-Service -Name "wslservice" -Force
Then run a command that creates the lifecycle event you need. For VM startup hooks, a simple command is enough:
wsl.exe echo "test"
For distribution startup hooks, target a specific distribution:
wsl.exe -d Ubuntu -- echo "test"
WSL Reports Plugin Error
Errors under these paths usually mean the plugin returned an error:
Wsl/Service/CreateInstance/CreateVm/Plugin/*
Wsl/Service/CreateInstance/Plugin/*
Check which hook can fail in that path:
CreateVm/Plugin/*: entry point initialization oron_vm_started;CreateInstance/Plugin/*:on_distribution_started.
Use PluginResult with a message for user-facing policy failures. Use ordinary Result handling
for I/O, parsing, and WSL API calls instead of panicking. See
Error Handling for the difference between plugin diagnostics and plain
Windows errors.
Runtime API Is Too Old
If the plugin requests a newer API version or capability than WSL provides, initialization fails
with WSL_E_PLUGIN_REQUIRES_UPDATE.
Use #[wsl_plugin_v1(...)] for requirements that are mandatory for the plugin as a whole. Keep the
requirement as low as practical, and use runtime Result checks for optional fields or optional
behavior. The capability list and minimum API versions are centralized in
Version Capabilities.
Cleanup After a Bad Deployment
Remove the registry value and restart WSL:
Remove-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Plugins" `
-Name "my-wsl-plugin" `
-Force
Stop-Service -Name "wslservice" -Force
If WSL still behaves unexpectedly, verify that no other plugin values remain under the plugins key:
Get-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss\Plugins"