Skip to content

rfay/wsl-fix-interop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wsl-fix-interop

Re-register the WSLInterop binfmt_misc entry in a running WSL2 distro so that you can run Windows .exe files from a Linux shell again, without needing wsl --shutdown.

Symptoms

You're in a WSL2 shell. You try to run a Windows executable (wsl.exe, notepad.exe, cmd.exe, anything) from bash, and instead of running, you see:

/mnt/c/Users/you/AppData/Local/Microsoft/WindowsApps/wsl.exe: line 1: MZ: command not found

Or if you tried cmd.exe:

/mnt/c/Windows/System32/cmd.exe: line 1: MZ@: command not found

bash is treating the Windows binary as a shell script. The first two bytes of every Windows PE executable are M and Z (the magic of the old DOS header), so bash reads MZ as a command name and obviously can't find it.

Direct check:

ls /proc/sys/fs/binfmt_misc/
# register  status
# (no WSLInterop)

If WSLInterop is missing from /proc/sys/fs/binfmt_misc/, you've hit this bug. The Linux kernel doesn't know to dispatch MZ-prefixed files to the WSL /init interpreter anymore.

Why this happens

WSLInterop is the binfmt_misc kernel entry that tells Linux: "When a file starts with the bytes MZ, run it via /init." WSL registers this entry at distro boot and (when systemd is enabled) protects it via an override.conf for systemd-binfmt.service.

But binfmt_misc is shared across all running WSL2 distros. You can confirm with:

$ cat /proc/self/mountinfo | awk '$3=="binfmt_misc"'
121 114 0:35 / /proc/sys/fs/binfmt_misc rw,relatime shared:22 - binfmt_misc binfmt_misc rw

The shared:22 is the smoking gun: this filesystem is in a shared mount propagation peer group. Changes made to binfmt_misc in any one WSL2 distro propagate to all of them.

That means any of the following can wipe WSLInterop for every running distro:

  • A distro starting up without WSL's protectBinfmt override (e.g. systemd=false in /etc/wsl.conf, or a custom init).
  • wsl --terminate <some-other-distro> leaving binfmt_misc in an inconsistent state.
  • A QEMU multi-arch helper container (docker run --privileged multiarch/qemu-user-static) calling --unregister on the global table.
  • A race between two distros booting near-simultaneously: one's echo -1 racing the other's echo register.
  • Any tool, in any distro, that writes -1 to /proc/sys/fs/binfmt_misc/status (which clears the entire table).

Normally the only recovery is wsl --shutdown, which fully restarts the WSL2 utility VM. That kills every running WSL session — annoying if you have a long-running shell, an editor, a build, or an AI agent (Claude Code, Cursor, etc.) running inside a distro.

This script avoids the shutdown.

Real-world contexts where this comes up

This is rarely the result of anything you did inside the distro you're working in. It's almost always a side effect of something else on the same Windows host touching binfmt_misc in another distro or in a privileged container. Observed triggers:

  • Rancher Desktop starting or restarting. Rancher Desktop runs its container engine inside its own managed WSL2 distro and manipulates binfmt_misc as part of bringing the runtime up. Because binfmt_misc propagates across distros, the WSLInterop entry can disappear from every other running distro at the moment Rancher starts. (This is what motivated writing this tool.)
  • Docker Desktop starting, restarting, or installing. Same general pattern as Rancher Desktop — Docker Desktop's WSL backend touches binfmt_misc for cross-arch image support, which collides with WSLInterop.
  • Booting a "minimal" WSL2 distro without WSL's systemd-binfmt override. Distros that ship with [boot] systemd=false (Parrot Security is a known example), or that have a non-standard init, or where the user has set protectBinfmt=false in /etc/wsl.conf. When these come up, their /init may register WSLInterop later than another distro's reset, or never at all.
  • docker run --privileged multiarch/qemu-user-static --reset or similar QEMU multi-arch setup containers. These call --unregister on /proc/sys/fs/binfmt_misc/status, which wipes the entire table — including WSLInterop.
  • wsl --terminate <some-other-distro> while you have a session open in this one. Teardown of one distro can leave the global binfmt_misc state inconsistent.
  • Two distros booting near-simultaneously. A race between their /init processes' echo -1 > WSLInterop; echo register sequences can land in a state with no WSLInterop registered.
  • AI coding agents (Claude Code, Cursor, Aider, etc.) running inside a WSL distro that themselves drive wsl.exe to query or modify other distros. The first time interop breaks in their session, they typically cannot recover on their own because they can't run wsl --shutdown from within the distro they're running in. This script + a scoped passwordless-sudo grant lets them recover transparently. (See the Install section.)

If you experience this regularly, the most common culprit on Windows desktops is having Docker Desktop or Rancher Desktop installed alongside other WSL2 distros that you also use for development.

How it works

/usr/local/sbin/wsl-fix-interop does exactly one thing:

printf ':WSLInterop:M::MZ::/init:P\n' > /proc/sys/fs/binfmt_misc/register

That's the exact same line WSL's own /init writes at distro boot (and the same line WSL's systemd override re-asserts). It re-registers the MZ/init dispatch. Because binfmt_misc is shared across all distros, running this script in any one distro restores interop everywhere.

The script:

  • Checks /proc/sys/fs/binfmt_misc/WSLInterop first; exits cleanly if already registered. Idempotent — safe to call speculatively.
  • Writes the fixed literal string (no input substitution), so even via sudo NOPASSWD it has no attack surface.
  • Is root-owned and root-writable only, so a passwordless-sudo grant for it isn't transitive.

The companion sudoers fragment at /etc/sudoers.d/fix-binfmt grants your user NOPASSWD for only this one script path, nothing else.

Subtlety: the /init path is namespace-resolved

The binfmt entry stores the literal string /init as the interpreter — not a resolved inode. When the kernel later dispatches an MZ-prefixed file to /init, it resolves the path in the executing process's mount namespace. So even though distro A may have done the registration, when you run wsl.exe from distro B, the kernel runs B's /init. Cross-distro repair works correctly.

Install

Clone the repo and run the installer as root:

git clone https://github.com/rfay/wsl-fix-interop.git
cd wsl-fix-interop
sudo bash install.sh

By default, the installer grants passwordless sudo for the script to $SUDO_USER. Override with an explicit username:

sudo bash install.sh someuser

The installer:

  1. Copies wsl-fix-interop to /usr/local/sbin/wsl-fix-interop (mode 0755, root:root).
  2. Writes /etc/sudoers.d/fix-binfmt granting only that user only passwordless sudo for only that script path. The fragment is validated with visudo -cf before being installed, so a syntax error can't lock you out.
  3. Runs a smoke test to confirm the user really can invoke it without a password.

Use

When interop breaks (you see MZ: command not found or similar):

sudo wsl-fix-interop

That's it. Run any .exe again — it should work. The script prints whether it actually did anything:

wsl-fix-interop: WSLInterop re-registered.

…or if there was nothing to do:

wsl-fix-interop: WSLInterop already registered, nothing to do.

Install on multiple distros?

Don't need to. The shared binfmt_misc mount means a repair from any one distro fixes all of them. Install it in your primary distro — the one whose shell you're most often in — and call from there when needed.

Why not just appendWindowsPath=false?

Some sources suggest setting appendWindowsPath=false in /etc/wsl.conf as a workaround. That doesn't fix this problem at all — it changes which Windows directories appear in PATH, but binfmt_misc is what dispatches the MZ-prefixed file, not PATH. Without WSLInterop registered, you can't run Windows binaries even if you give an absolute path.

License

Apache 2.0 — see LICENSE. Use at your own risk; it pokes at a kernel sysfs interface.

Related

About

When WSL interop is broken, we can fix it from a distro

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages