WSLg Architecture, Interop, and Remote GUI Launching¶
Deep technical reference for advanced Linux users. Covers the full stack from WSLg compositor internals through WSL interop sockets to the Windows session-isolation model that blocks GUI apps launched over SSH.
1. WSLg Architecture¶
1.1 High-Level Data Flow (Text Diagram)¶
Windows Desktop (Session 1)
+-----------------------------------------------+
| msrdc.exe / mstsc.exe (RDP client, silent) |
| ^ |
| | RDP over Hyper-V socket (VAIL mode) |
| | -- shared-memory via virtio-fs -- |
+------|-----------------------------------------+
|
====== | ==== VM boundary (Hyper-V lightweight VM) =========
|
System Distro (CBL-Mariner / Azure Linux, PID namespace)
+-----------------------------------------------+
| WSLGd (supervisor daemon, PID 1 equivalent) |
| | |
| +-- Weston compositor |
| | +-- RDP backend (libweston, FreeRDP) |
| | +-- RAIL/VAIL shell plugin |
| | +-- rdpapplist virtual channel |
| | +-- XWayland (X11 compat) |
| | |
| +-- PulseAudio server |
| +-- RDP sink/source plugins |
| |
| Exported sockets (bind-mounted into user |
| distro via /mnt/wslg): |
| /tmp/.X11-unix/X0 (X11) |
| /mnt/wslg/runtime-dir/wayland-0 (Wayland) |
| /mnt/wslg/PulseServer (PulseAudio) |
+-----------------------------------------------+
| sockets projected via bind-mount
v
User Distro (Ubuntu, Debian, etc.)
+-----------------------------------------------+
| Environment set by /init: |
| DISPLAY=:0 |
| WAYLAND_DISPLAY=wayland-0 |
| PULSE_SERVER=/mnt/wslg/PulseServer |
| XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir |
| |
| Linux GUI app --> connects to X11 socket |
| or Wayland socket --> Weston renders |
| --> FreeRDP encodes --> RDP to host |
+-----------------------------------------------+
1.2 The System Distro¶
WSLg runs inside a separate, hidden WSL distribution based on CBL-Mariner (now called Azure Linux), Microsoft's internal Linux distro. This system distro:
- Runs in the same Hyper-V lightweight utility VM as user distros but in its own PID/mount namespace (think of it as a container).
- Contains a minimal Linux userspace -- just enough to run Weston, XWayland, PulseAudio, and the WSLGd supervisor.
- Is independently serviced by Microsoft (updates come through the WSL package, not through your distro's package manager).
- Cannot be directly entered by the user (it has no shell by default; you
would need
wsl --systemto poke around).
The system distro approach isolates the display server from the user's distro, meaning WSLg works identically whether you run Ubuntu 22.04, Fedora, or Arch.
1.3 Weston Compositor + RDP Backend¶
Weston is the Wayland reference compositor. Microsoft forked it and made substantial additions:
| Component | Role |
|---|---|
RDP backend (rdp-backend.so) |
Replaces the DRM/KMS backend. Instead of driving real GPU hardware, Weston renders frames and hands them to FreeRDP for encoding as RDP traffic. |
| FreeRDP (embedded) | Open-source RDP implementation. Encodes all visual output from the Weston RDP Server; decodes input events from the Windows RDP Client (mstsc/msrdc). |
| RAIL shell | Remote Application Integrated Locally -- an RDP extension where individual application windows (not a full desktop) are transmitted. Each Linux window becomes a separate top-level Win32 window. |
| VAIL mode | Virtualized Application Integrated Locally -- optimized variant of RAIL for VM-local connections. Uses shared memory between guest and host via virtio-fs rather than copying pixels over the RDP transport. VAIL is what WSLg actually uses. |
| rdpapplist virtual channel | A custom RDP Dynamic Virtual Channel named Microsoft::Windows::RDS::RemoteApplicationList. Weston scans .desktop files in the user distro, sends application names, icons, and launch commands to the host. On the Windows side, the WSLDVCPlugin (loaded by mstsc) processes this list and creates Start Menu shortcuts. |
How VAIL shared memory works:
In VAIL, the RDP server (Weston inside the VM) and the RDP client (msrdc on Windows) share a memory region via virtio-fs (enabled in the WSL2 kernel since 5.10.16+). When Weston finishes compositing a window frame:
- It writes the pixel data into the shared-memory region (under
/mnt/wslg/). - It sends a lightweight RDP notification to the host side: "window X has new content at offset Y."
- The host-side RDP client reads directly from shared memory -- no serialization, no network copy.
This is dramatically faster than classic RAIL, which would encode every frame as an RDP bitmap update over the transport.
1.4 The RDP Connection Lifecycle¶
When WSL boots and GUI support is needed:
- WSLGd starts as the first process in the system distro.
- WSLGd launches Weston (with the RDP backend + XWayland) and PulseAudio.
- WSLGd invokes msrdc.exe (or mstsc.exe) on the Windows host in silent mode with a command line like:
msrdc.exe /v:<VM-GUID> /silent \
/hvsocketserviceid:34D15B6B-FACB-11E6-BD58-64006A7986D3 \
/wslg /plugin:WSLDVC \
/wslgsharedmemorypath:WSL\<VM-GUID>\wslg \
C:\ProgramData\Microsoft\WSL\wslg.rdp
Key flags:
- /silent -- no RDP connection dialog is shown to the user.
- /hvsocketserviceid -- identifies the Hyper-V socket endpoint for the VM.
- /plugin:WSLDVC -- loads the WSL Dynamic Virtual Channel plugin that
handles rdpapplist (Start Menu integration).
- /wslgsharedmemorypath -- path to the shared-memory region used by VAIL.
- The RDP connection stays permanently open. New GUI windows appear instantly without connection setup overhead.
- WSLGd monitors all child processes. If Weston or PulseAudio crash, they are restarted automatically.
1.5 X11 and Wayland Socket Sharing¶
The system distro exposes its servers to user distros through bind mounts:
| Socket | Path in user distro | Protocol |
|---|---|---|
| X11 display | /tmp/.X11-unix/X0 |
X11 (via XWayland) |
| Wayland compositor | /mnt/wslg/runtime-dir/wayland-0 |
Wayland |
| PulseAudio | /mnt/wslg/PulseServer |
PulseAudio native |
The environment variables DISPLAY=:0, WAYLAND_DISPLAY=wayland-0, and
PULSE_SERVER=/mnt/wslg/PulseServer are set automatically by WSL's /init
for every new shell session.
Important: Since WSL 0.60.0, /tmp/.X11-unix is a bind mount (not a
symlink). This matters for snap/flatpak containers that need to remount it.
WSL also bind-mounts /dev/null over /lib/tmpfiles.d/tmp.conf and x11.conf
to prevent systemd-tmpfiles from removing the X11 socket on boot.
1.6 Audio: PulseAudio and PipeWire¶
WSLg runs a PulseAudio server inside the system distro with custom RDP sink and source plugins:
- Audio from Linux apps flows into PulseAudio.
- The RDP audio plugins encode the PCM streams and inject them into the RDP transport (the same connection used for graphics).
- On the Windows host, the RDP client decodes the audio and plays it through the Windows audio subsystem (WASAPI).
- Microphone input follows the reverse path.
PipeWire: As of early 2026, WSLg still uses PulseAudio natively.
User-distro-side PipeWire works because PipeWire provides a PulseAudio
compatibility layer (pipewire-pulse). Applications that use PipeWire's
native API can still output audio as long as PipeWire's PulseAudio bridge is
configured to connect to the WSLg PulseAudio server.
1.7 Does WSLg Work Over SSH?¶
Short answer: not out of the box, but it can be made to work with caveats.
When you SSH into a WSL2 instance, you get a shell that was spawned by
sshd, which:
- Does not automatically set
DISPLAY=:0orWAYLAND_DISPLAY(those are set by/initfor interactive WSL terminal sessions, not by sshd). - Does not set
XAUTHORITYpointing to WSLg's auth cookie.
Workaround to display Linux GUI apps on the Windows desktop via SSH:
# In your SSH session inside WSL2:
export DISPLAY=:0
export WAYLAND_DISPLAY=wayland-0
export XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir
export PULSE_SERVER=/mnt/wslg/PulseServer
# Test:
xclock & # X11 app -- should appear on Windows desktop
This works because:
1. WSLg's Weston is always running (as long as the WSL VM is up).
2. The X11 socket at /tmp/.X11-unix/X0 is accessible to all processes in the
user distro (it is a bind mount from the system distro).
3. You are connecting to the local X server, not forwarding over the
network.
Caveats:
- XAUTHORITY may not be set. WSLg's XWayland may or may not enforce
authentication depending on version. Some apps (notably anything using
MIT-MAGIC-COOKIE-1) may fail with "X11 connection rejected because of wrong
authentication." Solution: copy or symlink the .Xauthority file from a
local WSL session, or disable xauth in WSLg's XWayland config.
- Wayland-native apps require both WAYLAND_DISPLAY and XDG_RUNTIME_DIR
to be set correctly.
- This only displays on the local Windows desktop. If you are SSHing from
a remote machine, you will not see the GUI windows (they render on the
machine running WSL, not on your SSH client).
X11 forwarding over SSH (ssh -X):
You can also SSH from WSLg to a remote Linux host with -X:
# From a WSL2 terminal (WSLg active):
ssh -X user@remote-linux-host
firefox & # renders on your local Windows desktop via WSLg's X server
DISPLAY=localhost:10.0 on the remote
side, and tunnels X11 traffic back to WSLg's XWayland on :0.
2. WSL Interop Architecture¶
2.1 How notepad.exe from Bash Actually Works¶
When you type notepad.exe in a WSL bash session, the following chain
executes:
User types: notepad.exe
|
v
Linux kernel exec() syscall
|
v
binfmt_misc handler detects MZ header (PE/COFF)
(registered via /proc/sys/fs/binfmt_misc/WSLInterop)
|
v
Kernel invokes: /init notepad.exe
|
v
/init reads $WSL_INTEROP env var
--> connects to Unix socket: /run/WSL/<pid>_interop
|
v
Interop server (Linux-side process)
--> sends LxInitMessageCreateProcessUtilityVm (WSL2)
over hvsocket to wslhost.exe on Windows
|
v
wslhost.exe on Windows
--> calls CreateProcess("notepad.exe")
--> relays stdout/stderr/stdin back to /init
--> /init relays to the calling bash process
|
v
notepad.exe appears on the Windows desktop
(launched in the same session as wslhost.exe)
2.2 binfmt_misc Registration¶
Linux's binfmt_misc subsystem allows the kernel to recognize non-native
binary formats and delegate execution. WSL registers a handler:
# View the registration:
cat /proc/sys/fs/binfmt_misc/WSLInterop
# Typical output:
enabled
interpreter /init
flags: PF
offset 0
magic 4d5a # "MZ" -- the DOS/PE magic number
- WSL1:
/initregisters the binfmt entry per-distribution. - WSL2:
mini_initregisters it at the VM level (once for all distros sharing the VM).
The F flag means "fix binary" -- the kernel resolves the interpreter path at
registration time, not at execution time. This ensures /init is always found
even if PATH changes.
2.3 The WSL_INTEROP Socket¶
WSL_INTEROP is an environment variable pointing to a Unix domain socket,
typically at /run/WSL/<pid>_interop.
Architecture:
bash (user shell, PID=1234)
|
| $WSL_INTEROP=/run/WSL/567_interop
|
v
/init (invoked by binfmt_misc)
|
| connect() to /run/WSL/567_interop
v
Interop Server (PID=567, runs inside WSL)
|
| hvsocket connection (AF_VSOCK on WSL2)
v
wslhost.exe (Windows side)
|
| CreateProcess / relay I/O
v
notepad.exe (Windows)
How socket resolution works:
/initchecks$WSL_INTEROP. If set, use that socket path.- If not set, try
/run/WSL/<own-PID>_interop. - If that fails, try parent's PID, then grandparent, walking up the
process tree until reaching PID 1 (
init).
Each session leader (the first process in a new login session, e.g., the
shell spawned by wsl.exe) gets its own interop server and socket. This is
why the PID is part of the socket name.
2.4 The 9P Filesystem Protocol (/mnt/c)¶
Windows drives are mounted in WSL2 via the 9P protocol (Plan 9 from Bell Labs' filesystem protocol):
Linux process reads /mnt/c/Users/foo/file.txt
|
v
VFS layer: recognizes /mnt/c as a 9P mount
|
v
9P client in Linux kernel
| (virtio transport over Hyper-V VMBus)
v
9P file server on Windows (started by wslservice.exe)
|
v
Windows NTFS: reads the actual file
|
Response back through 9P protocol
Mount types (configurable via .wslconfig):
| Mount type | Protocol | Notes |
|---|---|---|
drvfs |
Direct (WSL1 only) | Translates syscalls to NT API directly |
plan9 |
9P over VMBus | WSL2 default |
virtio-plan9 |
9P over virtio | Alternative WSL2 transport |
virtiofs |
virtio-fs (FUSE) | Newer, higher performance, not yet default everywhere |
Performance implications: Every file operation on /mnt/c requires a
round-trip through the 9P protocol across the VM boundary. This is why
cross-filesystem operations (e.g., git status on a repo in /mnt/c) are
dramatically slower than operations within the ext4 rootfs (/home/user/).
2.5 Why Interop Breaks in SSH Sessions¶
When you SSH into WSL2, the sshd daemon forks a child process to handle your
session. This child:
- Was not spawned by
wsl.exeor the Windows Terminal. - Does not have an interop server created for it.
- Has no
WSL_INTEROPenvironment variable set.
When you try notepad.exe, /init walks up the process tree trying to find a
valid interop socket. If it reaches PID 1 (the distribution's init) and finds
no socket, the execution fails with:
-bash: /mnt/c/Windows/system32/notepad.exe: cannot execute: required key not available
# or
ERROR: UtilConnectToInteropServer:300: connect failed 2
The fundamental issue: Interop sockets are session-scoped. They are
created when wsl.exe (or Windows Terminal) spawns a new shell. SSH sessions
bypass this path entirely.
2.6 Workaround: Manually Setting WSL_INTEROP¶
You can restore interop in SSH sessions by borrowing a socket from an existing interactive session:
# Find the most recent valid interop socket:
export WSL_INTEROP=$(ls -t /run/WSL/*_interop 2>/dev/null | head -1)
# Verify it works:
echo $WSL_INTEROP
notepad.exe # Should launch if a valid socket exists
Automated approach in .bashrc or .zshrc:
# Fix WSL interop for SSH/tmux/screen sessions
if [ -z "$WSL_INTEROP" ] || [ ! -S "$WSL_INTEROP" ]; then
for sock in /run/WSL/*_interop; do
if [ -S "$sock" ]; then
export WSL_INTEROP="$sock"
break
fi
done
fi
Important caveats:
- This only works if there is at least one active interactive WSL session
(e.g., a Windows Terminal tab running WSL). If all interactive sessions are
closed, no valid sockets exist in /run/WSL/.
- Sockets are owned by specific processes. When the owning process exits, the
socket file is deleted, and your WSL_INTEROP becomes a dangling reference.
- With systemd enabled, the binfmt service may be named WSLInterop-late
instead of WSLInterop, causing detection scripts to falsely report interop
as disabled.
3. Remotely Launching Windows GUI Applications¶
3.1 Windows Session Architecture¶
Windows isolates processes into sessions, each with its own window station and desktops:
Session 0 (Service Session)
+------------------------------------------+
| Services: sshd, wslservice, spooler... |
| Window Station: Service-0x0-xxxxx |
| No physical display connected |
| GUI rendered to invisible framebuffer |
+------------------------------------------+
Session 1 (First Interactive Logon)
+------------------------------------------+
| explorer.exe, user apps, taskbar |
| Window Station: WinSta0 |
| Desktop: \Default |
| Connected to physical display / RDP |
+------------------------------------------+
Session 2 (Second logon, e.g., RDP)
+------------------------------------------+
| Another user's desktop |
+------------------------------------------+
Key rules: - Before Windows Vista, services and the first interactive user shared Session 0. This was a massive security risk (shatter attacks). - Starting with Vista, Session 0 is always non-interactive. Services run here. No user desktop, no monitor output. - The first locally-logged-in user gets Session 1. RDP users get Session 2+. - A process in Session 0 cannot display a window on Session 1's desktop by default.
3.2 Why GUI Apps from SSH Are Invisible¶
Windows' OpenSSH server (sshd) runs as a Windows service in Session 0.
When you SSH in and run:
The process is created in Session 0. Notepad launches, creates a window, but that window is on Session 0's invisible desktop. From the user's perspective, nothing happens. The process may appear in Task Manager but produces no visible window.
This is by design -- Session 0 isolation prevents services from interfering with the user's desktop.
3.3 The schtasks /IT Workaround¶
The Task Scheduler can create tasks that run in the interactive user's
session. The /IT flag (Interactive Token) specifies that the task should
only run when the user is logged on and should use their interactive session.
# Create a task that launches notepad in the interactive session:
schtasks /create /tn "LaunchNotepad" /sc once /st 00:00 /tr "notepad.exe" /rl highest /it /f
# Trigger it immediately:
schtasks /run /tn "LaunchNotepad"
# Clean up:
schtasks /delete /tn "LaunchNotepad" /f
How it crosses session boundaries:
schtasks /createregisters the task with the Task Scheduler service (which runs in Session 0).- When triggered, the Task Scheduler determines the active interactive
session (using
WTSGetActiveConsoleSessionId()). - It obtains a token for the logged-in user (via
WTSQueryUserToken()). - It calls
CreateProcessAsUser()with that token, targetingWinSta0\Defaultin the interactive session. - The process starts in Session 1 (or whichever session the user is in), with full desktop access.
Limitations: - Requires a user to be interactively logged in (physically or via RDP). - The task runs as that user, with their permissions. - Error reporting is poor compared to direct process execution.
Wrapper script for SSH convenience:
# launch-gui.ps1 -- run from SSH to launch a GUI app on the desktop
param([string]$Command)
$taskName = "SSHLaunch_$(Get-Random)"
schtasks /create /tn $taskName /sc once /st 00:00 /tr $Command /rl highest /it /f | Out-Null
schtasks /run /tn $taskName | Out-Null
Start-Sleep -Seconds 2
schtasks /delete /tn $taskName /f | Out-Null
3.4 PsExec -i Approach¶
Sysinternals' PsExec can target a specific session ID with -i:
# Launch notepad on the interactive desktop (session 1):
psexec -accepteula -i 1 notepad.exe
# Find the current interactive session:
query session
# Look for the session marked "Active" with a user name.
# Then use that session ID with -i.
How PsExec works internally:
- PsExec installs a temporary Windows service (
PSEXESVC) on the target machine. - This service executes the requested command.
- The
-i <session>flag causes the service to callCreateProcessAsUser()targeting the specified session's window station.
Limitations:
- PsExec -i from an SSH session often fails on modern Windows because the
service may not have the required SeTcbPrivilege to call
WTSQueryUserToken().
- Administrative privileges are required.
- Some applications fail silently or hang.
- Not always reliable for complex GUI apps (only simple ones like notepad).
3.5 The CreateProcessAsUser / WTSQueryUserToken Pattern (Programmatic)¶
The "correct" Win32 API pattern for launching an interactive GUI from a service:
1. WTSGetActiveConsoleSessionId()
--> returns session ID of the physical console user (e.g., 1)
--> returns (DWORD)-1 if nobody is logged in locally
2. WTSQueryUserToken(sessionId, &hToken)
--> requires SE_TCB_NAME privilege (LocalSystem only)
--> returns a primary token for the interactive user
3. DuplicateTokenEx(hToken, ..., TokenPrimary, &hDupToken)
--> create a usable primary token
4. CreateEnvironmentBlock(&lpEnvironment, hDupToken, FALSE)
--> build the correct environment for that user
5. CreateProcessAsUser(hDupToken, NULL, "notepad.exe",
..., "WinSta0\\Default", ...)
--> launch the process on the interactive desktop
This is what Task Scheduler and PsExec do under the hood.
3.6 Named Pipe / COM Automation Approach¶
Instead of fighting session isolation, you can architect a solution with a helper process running in the interactive session:
Architecture:
Session 0 (SSH / Service) Session 1 (Interactive Desktop)
+-------------------------+ +-----------------------------+
| Your SSH command | | GuiLauncher.exe |
| writes to named pipe: | ---------> | (runs at user logon) |
| \\.\pipe\GUILaunch | named | reads pipe, calls |
| "notepad.exe C:\x.txt" | pipe IPC | CreateProcess("notepad") |
+-------------------------+ +-----------------------------+
Implementation sketch:
- Create a small "launcher agent" that runs at user logon (via Startup folder
or Task Scheduler
AtLogontrigger). - The agent listens on a named pipe (e.g.,
\\.\pipe\GUILaunch). - From SSH, write the command to the pipe:
- The agent reads the command and calls
CreateProcess()in its own session (which is the interactive session).
COM automation variant: Configure a COM server's identity as "Interactive User" in DCOMCNFG. When activated from Session 0, the COM runtime creates the object in the interactive session. However, this is fragile and increasingly restricted in modern Windows.
3.7 PowerShell Remoting (Enter-PSSession) to the Same Machine¶
# From SSH, try loopback remoting:
Enter-PSSession -ComputerName localhost
Start-Process notepad.exe
This does NOT solve the GUI problem. PowerShell remoting uses WinRM, which creates a network logon session (logon type 3), not an interactive session. The process runs in the WinRM host process's session, which is Session 0. The notepad window is invisible.
PowerShell remoting sessions do not provide desktop access regardless of whether you connect to localhost or a remote machine.
3.8 WinRM / CIM Sessions¶
WinRM (Windows Remote Management) and CIM (Common Information Model) sessions have the same fundamental limitation:
- WinRM executes commands through the
WinRMservice (Session 0). Invoke-Command -ComputerName localhost { Start-Process notepad }runs notepad in Session 0 -- invisible.- CIM sessions (
New-CimSession) are for WMI queries, not process launching.
WinRS (Windows Remote Shell) is similarly constrained -- it uses the WinRM backend.
Bottom line: No remote management protocol natively supports launching visible GUI applications. The only built-in Windows protocol that provides interactive desktop access is RDP (mstsc / Remote Desktop).
3.9 Summary: Methods to Launch Visible GUI from SSH¶
| Method | Works? | Mechanism | Reliability |
|---|---|---|---|
Direct notepad.exe from SSH |
No | Launches in Session 0 (invisible) | N/A |
schtasks /create ... /it + /run |
Yes | Task Scheduler uses CreateProcessAsUser |
Good; recommended |
psexec -i 1 notepad.exe |
Sometimes | Service + CreateProcessAsUser |
Unreliable on modern Windows |
| Named pipe launcher agent | Yes | Agent in interactive session reads commands | Excellent; requires setup |
| PowerShell remoting (loopback) | No | Network logon, Session 0 | N/A |
| WinRM / CIM | No | Service session | N/A |
| COM Interactive User | Fragile | COM runtime session bridging | Deprecated pattern |
CreateProcessAsUser (custom code) |
Yes | Direct Win32 API | Best if you can write C/C# |
4. Linux GUI Apps Over SSH (Reverse Direction)¶
4.1 SSH into WSL2, Display Linux GUI on Windows Desktop via WSLg¶
Yes, this works -- with configuration.
When you SSH into a WSL2 instance from a remote machine, WSLg's Weston compositor is already running (if any WSL distro is active). You just need to point your SSH session at the local display:
# SSH into WSL2:
ssh user@wsl2-host
# Set up WSLg display variables:
export DISPLAY=:0
export WAYLAND_DISPLAY=wayland-0
export XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir
export PULSE_SERVER=/mnt/wslg/PulseServer
# Launch a GUI app:
gedit & # Appears on the Windows desktop of the machine running WSL2
The GUI appears on the Windows desktop of the host machine, not on your SSH client's screen. This is useful for: - Headless automation where you need a GUI app running on the target. - VNC/RDP scenarios where someone is sitting at the Windows machine.
Automation tip: Add the exports to ~/.bashrc (guarded by a check for
WSLg's presence):
# Auto-configure WSLg for non-interactive sessions (SSH, cron, etc.)
if [ -d /mnt/wslg ] && [ -z "$DISPLAY" ]; then
export DISPLAY=:0
export WAYLAND_DISPLAY=wayland-0
export XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir
export PULSE_SERVER=/mnt/wslg/PulseServer
fi
4.2 X11 Forwarding Over SSH (ssh -X) with WSLg¶
Two scenarios:
Scenario A: WSLg as X11 client (SSHing OUT from WSL2):
This works. The remote firefox forwards X11 traffic back through the SSH
tunnel to WSLg's XWayland on :0. Firefox appears as a window on the
Windows desktop.
Scenario B: SSHing INTO WSL2 with X11 forwarding:
This forwards X11 traffic to your local X server (on your workstation). The app renders on your screen, not the Windows desktop. This is standard X11 forwarding -- WSLg is not involved at all.
For this to work:
- sshd inside WSL2 must have X11Forwarding yes in /etc/ssh/sshd_config.
- xauth must be installed in the WSL2 distro.
- Your local machine must have an X11 server (X.Org, XQuartz on Mac, WSLg
itself if you are on another WSL2 instance, or a third-party X server like
VcXsrv/X410 on Windows).
4.3 Wayland Forwarding Considerations¶
Wayland has no built-in network transparency. Unlike X11, which was designed as a client-server protocol that can run over a network, Wayland uses local Unix sockets and shared memory.
Current state:
- There is no wayland-forwarding equivalent of ssh -X in mainstream
SSH implementations.
- Wayland apps running over SSH will only work if they fall back to XWayland
(most GTK/Qt apps do this when WAYLAND_DISPLAY is unset but DISPLAY is).
- waypipe is an experimental tool that tunnels Wayland over SSH, but it is
not widely deployed and has compatibility issues.
- For WSLg specifically: if you SSH in and set DISPLAY=:0, Wayland-native
apps that have X11 fallback will use XWayland. Pure Wayland apps (rare) need
WAYLAND_DISPLAY set and the socket accessible.
Practical recommendation: For SSH scenarios, rely on X11 (XWayland) rather
than native Wayland. Set GDK_BACKEND=x11 or QT_QPA_PLATFORM=xcb to force
X11 mode for GTK/Qt apps if needed.
5. Quick Reference: Environment Variables¶
| Variable | Local WSL Terminal | SSH into WSL2 (manual) | SSH -X into WSL2 |
|---|---|---|---|
DISPLAY |
:0 (set by /init) |
Set to :0 for WSLg local display |
localhost:10.0 (set by sshd) |
WAYLAND_DISPLAY |
wayland-0 |
Set to wayland-0 for WSLg |
Not applicable |
PULSE_SERVER |
/mnt/wslg/PulseServer |
Set manually | Not forwarded |
WSL_INTEROP |
/run/WSL/<pid>_interop |
Not set; must borrow from active session | Not set |
XDG_RUNTIME_DIR |
/mnt/wslg/runtime-dir |
Set manually | Not applicable |
6. Troubleshooting Reference¶
WSLg GUI not appearing¶
# Verify WSLg system distro is running:
wsl.exe --system -- cat /etc/os-release # Should show CBL-Mariner or Azure Linux
# Check Weston is running:
wsl.exe --system -- pgrep -a weston
# Verify sockets exist:
ls -la /tmp/.X11-unix/X0
ls -la /mnt/wslg/runtime-dir/wayland-0
Interop not working in SSH¶
# Check binfmt registration:
cat /proc/sys/fs/binfmt_misc/WSLInterop 2>/dev/null || \
cat /proc/sys/fs/binfmt_misc/WSLInterop-late
# List available interop sockets:
ls -la /run/WSL/*_interop
# Set WSL_INTEROP to the newest valid socket:
export WSL_INTEROP=$(ls -t /run/WSL/*_interop 2>/dev/null | head -1)
# Test:
cmd.exe /c echo hello
Windows GUI app invisible from SSH¶
# Find interactive session ID:
query session
# Look for "Active" session (usually session ID 1 or 2)
# Launch via schtasks:
schtasks /create /tn "TempGUI" /sc once /st 00:00 /tr "notepad.exe" /it /f
schtasks /run /tn "TempGUI"
schtasks /delete /tn "TempGUI" /f
Created: 2026-03-16
Sources: - WSLg Architecture -- Microsoft DevBlogs - WSLg GitHub Repository - WSLg README - WSL Interop Technical Documentation - WSL Init Technical Documentation - WSL Boot Process - DrvFS & Plan9 -- WSL Technical Documentation - Weston Compositor -- DeepWiki - FreeRDP Integration -- DeepWiki - Session 0 Isolation -- Microsoft TechCommunity - Session 0 Isolation -- FireDaemon KB - Inside Session 0 Isolation -- Alex Ionescu - Interactive Services -- Win32 Microsoft Learn - Launching Interactive Process from Service -- Microsoft Learn - OpenSSH GUI Launch Issue #998 - WSL_INTEROP Socket Issue #5065 - WSLg X11 Forwarding Issue #153 - WSLg X11 Display Socket - PsExec -- Sysinternals - 9P Protocol -- Wikipedia - Windows Interoperability -- NixOS-WSL DeepWiki