The Ultimate Guide to User-Mode Linux: Run a Full Linux VM Without VM Software

The CyberSec Guru

The Ultimate Guide to User-Mode Linux (UML)

If you like this post, then please share it:

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Why your support matters: Zero paywalls: Keep the content 100% free for learners worldwide, Writeup Access: Get complete writeup access within 12 hours of machine drop along with scripts and commands.

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

Buy Me a Coffee Button

What if you could run an entire, separate Linux kernel… as a regular application? Not a virtual machine requiring tools like QEMU, KVM, or VirtualBox. Not a container sharing your main kernel. I mean literally launching a new, clean, isolated kernel from your command line, just like you’d launch vim or bash.

This isn’t a futuristic thought experiment. This is User-Mode Linux (UML).

If you dig deep into the official Linux kernel documentation, you’ll uncover a fascinating statement:

“Linux has also been ported to itself. You can now run the kernel as a userspace application – this is called UserMode Linux (UML).”

This single sentence opens a rabbit hole of possibilities. It implies you can boot a second, nested Linux system entirely within your user session, without root privileges, and without any traditional virtualization software.

Today, we’re not just peering into that rabbit hole—we’re diving in headfirst. This is the definitive, A-to-Z guide to User-Mode Linux. We will explore the deep theory, build a UML kernel from source, craft a custom userspace for it, configure complex networking, manage storage, and finally answer the critical question: what is this “VM without a VM” actually for?

By the time you finish this guide, you will have all the knowledge necessary to master UML for kernel debugging, secure sandboxing, complex network simulation, or simply for the sheer, mind-bending fun of it.

Linux-inside-Linux
Linux-inside-Linux

The Theoretical Foundations: What Is a Kernel, Anyway?

Before we can understand how to run a kernel in userspace, we must first agree on what a kernel is and where it lives.

In a traditional computing architecture (like x86), the CPU operates in different “rings” of privilege.

  • Ring 0 (Kernel Mode): This is the most privileged level. The code running here—the kernel—has unrestricted access to all hardware: the CPU, memory, disk controllers, network cards, everything. It manages system resources, schedules processes, and enforces security.
  • Ring 3 (User Mode): This is the least privileged level. This is where your applications (your web browser, your terminal, your text editor) live. If an application wants to do anything interesting, like open a file, send a network packet, or even just get the current time, it cannot do so directly. It must ask the kernel.

This request is made via a System Call (syscall). The application in Ring 3 bundles up a request and executes a special instruction (like int 0x80 or syscall) that “traps” into the kernel. The kernel (in Ring 0) takes over, validates the request, performs the action on the application’s behalf, and then hands control back to Ring 3.

This “user/kernel” divide is the fundamental basis of all modern operating systems. The kernel is a hardware abstraction layer. Its job is to abstract the messy, specific details of a thousand different disk controllers into a simple, consistent API: open(), read(), write(), close().

Virtualization vs. Containerization: Finding UML’s Place

So, if you want to run a “guest” operating system, you have to manage this Ring 0/Ring 3 relationship. Over the years, we’ve developed three main ways to do this.

  1. Full Virtualization (e.g., QEMU + KVM):
    • How it works: A Hypervisor (in this case, KVM) runs in Ring 0. It uses special CPU features (Intel VT-x, AMD-V) to create a “guest” Ring 0 and a “guest” Ring 3. When the guest kernel thinks it’s accessing hardware, the CPU automatically traps to the hypervisor, which emulates that hardware (often with help from a Ring 3 process like QEMU) and then resumes the guest.
    • Isolation: Extremely high. The guest kernel is completely separate and can crash without affecting the host.
    • Overhead: Low for CPU/memory (thanks to hardware assist) but can be higher for emulated I/O (though virtio drivers make this near-native).
  2. Containerization (e.g., Docker, Podman):
    • How it works: There is no guest kernel. All containers share the host’s Ring 0 kernel. Isolation is achieved in Ring 3 using two kernel features: Namespaces (which make a container think it’s the only process, with its own process IDs, network stack, etc.) and cgroups (which limit how much CPU/RAM/IOPS that container can use).
    • Isolation: Low. All processes, host and “guest” alike, are just regular processes to the host kernel. A kernel panic takes down everything. You cannot run a different kernel version.
    • Overhead: Virtually zero. It’s just a regular process in a box.
  3. Paravirtualization (e.g., early Xen):
    • How it works: This is a clever compromise. The guest kernel is modified—it is “enlightened.” It knows it’s a guest. Instead of trying to access fake hardware (which would cause a trap), it makes special “hypercalls” directly to the hypervisor. This is much more efficient than full emulation.

So where does User-Mode Linux fit?

UML is a unique, fourth category. It is a full port of the entire Linux kernel, but it’s not ported to run on bare-metal hardware (Ring 0). It’s ported to run on the Linux System Call API (Ring 3).

The “hardware” that a UML kernel “abstracts” is actually the system call interface of the host kernel.

This is why the original article’s author viewed it as a “paravirtualized kernel configuration.” The UML kernel is “enlightened.” Its drivers don’t talk to a physical UART controller; the UML console driver makes write() syscalls to the host’s stdout. The UML block device driver (UBD) doesn’t talk to a SATA controller; it makes open(), read(), and write() syscalls to a file on the host’s filesystem.

Full Virtualization vs Containerization vs User-Mode Linux
Full Virtualization vs Containerization vs User-Mode Linux

A Deep Dive into UML Architecture

Understanding this “kernel-on-a-syscall-API” concept is the key. Let’s look at how UML actually works, mapping its “virtual hardware” to host resources.

When you launch UML, you are launching a single, regular, unprivileged ELF executable named linux. This process, running in Ring 3, is the “UML Kernel.”

  • How UML Boots: The host kernel’s ELF loader (/lib64/ld-linux-x86-64.so.2) loads the linux binary into memory and starts it, just like any other program. The UML kernel then initializes itself, all within this one process.
  • Virtual “Physical” Memory: How does the guest kernel get its RAM? The UML process simply makes an mmap() syscall to the host, requesting a large, anonymous block of memory (e.g., 512MB). The host kernel gives it this memory, and the UML kernel then treats that memory block as its entire “physical” address space. It builds its own page tables to manage this memory for its own guest processes.
  • Virtual CPU and Guest Processes: This is the most brilliant and complex part. The original author was “not entirely clear” on this, so let’s be explicitly clear. UML uses one of two modes:
    1. Tracing Thread (TT) Mode (Older): All guest processes (like /sbin/init or /bin/bash inside the guest) are run as threads within the main UML kernel process. The UML kernel itself acts as a scheduler, context-switching between these threads.
    2. Separate Kernel Address Space (SKAS) Mode (Modern): This is the default. When the UML kernel wants to “create” a new guest process (e.g., it runs init), it creates a real, new host process using the clone() syscall. The “magic” is that this new host process is created in a stopped state and is ptrace()d by the main UML kernel process.
    ptrace() (process trace) is the same tool gdb uses for debugging. It allows a parent process to intercept every single system call made by its child.When the guest /bin/bash tries to open() a file, its host process (traced by UML) makes an open() syscall. The host kernel intercepts this (because of ptrace), pauses the process, and notifies the UML kernel process. The UML kernel process inspects the syscall, routes it through its own virtual filesystem (VFS) layer, figures out it needs to read from its virtual ubd device, and then makes its own read() syscall to the host file. Once it has the data, it injects it back into the traced guest process and resumes it.This is the core of UML: it’s a syscall-level hypervisor, using ptrace as its “trap-and-emulate” mechanism.
  • Virtual “Hardware” Interrupts: How does the UML guest handle an “interrupt,” like a network packet arriving?
    1. A real packet arrives at the host’s tap0 device (more on this later).
    2. The host kernel sees the tap0 device is owned by the UML process.
    3. The host kernel notifies the UML process (via a file descriptor) that data is ready.
    4. The UML kernel process, which was monitoring this file descriptor (using select() or epoll()), wakes up.
    5. It then sends a signal (like SIGIO) to itself (or its traced process).
    6. The UML kernel’s signal handler for SIGIO fires. This handler is the “virtual interrupt handler.” It “wakes up” the UML kernel’s network driver, which reads the data and processes the packet.

In short, UML is a mind-bendingly clever system that maps host-level, asynchronous, userspace APIs (files, signals, ptrace) to the guest-level, synchronous, privileged-mode concepts (block devices, interrupts, CPU traps) that a kernel expects.

And yes, as the original article states, there’s a compelling reason to do this:

“It’s extremely fun.”

A detailed architectural diagram of UML in SKAS mode
A detailed architectural diagram of UML in SKAS mode

The Complete Guide to Building Your UML Kernel

Let’s get our hands dirty. The original article mentions this only works on x86. This is a crucial constraint to remember. You cannot build an ARCH=um kernel on an ARM-based Mac, for example. You must be on an x86/x86_64 Linux host.

Step 1: Install Prerequisites

First, we need the build tools. This is a comprehensive list. Without these, your build will fail.

# For Debian/Ubuntu-based systems
sudo apt update
sudo apt install git build-essential bison flex libncurses-dev \
                 libssl-dev libelf-dev bc libslirp-dev

# For Fedora/RHEL-based systems
sudo dnf groupinstall "Development Tools"
sudo dnf install git ncurses-devel openssl-devel elfutils-libelf-devel \
                 bc bison flex slirp-devel

Step 2: Get the Linux Kernel Source

We’ll clone the official stable kernel tree.

# Clone the entire kernel (can be large, but is comprehensive)
git clone [https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git)
cd linux

# Optional: Check out a specific, recent tag to ensure stability
# git tag -l "v6.*"
# git checkout v6.9.1

Step 3: Configure the UML Kernel (menuconfig)

This is the most critical step. We will now configure the guest kernel.

# This is the magic command.
ARCH=um make menuconfig

This command opens the text-based kernel configuration tool. You’ll be greeted by a blue screen. Use the arrow keys to navigate, Enter to enter a sub-menu, Space to cycle options (< > = off, [M] = module, [*] = built-in), and / to search for a config.

For a UML build, we need to enable the “virtual” drivers.

  1. Enable 64-bit (if on a 64-bit host):
    • Go to: Virtualization ->
    • Enable: [*] 64-bit kernel (This is CONFIG_64BIT)
  2. Enable Core UML Drivers:
    • Go to: User-mode Linux virtual devices
    • Enable: [*] User-mode console driver (CONFIG_UML_CONSOLE) – This lets us see the boot messages and log in via our terminal’s stdin/stdout.
    • Enable: [*] User-mode network devices (uml_net) (CONFIG_NET_UML) – This is our “virtual NIC.”
    • Enable: [*] User-mode block devices (UBD) (CONFIG_BLK_DEV_UBD) – As the original article noted, this is essential. It’s the driver that lets us use a host file as a guest hard drive.
    • Enable: [*] MMAP to host mmap() backend (CONFIG_UM_MMAP) – This is the virtual memory driver.
  3. Enable hostfs (Optional but HIGHLY Recommended):
    • Go to: User-mode Linux virtual devices -> Host-side device drivers
    • Enable: [*] Filesystem-based host device (CONFIG_HOSTFS) – This driver is different from UBD. It’s a passthrough filesystem, like a “shared folder.” We will explore this in-depth later.
  4. Enable Filesystems (For the Guest):
    • Go to: File systems ->
    • You must enable the filesystem your rootfs will use. For our guide, we need ext4.
    • Enable: [*] The Second Extended Filesystem (CONFIG_EXT2_FS)
    • Enable: [*] The Third Extended Filesystem (CONFIG_EXT3_FS)
    • Enable: [*] The Fourth Extended Filesystem (CONFIG_EXT4_FS)
  5. Enable Networking (For the Guest):
    • Go to: Networking support -> Networking options ->
    • Enable: [*] TCP/IP networking
    • Enable: [*] IP: kernel level autoconfiguration (Useful for DHCP)

When you’re done, navigate to < Save >, accept the default .config filename, and then < Exit >.

Make menuconfig Interface
Make menuconfig Interface

Step 4: Build the Kernel

Now, we compile. This will create the linux executable.

# This tells the build system to target the "User-Mode" architecture.
# We use -j$(nproc) to use all available CPU cores for a fast build.
ARCH=um make -j$(nproc)

This will take anywhere from 5 to 30 minutes, depending on your machine.

When it’s finished, you will have a new, magical file in your directory:

$ ls -lh linux
-rwxr-xr-x 1 user user 20M Oct 27 13:30 linux

$ file linux
linux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
for GNU/Linux 3.2.0, BuildID[...], with debug_info, not stripped

Notice, as the original article did, it’s an “ELF 64-bit LSB executable… dynamically linked.” Let’s check its dependencies:

$ ldd linux
        linux-vdso.so.1 (0x00007ffc1b7f5000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3e1a800000)
        libslirp.so.0 => /lib/x86_64-linux-gnu/libslirp.so.0 (0x00007f3e1a7ba000)
        ...
        /lib64/ld-linux-x86-64.so.2 (0x00007f3e1abf0000)

This is the proof. Our “kernel” is a userspace application. It depends on libc to make system calls and libslirp for easy networking. This is the entire “meta” concept in action.

Crafting the Guest: Your UML Userspace

A kernel is useless on its own. A kernel’s only job is to boot up and, as its final act, run the /sbin/init program. If it can’t find init, it will panic.

We need to provide this init program, along with a C library, a shell, and other basic utilities. This is the “userspace.” We’ll do this by creating a file on our host that will serve as the guest’s hard drive.

We have two main options: the easy way (Buildroot) and the “from scratch” way (BusyBox). This guide will cover both.

Buildroot is a fantastic tool for creating complete, minimal, embedded Linux systems.

# 1. Clone Buildroot
git clone git://git.buildroot.net/buildroot
cd buildroot

# 2. Configure Buildroot for a standard x86_64 system
# This sets up a default config for our "target"
make x86_64_defconfig

# 3. (Optional) Customize the userspace
make menuconfig

Inside Buildroot’s menuconfig, you can add packages (like vim, openssh, etc.) under Target packages ->. For now, the most important settings are under Filesystem images ->:

  • Ensure [*] ext2/3/4 root filesystem is enabled.
  • You can set a (100M) size for the ext4 image.
# 4. Build the userspace
make -j$(nproc)

This will take a while. It’s downloading and compiling an entire toolchain and all the packages. When it’s done, you’ll have your root filesystem image:

$ ls -lh output/images/rootfs.ext4
-rw-r--r-- 1 user user 100M Oct 27 14:00 output/images/rootfs.ext4

This file, rootfs.ext4, is a complete, bootable Linux filesystem.

Option 2: BusyBox (The “Minimalist” Way)

If you want a truly tiny system and more control, we can build one with BusyBox, “The Swiss Army Knife of Embedded Linux.”

# 1. Download and extract BusyBox
# Go to busybox.net, get the latest source
wget [https://busybox.net/downloads/busybox-1.36.1.tar.bz2](https://busybox.net/downloads/busybox-1.36.1.tar.bz2)
tar -xvf busybox-1.36.1.tar.bz2
cd busybox-1.36.1

# 2. Configure BusyBox
make defconfig
make menuconfig

Inside this menuconfig, go to Settings -> [*] Build static binary (no shared libs). This makes our life much easier, as we won’t need to copy C libraries.

# 3. Compile and "install" BusyBox
make -j$(nproc)
make install

This creates a _install directory containing the busybox binary and symlinks for all common commands (ls, sh, mount, etc.).

# 4. Create the disk image file
cd ..
dd if=/dev/zero of=busybox.ext4 bs=1M count=100
mkfs.ext4 busybox.ext4

# 5. Mount the image and copy files
sudo mkdir -p /mnt/uml-guest
sudo mount -o loop busybox.ext4 /mnt/uml-guest

# 6. Copy BusyBox and create the device nodes
sudo cp -a busybox-1.36.1/_install/* /mnt/uml-guest/
sudo mkdir -p /mnt/uml-guest/{proc,sys,dev,etc,root,tmp,mnt,home}

# 7. Create the *critical* /etc/inittab
# This tells BusyBox's `init` what to do on boot
sudo tee /mnt/uml-guest/etc/inittab <<'EOF'
::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty -L console 0 vt100
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
EOF

# 8. Create the init script
sudo mkdir -p /mnt/uml-guest/etc/init.d
sudo tee /mnt/uml-guest/etc/init.d/rcS <<'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "Welcome to BusyBox-on-UML!"
EOF
sudo chmod +x /mnt/uml-guest/etc/init.d/rcS

# 9. Unmount the image
sudo umount /mnt/uml-guest

Now, busybox.ext4 is also a bootable root filesystem.

Host Filesystem, UML Kernel, Guest System Relationship
Host Filesystem, UML Kernel, Guest System Relationship

Booting and Using Your UML Instance

We have our kernel (linux) and our hard drive (rootfs.ext4 from Buildroot). Let’s boot.

Go back to your linux kernel source directory, where the linux executable is. Copy your rootfs.ext4 file here for simplicity.

# For organization
mkdir -p /tmp/uml-test
cp /path/to/linux/kernel/source/linux /tmp/uml-test/
cp /path/to/buildroot/output/images/rootfs.ext4 /tmp/uml-test/
cd /tmp/uml-test

The Launch Command Explained

This is the magic moment. The original article showed a simple launch. Let’s look at a more robust one, explaining every parameter.

./linux mem=512M \
        ubd0=./rootfs.ext4 \
        root=/dev/ubda \
        console=tty0
  • ./linux: The UML kernel executable.
  • mem=512M: A guest kernel parameter, telling it to use 512MB of RAM. If you omit this, it will take a small default (like 64M).
  • ubd0=./rootfs.ext4: This is a UML parameter. It tells the ubd (User Block Device) driver to map “device 0” to the host file ./rootfs.ext4. Inside the guest, this will appear as /dev/ubda.
  • root=/dev/ubda: This is a guest kernel parameter. It tells the guest kernel’s init process where to find its root filesystem. We’re telling it to use the ubda device we just defined.
  • console=tty0: This is a guest kernel parameter telling it to send its console output to the tty0 device, which the UML console driver maps to our host’s stdout and stdin.

Press Enter.

You will be greeted by a flood of text, identical to a real Linux boot sequence.

Core dump limits :
        soft - 0
        hard - NONE
Checking that ptrace can change system call numbers...OK
Checking syscall emulation for ptrace...OK
...
Linux version 6.9.1 (user@host) (gcc (Ubuntu 13.2.0-6ubuntu1) 13.2.0, GNU ld ...)
Zone ranges:
  Normal   [mem 0x0000000000000000-0x000000001fffffff]
...
Kernel command line: mem=512M ubd0=./rootfs.ext4 root=/dev/ubda console=tty0
...
ubd0: device 0 size 104857600 bytes (100 MB)
...
Run /sbin/init as init process
EXT4-fs (ubda): mounted filesystem with ordered data mode.
Starting syslogd: OK
Starting klogd: OK
...
Welcome to Buildroot
buildroot login:

You are now at a login prompt for a completely separate Linux kernel.

Log in. The default for Buildroot is root with no password.

# (Inside the UML guest)
buildroot login: root

# ls /
bin   dev   etc   lib   lib64 mnt   proc  root  sbin  sys   tmp   usr   var

# uname -a
Linux buildroot 6.9.1 #1 PREEMPT_DYNAMIC ... x86_64 GNU/Linux

# free -h
              total        used        free      shared  buff/cache   available
Mem:          503Mi        10Mi       484Mi       0.0Ki         9Mi       479Mi
Swap:            0B          0B          0B

It sees the 512MB of RAM we gave it, and it’s running the kernel we just compiled.

Writing Persistent Data

Let’s replicate the original article’s experiment, but with more detail. We’ll create a second “data” disk, write to it from the guest, and read it from the host.

First, shut down your guest:

# (Inside the UML guest)
poweroff

This will cleanly unmount the filesystems and the main linux process will exit, returning you to your host prompt.

Now, on the host, let’s create a new 100MB data disk image:

# (On the host)
dd if=/dev/zero of=./data.ext4 bs=1M count=100
mkfs.ext4 ./data.ext4

This data.ext4 is just a file, but it’s internally formatted as a clean ext4 filesystem.

Now, let’s boot UML again, but this time we’ll attach both disks:

# (On the host)
./linux mem=512M \
        ubd0=./rootfs.ext4 \
        ubd1=./data.ext4 \
        root=/dev/ubda \
        console=tty0
  • ubd0 maps to rootfs.ext4 (which becomes /dev/ubda).
  • ubd1 maps to data.ext4 (which becomes /dev/ubdb).

Log in as root again.

# (Inside the UML guest)

# Check our block devices
# ls -l /dev/ubd*
brw-rw----    1 root     root       98,   0 Jan  1 00:00 /dev/ubda
brw-rw----    1 root     root       98,  16 Jan  1 00:00 /dev/ubdb

# Create a mount point for our data disk
mkdir /mnt/data

# Mount the second disk
mount /dev/ubdb /mnt/data

# Write a file to it
echo "Hello from inside the UML guest!" > /mnt/data/test_file.txt

# Verify it's there
cat /mnt/data/test_file.txt
Hello from inside the UML guest!

# Unmount and shut down
umount /mnt/data
poweroff

You’re back on the host. Now for the final proof. Let’s mount that data.ext4 file on the host and see what’s inside.

# (On the host)
sudo mkdir -p /mnt/uml-check
sudo mount -o loop ./data.ext4 /mnt/uml-check

# And the magic...
cat /mnt/uml-check/test_file.txt
Hello from inside the UML guest!

# Clean up
sudo umount /mnt/uml-check

This is a powerful confirmation. We booted a separate kernel, it wrote data to a “hard drive” (which was just a file), and that data persisted.

Advanced UML: Networking

This is one of the most powerful and common use cases for UML. A disk image is simple, but networking requires connecting the isolated guest to the host’s network stack (and the wider world).

We have three primary methods, from simple-but-limited to complex-but-powerful.

Method 1: slirp (User-Mode NAT)

This is the easiest method. It requires zero root privileges on the host. It uses the libslirp library to create a userspace-only NAT.

  • Pros: No sudo needed. Works out of the box.
  • Cons: Slower (it’s NAT in userspace). The guest is not on your LAN; it’s in its own firewalled subnet. Most ping (ICMP) traffic won’t work.

How to use: Just add eth0=slirp to your launch command.

# (On the host)
./linux mem=512M \
        ubd0=./rootfs.ext4 \
        root=/dev/ubda \
        eth0=slirp

Log into your guest and configure the (guest’s) eth0:

# (Inside the UML guest)
# `slirp` provides a built-in DHCP server
udhcpc -i eth0

# You'll get an IP, usually 10.0.2.15
# ip a
...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0
...

# Test internet connectivity
ping -c 3 8.8.8.8
# This might not work (slirp limitation)

# But this will!
wget -q -O - [http://info.cern.ch](http://info.cern.ch)
# (You will see the HTML of the world's first website)

Method 2: TUN/TAP (Virtual Ethernet)

This is the standard, high-performance method. It creates a “virtual wire” between the host and the guest. One end of the wire is the tap0 device on your host. The other end is the eth0 device in your guest.

  • Pros: High performance. Full network connectivity. The guest can be on the host’s LAN.
  • Cons: Requires sudo on the host to set up.

Step 1: Host Setup (Requires sudo)

You need to create the tap0 device and give your user account permission to use it.

# (On the host)

# 1. Install helper tools (if you don't have `tunctl`)
sudo apt install uml-utilities   # Debian/Ubuntu
# (On Fedora, it's part of the kernel-tools package)

# 2. Create the tap0 device and assign ownership to your user
sudo tunctl -u $(whoami) -t tap0

# 3. Bring the host-side of the "wire" up
sudo ip link set tap0 up

# 4. Give the host-side an IP (this will be the guest's gateway)
sudo ip addr add 192.168.100.1/24 dev tap0

Step 2: UML Launch

Now, tell UML to connect its eth0 to the host’s tap0.

# (On the host)
./linux mem=512M \
        ubd0=./rootfs.ext4 \
        root=/dev/ubda \
        eth0=tap,tap0

Step 3: Guest Configuration

Log into your guest. Its eth0 is “plugged in” but has no IP.

# (Inside the UML guest)

# 1. Bring the guest-side of the "wire" up
ip link set eth0 up

# 2. Assign a static IP on the same subnet as the host's tap0
ip addr add 192.168.100.2/24 dev eth0

# 3. Test connectivity to the host
ping -c 3 192.168.100.1
PING 192.168.100.1 (192.168.100.1): 56 data bytes
64 bytes from 192.168.100.1: seq=0 ttl=64 time=0.052 ms
...

It works! You have a private network between your host and guest.

Step 4: Providing Internet (NAT on Host)

The guest can ping the host, but not the internet. We need to set up NAT (Masquerading) on the host.

# (On the host)
# 1. Enable IP forwarding
sudo sysctl -w net.ipv4.ip_forward=1

# 2. Add the NAT rule (replace 'wlan0' with your host's *real* internet iface)
sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
sudo iptables -A FORWARD -i tap0 -j ACCEPT
sudo iptables -A FORWARD -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT

Step 5: Final Guest Setup (Internet)

# (Inside the UML guest)
# 1. Add a default route via the host's tap0 IP
ip route add default via 192.168.100.1

# 2. Add a DNS server
echo "nameserver 8.8.8.8" > /etc/resolv.conf

# 3. Test!
ping -c 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=114 time=10.2 ms
...

You now have a fully-networked UML instance.

UML Host-Guest Networking
UML Host-Guest Networking

Method 3: Bridging (Full LAN Access)

This is the most advanced setup. We create a network bridge on the host, and add both the host’s physical NIC (e.g., eth0) and the tap0 device to it.

The result is that the UML guest appears as a first-class citizen on your home/office LAN, and can even get an IP from your router’s DHCP server. This is too complex for this (already massive) guide, but it’s the final step for “production-like” network simulation.

Advanced UML: hostfs Shared Folders

What if you don’t want to mess with disk images? ubd is a block device. The guest has no idea it’s a file. But what if you just want to share a directory?

This is where hostfs comes in (which we enabled in our kernel config). It’s a passthrough filesystem.

  1. Boot your UML instance (no special args needed).
  2. On your host, create a directory to share: mkdir -p /tmp/uml-share echo "This file is on the host" > /tmp/uml-share/host_file.txt
  3. Inside the guest, mount this directory:# (Inside the UML guest) mkdir /mnt/host # This is the magic. Mount type 'hostfs', device 'none', # and pass the *host path* as an option. mount -t hostfs none /mnt/host -o /tmp/uml-share # Look inside ls /mnt/host host_file.txt cat /mnt/host/host_file.txt This file is on the host # Write back to the host echo "This file was written from the guest" > /mnt/host/guest_file.txt
  4. On your host, check the directory: cat /tmp/uml-share/guest_file.txt This file was written from the guest

Security Warning: This is fast and convenient, but it’s a major security consideration. The guest now has direct read/write access to that part of your host filesystem, with the same permissions as the user who launched UML. This is fantastic for development, but be careful.

UML vs. The World: KVM, Containers, and Killer Use Cases

We’ve now mastered building, running, storing, and networking UML. So we return to the original article’s conclusion: what is the point?

The author was right to “raise an eyebrow” at calling it a VM. It’s not a VM in the KVM sense, and it’s not a container. It’s a unique tool for a specific job.

UML vs. KVM/QEMU (Full Virtualization)

  • Isolation: KVM wins, hands down. KVM uses hardware-enforced isolation (VT-x/AMD-V). A guest kernel panic is a non-event for the host. A UML guest is just a host process. A bug in the UML ptrace handling could potentially be a security risk, and a UML kernel “panic” is just that process crashing.
  • Performance: KVM wins. It’s near-native. UML has the overhead of intercepting every single syscall from every guest process.
  • Ease of Use: UML wins. Notice our entire guide never used sudo except for the optional advanced networking. You can build, launch, and network (with slirp) a full-kernel environment 100% as a regular user. This is impossible with KVM.

UML vs. Docker/Containers (OS-Level Virtualization)

  • The Kernel: This is the fundamental difference. A Docker container shares the host kernel. You cannot test a kernel change in Docker. You cannot run a v6.8 kernel in a container on a v5.15 host.
  • With UML, you bring your own kernel. This is its entire reason for existing.

The Killer Use Cases for UML

This brings us to why you would use UML.

1. The #1 Use Case: Kernel Debugging

This is UML’s “killer app.”

Want to debug a normal kernel? You need two machines, a serial cable (or kgdb-over-ethernet), and a complex gdb remote setup.

Want to debug a UML kernel?

# (On the host)
gdb ./linux

That’s it. You are now debugging a Linux kernel in gdb like a normal program.

(gdb) # Set a breakpoint on the "open" system call
(gdb) b sys_open
(gdb) # Run the kernel
(gdb) run mem=512M ubd0=rootfs.ext4 root=/dev/ubda

The kernel will boot, and the moment its init process tries to open any file, gdb will break, and you can inspect the live, internal state of a running Linux kernel.

This is a revolutionary tool for kernel developers and students. You can step through the VFS, the scheduler, the memory manager, all from the comfort of a simple GDB session.

2. Education and Learning There is no better tool for learning how Linux works. You can add printf statements to the UML kernel code, recompile (it’s fast), and boot it to see what’s happening. You can trace syscalls, watch the boot process, and break it without any risk to your host machine.

3. Complex Network Simulation Want to test a complex routing protocol? With KVM, you’d need to spin up 10 full VMs, consuming 20GB of RAM. With UML, you can launch 10 linux processes, each with mem=128M, connect them all with tap devices and host-based bridges, and simulate an entire company’s network on one laptop.

4. Secure(ish) Sandboxing and Honeypots Want to analyze malware? Running it in a Docker container is risky (kernel exploits). Running it in a full KVM is heavy. UML is a great middle-ground. It provides a full, separate kernel, so a guest-level exploit is contained. It’s lightweight and can be automated and torn down easily.

Conclusion: The Ultimate Hacker’s Sandbox

So, is User-Mode Linux a replacement for KVM or Docker in production? As the original article wisely concluded, no. KVM is far more battle-tested and secure for running production VMs. Docker is far more efficient for deploying applications.

User-Mode Linux carves out its own, brilliant niche. It’s not a tool for SysAdmins; it’s a tool for Kernel Hackers, Security Researchers, and Students.

It’s the ultimate sandbox. It’s an entire, bootable, networkable Linux system contained in a single file, runnable as a single command, and debuggable with a single tool. It achieves a unique, fascinating place in the ecosystem: offering a separate, full kernel instance without requiring the root privileges and hardware support of a true hypervisor.

It’s a fascinating, mind-bending piece of technology. And more than anything, it is just, as the kernel docs promised, extremely fun.

Now go build a kernel.

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Your contribution powers free tutorials, hands-on labs, and security resources.

Why your support matters:
  • Writeup Access: Get complete writeup access within 24 hours
  • Zero paywalls: Keep the content 100% free for learners worldwide

Perks for one-time supporters:
☕️ $5: Shoutout in Buy Me a Coffee
🛡️ $8: Fast-track Access to Live Webinars
💻 $10: Vote on future tutorial topics + exclusive AMA access

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

Buy Me a Coffee Button

If you like this post, then please share it:

Tutorials

Discover more from The CyberSec Guru

Subscribe to get the latest posts sent to your email!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from The CyberSec Guru

Subscribe now to keep reading and get access to the full archive.

Continue reading