Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 WSLPluginV1 for 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:

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/version when 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_new creates the plugin state.
  • on_vm_started runs after a VM starts.
  • on_vm_stopping runs before a VM stops.
  • on_distribution_started runs after a distribution starts.
  • on_distribution_stopping runs before a distribution stops.
  • on_distribution_registered runs when a distribution is registered.
  • on_distribution_unregistered runs 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 to ApiV1.
  • WSLSessionInformation: current WSL session metadata.
  • WSLDistributionInformation: online distribution metadata.
  • WSLOfflineDistributionInformation: offline distribution metadata used by registration hooks.
  • WSLVmCreationSettings: VM creation settings.
  • SessionID, DistributionID, and UserDistributionID: 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

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

CapabilityEnablesMinimum API version
DistributionInitPidWSLDistributionInformation::init_pid()2.0.5
DistributionRegisteredHookon_distribution_registered hook wiring2.1.2
DistributionUnregisteredHookon_distribution_unregistered hook wiring2.1.2
ExecuteBinaryInDistributioncommand execution inside a user distribution2.1.2
DistributionFlavorCoreWSLDistributionInformation::flavor()2.4.4
DistributionVersionCoreWSLDistributionInformation::version()2.4.4

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, and expect in 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 Windows HRESULT to the host.
  • PluginResult<T> returns PluginError, which contains an HRESULT and 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;
  • ExecuteBinary argument 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:

  1. Build the target example in release mode.
  2. Sign the DLL.
  3. Register the DLL under the WSL plugins registry key.
  4. Restart the WSL service.
  5. Run a WSL command that triggers the plugin.
  6. 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:

  1. Build the plugin DLL.
  2. Sign the DLL.
  3. Register the DLL path in the WSL plugins registry key.
  4. 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 or OnVMStarted.
  • Wsl/Service/CreateInstance/Plugin/*: the plugin returned an error from OnDistributionStarted.

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

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 or on_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"