- systemd-user environment generator and appsandbox-gpu wrapper both
append /usr/lib/wsl/lib to LD_LIBRARY_PATH. ldconfig has the .so.1
entries cached, but the 9P mount is read-only so the unversioned
symlinks ldconfig wants to create can't be written. dlopen() of
unversioned names (libcuda.so, libnvidia-encode.so, libnvcuvid.so)
needs the path on LD_LIBRARY_PATH — without it ffmpeg's h264_nvenc /
h264_cuvid plugins fail with 'Cannot load libcuda.so.1'.
- setup.sh: symlink /usr/lib/wsl/lib/nvidia-smi to /usr/local/bin so
the bare command works without env tweaks. Created unconditionally
because the symlink target is resolved at exec time — the file
appears under the agent's 9P mount on first boot regardless of when
the symlink was created.
Ubuntu 26.04 LTS guest end-to-end:
- cidata-driven autoinstall pipeline (subiquity user-data + nested
cloud-init bootcmd) in disk_util.c. Static NAT IP, ssh-via-vsock
marker, serial-debug kernel cmdline injected into ESP grub.cfg by
iso-patch.exe. Five separate firstboot pipeline fixes get the VM
to GDM unattended on Hyper-V Gen 2.
- Linux guest stack under tools/linux/:
- appsandbox-agent (vsock :1 control + clipboard helper spawn)
- appsandbox-audio (snd-aloop -> vsock :4)
- appsandbox-display (KMS framebuffer capture -> vsock :2)
- appsandbox-input (uinput injection <- vsock :3)
- appsandbox-clipboard (XCB selection bridge <-> vsock :5/:6)
Helper binaries staged via release/resources/linux/.
- asb_drm DRM module: virtual single-CRTC display sink the host
captures over vsock. Platform-bus, DRIVER_GEM/MODESET/ATOMIC/RENDER,
linear shmem framebuffers. asb-evict-simpledrm.service unbinds the
EFI simple-framebuffer before display-manager so mutter binds
asb_drm as the sole DRM card.
- dxgkrnl vendored byte-identical from microsoft/WSL2-Linux-Kernel
linux-msft-wsl-6.18.y with our DKMS/out-of-tree shim (dxgkrnl_compat.h
+ Kbuild + dkms.conf). Autoinstall late-commands dkms-builds against
every /target/lib/modules/<kver> so kernel-version drift between ISO
and post-update reboot still leaves a working .ko.
- wsl-mesa custom build (Mesa 25.3 + d3d12 gallium + microsoft-
experimental dzn Vulkan). Installs to /opt/wsl-mesa parallel to
Ubuntu's stock Mesa. Prebuilt tarball for ubuntu-26.04-amd64
shipped under tools/linux/wsl-mesa/prebuilt/ (21 MB).
- Per-VM GPU acceleration policy:
- /etc/systemd/user-environment-generators/50-appsandbox-gpu
emits GALLIUM_DRIVER=d3d12 + LD_LIBRARY_PATH + VK_DRIVER_FILES
into the user@1000 systemd-user environment when /dev/dxg and
/opt/wsl-mesa are present. Inherited by every app-*.scope a
.desktop launch creates.
- /etc/systemd/user/org.gnome.Shell@.service.d/no-gpu.conf
UnsetEnvironment= strips these for the compositor — mutter
stays on llvmpipe so asb_drm scanout works (the d3d12 pipe has
no GBM compatibility with asb_drm).
- /usr/local/bin/appsandbox-gpu wrapper for terminal-launched
opt-in GPU acceleration (probes /dev/dxg; no-op on no-GPU VMs).
- Host changes:
- vm_display_idd.c, vm_clipboard.c, vm_agent.c, vm_ssh_proxy.c,
asb_display.c: hcs_service_guid(os_type, port) so the same code
paths reach the per-OS service identifiers (HV_GUID for Windows,
vsock template for Linux).
- asb_core.c: linux_create_thread builds an empty VHDX, cidata
ISO with extras (agent ELFs, dxgkrnl src, asb_drm src, wsl-mesa
tarball, wsl-deps libs), and boots the desktop ISO + cidata.
- hcs_vm.c: HostLxssLib Plan9 share appended for Linux VMs;
VideoMonitor + ComPorts retained for serial debug.
- gpu_enum.c/h: gpu_append_lxsslib_share helper.
- vm_clipboard.c file-drop support (CF_HDROP / x-special/gnome-
copied-files round-trip).
- vm_display_idd.c: 32-bit S32_LE PCM audio path matches the
Linux daemon's snd-aloop capture format.
- iso-patch.c gains a patch_ubuntu_esp_grub_for_debug pass that
rewrites /EFI/ubuntu/grub.cfg via the FAT32 ESP, plus a qcow2->
vhdx converter with a RFC 1951 DEFLATE inflater for compressed
cluster handling.
- wsl-deps: Microsoft's libd3d12.so / libd3d12core.so / libdxcore.so
fetched on demand from the public NuGet feed
pkgs.dev.azure.com/shine-oss/wsl (no auth). Staged at
/opt/appsandbox/wsl-deps on the guest and put on /etc/ld.so.conf.d
rather than /usr/lib/wsl/lib (which is overlaid by the agent's
9P mount).
UI: web/{app.js,index.html} expose Linux VM creation + GPU selection,
distribution dropdown for cached Ubuntu ISOs, and the ISO picker for
custom images.
Two related bugs surfaced by the user's create-fails-then-retry sequence:
1. When hcs_create_vm_with_endpoint fails after we already created the
HCN endpoint (e.g. WBEM_E_INVALID_PARAMETER from a transient HCS
error, or insufficient RAM surfaced through HCS), the endpoint was
never deleted. The IP reservation lingered in HNS even though the
endpoint owner VM never came into existence. The next attempt at the
same IP failed with HCN_E_ADDR_INVALID_OR_RESERVED (0x803B002F).
Fixed by deleting the endpoint in every HCS-create or hcs_start_vm
failure path that follows a successful endpoint create, across all
three call sites (start_vm_thread, vhdx_create_thread, asb_vm_create).
2. Master had the retry-on-RESERVED loop only at start_vm_thread.
vhdx_create_thread and asb_vm_create created endpoints with no
retry, so even when the phantom reservation existed, those paths
silently fell back to creating the VM without a network.
Factored the retry into try_endpoint_with_retry() and used it at
all three call sites.
Root-cause fix for the bug that dc3fefa band-aided: g_vms[] is compacted
(shift+ZeroMemory) when a VM or template at a lower index is removed,
which silently invalidates any background thread's cached VmInstance*.
Affected threads were the agent connection (vm_agent.c::agent_thread_proc)
and the IDD probe (asb_core.c::idd_probe_thread_proc). The probe also
posted a stale VmInstance* to the UI via WM_VM_IDD_READY.
Fix:
* VmInstance gains a UINT64 unique_id, set on creation from a process-
global monotonic counter, never reused. The id rides along when an
entry is shifted (it's part of the struct that gets memcpy'd) and is
zeroed when a slot is freed. asb_find_vm_by_id() returns the current
&g_vms[i] for that id or NULL if the VM was deleted.
* AgentConn now stores vm_id instead of vm. agent_thread_proc resolves
via asb_find_vm_by_id at every iteration; loop exits cleanly when
the id no longer maps to a live VM.
* idd_probe_thread_proc is launched with the id (cast through ULONG_PTR)
and resolves every iteration the same way. WM_VM_IDD_READY now
carries the id in LPARAM; the UI handler resolves it freshly.
Obsoletes dc3fefa's symptom-level name-guard, which is reverted in the
next commit.
Replaces master's hardcoded 172.20.0.0/16, which overlapped the
built-in Default Switch (172.20.48.0/20) on every Windows 11 host.
The chosen subnet is decided once per process from a tiny static
candidate list:
192.168.42.0/24 (default)
192.168.142.0/24 (fallback if .42 conflicts with a host adapter)
Picked by collect_inuse_subnets() + ranges_overlap() at first call,
stable for process lifetime. If both candidates collide with a host
adapter (rare), we fall through to the first and log a clear warning
pointing at the candidate list in hcn_network.c.
No saved-state coordination, no per-VM seed, no HCN endpoint sweep --
master's startup network-delete (hcn_cleanup_stale_networks) wipes the
prior run's network so the freshly-picked subnet is always honored.
Replaces the over-engineered runtime picker that was reverted in the
previous two commits.
When a VM or template at a lower index is removed (deleted, or template
finishes sysprep), the g_vms[] array is compacted: entries shift down
and the now-vacated last slot is zeroed. Any background thread that
cached its VM pointer as &g_vms[idx] before the shift is left pointing
at the wrong slot (either zeroed or holding another VM's data).
Concrete repro: create a template, then create a regular VM while the
template is still building. The regular VM's agent thread holds
&g_vms[1]. When the template at g_vms[0] finishes and gets removed,
the regular VM's data shifts to g_vms[0] and &g_vms[1] is zeroed. The
agent thread now loops every few seconds calling
hcs_find_runtime_id(L"", ...) -- which logs the symptom the user saw:
HCS enumerate: [{...running VM...}]
hcs_find_runtime_id: VM "" not found in enumeration
Surgical fix: the agent loop and IDD-probe loop both bail out cleanly
when vm->name[0] is empty, which is the post-zero state. Stops the
log spam and the wasted IOCTLs.
This is a defensive guard, not a root-cause fix. The deeper issue is
that g_vms[] uses unstable indices: any cached VmInstance* can be
invalidated by compaction. A proper fix replaces shift+truncate with
stable slots marked as deleted, which is a larger refactor.
Previously, hcn_cleanup_stale_networks() at app init blindly deleted our
three fixed-GUID networks. That had two bad effects:
1. The vEthernet adapter for AppSandboxNAT was destroyed on every start,
forcing a re-pick of the subnet. If a saved VM existed (with a
previously-allocated 192.168.x.y), and the picker -- not knowing the
old subnet -- happened to choose a different one, the new endpoint
create would fail with HCN_E_ADDR_INVALID_OR_RESERVED because the
IP wasn't in the live network's range.
2. asb_cleanup() at app exit terminates VMs but does not call
asb_vm_cleanup_network. It relied on the async HCS SystemExited
callback to fire asb_hcs_state_changed -> asb_vm_cleanup_network for
each VM, but the process often exited before the callback completed.
The startup network-delete was the safety net catching the resulting
orphans -- which is exactly the destructive behavior in (1).
Replaced with:
* hcn_cleanup_stale_endpoints() at startup. Enumerates every HCN endpoint,
uses HcnQueryEndpointProperties to verify it belongs to one of our
three networks (so we never touch foreign endpoints), and deletes
just those. Networks themselves are left intact.
* hcn_seed_nat_subnet_from_ip() called from asb_init after load_vm_list
with the first saved VM's IP. The picker adopts that subnet so the
globals match the live network across restarts.
* asb_cleanup now calls asb_vm_cleanup_network for each VM after
hcs_terminate_vm. This is the reference-counted teardown that already
existed for clean per-VM stops; it deletes the endpoint and (only when
no other VM uses the shared network) the network. The adapter goes
away cleanly on clean exit and the next startup will pick a fresh
subnet -- no leak.
* try_create_endpoint_with_retry() factors the increment-on-fail loop
out of start_vm_thread and applies it to the two other endpoint-create
sites (vhdx_create_thread, asb_vm_create). Defense in depth in case
HNS ever holds a phantom reservation past the endpoint sweep.
The repo's README claims WebView2Loader.dll lives under vendor/webview2/,
but only the headers and devcon.exe were tracked. The DLL+import lib for
both x64 and arm64 sat in developers' working trees and were blocked
from git by the over-broad Visual Studio .gitignore rules x64/ and
[Aa][Rr][Mm]64/ (which match any directory of that name at any depth).
Fresh clones therefore hit MSB3073 (xcopy exit 4) at the AppSandbox
post-build step because the source DLL was missing.
Microsoft's WebView2 SDK license permits redistribution of
WebView2Loader.dll; the files are 150 KB each, stable, and required at
runtime. Added negation rules in .gitignore so future vendor binaries
in those paths don't get re-blocked.
Every PostBuildEvent xcopy command ended a quoted path with a trailing
backslash (e.g. "$(OutDir)resources\"). cmd.exe / CommandLineToArgvW
interpret the \" as an escaped literal quote, so xcopy receives a path
with an embedded " and an unclosed quote state - works fine when no
quotes are needed (no spaces in the path) but fails the moment the
parent path contains a space.
Drop the trailing backslash on each destination; xcopy /I infers
"directory" without it. For the bare "$(OutDir)" case, append a . so
the closing quote isn't preceded by a backslash.
Default Switch ships on every Windows 11 at 172.20.48.0/20, which our
hardcoded 172.20.0.0/16 superseded. HCN rejected the create with
ERROR_DUP_NAME (Microsoft re-uses that error code for subnet conflicts;
the user-facing message is misleading).
New behavior: on first NAT VM start of a process, enumerate every IPv4
subnet bound to a host adapter and pick the first 192.168.N.0/24 with
N>=42 that does not overlap any of them. Cached for process lifetime.
Consumers (allocate_nat_ip, set_ip agent command, endpoint create) now
read the picked prefix/gateway/base via hcn_nat_* accessors instead of
hardcoded 172.20 literals.
The GPU attached to a macOS VM is implicit (shared with the host via
paravirtualized graphics), so populate every VM row's gpuName with the
host's Metal default device name. Silences the module-map warning from
DEFINES_MODULE and links Metal.framework into AppSandboxCore.
Move Windows app shell (main.c, ui.c, webview2_bridge) into src/app_win
and macOS app shell into src/app_mac. Move Xcode project to repo root
alongside the Visual Studio .sln. Both platforms now follow the same
structure:
src/app_win/ Windows app shell
src/app_mac/ macOS app shell
src/backend_win/ Windows HCS engine
src/backend_mac/ macOS VZ engine
src/core/ shared types and backend vtable
- Asset catalog now only assigns the 1024x1024 PNG to the
512x512@2x slot, leaving other sizes empty
- Replace deprecated allowedFileTypes with allowedContentTypes
- Add NS_ASSUME_NONNULL_BEGIN/END to all backend_mac headers