Nested Virtualization
Nested virtualization runs a virtual machine inside a machine that is already virtualized. It needs the host to expose hardware virtualization to the guest.
Nested virtualization is running a virtual machine inside a machine that is already virtualized. It works only when the host exposes the CPU's hardware virtualization extensions to its guest, because the guest kernel needs those extensions to act as a hypervisor for a second layer of virtual machines.
The term shows up in GitHub Actions whenever a job wants to boot something heavier than a container: an Android emulator, a QEMU guest, a libvirt domain, or a virtual machine image under test. Every runner is already a guest of some hypervisor, so the second layer exists only if the first layer permits it.
Definition
Hardware virtualization on x86 is a set of CPU instructions, published by Intel as VT-x and by AMD as AMD-V, that lets a hypervisor run guest code directly on the processor and trap the privileged operations it cares about. Arm publishes an equivalent set of virtualization extensions. Without those instructions a hypervisor has to interpret or rewrite guest instructions in software, which is correct and slow.
Nested virtualization is the case where the guest itself wants those instructions. Kernel documentation names the layers L0 for the bare metal host and its hypervisor, L1 for the guest that hypervisor runs, and L2 for the guest that L1 runs (KVM nested VMX documentation, checked on 2026-08-13). L0 keeps real control of the hardware and emulates the virtualization instructions on behalf of L1, so L1 believes it is talking to a bare processor.
| Layer | What it is | What runs there |
|---|---|---|
| L0 | The physical machine | The cloud provider's hypervisor, with real access to VT-x, AMD-V, or the Arm extensions |
| L1 | The virtual machine you rent, such as a workflow runner | The guest kernel, plus a hypervisor of its own when L0 exposes the extensions |
| L2 | The virtual machine the job starts | An Android emulator, a QEMU guest, or a virtual appliance under test |
The decision belongs to L0. A guest cannot switch nested virtualization on from inside, because the instructions either appear in the virtual processor it was handed or they do not. Google Cloud states the same division in its own terms and requires the feature to be enabled on the instance or machine image before a guest can use it (Compute Engine nested virtualization overview, checked on 2026-08-13).
How the guest sees the result on Linux
On Linux the visible outcome is a character device at /dev/kvm. The kernel creates it when the KVM module loads, and KVM loads only when the processor the guest can see offers usable virtualization extensions. Three states are worth telling apart, because two of them produce a slow job rather than a failed one.
| What the job finds | Cause | What happens next |
|---|---|---|
No /dev/kvm | The outer hypervisor kept the virtualization extensions to itself | Any tool that wants hardware acceleration falls back to software emulation |
/dev/kvm with crw-rw---- root:kvm | Extensions exposed, and the job user is outside the kvm group | Opening the device is refused, and the same software fallback follows |
/dev/kvm readable and writable by the job user | Extensions exposed and permissions widened by a udev rule | The second layer of virtual machines runs with hardware acceleration |
Nested virtualization is a different mechanism from containers. A container shares the kernel of the machine it runs on and isolates processes with namespaces and cgroups, so it needs nothing from the CPU beyond ordinary user mode. A nested virtual machine brings its own kernel, its own virtual devices, and its own memory map, which is why the CPU has to help.
Example
An Android instrumentation suite is the common case in GitHub Actions. The emulator boots a full Android system image, and it runs at a usable speed only when it can hand the guest to the processor through /dev/kvm.
name: instrumentation-tests
on:
push:
branches: [main]
jobs:
android-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Report virtualization support
run: |
grep -c -E 'vmx|svm' /proc/cpuinfo || echo "no virtualization flags"
ls -l /dev/kvm || echo "no /dev/kvm on this runner"
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
script: ./gradlew connectedCheckOn a runner whose host withholds the extensions, the reporting step prints no /dev/kvm on this runner and the workflow keeps going. The emulator action probes the device before launch, logs ProbeKVM: This user doesn't have permissions to use KVM (/dev/kvm)., logs Disabling Linux hardware acceleration., and starts the emulator with -accel off (android-emulator-runner README, checked on 2026-08-13). The suite still passes, on a timescale that makes people blame the tests.
The device existing is half the requirement. Default permissions on /dev/kvm are crw-rw---- root:kvm, which leaves an ordinary job user unable to open it, so a udev rule has to widen them first:
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvmThat step is a property of the device permissions rather than of any one runner fleet, so it belongs in the workflow wherever the emulator runs. GitHub announced hardware accelerated Android virtualization for a subset of its own hosted runner classes and pointed at the same permission step (GitHub changelog, 23 February 2023, checked on 2026-08-13), which is a reminder that support varies by runner class rather than by operating system.
Two habits keep this cheap to diagnose. Print ls -l /dev/kvm early in any job that expects nested virtualization, and fail the job when the device is absent instead of letting the run continue, so a change in runner class shows up as a red build rather than as a duration that creeps up over a quarter.
Related Terms
- Nested virtualization runners and the labels that request them: which runner classes expose
/dev/kvmand how a workflow asks for one. - KVM: the Linux kernel module that turns exposed virtualization extensions into a usable hypervisor interface.
- Android emulator tests in GitHub Actions: a full workflow for instrumentation tests, including emulator startup and caching.
- Nested virtualization documentation: the runner class support matrix and the configuration for enabling it.
- Common issues documentation: the symptoms of a silent software emulation fallback and the fix.
- Runner pricing: per minute rates by runner type.
FAQ
What is nested virtualization?
Nested virtualization is running a virtual machine inside a machine that is already a virtual machine. The outer hypervisor has to expose the host CPU's hardware virtualization extensions to its guest, so that the guest kernel can act as a hypervisor for a second layer of virtual machines.
How do I tell whether a GitHub Actions runner supports nested virtualization?
Run `ls -l /dev/kvm` in a step on a Linux runner. A missing device means the outer hypervisor kept the virtualization extensions to itself. A device that exists with `crw-rw---- root:kvm` permissions means the extensions are exposed and the job user still needs group access before it can open the device.
Why does an Android emulator job get slow instead of failing?
The android-emulator-runner action probes /dev/kvm before it launches. When the probe fails it logs a ProbeKVM line, disables Linux hardware acceleration, and starts the emulator with `-accel off`, so the job keeps running under software emulation and finishes much later than usual.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.