WSL2: screen/tmux Sessions Die When the Last Terminal Closes (Post-July-2026 Lifecycle Change)¶
Problem¶
Detached screen (or tmux) sessions inside WSL2 die whenever the last terminal window closes. Reopening a terminal shows a freshly booted distro and screen -ls finds nothing. The behavior appeared suddenly on a machine where background sessions had survived terminal closes for months.
Diagnosis¶
The key insight: don't assume screen is the problem — check whether the whole distro is being torn down. The distro's systemd journal keeps a boot ledger:
On MajorRig this showed a hard behavioral flip:
- Before 2026-07-15: distro uptimes of 2–4.5 days, each ending exactly at a Windows reboot. The VM never idle-terminated; background sessions rode on that.
- After 2026-07-15: distro lifetimes of 17 seconds to 4 minutes — seven boots in two days. The VM now tears down seconds-to-minutes after the last WSL session ends.
The flip coincided with the 2026-07-15 02:31 patch-Tuesday reboot (KB5101650, KB5120102, KB5100998). Everything else was ruled out with evidence:
| Suspect | Verdict | Evidence |
|---|---|---|
| WSL package update | No | Appx 2.7.10 unchanged since Jun 30, already live during a working session Jul 14 (Windows rebooted Jul 12) |
wsl.conf / systemd enablement |
No | Unchanged since March |
| logind killing user processes | No | KillUserProcesses=no (default) |
| In-distro packages (screen, systemd, glibc) | No | All installed and live before the working Jul 14 session |
| Scheduled-task guards | No | Sessions survived before the guards existed, while they ran, and after they were disabled |
Useful commands for the elimination sweep:
# Windows boot history (which reboot activated what)
powershell.exe -Command "Get-WinEvent -FilterHashtable @{LogName='System'; Id=6005} -MaxEvents 6 | Select TimeCreated"
# Recent Windows patches
powershell.exe -Command "Get-HotFix | Sort InstalledOn -Descending | Select -First 5"
# logind kill setting
busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager KillUserProcesses
# When did in-distro packages actually change
rpm -q screen systemd --qf '%{NAME} %{VERSION}-%{RELEASE} installed %{INSTALLTIME:date}\n'
sudo dnf history list | head
Root Cause¶
One of the July 2026 Windows cumulative updates changed host-side WSL VM lifecycle enforcement: background processes inside the distro no longer count as a reason to keep it alive. This matches long-standing reports for systemd-enabled distros (microsoft/WSL#9245); it now applies here too. It is not fixable from inside the distro — no Fedora package, config, or systemd setting can stop the host from deallocating the VM.
WSL has enforced idle teardown since ~2.5.7 (mid-2025) via two separate timers: an instance timer that terminates the distro after its last client session ends, and a VM timer that deallocates the utility VM after the last distro stops. Screens die with the instance — which is why the well-known vmIdleTimeout setting alone does not help.
Fix: disable both idle timeouts in .wslconfig¶
The proper fix is two settings in %UserProfile%\.wslconfig, confirmed by a WSL maintainer in microsoft/WSL#13291:
Gotchas that cost people weeks in that issue thread:
instanceIdleTimeoutis undocumented — it appears nowhere in the official.wslconfigdocs as of 2026-07. It keeps the distro instance (where your screens live) alive.- It goes under
[general], not[wsl2]. Put it in the wrong section and it silently does nothing. vmIdleTimeout=-1alone does not work — the instance still dies, taking screen/tmux with it. Both lines are required.
Apply with wsl --shutdown and relaunch. Verified on MajorRig 2026-07-16: after the config, detached screens survived closing every terminal and SSH session, with no keep-alive process running.
Limits: like any in-WSL solution, this does not survive Windows reboots or sleep — nothing auto-starts the distro after a reboot. Anything that must outlive those belongs on an always-on host.
Fallback: hidden keep-alive scheduled task¶
If a future WSL update breaks the undocumented setting, the version-proof workaround is to hold a wsl.exe client open permanently (WSL never idles an instance with an attached client). This was MajorRig's original same-day fix and is kept registered-but-disabled as a fallback. Windowless, so it never flashes a console:
$action = New-ScheduledTaskAction -Execute 'conhost.exe' `
-Argument '--headless wsl.exe -d FedoraLinux-43 --exec sleep infinity'
$trigger = New-ScheduledTaskTrigger -AtLogOn -User 'MajorRig\majorlinux'
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName 'MajorRig-WSL-KeepAlive' -Action $action -Trigger $trigger -Settings $settings
Start-ScheduledTask -TaskName 'MajorRig-WSL-KeepAlive'
Notes:
conhost.exe --headlessis the no-window trick. A bare console app in an interactive scheduled task flashes (or keeps) a console window — that popup is what got MajorRig's 15-minute guard tasks disabled on 2026-07-09.- The task must run as the logged-on user with
LogonType Interactive:wsl.exerefuses to run as SYSTEM (WSL_E_LOCAL_SYSTEM_NOT_SUPPORTED). - No
ExecutionTimeLimit, auto-restart on failure,IgnoreNewfor duplicate instances. - Limits: does not survive Windows reboots or sleep. Anything that must outlive those belongs on an always-on host, not in WSL.
Verify: close every terminal, wait two minutes, reopen, screen -ls — the detached session should still be there.
Bonus gotcha: portproxy rules that look right but bind nothing¶
While re-verifying MajorRig's port-2222 break-glass channel (netsh portproxy → WSL-native sshd), the rule text was correct but nothing was listening on the port:
netsh interface portproxy show v4tov4 # rule present and correct
netstat -an | findstr :2222 # ...but no LISTENING line
Cause: if the IP Helper service (iphlpsvc) evaluates the rule before the listen address exists — e.g. a Tailscale IP shortly after boot — the bind fails silently and is not retried on a schedule. A guard script that only compares rule text will report OK forever while the channel is dead.
Fix: make the guard also check for a live listener and force a rebind by deleting and re-adding the rule:
$listening = (netstat -an | Select-String "TCP\s+$([regex]::Escape($listenAddr)):$listenPort\s.*LISTENING") -ne $null
if (-not $currentlyPointsHere -or -not $listening) {
netsh interface portproxy delete v4tov4 listenaddress=$listenAddr listenport=$listenPort | Out-Null
netsh interface portproxy add v4tov4 listenaddress=$listenAddr listenport=$listenPort connectaddress=$wslIp connectport=$connectPort | Out-Null
}
End-to-end test from another host — a TCP connect is not enough, demand the banner:
Also worth doing while in there: netsh interface portproxy show v4tov4 and delete any rules pointing at WSL IPs from previous boots — they're dead weight that fails silently.
Bonus gotcha 2: stale $WSL_INTEROP sockets in surviving sessions¶
Once sessions do survive, a follow-on failure appears: Windows interop calls (cmd.exe, powershell.exe, wsl.exe) from inside the surviving shell fail with:
Cause: $WSL_INTEROP points at a per-session socket (/run/WSL/<pid>_interop) that died with the login session that spawned the shell. Any process living in screen/tmux past its original terminal inherits the dead pointer. Fix from inside the surviving shell:
The init-owned sockets (1_interop, 2_interop) persist for the life of the instance. To find live ones generically:
for s in /run/WSL/*_interop; do
WSL_INTEROP=$s timeout 5 cmd.exe /c "echo ok" >/dev/null 2>&1 && echo "LIVE: $s"
done
Lessons¶
- When background sessions die, check the distro boot ledger first.
journalctl --list-bootsdistinguishes "my process was killed" from "the whole VM was torn down" in one command. - Correlate with Windows patch/reboot history before blaming in-distro changes. Package installs only take effect at the next distro restart, which on WSL usually means the next Windows reboot — the activation date is the reboot, not the install.
- A netsh portproxy rule is not a listener. Verify with
netstat, and test remotely by demanding the SSH banner, not just a TCP connect. - When the documented knob doesn't work, search the project's issue tracker before building a workaround. The real fix (
instanceIdleTimeoutunder[general]) exists only in a maintainer comment on microsoft/WSL#13291 — the docs still don't mention it. - Interop failures in screen/tmux survivors are usually a stale
$WSL_INTEROP, not a broken WSL — re-point it at/run/WSL/1_interop.