Void Linux musl serves as the "Linux Pre-Execution Environment" (UnixPE) in NETSetup. It boots from a squashfs image on the target disk’s ESP partition and runs NETSetup to install Linux-based operating systems (Proxmox, NixOS, etc.).

Architecture Overview

USB Stick (WinPE)
  │
  ├─ NETSetup.exe install (Phase 1: WinPE)
  │   ├─ Partitions target disk: GPT, 4GB FAT32 ESP (label=SYSTEM)
  │   ├─ Copies linux/ folder → Z:\ (GRUB + Void squashfs)
  │   ├─ Copies NETSetup binary → Z:\NETSetup\NETSetup.musl
  │   ├─ Copies config + OS image (e.g. proxmox.iso) → Z:\NETSetup\
  │   └─ Reboots (USB can be removed)
  │
Target Disk ESP (label=SYSTEM)
  │
  ├─ /EFI/BOOT/BOOTX64.EFI          ← GRUB bootloader
  ├─ /boot/vmlinuz                    ← Void Linux kernel
  ├─ /boot/initrd                     ← Void Linux initramfs
  ├─ /boot/grub/grub.cfg → grub_void.cfg
  ├─ /LiveOS/squashfs.img             ← Void Linux rootfs (compressed)
  │   └─ LiveOS/ext3fs.img            ← Void Linux rootfs (ext3, ~3GB)
  │       └─ / (full Void musl system)
  │           └─ /etc/rc.local        ← entrypoint: searches for autoexecute.sh
  ├─ /NETSetup/NETSetup.musl          ← NETSetup binary (linux-musl-x64)
  ├─ /NETSetup/Config/*.json           ← customer config
  └─ /NETSetup/Images/{OS}/            ← OS installer ISO

Boot Chain

  1. UEFI firmware boots GRUB from ESP (/EFI/BOOT/BOOTX64.EFI)

  2. GRUB loads grub_void.cfg → boots "UnixPE" menu entry

  3. Kernel + initrd boot, dracut live module:

    1. Finds squashfs.img on partition LABEL=SYSTEM

    2. Mounts squashfs (read-only) → loop-mounts ext3fs.img

    3. Creates overlayfs (rw tmpfs on top of ro rootfs)

    4. Pivots root to overlayfs

  4. Void Linux init runs → /etc/rc.local executes

  5. rc.local scans all partitions for autoexecute.sh

  6. Finds it on the SYSTEM partition → runs it

  7. autoexecute.sh launches NETSetup.musl install

  8. NETSetup Stage 4 (LinuxPE) installs the target OS via GRUB chainload

Disk Layout on Target (after Phase 1)

/dev/sdX (or nvme0n1)
├─ Partition 1: FAT32 ESP, 4GB, label=SYSTEM
│   ├─ EFI/BOOT/BOOTX64.EFI
│   ├─ boot/ (vmlinuz, initrd, grub/)
│   ├─ LiveOS/squashfs.img
│   ├─ NETSetup/ (binary, config, images)
│   └─ answer.toml (for Proxmox auto-install)
└─ Partition 2: (created by Stage 4) raw ISO partition for OS installer

squashfs.img Structure

The squashfs contains a single file: LiveOS/ext3fs.img — a ~3GB ext3 filesystem with the full Void Linux musl rootfs.

Key files inside:

Path Purpose

/etc/rc.local

Boot entrypoint — scans drives for autoexecute.sh, runs NETSetup

/usr/bin/ntfs-3g

NTFS mount support (needed to read USB sticks)

/usr/lib/libstdc++.so.6

C++ standard library — must match .NET runtime requirements

How to Modify the Void PE Image

Prerequisites

  • WSL (Ubuntu) with squashfs-tools installed: sudo apt install squashfs-tools

  • The squashfs.img lives at: src/NETSetup/linux/LiveOS/squashfs.img

  • Helper scripts in the same folder: open-void-pe.sh, close-void-pe.sh, edit-netsetup-call.sh

Open (unsquash + mount)

From WSL Ubuntu, cd to the LiveOS/ folder:

cd /mnt/c/_gh/main/netsetup/src/NETSetup/linux/LiveOS

# Step 1: unsquash (creates void/LiveOS/ext3fs.img)
sudo unsquashfs -f -d void squashfs.img

# Step 2: copy ext3fs.img to Linux FS (loop mount doesn't work on /mnt/c)
sudo mkdir -p /mnt/void
sudo cp void/LiveOS/ext3fs.img /tmp/ext3fs.img
sudo mount -o loop /tmp/ext3fs.img /mnt/void
Note
Loop-mounting on /mnt/c (Windows FS via 9p/DrvFs) does not work. You MUST copy ext3fs.img to the native Linux filesystem (e.g. /tmp/) before mounting.

Modify files

# Edit rc.local
sudo nano /mnt/void/etc/rc.local

# Or browse the filesystem
ls /mnt/void/usr/lib/libstdc++*

Chroot (for package management)

Important
You CANNOT chroot from glibc Ubuntu into musl Void — the binaries will not run. Use static xbps instead:
# Download static xbps (runs on any Linux x86_64)
cd /tmp
wget https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz
tar xf xbps-static-latest.x86_64-musl.tar.xz

# Upgrade all packages in the mounted rootfs
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -Su

# Install a specific package
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -S <package-name>

# Query installed packages
sudo /tmp/usr/bin/xbps-query.static -r /mnt/void -l

Alternatively, if you have a real Void Linux musl system (VM, live USB, etc.), you can chroot /mnt/void directly and use normal xbps-install -Su.

Close (unmount + repack)

# Step 1: unmount
sudo umount /mnt/void

# Step 2: copy modified image back
sudo cp /tmp/ext3fs.img void/LiveOS/ext3fs.img

# Step 3: fsck
sudo e2fsck -p -f void/LiveOS/ext3fs.img

# Step 4: verify
dumpe2fs -h void/LiveOS/ext3fs.img | grep "Filesystem state"

# Step 5: repack squashfs (xz compression)
sudo rm squashfs.img
sudo mksquashfs void squashfs.img -comp xz -noappend

# Step 6: cleanup
sudo rm -rf void
sudo rm /tmp/ext3fs.img

How to Create a New Void PE Image from Scratch

When upgrading to a new Void Linux version or starting clean.

Step 1: Download Void Linux musl ROOTFS

cd /tmp
# Get the latest musl rootfs tarball from:
# https://voidlinux.org/download/ → "Base Tarballs" → x86_64-musl
wget https://repo-default.voidlinux.org/live/current/void-x86_64-musl-ROOTFS-<DATE>.tar.xz

Step 2: Create ext3fs.img

# Create a 3GB ext3 image (adjust size as needed)
dd if=/dev/zero of=/tmp/ext3fs.img bs=1M count=3072
mkfs.ext3 /tmp/ext3fs.img

# Mount it
sudo mkdir -p /mnt/void
sudo mount -o loop /tmp/ext3fs.img /mnt/void

# Extract rootfs
sudo tar xpf void-x86_64-musl-ROOTFS-*.tar.xz -C /mnt/void

Step 3: Install required packages

# Copy DNS config so xbps can resolve repos
sudo cp /etc/resolv.conf /mnt/void/etc/resolv.conf

# Use static xbps to install packages
cd /tmp
wget https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz
tar xf xbps-static-latest.x86_64-musl.tar.xz

# Update package database and install required packages
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -Syu
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -S \
    ntfs-3g \
    parted \
    dosfstools \
    e2fsprogs \
    grub-x86_64-efi \
    lsblk \
    udev \
    kmod \
    dracut \
    wifi-firmware \
    wpa_supplicant \
    dhclient

Step 4: Configure rc.local

Copy the rc.local from the existing image or write a new one. The rc.local must:

  1. Scan all partitions (lsblk) for autoexecute.sh

  2. Mount each partition (handle NTFS via ntfs-3g, other via mount -t)

  3. When found, copy to /root/autoexecute.sh, chmod +x, and exec it

See the current rc.local in the existing image for the full implementation.

Step 5: Extract kernel + initrd

# The kernel and initrd for GRUB must come from this rootfs
sudo cp /mnt/void/boot/vmlinuz-<version> /mnt/c/_gh/main/netsetup/src/NETSetup/linux/boot/vmlinuz
sudo cp /mnt/void/boot/initramfs-<version>.img /mnt/c/_gh/main/netsetup/src/NETSetup/linux/boot/initrd
Important
The kernel (vmlinuz) and initrd in linux/boot/ must match the kernel installed in the squashfs rootfs. If you update the kernel inside the image, you MUST also update these files.

Step 6: Pack into squashfs

sudo umount /mnt/void

# Create squashfs directory structure
mkdir -p /tmp/void-squash/LiveOS
mv /tmp/ext3fs.img /tmp/void-squash/LiveOS/ext3fs.img

# Pack
sudo mksquashfs /tmp/void-squash /mnt/c/_gh/main/netsetup/src/NETSetup/linux/LiveOS/squashfs.img -comp xz -noappend

# Cleanup
sudo rm -rf /tmp/void-squash

Updating libstdc++ (fixing .NET 10 compatibility)

NET 10’s native runtime components require libstdc++ from GCC 12+.

The current image has libstdc++.so.6.0.28 (GCC 10) which causes:

Error relocating NETSetup.musl: _ZSt28__throw_bad_array_new_lengthv: symbol not found

Fix: Update packages in existing image

Follow "How to Modify the Void PE Image" above, then:

# Upgrade all packages (pulls in GCC 12+ libstdc++)
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -Su

# Verify libstdc++ version (should be 6.0.32 or newer)
ls -la /mnt/void/usr/lib/libstdc++.so*

Then close/repack the image.

Updating the kernel + initrd

After a full system upgrade, the kernel version may change. You need to update the boot files in linux/boot/:

# Check which kernel version is installed
ls /mnt/void/boot/vmlinuz-*
ls /mnt/void/boot/initramfs-*.img

# Copy to the boot folder
sudo cp /mnt/void/boot/vmlinuz-<new-version> /mnt/c/_gh/main/netsetup/src/NETSetup/linux/boot/vmlinuz
sudo cp /mnt/void/boot/initramfs-<new-version>.img /mnt/c/_gh/main/netsetup/src/NETSetup/linux/boot/initrd

GRUB Configuration

grub.cfg (main)

Loads filesystem modules and sources grub_void.cfg:

insmod usbms
insmod part_gpt
insmod fat
insmod iso9660
insmod ntfs
insmod linux
search --file --no-floppy --set=voidlive "/boot/grub/grub_void.cfg"
source "(${voidlive})/boot/grub/grub_void.cfg"

grub_void.cfg (Void Linux boot entry)

menuentry "UnixPE" {
    linux (${voidlive})/boot/vmlinuz \
        root=live:LABEL=SYSTEM ro init=/sbin/init \
        rd.live.overlay.overlayfs=1 lockdown=none
    initrd (${voidlive})/boot/initrd
}

Key kernel parameters:

  • root=live:LABEL=SYSTEM — dracut live module finds squashfs on partition labeled SYSTEM

  • rd.live.overlay.overlayfs=1 — use overlayfs (writable tmpfs layer on top of read-only rootfs)

  • lockdown=none — allow unsigned modules

NETSetup adds installer entry at runtime

Stage 4 (LinuxPE) appends a second GRUB menu entry to grub_void.cfg that boots the OS installer ISO from partition 2 via rdinit= wrapper. See BootISOGrub2.cs and the process diagram in docs/NETSetup.puml.

rc.local Boot Flow

/etc/rc.local
  │
  ├─ Lock file check (/var/lock/autoexecute.lock)
  ├─ Scan all partitions (lsblk: sd*, nvme*, hd*, sr*)
  │   ├─ Skip: already mounted, no filesystem, swap
  │   ├─ Mount: NTFS via ntfs-3g, others via mount -t $fstype
  │   └─ Search for autoexecute.sh on each partition
  │
  ├─ Found autoexecute.sh:
  │   ├─ Copy to /root/autoexecute.sh (strip \r)
  │   ├─ chmod +x
  │   └─ exec → NETSetup.musl install
  │
  └─ Already has /root/autoexecute.sh:
      └─ Re-exec if not already running

File Inventory

File Purpose Tracked in git?

linux/EFI/BOOT/BOOTX64.EFI

GRUB EFI bootloader

Yes

linux/boot/vmlinuz

Void Linux kernel

Yes (LFS recommended)

linux/boot/initrd

Void Linux initramfs

Yes (LFS recommended)

linux/boot/grub/grub.cfg

GRUB main config

Yes

linux/boot/grub/grub_void.cfg

Void Linux boot menu

Yes

linux/boot/grub/splash.png

GRUB background

Yes

linux/LiveOS/squashfs.img

Void Linux rootfs (compressed)

Yes (LFS recommended)

linux/LiveOS/void/

Extracted squashfs (working dir)

No (.gitignore)

linux/LiveOS/open-void-pe.sh

Helper: unsquash + mount

Yes

linux/LiveOS/close-void-pe.sh

Helper: unmount + repack

Yes

linux/LiveOS/edit-netsetup-call.sh

Helper: edit rc.local

Yes

Troubleshooting

_ZSt28__throw_bad_array_new_lengthv: symbol not found

libstdc is too old for .NET 10. See <<Updating libstdc (fixing .NET 10 compatibility)>>.

Loop mount fails on /mnt/c

WSL’s DrvFs (9p) does not support loop devices. Copy ext3fs.img to native Linux FS (/tmp/) first.

Cannot chroot from Ubuntu into Void

Ubuntu uses glibc, Void uses musl — binaries are incompatible. Use static xbps (xbps-install.static -r /mnt/void) or a real Void Linux system.

Kernel panic on boot

Kernel (vmlinuz) and initrd in linux/boot/ don’t match the rootfs. After upgrading the kernel inside the squashfs, copy the new vmlinuz + initrd to linux/boot/.