LinuxLive.exe on Windows 11: deployment source, data folder, ESP, the UEFI boot entry list with the Linux Live entry, and the deployment log.
Engineering status
LinuxLive.exedeploys and boots a complete Linux live system that resides as a single file on the Windows NTFS volume. The boot chain is the standard Microsoft-signed shim → distribution-signed GRUB → distribution-signed kernel, so Secure Boot stays fully enforced and no MOK enrolment is needed. BitLocker-encrypted volumes unlock at boot. Verified end to end on a Dell XPS 7590 (Windows 11, Secure Boot ON, BitLocker ON). Debian 13 with the signed 7.2.11 kernel is the reference image; any distribution that ships a signed shim, GRUB and kernel works the same way.Repository policy: the release archive is not password-protected. Git tracks the native Windows C++23 source, project files, language files, test suites and the image build scripts. Binary build artifacts, releases and local packaging tools stay outside the repository.
LinuxLive.exedeploys a Linux live environment to a Windows machine without repartitioning or USB media. The root filesystem is onefilesystem.squashfsstored inC:\linux-live. The signed boot loader lives on the EFI System Partition (ESP) and is started through a dedicated UEFI NVRAM boot entry. After the kernel starts, Linux finds its NTFS container, unlocks BitLocker with the recovery password (stored only after an explicit yes), imports Wi-Fi credentials and SSH keys straight from the Windows installation and mounts the Windows partition read-write at/mnt/win. Runtime changes can be committed to a compressed delta layer (persistence.squashfs). Reverting the installation means removing the folder, the ESP directory and the NVRAM entry.Debian is the representative case, not a special one. The program reads what it needs from the package: the ESP folder is the prefix compiled into the signed GRUB, the folder on the Windows volume is
live-media-pathingrub.cfg. A Fedora package (/EFI/fedora,rd.live.image) goes through the same code.
Ways of running Linux on a Windows computer, and what each one costs:
| Way | What it costs | Secure Boot | Hardware access |
|---|---|---|---|
| WSL 2 | a VM without its own kernel on the hardware | stays on | virtualised GPU, no Wi-Fi card, no firmware |
| Live USB | a stick and the boot menu every time | depends on the distribution | full |
| Dual boot | repartitioning, a second installed system | needs a signed distribution | full |
| Disabling Secure Boot | a weaker platform, BitLocker recovery prompts | off | full |
| Linux Live | 1.1 GB file on C:, 57 MB on the ESP | stays on | full — real kernel, real drivers |
Typical uses: Linux with full hardware access on a BitLocker-protected laptop without touching its partitioning; a rescue and diagnostics system that is always there (efibootmgr, mokutil, nvme-cli, cryptsetup, parted, gdisk, msr-tools, tcpdump); new hardware that needs a current kernel — the reference image runs the signed 7.2.11 kernel from Debian backports.
Design principles:
--bitlocker-key) and shrinking the Windows partition for a dedicated boot partition (--boot-partition)./W4 /WX /sdl /guard:cf /CETCompat /Brepro on MSVC.LinuxLive.exe is one executable with two front ends: an interactive Direct2D application and an automation-friendly command line. Both need an elevated (Administrator) session and a UEFI system.
LinuxLive.7z and extract it.LinuxLive.exe as Administrator.filesystem.squashfs and esp.tar, verifies SHA-256 digests, fills the ESP, creates C:\linux-live\authorized_keys and adds a UEFI NVRAM entry.:: 1. Deploy: download release assets, verify SHA-256, configure the ESP, store the BitLocker key
LinuxLive.exe deploy --github --bitlocker-key
:: 2. Boot Linux once, on the next restart only
LinuxLive.exe once
:: 3. Restart now
LinuxLive.exe reboot --yes
Other everyday commands: status (firmware, Secure Boot, BitLocker, layout, ESP, entry), list (UEFI boot entries), default (make Linux the default entry), delete linux --yes (remove the entry and its BCD copies). The complete reference is in Command line.
user (user@debian:~$) with passwordless sudo.ping -c 4 linux.org.~/.ssh/id_pi), or with any key Windows already trusts — see SSH with the keys of Windows./mnt/win.save-session writes the session to C:\linux-live\persistence.squashfs; save-session --reset returns to the pristine image.reboot. Windows Boot Manager starts normally; to enter firmware setup use systemctl reboot --firmware-setup.Two independent halves meet in two places only: the ESP package (esp.tar) and the image (filesystem.squashfs). Everything the Windows program needs to know about the Linux side it reads from the package; everything the Linux side needs to know about the Windows side it reads from the kernel command line and from the Windows volume itself.
| Part | Verified by | Where it lives |
|---|---|---|
shimx64.efi |
firmware, Microsoft UEFI CA in db |
ESP |
grubx64.efi |
shim, distribution key | ESP |
vmlinuz |
shim (shim_lock), distribution key |
ESP or boot partition |
grub.cfg, initrd.img |
not verified — true of every signed distribution | ESP or boot partition |
filesystem.squashfs |
not verified at boot; SHA-256 at download | Windows volume |
This is the normal trust model of signed Linux distributions, not something this project relaxes. See Security Boundaries.
A signed GRUB cannot be told where to look: the folder it reads grub.cfg from is compiled into the signed image. Debian's grubx64.efi has a PE section mods; it starts with a grub_module_info64 header (magic mimg, offset, size) followed by typed modules, and the module of type 3 (OBJ_TYPE_PREFIX) holds /EFI/debian. There is no embedded configuration and no cmdpath fallback. Renaming the ESP folder breaks the boot — and on machines that still have \EFI\debian from an earlier installation this goes unnoticed, because GRUB silently reads the old folder.
So the folders are read, not chosen:
| Folder | Source of truth | Read by |
|---|---|---|
ESP folder (EFI\debian) |
the prefix in the signed grubx64.efi |
Install::GrubPrefix() |
Media folder (linux-live) |
live-media-path in grub.cfg, usually through set media= |
Install::MediaFolder() on Windows, /proc/cmdline in Linux |
GrubPrefix() walks the PE section table (bounds-checked reads at every step, 64 MiB limit for the image), strips a device part such as (hd0,gpt1) and accepts only EFI\<name>. MediaFolder() resolves $var and ${var} from set lines, falls back to live (the live-boot default) when nothing is given, and refuses absolute paths, .., reserved characters and undefined variables.
The grub.cfg of the reference package:
set media=linux-live
set live=$prefix
if [ ! -e $live/vmlinuz ]; then
search --no-floppy --set=root --file /$media/vmlinuz
set live=/$media
fi
linux $live/vmlinuz boot=live components live-media-path=$media ignore_uuid noeject noprompt nonetworking ...
initrd $live/initrd.img
The search fallback is what makes the boot partition possible: when the ESP folder holds only the boot loader, GRUB finds the kernel on another FAT partition, and shim still verifies it. The program checks that a package has this fallback before it splits a deployment.
Collisions. The ESP folder is fixed by the signature, so an installed Debian uses the same \EFI\debian. Before deploying, the program reads the grub.cfg already there. If it does not start a live system (boot=live, rd.live.image, root=live:) — or cannot be read — the folder belongs to an installed system and nothing is changed.
The reference image is Debian 13 (trixie) live, rebuilt in 16 audited steps (Building the Image). The sections below describe what makes it work on somebody else's Windows computer.
The image is machine-agnostic: no hardcoded partition IDs, UUIDs, interface names, IP addresses or embedded credentials. Every parameter is discovered at start-up:
| What | How it is found at boot |
|---|---|
| media folder | live-media-path= on the kernel command line (from grub.cfg) |
| encrypted volumes | blkid -t TYPE=BitLocker |
| BitLocker recovery password | every FAT partition is mounted read-only and searched for EFI/*/bitlocker.key and <media>/bitlocker.key; the console asks when none works |
| the live medium | live-boot scans block devices, including /dev/mapper/bitlocker*, for <media>/filesystem.squashfs |
| writable Windows view | dmsetup info bitlocker0 decides: second BitLocker mapping, or the plain device |
| Wi-Fi networks and passwords | decrypted from the Windows installation on this disk by regedlin, at every boot |
| wireless interface | the first wl* entry in /sys/class/net |
| addresses, routes | DHCP through systemd-networkd, MAC as client identifier |
| DNS servers | collected from all current DHCP leases whenever a lease changes |
| "network is up" | a default route exists (polled, 8 s at most) |
| SSH keys and host key | C:\ProgramData\ssh and <media>\authorized_keys of this Windows |
| live user | getent passwd 1000 |
| time zone | live-config.timezone= on the kernel command line |
| console font | framebuffer width in /sys/class/graphics/fb0/virtual_size |
| persistence file | <media>/persistence.squashfs on the Windows volume |
The discovery is plain shell in /usr/local/bin and the initrd (console-font.sh, network-ready.sh, update-dns.sh, wifi-connect.sh, ssh-import.sh, mount-win.sh, wll-commit, unlock-bitlocker) plus the compact regedlin binary (60 KB). All logic is open and can be read on the running system.
Three concatenated parts, each kept the way the kernel expects it:
The steps split the file by walking the newc headers (110-byte header, name and data aligned to 4 bytes) to each TRAILER!!!, skipping the zero padding between archives. Searching for the zstd magic instead would be wrong: the magic can occur inside a module. A step repacks only the part it changes — archives are written in LC_ALL=C sort order — and compares the other parts byte for byte afterwards.
The initrd is trimmed so that it fits small ESPs, and the trimming is checked, not guessed:
sd_mod, sr_mod, sg, virtio, Hyper-V and VMware paravirtual; device-mapper except dm-mod, dm-crypt, dm-zero; network and server file systems (NFS, btrfs, xfs, f2fs, …); sound, FireWire, NVDIMM, watchdogs.modules.dep never points at a missing file; hv_storvsc is restored for Hyper-V guests.depmod rebuilds the index and modprobe --show-depends against the trimmed tree must resolve every module needed to reach the image: vfat ntfs squashfs overlay loop nvme xhci_pci usb_storage uas sd_mod ahci dm_crypt dm_zero xts af_alg algif_skcipher hv_storvsc hv_vmbus virtio_blk virtio_scsi vmd mmc_block sdhci_pci ufshcd_pci usbhid hid_generic efivarfs nls_cp437 nls_ascii nls_utf8. One missing module stops the step and leaves the previous initrd in place.Every initrd change in the build steps follows the same procedure:
initrd_split.py <initrd> <dir> save walks the file: zero padding between archives is skipped, each newc header is read (file size at bytes 54–62, name size at 94–102, both followed by 4-byte alignment) and entries are counted up to TRAILER!!!. The first zstd frame (28 b5 2f fd) ends the walk; everything from there is written as partc.zst. In save mode the helper refuses unless exactly two cpio archives precede it — the structure this initrd must have.cpio -idm, part c through zstd -dcq | cpio -idm.depmod -b expects <root>/lib/modules; a merged-/usr tree may have no lib, so the step creates a temporary lib -> usr/lib link for depmod and removes it again — only when it created it itself. modules.order is filtered to files that exist; modules.builtin* comes from the full module tree of the image.modules.* files into part c as well.find . -mindepth 1 | LC_ALL=C sort | cpio -H newc -o for a stable order; part b padded with zeros to a 512-byte boundary; part c compressed with zstd -19 -T0; part a padded to 512 bytes; the three concatenated.zstd -t tests the frame, and cpio -it must list the files the step is about (init, 9990-main.sh, 9990-misc-helpers.sh, 00_mount_efivarfs, unlock-bitlocker, cryptsetup, dm-crypt, vfat, ntfs, nvme, …).Modules are always copied as the .ko.xz files of the Debian package — never rebuilt, never recompressed. Their signatures stay valid, which matters: with Secure Boot on the kernel loads only signed modules.
linux-image-amd64 from trixie-backports, 7.2.11+deb13. Moving to it, step 10 carried the trimmed module set over by name, added the dependencies the new kernel needs and checked the list above again.firmware-iwlwifi) comes from the Debian archive; its SHA-256 is compared with the Packages.xz index of the archive before it is installed, not only with what was downloaded.ntfs driver (NTFSPlus) instead of ntfs3 from Paragon; live-boot is patched to mount the live medium with it (ro,noatime,nocase,windows_names,iocharset=utf8). NTFSPlus was chosen for its write path, which /mnt/win and persistence depend on. It is a young driver and still has a few bugs — see Known Issues./usr/sbin/unlock-bitlocker runs before live-boot scans the disks (a one-line patch in 9990-main.sh, after udevadm settle):
blkid -t TYPE=BitLocker finds encrypted volumes; without one nothing happens.dm_mod dm_crypt dm_zero xts cbc ecb aesni_intel af_alg algif_skcipher algif_hash vfat nls_*); DM_DISABLE_UDEV=1, because there is no udev to create the nodes.EFI/*/bitlocker.key (where LinuxLive.exe writes it, next to the boot loader) and <media>/bitlocker.key, with the media folder taken from live-media-path. No folder name is assumed. The copies stay in /run/bitlocker-keys (RAM, mode 700); the directory is removed before the real system starts.cryptsetup open --type bitlk --readonly --disable-locks --disable-keyring as /dev/mapper/bitlockerN, trying each candidate; without a working key the password is asked on the console.9990-misc-helpers.sh is patched to check /dev/mapper/bitlocker* first; it finds /linux-live/filesystem.squashfs inside.The unlock is read-only, the key protectors are not changed, and the recovery password is the one Windows already has. cryptsetup was copied into the initrd together with the libraries ldd reports; a library that already exists is compared, and libc or the loader differing between image and initrd stops the step.
BitLocker on a Windows laptop normally has a TPM protector and a numerical recovery password protector. The 48-digit recovery password is what Windows itself asks for when the TPM refuses to release the key, and what Microsoft account, Entra ID or Active Directory backups store. Linux Live uses exactly this protector — the one Windows designed for "the boot path has changed" — and nothing else:
Win32_EncryptableVolume.GetKeyProtectorNumericalPassword call, after an explicit yes (BitLocker on the Windows side).cryptsetup open --type bitlk --readonly reads the FVE metadata and builds a device-mapper table: crypt segments for the NTFS data and zero segments over the BitLocker metadata areas — this is why dm_zero belongs to the required modules. Neither the read-only view nor the read-write /mnt/win mapping, which reuses the same table, can reach the metadata: reads there return zeros, writes are discarded.mokutil --disable-validation, no change to db or dbx. shim verifies GRUB and the kernel with the distribution certificate built into it./dev/mem and raw MSR writes are refused.mokutil --sb-state reports SecureBoot enabled inside the running system; LinuxLive.exe status reports Secure Boot on before and after.The running root file system is a squashfs file on the Windows volume, so the live medium has to stay mounted read-only for the whole session. Writing to Windows through that mount would mean remounting the medium under the running system. mount-win.sh gives Linux a second, independent view instead:
dmsetup table --showkeys bitlocker0 | dmsetup create bitlocker_rw
mount -t ntfs -o rw,noatime,nocase,windows_names,iocharset=utf8 /dev/mapper/bitlocker_rw /mnt/win
The second device-mapper target reuses the decryption table of the first one — no password is asked twice and no key is written anywhere. To the kernel these are two block devices and two file systems: the read-only one keeps serving the root image, the read-write one is free to change files. The images are never modified in place (persistence is written as a new file and renamed), so the running root is not affected by writes through /mnt/win. Without BitLocker the medium device itself is mounted a second time. nocase,windows_names keeps Windows semantics: case-insensitive lookups and no names Windows could not open. No FUSE, no ntfs-3g: the build checks that neither package is installed.
mount-win.service starts after run-live-medium.mount and stops before umount.target; /mnt/win also serves wll-commit.
wpa_supplicant.conf holds only ctrl_interface, update_config and country. When /run/live/medium/Windows exists, wifi-connect.sh runs regedlin wifi --sync /run/live/medium, and the networks Windows knows work in Linux without typing a password. When no Windows tree is readable nothing is synced and nothing fails.systemd-networkd owns every interface. networking.service, ifup@.service, ifupdown-pre.service and ifupdown-wait-online.service are masked, so ifupdown cannot start a second client; nonetworking on the kernel command line keeps live-boot's own initramfs networking out.ClientIdentifier=mac makes the DHCP server see the same client as under Windows, so Linux usually gets the same lease — and SSH reaches the machine at the same address in both systems.IgnoreCarrierLoss=3s on wireless rides out roaming between access points without dropping addresses.update-dns.path watches /run/systemd/netif/leases; update-dns.sh collects the DNS servers of every lease, appends 1.1.1.1, 8.8.8.8 and 1.0.0.1 as fallbacks and writes resolv.conf without duplicates, in order.network-ready.service (before getty@tty1) polls for a default route every 0.2 s for at most 8 s, so the login prompt appears when the machine is online, without the long systemd-networkd-wait-online timeout.regedlin (regedlin-cli 0.2.2, 60 KB, links only GLib and OpenSSL's libcrypto) is the command-line component of RegEdLin, an offline Windows registry editor I developed from scratch for Linux. It does not depend on libhivex or Python wrappers; it has its own binary parser and writer for the regf hive format. Windows stores Wi-Fi keys encrypted with the machine's DPAPI, so reading them offline means repeating what Windows does at boot:
Windows\System32\config\SYSTEM: the boot key, scattered over the class names of four Control\Lsa subkeys of the current control set;Windows\System32\config\SECURITY: Policy\PolEKList decrypted with the boot key gives the LSA key, which decrypts the LSA secret Policy\Secrets\DPAPI_SYSTEM\CurrVal — the DPAPI keys of the machine;Windows\System32\Microsoft\Protect\S-1-5-18\User: the SYSTEM account's DPAPI master keys, unlocked with those keys;ProgramData\Microsoft\Wlansvc\Profiles\Interfaces\{interface}\*.xml: each profile's <keyMaterial> is a DPAPI blob, decrypted with the master key (AES-256, SHA-1 HMAC, OpenSSL EVP);network={ ssid=... psk=... } blocks to /etc/wpa_supplicant/wpa_supplicant.conf — on the overlay's tmpfs, so in RAM.Nothing is written to Windows and nothing is cached in the image; the passwords are recovered again at every boot from the installation that is actually on the disk. The same binary offers info, ls, dump and find on any hive file and wifi <SOFTWARE> to list the networks Windows remembers (NetworkList\Profiles) — small, scriptable diagnostics for an offline Windows from the live system.
Why ClientIdentifier=mac matters. DHCP servers identify a client by option 61 (client identifier) when it is present, not by the MAC address in the packet header. Windows sends hardware type 1 followed by the MAC. systemd-networkd sends a DUID-based identifier by default (RFC 4361) — to the server a different client: a different lease, a different address, and no match for reservations or access rules bound to the Windows client. ClientIdentifier=mac makes networkd send 01:<MAC> exactly like Windows, so the server sees the same machine. Without it Linux would land on a different address, or not get onto networks that admit only reserved clients at all.
Everything is chosen by priority, nothing by fixed name:
en*, eth*, wl*), never by name or MAC; a USB adapter or a docking station works like the built-in port.sshd accepts public keys only:
HostKey /etc/ssh/ssh_host_ed25519_key
PubkeyAuthentication yes
AuthenticationMethods publickey
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
At boot ssh-import.sh:
C:\ProgramData\ssh\administrators_authorized_keys — the keys that already open an administrator session on this Windows — and C:\<media>\authorized_keys; NUL bytes, CR, UTF-8 BOM and UTF-16 byte-order marks are stripped, so files saved by Windows editors work;Users\*\.ssh keys are deliberately ignored);ssh_host_ed25519_key, so clients that know the machine from Windows see the same host key in Linux;/run/ssh-keys-imported; ssh.service has ConditionPathExists= on it and does not start without imported keys.The Windows OpenSSH server is an optional feature, so many machines have no C:\ProgramData\ssh\administrators_authorized_keys. To make headless SSH work out of the box on any system, LinuxLive.exe creates C:\linux-live\authorized_keys during deployment when the file does not exist yet. It holds a deterministic emergency public key derived from the mathematical constant π:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIdXmgotoDC4ToQLmyyveTQDpuaV/QLio8d/qPXejYUI
The Ed25519 private seed is the 32 ASCII characters 31415926535897932384626433832795 (the first 32 digits of π) behind the standard PKCS#8 header 302e020100300506032b657004220420. To connect from another machine, save the private key as ~/.ssh/id_pi:
printf -- '-----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----\n' \
"$(printf '\x30\x2e\x02\x01\x00\x30\x05\x06\x03\x2b\x65\x70\x04\x22\x04\x2031415926535897932384626433832795' | base64)" \
> ~/.ssh/id_pi && chmod 600 ~/.ssh/id_pi
ssh-keygen -y -f ~/.ssh/id_pi # prints the public line above
ssh -i ~/.ssh/id_pi root@<IP> # or user@<IP>
From Windows PowerShell:
ssh -i $HOME\.ssh\id_pi root@<IP>
Replace the emergency key. Anyone can compute the private key from the constant π, so it gives no secrecy on an open network:
ssh-keygen -t ed25519) and replace C:\linux-live\authorized_keys with your public key.C:\ProgramData\ssh\administrators_authorized_keys. It belongs in the live Linux bootstrap only.From an elevated PowerShell:
ssh-keygen -t ed25519 # creates %USERPROFILE%\.ssh\id_ed25519(.pub)
Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub |
Set-Content C:\linux-live\authorized_keys # replaces the emergency key with your own
ssh-import.sh copes with every encoding Windows tools produce: Add-Content, Set-Content, PowerShell 5.1 redirection (UTF-16) and Notepad (UTF-8 with or without BOM, CRLF). NUL bytes, CR and byte-order marks are stripped, and keys present in both files are imported once.
The image contains no host keys; the HostKey lines for RSA and ECDSA are removed so live-config does not generate them. The build steps tested this in a RAM overlay with a temporary sshd on 127.0.0.1:2222 and a fake Windows volume: key login works for root and the user; a wrong key, a password and keyboard-interactive are refused; files with BOM + CRLF and a UTF-16 file are imported.
save-session (a symlink to wll-commit) packs the upper layer of the overlay — everything changed during the live session — into C:\<media>\persistence.squashfs (zstd level 15, 1 MiB blocks), merged with the persistence layer loaded at boot. On the next boot live-boot mounts the squashfs containers in alphabetical order and stacks filesystem.squashfs, persistence.squashfs and a fresh RAM tmpfs.
save-session # persist session changes to C:\linux-live\persistence.squashfs
save-session --status # layer size and modified files still residing in RAM
save-session --auto-on # save automatically at shutdown (--auto-off to disable)
save-session --reset # remove the persistence layer, back to the pristine base image
Excluded from a save: tmp, var/tmp, var/log, var/cache/apt/archives/*.deb, run, mnt, media. The new layer is written as persistence.squashfs.new.<pid>, synced and renamed over the old one, so an interrupted save leaves the previous layer intact. The media folder comes from live-media-path, like everywhere else.
console-font.sh reads the framebuffer width from /sys/class/graphics/fb0/virtual_size and switches tty1–6 to Lat2-TerminusBold32x16 on screens 2560 pixels wide or more./etc/adjtime says LOCAL. live-boot is patched to link /etc/localtime from live-config.timezone= on the kernel command line before systemd starts — live-config would set it too late and the clock would be off by the zone offset until then.en_US.UTF-8 is prebuilt in locale-archive; etc/.updated and var/.updated carry the timestamp of /usr, so ldconfig and the other "needs update" units do not run at every boot.The image carries the tools for recovery, partition management and hardware diagnosis: efibootmgr, mokutil, cryptsetup, nvme-cli, parted, gdisk, msr-tools, tcpdump, dosfstools, squashfs-tools and regedlin. Packages follow Debian backports.
Build verification keeps the squashfs clean and stateless: zero SSH host keys, an uninitialised machine-id, no shell history, empty /var/log and /tmp, no cached APT archives, no credential backups, and no FUSE or ntfs-3g packages (only the in-kernel NTFS module is used).
tests/linux/build-steps/ is the complete recipe of the reference image, in the order it was applied:
| Step | What it does |
|---|---|
| 01 | purge leftover packages, systemd-networkd profiles, DNS from leases, local-time RTC; trim GPU/network drivers from the initrd |
| 02 | cryptsetup and the BitLocker unlock in the initrd; live-boot patches for /dev/mapper/bitlocker* |
| 03 | key lookup on every FAT partition; console font by resolution |
| 04 | remove Wi-Fi credentials from the image; install regedlin |
| 05 | trim server storage, network file systems and hwdb from the initrd |
| 06 | key-only sshd with Windows keys and host key; functional login tests |
| 07 | second key source in the media folder; tests with BOM, CRLF and UTF-16 files |
| 08 | dependency-consistent module set, timezone before systemd, volatile journal, network-ready |
| 09 | prebuilt locale, faster network wait, console-only service set |
| 10 | signed 7.2 kernel from backports, newer Wi-Fi firmware (index-verified), NTFSPlus in live-boot |
| 11 | root login with the same keys |
| 12 | low-level tools, /mnt/win read-write through the second BitLocker mapping |
| 13 | full upgrade, nothing left upgradable |
| 14 | squashfs-tools and the persistence tools |
| 15 | no folder names in the system: layout from grub.cfg, folder-agnostic BitLocker key lookup |
| 16 | real unmount of /mnt/win at shutdown, persistence auto-save ordered before it (prepared, not yet in the released image) |
Every step:
/dev/shm, removed by an EXIT INT TERM HUP trap — also on failure; the /dev, /proc and /sys bind mounts used for chroot work are unmounted by the same trap;type mode uid gid size path -> target) and the SHA-256 of every file before and after, and prints the difference;sh -n, bash -n) and units (systemd-analyze verify) afterwards;policy-rc.d returning 101, so no daemon starts inside the build, and removes it again;.part, compares SHA-256 and renames.unsquashfs into the tmpfs, works on the tree — package changes in a chroot with /dev, /proc and /sys bound in — and packs it again with mksquashfs -comp xz -Xbcj x86 -b 1M -noappend. The BCJ filter turns relative x86 call targets into absolute ones before compression, so machine code compresses noticeably better; 1 MiB blocks suit an image that is read sequentially at boot and randomly afterwards.vmlinuz and the initrd travel in esp.tar, and /boot, /vmlinuz, /initrd.img and the old kernel's modules are removed from the image, so nothing is stored twice.pkgcache.bin, *-old files of dpkg and debconf, compressed apt logs, __pycache__, /root/.cache, account backup files — empties every log file and both temporary directories, and refreshes the .updated stamps so they match /usr again.At run time the image is never written: live-boot stacks it read-only under the optional persistence layer and a tmpfs (overlayfs), so a session changes only RAM until wll-commit saves it.
Run the steps in WSL 2 as root; initrd_split.py is the helper the initrd steps use.
Each step names what it changes and how the change is checked. The scripts are in tests/linux/build-steps/; they record the path the reference image took, starting from a Debian 13 live image.
dpkg --purge in a chroot (Python crypto packages left from earlier tooling), followed by dpkg --audit and a listing of every package not in state ii.__pycache__ directories, /mnt/c, systemd-units-load, sources.list, apt caches, *-old files of dpkg and debconf, /boot/initrd.img-*, /initrd.img.20-wired.network (en* eth*, DHCP, metric 100) and 25-wireless.network (wl*, DHCP, metric 600, IgnoreCarrierLoss=3s); update-dns.sh with update-dns.path / .service; wifi-connect.sh; ifupdown masked (networking, ifup@, ifupdown-pre, ifupdown-wait-online linked to /dev/null) and removed from the targets that wanted it./etc/adjtime with LOCAL.sh -n for the new scripts, systemd-analyze --root verify for the new units; after packing, the image is unpacked again and compared file by file with the original — metadata and content differences are listed, and kernel modules, firmware, regedlin, mc and wpa_supplicant must be untouched.modules.order filtered; depmod; the presence of vfat ntfs3 squashfs overlay loop nvme xhci-pci ahci in modules.dep is counted.cryptsetup is copied into part c together with every library ldd reports. A library already in the initrd is compared: identical — kept; different — replaced; libc.so.6 or ld-linux-x86-64.so.2 different — the step stops, because a mismatched C library would break everything else in the initrd.modprobe -d <image> --show-depends for dm_mod dm_crypt dm_zero xts cbc ecb aesni_intel aes_generic af_alg algif_skcipher algif_hash lists every module and dependency; the missing ones are copied from the image's module tree into part b, then depmod.unlock-bitlocker and the two live-boot patches (the call after udevadm settle in 9990-main.sh, the /dev/mapper/bitlocker* scan in 9990-misc-helpers.sh), each applied only when its anchor text occurs exactly once, then sh -n on all three files.chroot <copy> /usr/sbin/cryptsetup --version and --help | grep bitlk must succeed — cryptsetup starts with nothing but the initrd's own libraries and supports BITLK.vfat partition instead of one with a fixed label; part b is compared byte for byte with its previous version and must be unchanged.console-font.sh and console-font.service (after console-setup and systemd-vconsole-setup); metadata difference printed after packing.psk= values of the old wpa_supplicant.conf are collected into the tmpfs only to search the rest of the image for copies; the search prints paths, never values. The list is deleted afterwards.wpa_supplicant.conf is reset to ctrl_interface, update_config, country, mode 600; the check after packing counts psk= lines — zero.regedlin is installed with mode 755, owner root, and its SHA-256 compared with the source binary after packing.wifi-connect.sh is patched by exact lines: regedlin's output goes to /dev/null, and the comment says the image ships no credentials.scsi_mod scsi_common sd_mod sr_mod sg virtio_scsi hv_storvsc vmw_pvscsi; device-mapper except dm-mod dm-crypt dm-zero; block drivers except loop virtio_blk; btrfs xfs nfs nfs_common nfsd lockd netfs f2fs reiserfs jfs jffs2 udf hfs hfsplus; network stack modules, sound, FireWire, NVDIMM, watchdogs, message-based and target drivers.hwdb.bin and the thin-provisioning tools (pdata_tools, thin_*, cache_*, era_*) — after checking that no initrd script refers to them.modprobe --show-depends against the trimmed tree; one missing module and the step ends without writing anything.openssh-server installed in a chroot with policy-rc.d returning 101 (no daemon starts in the build); host keys deleted; 10-key-only.conf; ssh-import.sh, ssh-import.service and the ssh.service drop-in with ConditionPathExists=/run/ssh-keys-imported.ClientIdentifier=mac added to both network profiles.tmp; machine-id size and host key count printed.ssh-import.sh runs, the permissions of .ssh and authorized_keys and the imported host key's fingerprint are compared with the Windows one, sshd -t and the effective sshd -T settings are printed, and sshd on 127.0.0.1:2222 must accept a test key for the user and refuse a wrong key, a password and root.ssh-import.sh merges ProgramData\ssh\administrators_authorized_keys and <media>\authorized_keys.authorized_keys with a BOM and CRLF; both sources plus the Windows host key; the Windows file saved as UTF-16.passwd-, shadow-, group-, gshadow-, subuid-, subgid- and .pwd.lock removed.HostKey lines for RSA and ECDSA removed, so live-config does not generate those keys at boot./etc/localtime defaults to UTC; the zone comes from the kernel command line through a live-boot patch inserted before the Netbase call in 9990-main.sh.Storage=volatile, RuntimeMaxUse=16M; /var/log/journal removed; sysstat and apt-listchanges masked.network-ready.service before getty@tty1; the Wi-Fi interface wait reduced from 15 to 5 seconds.etc/.updated and var/.updated written with the timestamp of /usr and given its mtime, so the "update done" units are skipped.hv_storvsc and its dependencies restored from the image's full module tree; then every module with a dependency that is no longer present is removed, repeated until nothing changes; modules.order filtered; depmod; the required-module gate again; the number of kept modules with an absent dependency is printed — zero.en_US.UTF-8 enabled in locale.gen and built with locale-gen in a chroot; locale -a printed.network-ready.sh: a default route, polled every 0.2 s, 8 s at most./usr did not change, so the stamps from step 08 stay valid.trixie-backports source (signed by the Debian archive keyring), linux-image-amd64 installed from it with INITRD=No, the 6.12 kernel purged; the remaining linux-* packages listed. The source file and policy-rc.d are removed again.firmware-iwlwifi is downloaded from the Debian pool and its SHA-256 compared with the entry in the archive's Packages.xz index before dpkg -i; the presence of the newest iwlwifi blobs is listed./boot is emptied in the image.ntfs.ko is added, and the dependency closure from the full modules.dep is copied; modules that no longer exist in 7.2 are listed, as are the ones pulled in only as dependencies. Then depmod, the required-module gate (now with ntfs instead of ntfs3), the absent-dependency count, and a search for the old kernel version in conf, scripts and etc of the initrd — nothing may refer to it.ntfs3-specific branches are replaced — mount options without force, modprobe -q ntfs, the file system type left as detected; sh -n on both files and a count of remaining ntfs3 references.PermitRootLogin prohibit-password; ssh-import.sh also installs the keys for root.efibootmgr mokutil cryptsetup nvme-cli parted gdisk msr-tools tcpdump dosfstools installed without recommends; the step aborts if any ntfs-3g or fuse package was pulled in; smartmontools masked.mount-win.sh, mount-win.service and the mnt-win.mount drop-in.--version) to prove it runs.resolv.conf is used for apt and replaced afterwards; update-dns rewrites it at run time anyway.apt-get dist-upgrade without recommends, autoremove --purge, clean, autoclean; the fuse / ntfs-3g check again; the step refuses to finish while apt list --upgradable shows any package.squashfs-tools installed; wll-commit and the save-session link; /etc/default/wll-persistence with AUTO_SAVE_ON_SHUTDOWN=0; wll-persistence.service enabled; nothing left upgradable.mods of the signed grubx64.efi (an inline parser), and grub.cfg is generated with set media= and the search fallback.unlock-bitlocker is replaced by the folder-agnostic version.ssh-import.sh and wll-commit read live-media-path from /proc/cmdline; unit and configuration descriptions renamed. A search for old folder names in /usr/local, /etc/systemd/system and /etc/default must find nothing.shimx64.efi, grubx64.efi, vmlinuz and unicode.pf2 are hashed before and after — the step fails if any of them differs.esp.tar is written as ustar with owner and group 0; both outputs go to .part files, are compared with SHA-256, renamed, and get .sha256 files.mount-win.sh stop: sync, a real umount (retried; processes still using /mnt/win are ended after the third attempt), remount read-only as the fallback, umount -l only as the last resort; the read-write BitLocker mapping is removed afterwards with retries; without BitLocker /run/live/medium goes back to read-only.wll-persistence.service gets After=mount-win.service, so the auto-save finishes before /mnt/win goes away; mount-win.service gets a 30-second stop timeout; the LazyUnmount=true drop-in of mnt-win.mount is removed.One executable is both the graphical interface and the command line. It is a GUI-subsystem program; with arguments it attaches to the parent console and runs the command line instead of opening a window. No installer, no service, no driver; settings live in HKCU\Software\KVC\LinuxLive.
| Layer | Content |
|---|---|
app |
command parser, commands, console output and progress bars |
gui |
window, list, log, menus, dialogs, theme |
core |
Deploy (shared by both front ends through a Hooks struct: log, progress, confirmations), BootList (one snapshot of NVRAM for both front ends), Actions |
install |
tar reader and ESP writer, GRUB prefix and grub.cfg parsing |
efi |
NVRAM variables, load options, device paths, BCD copies |
storage |
volume discovery, BitLocker, the boot partition |
net, support |
WinHTTP downloader, SHA-256 (CNG), WMI client, strings, settings, RAII handles |
Errors travel as std::expected<T, std::wstring> with messages ready for the user; cancellation is a std::stop_token passed down to the download and write loops; handles, COM pointers and BSTRs are owned by small RAII types. The front ends never touch Win32 storage or firmware APIs directly.
The program works with volume GUID paths (\\?\Volume{...}\) and never assigns or needs a drive letter — the ESP is never mounted to one. FindFirstVolumeW / FindNextVolumeW enumerate the volumes, GetVolumePathNamesForVolumeNameW maps them to mount points for display (C:\linux-live instead of the GUID path). The ESP is located through IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS (disk number and offset of the volume) and IOCTL_DISK_GET_DRIVE_LAYOUT_EX (partition number, type, GPT partition GUID, start and length) — exactly the data an EFI HD() device path needs. With several ESPs, the one that holds a deployed live package wins, then the one on the Windows system disk.
Boot#### variables are ordinary, non-authenticated UEFI variables: they can be changed with Secure Boot enabled. The program checks GetFirmwareType, enables SeSystemEnvironmentPrivilege (and SeShutdownPrivilege for restarts) with AdjustTokenPrivileges, and uses GetFirmwareEnvironmentVariableExW / SetFirmwareEnvironmentVariableExW in the EFI global-variable namespace.
A new entry is an EFI_LOAD_OPTION built from scratch: attributes, the length of the device path list, the UTF-16 description, then a media HD() node (partition number, start LBA, size in sectors, GPT partition GUID, signature type), a File() node with the loader path and the end node. tests/verify.cpp rebuilds existing entries from the partition data and compares them byte for byte with what the firmware stored. Reading goes the other way: device path nodes are parsed into a short summary (HD(1,GPT), NVMe, firmware application), and entries are matched to the ESP by GPT partition GUID or MBR signature.
LOAD_OPTION_ACTIVE — the rest of the variable is rewritten byte for byte).NV | BS | RT (0x7). Deleting with attributes 0 fails with error 87 on real firmware.--force; disabling it asks first.Found while testing: an entry made with bcdedit /copy {bootmgr} — a common way of adding a boot entry from Windows — lives in the BCD store, and its NVRAM variable carries WINDOWS + BCDOBJECT={guid} as optional data. When only the variable is deleted, Windows writes it back at its next start, at the front of BootOrder. On the test laptop such a stale "Debian" entry went before UnderVolter and started Linux instead of Windows.
delete therefore removes the BCD copies too. The optional data of each entry is parsed for BCDOBJECT=; after the variable is deleted, the BCD store is searched for that object and for firmware-application and boot-manager objects with the same loader path and description. The objects are read from the registry: the system store is mounted as the hive HKLM\BCD00000000, readable by SYSTEM only, and an administrator opens it with SeBackupPrivilege and REG_OPTION_BACKUP_RESTORE, so type, ApplicationPath and Description of every object are read locally by plain WinAPI — no process, no file, no text parsing, so the Windows language does not matter. This replaces reading them over the BCD WMI provider, where every BcdObject call re-opens the system store (about 300 ms each, 10 s for a store with 33 objects). If the hive cannot be opened or read, the same search runs over WMI (root\WMI: BcdStore.EnumerateObjects, BcdObject.GetElement). The objects found are deleted through the BCD WMI provider (BcdStore.OpenStore, BcdStore.DeleteObject) — the documented interface behind bcdedit — with the store opened once, and only when there is something to delete. {bootmgr}, {fwbootmgr} and \EFI\Microsoft\Boot\bootmgfw.efi are never touched, and a copy shared with another remaining entry is kept. A failure to clean the store is reported as a warning; the entry itself is deleted either way.
<url>.sha256 (plain digest or sha256sum format), hashed with CNG (BCrypt). A copy that was verified once is remembered by path, size, time stamp and hash and is not downloaded or hashed again while the server reports the same file. A missing .sha256 file is logged as a warning; the compile-time switch Config::kRequireSha256 (off by default in the source) turns it into a refusal.<name>.part and renamed with MOVEFILE_WRITE_THROUGH after the hash matched, so an interrupted download never replaces a good file.path, size, mtime) and GNU (long names, base-256 sizes); every header checksum is verified, every path sanitised (no .., no drive or reserved characters), files over the FAT32 limit of 4 GB refused. The whole archive is validated — paths, types, sizes and free space — before the first byte is written.<name>.tmp, is flushed with FlushFileBuffers and renamed; an unchanged file (same size, time within FAT's 2-second resolution) is skipped. A temporary file is deleted again on any error.A repeated deploy with nothing new finishes in about two seconds.
Firmware (BIOS/UEFI) updates stage their capsules on the ESP; a laptop BIOS image alone is 20–40 MB. A deployment always leaves 20 % of the ESP free, never less than 20 MB — 20 MB on an old 100 MB ESP, 40 MB on the 200 MB ESP of current Windows installations. Linux is not worth losing the ability to update the firmware.
Windows 11 24H2 and later create a 200 MB ESP; older installations often have 100 MB, most of it used by Windows. The kernel and initrd (about 54 MB) do not fit there together with the reserve. The signed boot loader has to stay on the ESP — its prefix is part of the signature — but the kernel and initrd do not.
With consent (deploy --boot-partition, or a question in the interface):
FSCTL_IS_VOLUME_DIRTY; Windows neither shrinks nor grows a volume that needs repair.root\Microsoft\Windows\Storage): MSFT_Partition.GetSupportedSize, then Resize. This is the interface Disk Management, diskpart and Resize-Partition use; it moves NTFS data out of the way and works on BitLocker volumes. Sizes are aligned to 1 MiB.MSFT_Disk.CreatePartition creates a GPT basic data partition in the freed space directly after it, without a drive letter; MSFT_Volume.Format formats it FAT32 with the label LINUXLIVE.IOCTL_DISK_SET_PARTITION_INFO_EX names the partition Linux Live boot and sets GPT_BASIC_DATA_ATTRIBUTE_NO_DRIVE_LETTER — the Storage API cannot set a GPT partition name. Explorer never shows the partition, and the program finds it again by its GPT name (IOCTL_DISK_GET_PARTITION_INFO_EX) without the registry.<partition>\<media folder>; then their old copies are deleted from the ESP folder — GRUB prefers its own folder, so stale copies there would win — and only shim, GRUB and grub.cfg are written to the ESP.After the shrink every failure is rolled back: the new partition is deleted and the Windows volume grown back to its previous size. boot-partition remove --yes (interface: File → Remove boot partition) checks the volume first, deletes the partition and grows the Windows volume to the maximum GetSupportedSize reports. Storage API errors are reported with their extended status; StorageWMI 42008 ("a volume with errors") is translated into the chkdsk instruction.
Tested on the XPS: C: shrunk by 200 MB, the partition created between C: and the Recovery partition, Linux started with BOOT_IMAGE=/linux-live/vmlinuz from it, Secure Boot on — 15 seconds for the whole deployment. When the Windows Recovery partition follows C: (the usual layout) the boot partition sits between them; Microsoft's manual procedure for enlarging the Recovery partition needs that space, so remove the boot partition first.
Win32_EncryptableVolume (WMI, root\CIMV2\Security\MicrosoftVolumeEncryption): GetConversionStatus decides whether the volume is encrypted — "protection off" alone does not mean decrypted, a suspended volume is still encrypted — GetProtectionStatus reports protection, GetKeyProtectors with type 3 lists numerical passwords and GetKeyProtectorNumericalPassword returns one. The password's BSTR is wiped with SecureZeroMemory after use. bitlocker.key is written atomically (.tmp, then MoveFileExW with MOVEFILE_WRITE_THROUGH), compared first so an unchanged key is neither rewritten nor asked about, and removed when the volume is no longer encrypted.
LinuxLive.exe status firmware, Secure Boot, BitLocker, layout, ESP, boot partition, entry
LinuxLive.exe list [--all] NVRAM entries: BootOrder first, then the others
LinuxLive.exe deploy [--image <url|file>] [--esp <url|file>] [--github]
[--bitlocker-key] [--boot-partition] [--no-entry] [--no-progress]
LinuxLive.exe source | source github | source custom --image <..> --esp <..>
LinuxLive.exe add | once [<entry>] | once --clear | default [<entry>]
LinuxLive.exe up|down <entry> | move <entry> <pos> | unlist|enable|disable <entry>
LinuxLive.exe delete <entry> --yes [--force]
LinuxLive.exe boot-partition | boot-partition remove --yes
LinuxLive.exe reboot --yes [--force]
LinuxLive.exe lng-export <file> | lng-check [<file>]
<entry> is a Boot#### number (0006, Boot0006, 6) or linux.
AttachConsole(ATTACH_PARENT_PROCESS); colours and redrawn progress bars use virtual-terminal sequences when the console supports them (ENABLE_VIRTUAL_TERMINAL_PROCESSING) and plain lines otherwise. cmd does not wait for a GUI-subsystem program and has already printed its prompt; a synthetic Enter written with WriteConsoleInputW at exit makes cmd print a fresh prompt below the output.--yes, --force, --bitlocker-key, --boot-partition), so ssh sessions and scripts behave like a terminal. Output is English, for scripts and bug reports.SetConsoleCtrlHandler to the same stop_token the interface uses; the deployment stops cleanly and the program exits with 4.| Code | Meaning |
|---|---|
| 0 | success |
| 1 | failed |
| 2 | invalid arguments |
| 3 | not possible here (no administrator rights, no UEFI, no ESP) |
| 4 | canceled |
ssh host command wait for the program; interactive PowerShell does not wait for GUI-subsystem programs — use LinuxLive.exe list | Out-Host there.InitiateShutdownW with a planned reconfiguration reason; --force also ends other users' sessions.A typical remote session:
ssh admin@laptop "C:\Tools\LinuxLive.exe deploy --bitlocker-key"
ssh admin@laptop "C:\Tools\LinuxLive.exe once"
ssh admin@laptop "C:\Tools\LinuxLive.exe reboot --yes"
ssh root@laptop # the same address, now Linux, the same keys
WM_DPICHANGED.DWMWA_SYSTEMBACKDROP_TYPE and DwmExtendFrameIntoClientArea; the client area is drawn with Direct2D into a DC render target with premultiplied alpha, so text and controls blend with the backdrop per pixel. In high-contrast mode the backdrop is off and system colours are used.WM_MEASUREITEM / WM_DRAWITEM) so they match the theme, with keyboard navigation kept through a WH_MSGFILTER hook; every action is in the menus and has a context menu or a button where it belongs.std::jthread; log lines and progress are posted to the window with ownership-transferring messages, and confirmations are sent synchronously, so the worker waits for the answer of the dialog. Closing the window during a deployment asks first and requests a stop.ITaskbarList3), the NVRAM list with move up/down, one-time boot, default, enable/disable and delete, a collapsible log, and the source dialog for custom URLs or files.English is built in. Additional languages are plain UTF-8 files in language\<code>.lng next to the executable (<id>=<text>, #name= and #locale= headers, \n for line breaks). The UI language follows the Windows display language through GetUserPreferredUILanguages or is chosen under View → Language, where each translation shows its native name and coverage. Strings are formatted with C++20 std::vformat; a missing string or a mismatched format argument falls back to English.
Shipped: 21 languages (English built in plus 20 files), 293 strings each, 100 % coverage: English, Polish (pl), German (de), Spanish (es), French (fr), Italian (it), Russian (ru), Japanese (ja), Simplified Chinese (zh-cn), Arabic (ar), Portuguese (pt), Ukrainian (uk), Turkish (tr), Korean (ko), Dutch (nl), Czech (cs), Traditional Chinese (zh-tw), Indonesian (id), Vietnamese (vi), Persian (fa), Hindi (hi).
More languages are welcome: export a template with lng-export, translate it and open a pull request.
LinuxLive.exe lng-export language\custom.lng :: export a template with all IDs
LinuxLive.exe lng-check :: validate coverage, missing IDs and format fields
Linux Live works inside the native Windows security boundary and uses only data that an elevated Windows administrator already has access to through built-in tools:
| What Linux Live uses | Where it comes from | How the Windows administrator already sees it |
|---|---|---|
| BitLocker recovery password | the volume's numerical password protector, read through WMI | manage-bde -protectors -get C: |
| Wi-Fi networks and passwords | WLAN profiles, decrypted with the machine's DPAPI keys | netsh wlan show profile name=<network> key=clear |
| SSH login keys | C:\ProgramData\ssh\administrators_authorized_keys |
the same keys already open an administrator session on Windows |
| SSH host key | C:\ProgramData\ssh\ssh_host_ed25519_key |
Windows' OpenSSH server uses it for every connection |
| the Windows volume | BitLocker unlocked with the recovery password, NTFS mounted | Explorer, as an administrator |
The one place where Linux Live stores a secret somewhere new is bitlocker.key on the ESP — in plain text, on a partition that is not encrypted. It is the price of starting Linux without typing 48 digits, so it is written only after an explicit yes, and the program says so in the question.
grub.cfg, the initrd and the squashfs are unsigned on every signed distribution. They are checked with SHA-256 when downloaded; on disk they are as safe as any file an administrator of this Windows can change — an administrator can also replace Windows' own boot files.administrators_authorized_keys and the media folder's authorized_keys are imported — keys that already give administrator access to this machine.wll-commit is that user's private layer on their own Windows volume. The passwords are the ones an administrator of that Windows already sees with netsh wlan show profile name=<network> key=clear.Config::kRequireSha256 is switched on at build time).C:\linux-live runs as root in Linux: the image, the persistence layer, authorized_keys, and the cached esp.tar that a later deployment may copy to the ESP. The default permissions of C:\ give every authenticated user Modify on new folders below it, so a standard user could otherwise plant a layer or a key and become root in Linux — and reach C: past its Windows permissions. Every deployment therefore sets the folder's DACL explicitly (SetNamedSecurityInfoW, protected): full control for SYSTEM and Administrators, read and execute for Users, nothing inherited from C:\; the files inside take the same entries. Linux writes there as root through the NTFS driver, which does not apply NTFS permissions, so wll-commit keeps working.NTFSPlus is young and not free of bugs. The kernel's new NTFS driver replaced the old read-only ntfs driver recently, and as an NTFS implementation it still has a few defects. I write NTFS code myself — NTFS_EFI, a read-write NTFS driver for UEFI — and while building this project I found and reproduced defects in NTFSPlus and contributed to fixing them upstream: some twenty to thirty threads of reports, reproductions and patches on the driver's mailing list (ntfs@lists.linux.dev), reviewed by its maintainers (one of the test runs in the driver's repository). One example of the kind of defect involved: $MFT writeback could look up a VCN past the allocated end of the MFT when a folio straddled it, marking the volume erroneous after creating a single file; I reproduced it on arm64 with 16 KiB pages and sent a patch, and the maintainers' ntfs-next branch had meanwhile replaced that code path (commit e543dc6c5f1e). Fixing continues.
C: is left marked dirty after a session that wrote to it. With the Debian 7.2.11 kernel, unmounting /mnt/win — also cleanly, without -l — logs:
ntfs: (device dm-1): ntfs_put_super(): Volume has errors. Leaving volume marked dirty. Run chkdsk.
A mount and unmount without writes stays clean. The only other driver message of such a session is is_boot_sector_ntfs(): Invalid boot sector checksum. — the checksum field of the NTFS boot sector is stale on Windows volumes resized after formatting (Windows does not maintain it); in the driver's source that warning alone does not set the error flag. The cause is not identified yet. Consequences on the Windows side: the volume is flagged Scan Needed, Repair-Volume -Scan asks for an offline fix, and the Storage Optimizer refuses to shrink it (0x89000015, "the dirty bit is set"). A restart with the dirty bit set lets autochk repair it in seconds. LinuxLive.exe detects the state before changing partitions and says so instead of failing inside the storage API. Until this is fixed: write to /mnt/win when you need to, and let Windows check C: afterwards.
Fast Startup. A Windows "shut down" with Fast Startup enabled leaves C: hibernated, and the NTFS driver then mounts it read-only (hibernation safety comes first). reboot and once restart Windows fully, so this only matters when Linux is chosen after a shutdown.
BitLocker and the boot path. BitLocker seals its key to the measured boot path. Changing what runs before Windows Boot Manager — for example starting Windows from the firmware menu instead of through an EFI application that normally chains it — makes Windows ask for the recovery password once.
End to end on a Dell XPS 7590 (i9-9980HK, Windows 11, Secure Boot on, BitLocker on, UnderVolter as the first boot entry), driven over SSH:
| Test | Result |
|---|---|
| Deploy from HTTPS (LAN): package, layout, 1.07 GB image, SHA-256, ESP, BitLocker key, NVRAM entry | 25 s |
| Deploy again, nothing changed | 1.8 s, 6 files unchanged |
One-time boot: live-media-path=linux-live, BitLocker unlocked from \EFI\debian\bitlocker.key, key copies gone from RAM |
pass |
Linux: Secure Boot enabled, Wi-Fi from Windows profiles, SSH with Windows keys and host key, /mnt/win read-write, no failed units |
pass, 18.9 s to userspace |
Persistence: save, reboot, file present; --reset |
pass |
delete of a bcdedit /copy entry: BCD object removed with it, entry does not come back |
pass |
| ESP filled to 10 % free: deploy without consent refuses, nothing changed | pass |
Deploy --boot-partition: C: shrunk 200 MB, FAT32 partition between C: and Recovery, no drive letter, kernel moved, ESP back to 37 % free |
15 s |
Linux from the boot partition (BOOT_IMAGE=/linux-live/vmlinuz), Secure Boot on |
pass |
tests/ also holds Windows checks against the real machine:
verify.cpp — read-only: every entry rebuilt from the partition data must equal the NVRAM variable byte for byte;nvram_roundtrip.cpp — create, reorder, BootNext, disable/enable, delete; BootOrder and BootNext end byte for byte as they started, restored also when a step fails;test_tar.cpp (extracts a package through Install::DeployEsp, twice, to see the unchanged-file skip), test_head.cpp (a raw WinHTTP request for the headers of a package URL), test_remote_check.cpp (Net::CheckRemote on the image and package URLs) and test_bitlocker_wmi.cpp (the BitLocker WMI calls on their own).About 10 000 lines of C++23 in src/, plus the image recipe (shell and Python) in tests/linux/build-steps/.
src/app/ Cli.cpp (commands, help, exit codes), Console.cpp (attach, VT output, progress bars), main.cpp
src/core/ Deploy.cpp (download, verify, layout, ESP, boot partition, BitLocker), Actions.cpp,
BootList.cpp (one NVRAM snapshot for both front ends), Sources.cpp
src/efi/ Efi.cpp (NVRAM variables, load options, device paths), Bcd.cpp (BCD copies of entries)
src/install/ EspInstaller.cpp (tar reader, ESP writer, kernel routing), Layout.cpp (GRUB prefix, grub.cfg)
src/storage/ Volumes.cpp (volume GUID paths, ESP and data volume), BitLocker.cpp (WMI),
BootPartition.cpp (Storage Management API, GPT name, dirty bit)
src/net/ Downloader.cpp (WinHTTP, progress, .part files)
src/gui/ MainWindow*.cpp, Widgets.cpp, Menus.cpp, Dialogs.cpp, Theme.cpp (Direct2D, DirectWrite, Mica)
src/support/ Wmi.cpp, Text.cpp (strings, .lng files), Settings.cpp, Sha256.cpp (CNG), Handle.h (RAII)
src/resources/ resource.h, lang/en.rc (built-in English), language/*.lng
tests/ Windows checks; linux/build-steps - the image recipe
| Area | Support |
|---|---|
| Host | Windows 10 / 11 x64, UEFI firmware, elevated session |
| Secure Boot | on or off; with it on, only distribution-signed shim, GRUB, kernel and modules are used |
| BitLocker | numerical (recovery) password protector; read-only unlock, read-write second mapping for /mnt/win |
| ESP | any size that can hold the boot loader with a 20 % (min. 20 MB) reserve; kernel and initrd move to a 200 MB FAT32 boot partition on small ESPs |
| Boot entries | create or reuse, BootOrder, BootNext, enable/disable, delete with BCD clean-up |
| Sources | GitHub release, custom URL or local file for the image and the ESP package |
| Interfaces | Direct2D GUI (Per-Monitor V2, Mica) and a scriptable CLI with exit codes |
| Linux side | Debian 13, signed kernel 7.2.11, NTFSPlus, systemd-networkd, key-only sshd, save-session persistence |
| Other distributions | any package with a signed shim, GRUB and kernel and a grub.cfg that passes the layout checks |
| Languages | 21, detected from the Windows display language |
LinuxLive.exe delete removes the boot entry and its BCD copies; it does not delete files. A complete removal is therefore four explicit steps from an elevated prompt:
:: 1. Remove the UEFI entry (and the BCD copies Windows would write back)
LinuxLive.exe delete linux --yes
:: 2. Only if a boot partition was created: delete it and grow C: back
LinuxLive.exe boot-partition remove --yes
:: 3. Remove the image folder (image, persistence layer, authorized_keys, cached esp.tar)
rmdir /s /q C:\linux-live
:: 4. Remove the ESP folder: mount the ESP temporarily, delete the folder, unmount
mountvol S: /S
rmdir /s /q S:\EFI\debian
mountvol S: /D
The ESP folder also holds bitlocker.key (plain text), so step 4 removes it too. Skip step 4 when an installed Debian uses the same \EFI\debian; the program does the same check before deploying.
pwsh -File .\build.ps1
build.ps1 finds the toolchain by evidence, not by version numbers: vswhere for Visual Studio instances, then the folder that really contains cl.exe for x64, and the Windows SDK whose rc.exe, Windows.h and kernel32.lib exist. Products without a C++ compiler (such as SQL Server Management Studio) are skipped, and versions are compared as versions, not as text. The result is bin\LinuxLive.exe (about 1 MB) with bin\language\, and bin\build-receipt.json with the SHA-256 of every output. Compiler settings: C++23, /W4 /WX /sdl /guard:cf, CET-compatible (/CETCompat), reproducible (/Brepro), x64.
| Download | Contents |
|---|---|
LinuxLive.7z |
LinuxLive.exe and all 20 language files |
| Source archive | Windows C++23 source, project files, language files, tests and the image build scripts |
filesystem.squashfs |
the reference image (1.07 GB) — for offline installs only |
esp.tar |
the ESP package: shim, GRUB, grub.cfg, kernel, initrd (57 MB) — for offline installs only |
| GitHub releases | all releases and their .sha256 files |
Download only LinuxLive.7z. The program fetches filesystem.squashfs and esp.tar itself and checks both against their .sha256 files; the direct links are for offline installations, used through Source → Edit custom sources… or deploy --image <file> --esp <file>.
LinuxLive.7z
├── LinuxLive.exe
└── language\
├── ar.lng cs.lng de.lng es.lng fa.lng fr.lng hi.lng id.lng it.lng ja.lng
└── ko.lng nl.lng pl.lng pt.lng ru.lng tr.lng uk.lng vi.lng zh-cn.lng zh-tw.lng
Reference release (Debian 13, kernel 7.2.11):
| File | Size | SHA-256 |
|---|---|---|
LinuxLive.7z |
455,066 bytes | b66541f9fa71a202a7e3ac93630ccfc7dc8adf4e4795efadf451d9f83fdad89f |
LinuxLive_source_code.zip |
344,954 bytes | 1306508d44bbc673aaa15805ccab99eb2921f45dd7cc7ef33754eef01662f4e7 |
filesystem.squashfs |
1,147,117,568 bytes | a0f4fc01776e5d64da934e60dd6b945bde7759c99fdda4352a3f22f6127ecbe1 |
esp.tar |
59,637,760 bytes | 31d221302b75059711da47c48d3af634f14ee3187a0309a26b9bf9dc8f41a5f0 |
MIT — see LICENSE.md.
Debian, its kernel, shim and GRUB are distributed under their own licences inside the image and the ESP package. RegEdLin, used in the image for the offline Wi-Fi profiles, is a separate project for offline Windows registry manipulation.
Add a comment