2

Imagine we have a statically linked Linux executable.

How should I name it in the imported tar.gz so the WSL 1 will run it by default, when created and started like:

# import an archive as a WSL distro
wsl --import static tmp-root-dir static.tar.gz
# boot distro to a default app??
wsl -d static

PS WSL uses own proprietary boot process and seems doesn't use traditional Unix /sbin/init.

gavenkoa
  • 45,285
  • 19
  • 251
  • 303

2 Answers2

2

Short answer:

The smallest bootable (without errors or warnings) WSL rootfs will consist of three files:

  • /main: Your statically-linked application. It can be named whatever you want, as long as the name matches what is in passwd.
  • /etc/passwd: Defines the application (i.e. shell) to load for the default user.
  • /etc/wsl.conf: To suppress normal WSL functionality and (optionally) define the non-root user.

More detail:

This probably isn't exactly what you are wanting, but it will hopefully meet your needs.

To start with, the entry point for WSL (the first time a Linux ELF binary is started inside the instance) seems to be its /init binary, which, in addition to some "normal" Linux init process tasks, sets up some of the Windows-interop functionality. To my knowledge, it cannot currently be changed. As far as I can tell, for WSL1, it is injected into the instance by the LXSS manager when starting a WSL instance.

Note: WSL2 might be slightly different in this regard, as it does seem to use a kernel-processed initrd to load /init. It is possible to override the kernel command-line, but that would impact all WSL2 instances, so it's probably not a practical solution.

It's not quite clear from your question whether you want the "default application" to:

  • Run as the default application/shell every time wsl -d static is run, even if it was already running.
  • Or just run once when starting the WSL1 instance for the first time.

I believe you are looking for the first option.

Run as the default application

In the first case, the standard WSL1 /init process might get you to where you need to be. As part of the startup, as you would expect, it reads /etc/passwd to determine the user shell to start. It also reads /etc/wsl.conf to determine the default user ID (but falls back to the registry if there is no default user set in wsl.conf).

So, to start a different application (let's call it main), you can:

  • Place the binary in the root directory of your image.

  • Set the application as the "shell" of the root user in a single-line /etc/passwd:

    user:x:1000:1000:user:/:/main
    

    Side-note that this also sets the home directory to / so we don't have to create another directory.

  • Define a etc/wsl.conf with the following contents:

    [user]
    default=user
    
    [automount]
    enabled=false
    mountFsTab=false
    
    [interop]
    appendWindowsPath=false
    

    This will prevent WSL from performing the following startup tasks, which would produce an error without additional image support:

    • Mounting Windows drives into the instance
    • Attempting to process /etc/fstab (since we have no mount command in the image).
    • Appending Windows paths (since our instance won't have access to the Windows drives)

    It also sets the default user to the UID 1000 user we created in /etc/passwd. This isn't strictly necessary - There's likely no concern with running as root in this single-use instance, but I've included a non-root user as a "best practice".

That should be it. The smallest bootable WSL rootfs will consist of just those three files:

/etc/wsl.conf
/etc/passwd
/main

This will work on WSL1 as well as WSL2, although for WSL2, you should invoke with wsl ~ -d static to make sure that it doesn't try to start on a Windows drive that it can't access. Otherwise, you'll receive an init error, but your application will still be invoked.

Run once

If you are looking for something that will, for instance, start up a daemon when the instance is started for the first time, then there are a few alternatives that I document in this answer. If you are on Windows 11, then there's a built-in mechanism via /etc/wsl.conf. Otherwise, on Windows 10, you'll probably need to include some binary that can handle conditional logic. Something like execline would probably be perfect for this, but I've had issues with it under WSL2, at least, and I'm not sure that it would run under WSL1 (but it might).

Side-note for WSL1/musl

musl is a commonly used alternative libc implementation. For instance, Rust (AFAICT), can only generate truly statically-linked executables using musl. Note, however, that WSL1 cannot run musl-based statically linked binaries.

WSL2 can handle them just fine.

NotTheDr01ds
  • 15,620
  • 5
  • 44
  • 70
0

I managed to get it working. Initially I missed an executable bit on the app when created TAR archive.

Take standard 64-bit assembly:

.data
msg:
  .ascii "Hello, world!\n"
  .set len, . - msg

.text

.globl _start
_start:
  # write
  mov  $1,   %rax
  mov  $1,   %rdi
  mov  $msg, %rsi
  mov  $len, %rdx
  syscall

  # exit
  mov  $60, %rax
  xor  %rdi, %rdi
  syscall

and create a minimal WSL system:

wsl as -64 -o minimal.o minimal.s
wsl ld -melf_x86_64 -o minimal minimal.o
tar czf minimal.tar.gz \
  --mode=a=rx \
  --xform='s#^minimal#/\0#' minimal
wsl --import minimal rootfs-minimal minimal.tar.gz --version 1
wsl --list
wsl -d minimal -e /minimal

To make executable default (shorten wsl -d minimal -e /minimal to wsl -d minimal) we need an extra file /etc/passwd:

root:x:0:0:root:/root:/minimal

First line of this file determine a default user and so path to the executable (entry point) unless you override the user with /etc/wsl.conf:

[user]
default=user

Basically WSL 1 treats only 2 files as magical (in addition to ignoring /sbin/init):

  • /etc/wsl.conf
  • /etc/passwd
gavenkoa
  • 45,285
  • 19
  • 251
  • 303