Ring -1: el hipervisor que vigila a tu kernel Ring -1: the hypervisor watching over your kernel

"Virtualization support not detected"

Preparando esta entrada, Docker Desktop se negó a arrancar en mi equipo con un mensaje seco: Virtualization support not detected. Curioso, porque el procesador es un Intel Core i9-14900KF, que soporta virtualización por hardware de sobra, y la BIOS la tiene activada. Windows lo confirma:

PS> Get-CimInstance Win32_Processor |
>>     Format-List Virtualization*, VMMonitor*, SecondLevel*
VirtualizationFirmwareEnabled           : True
VMMonitorModeExtensions                 : True
SecondLevelAddressTranslationExtensions : True

PS> (Get-CimInstance Win32_ComputerSystem).HypervisorPresent
False

La clave está en la última línea. El hardware está listo, pero no hay ningún hipervisor cargado: nadie ocupa el nivel que hay por debajo del kernel. Docker Desktop en Windows funciona normalmente sobre WSL2, y WSL2 es una máquina virtual ligera que necesita que Windows arranque su propio hipervisor. Sin él, no hay contenedores.

En la entrada anterior bajamos de Ring 3 a Ring 0 y dijimos que el kernel es quien manda. Hoy toca matizarlo: por debajo del kernel puede haber alguien más. Informalmente se le llama Ring -1.

Qué es un hipervisor

Un hipervisor es el software que crea y gestiona máquinas virtuales: reparte la CPU, la memoria y los dispositivos entre varios sistemas operativos y hace creer a cada uno que tiene la máquina para él solo. Se suelen distinguir dos tipos:

Tipo 1 (bare metal) Tipo 2 (alojado)
Dónde se ejecuta Directamente sobre el hardware Como una aplicación dentro de otro sistema operativo
Ejemplos Hyper-V, VMware ESXi, Xen VirtualBox, VMware Workstation
Uso típico Servidores, cloud, seguridad del propio sistema Escritorio, pruebas, desarrollo

La frontera no es tan nítida como parece. KVM, por ejemplo, es un módulo del kernel que convierte al propio Linux en hipervisor. Y cuando activas Hyper-V en Windows ocurre algo que sorprende a mucha gente: tu Windows deja de ejecutarse directamente sobre el hardware y pasa a ser la partición raíz, una máquina virtual con privilegios especiales que corre encima de Hyper-V.

RING 3 aplicaciones del invitado RING 0 kernel del invitado (Windows, Linux...) ▲ VMX non-root VM exit ▼ ▲ VM entry VMX root ▼ RING -1 hipervisor · VMCS/VMCB · tablas EPT/NPT HARDWARE CPU con VT-x o AMD-V · memoria · dispositivos
Fig. 1 — El sistema invitado conserva sus anillos 3 y 0, pero todo él vive en VMX non-root. El hipervisor está por debajo.

Cómo lo hace el hardware: VT-x y AMD-V

Virtualizar x86 solo por software era lento y complicado, porque el sistema invitado espera ejecutarse en Ring 0 y tocar a su antojo registros de control, tablas de páginas e interrupciones. Desde 2005–2006, Intel (VT-x) y AMD (AMD-V, también llamado SVM) lo resuelven en hardware con un modo de ejecución nuevo, independiente de los anillos:

  • VMX root: donde vive el hipervisor. Es lo que llamamos Ring -1, aunque técnicamente no es un anillo nuevo, sino un modo de la CPU.
  • VMX non-root: donde vive el sistema invitado, con sus Ring 0 y Ring 3 intactos. El kernel invitado sigue en Ring 0, pero bajo supervisión.

El hipervisor describe cada máquina virtual en una estructura de memoria (la VMCS en Intel, la VMCB en AMD) con el estado de los registros del invitado y, sobre todo, qué operaciones del invitado quiere interceptar. Cuando el invitado hace una de ellas —acceder a ciertos registros de la CPU, a un puerto de E/S o a memoria que no le corresponde—, la CPU lo detiene y salta al hipervisor. Eso es un VM exit. El hipervisor decide qué hacer y le devuelve el control con un VM entry.

El coste de un VM exit

Cada VM exit guarda el estado del invitado, carga el del hipervisor y hace después el camino de vuelta. Según la CPU y el trabajo que tenga que hacer el hipervisor, eso cuesta del orden de cientos a unos pocos miles de ciclos, frente a los pocos ciclos de una instrucción normal. Algunas instrucciones, como CPUID en Intel VT-x, provocan siempre un VM exit. Por eso buena parte del trabajo de un hipervisor moderno consiste en evitar salidas: cuanto menos interviene, más rápido va el invitado.

La memoria de la memoria: EPT y NPT

En la entrada anterior vimos que cada proceso usa direcciones virtuales que el kernel traduce a físicas con sus tablas de páginas. En una máquina virtual eso ya no basta: la "memoria física" que ve el invitado tampoco es real. Hace falta una segunda traducción:

  1. Dirección virtual del invitado → dirección física del invitado. La hace el kernel invitado, con sus propias tablas de páginas, como siempre.
  2. Dirección física del invitado → dirección física real. La hace el hipervisor, con una segunda tabla que el invitado ni ve ni puede tocar.

Intel llama a esa segunda tabla EPT (Extended Page Tables) y AMD, NPT (Nested Page Tables); el nombre genérico es SLAT. Antes de que existiera, hacia 2007–2008, los hipervisores mantenían por software unas "tablas sombra" que había que resincronizar cada vez que el invitado cambiaba las suyas, y eso era carísimo. Con SLAT, el hardware recorre las dos tablas por sí solo. No es gratis: en el peor caso, un fallo de TLB con paginación de cuatro niveles puede requerir hasta 24 accesos a memoria en lugar de 4. Por eso las cachés de traducción importan tanto en virtualización.

Lo interesante para la seguridad es que cada entrada de EPT tiene sus propios permisos de lectura, escritura y ejecución, independientes de los que ponga el invitado. El hipervisor puede decidir que una página no se puede modificar, aunque el kernel invitado, desde su Ring 0, diga lo contrario.

El hipervisor como guardián del kernel

Esa última idea es la base de la Seguridad basada en virtualización (VBS) de Windows. Con VBS activado, Hyper-V divide el sistema en dos niveles de confianza virtuales: el Windows de siempre, con su kernel, en el nivel 0 (VTL0), y un pequeño kernel seguro en el nivel 1 (VTL1), aislado por el hipervisor. Sobre esa base funcionan dos de las defensas más importantes del Windows actual:

  • HVCI (Integridad de memoria): la verificación de firmas del código del kernel se hace en VTL1, y el hipervisor usa SLAT para que ninguna página del kernel sea a la vez escribible y ejecutable. Aunque un atacante consiga ejecutar código en Ring 0, no puede inyectar código nuevo en el kernel: la página no se deja.
  • Credential Guard: los secretos de autenticación se guardan en un proceso aislado en VTL1, fuera del alcance incluso del propio kernel de Windows.

Es la respuesta directa a la idea con la que cerramos la entrada anterior: si Ring 0 no perdona, pon a alguien por debajo que vigile a Ring 0. En muchos equipos nuevos con Windows 11, VBS e HVCI vienen activados de serie.

Compruébalo tú mismo con CPUID

CPUID es la instrucción con la que cualquier programa pregunta a la CPU qué es y qué sabe hacer. No requiere privilegios, así que podemos usarla desde Ring 3. Primero, una pequeña cabecera portable para GCC, Clang y MSVC:

// cpuid.hpp  —  lectura portable de CPUID (GCC/Clang y MSVC)
#pragma once
#include <cstdint>
#include <cstring>
#include <string>
#if defined(_MSC_VER)
#include <intrin.h>
#else
#include <cpuid.h>
#endif

struct CpuidRegs { std::uint32_t eax, ebx, ecx, edx; };

inline CpuidRegs cpuid(std::uint32_t leaf) {
    CpuidRegs r{};
#if defined(_MSC_VER)
    int out[4];
    __cpuid(out, static_cast<int>(leaf));
    std::memcpy(&r, out, sizeof out);
#else
    __cpuid(leaf, r.eax, r.ebx, r.ecx, r.edx);
#endif
    return r;
}

// Une tres registros en el texto ASCII de 12 bytes que codifican.
inline std::string cpuid_text(std::uint32_t a, std::uint32_t b,
                              std::uint32_t c) {
    char s[12];
    std::memcpy(s, &a, 4);
    std::memcpy(s + 4, &b, 4);
    std::memcpy(s + 8, &c, 4);
    std::string t(s, sizeof s);
    t.erase(t.find_last_not_of('\0') + 1);   // quita el relleno final
    return t;
}

¿Tu CPU soporta virtualización?

// virt.cpp  —  g++ -std=c++20 virt.cpp -o virt
#include "cpuid.hpp"
#include <format>
#include <iostream>

int main() {
    const CpuidRegs v = cpuid(0);                         // fabricante
    const bool vmx = cpuid(1).ecx & (1u << 5);            // Intel VT-x
    const bool svm = cpuid(0x8000'0001).ecx & (1u << 2);  // AMD-V
    const std::string vendor = cpuid_text(v.ebx, v.edx, v.ecx);
    auto yes = [](bool b) { return b ? "soportado" : "no soportado"; };

    std::cout << std::format("CPU:         {}\n", vendor)
              << std::format("VT-x (VMX):  {}\n", yes(vmx))
              << std::format("AMD-V (SVM): {}\n", yes(svm));
}

Estas son las salidas reales en mi equipo y dentro de un runner de GitHub Actions, que es una máquina virtual en Azure:

# Intel Core i9-14900KF (hardware físico)
CPU:         GenuineIntel
VT-x (VMX):  soportado
AMD-V (SVM): no soportado

# Runner de GitHub Actions (VM en Azure, AMD EPYC 7763)
CPU:         AuthenticAMD
VT-x (VMX):  no soportado
AMD-V (SVM): soportado

Hay dos matices importantes. El primero: que la CPU soporte la virtualización no significa que esté activada. Eso lo decide la BIOS en un registro especial de la CPU (un MSR) que solo se puede leer desde Ring 0; por eso Windows lo consulta desde el kernel y te lo enseña en el Administrador de tareas, en Rendimiento → CPU → Virtualización. El segundo: dentro de una máquina virtual, CPUID responde lo que el hipervisor decida exponer. Que el runner de Azure muestre AMD-V significa que ese hipervisor permite virtualización anidada: crear máquinas virtuales dentro de la máquina virtual.

¿Hay un hipervisor debajo de ti?

// hv.cpp  —  g++ -std=c++20 hv.cpp -o hv
#include "cpuid.hpp"
#include <format>
#include <iostream>

int main() {
    // Hoja 1, ECX bit 31: reservado para que el hipervisor se anuncie.
    if (!(cpuid(1).ecx & (1u << 31))) {
        std::cout << "Sin hipervisor anunciado: corre sobre el hardware.\n";
        return 0;
    }
    // Hoja 0x40000000: firma de 12 caracteres del hipervisor.
    const CpuidRegs h = cpuid(0x4000'0000);
    std::cout << std::format("Hipervisor: \"{}\"\n",
                             cpuid_text(h.ebx, h.ecx, h.edx));
}
# Intel Core i9-14900KF (hardware físico)
Sin hipervisor anunciado: corre sobre el hardware.

# Runner de GitHub Actions (VM en Azure)
Hipervisor: "Microsoft Hv"

"Microsoft Hv" es la firma de Hyper-V: Azure funciona sobre una versión adaptada de Hyper-V. En un Linux con KVM verías KVMKVMKVM, y en VMware, VMwareVMware. Y si lo ejecutas en tu propio Windows con VBS activado, también verás "Microsoft Hv": tu sistema "real" es la partición raíz de Hyper-V.

Fíjate en que el programa comprueba primero el bit 31: sin hipervisor, la hoja 0x40000000 no está definida y lo que devuelve no significa nada. Ese bit es, además, una declaración voluntaria. Los hipervisores lo activan para que el sistema invitado sepa que está virtualizado y use mecanismos pensados para ello (lo que se conoce como paravirtualización), como relojes y dispositivos que no necesitan simular hardware antiguo.

El cloud es esto

Cuando alquilas una máquina en la nube, tu sistema operativo entero vive en VMX non-root. Azure funciona sobre Hyper-V, como acabamos de ver; AWS usa Nitro, un hipervisor basado en KVM, y Google Cloud también usa KVM. Tu "servidor" es en realidad un invitado, y el Ring -1 de otro es quien garantiza que tu máquina y la del cliente de al lado no puedan verse.

Una regla que se aprende por las malas

Nota personal

Hace un tiempo escribí un hipervisor académico sobre Intel VT-x como driver de kernel de Windows: activa VMX en cada núcleo, virtualiza el propio sistema que ya está en marcha y le pone debajo una tabla EPT que, de momento, traduce cada dirección a sí misma. Es el enfoque de proyectos de investigación como HyperPlatform y, en el fondo, el mismo que usa VBS, solo que Windows lo hace desde el arranque. La lección que más me costó asumir está en mi propia guía de laboratorio, en un recuadro de advertencia: en VMX root solo cabe un hipervisor. Si Hyper-V, WSL2 o VBS ya están activos, el tuyo no puede entrar; y si apagas el de Windows para hacerle sitio (bcdedit /set hypervisorlaunchtype off), te quedas sin WSL2 y sin Docker. No es un fallo, es una decisión que hay que tomar. Por eso las pruebas se hacían dentro de una máquina virtual con virtualización anidada.

Si Docker o WSL2 dicen que no hay virtualización
  1. Comprueba en el Administrador de tareas (Rendimiento → CPU) que la virtualización aparece como habilitada. Si no, actívala en la BIOS/UEFI.
  2. Activa la Plataforma de máquina virtual de Windows, por ejemplo con wsl --install desde una terminal de administrador, y reinicia.
  3. Si alguna vez desactivaste el hipervisor de Windows, vuelve a activarlo con bcdedit /set hypervisorlaunchtype auto y reinicia.

Y todavía hay más abajo

Ring -1 tampoco es el fondo. Por debajo del hipervisor está el System Management Mode (SMM), a veces llamado Ring -2: un modo de la CPU reservado al firmware, que se activa con interrupciones especiales y ejecuta código en una zona de memoria que ni siquiera el hipervisor puede ver. Y fuera de la CPU principal viven microcontroladores como Intel ME o AMD PSP, a los que algunos llaman Ring -3. Pero eso ya es otra historia.

La próxima vez que Docker te diga que no encuentra virtualización, ya sabes lo que significa: el hardware está ahí, pero nadie ocupa el Ring -1.

"Virtualization support not detected"

While preparing this post, Docker Desktop refused to start on my machine with a curt message: Virtualization support not detected. Odd, because the processor is an Intel Core i9-14900KF, which has hardware virtualization to spare, and it's enabled in the BIOS. Windows confirms it:

PS> Get-CimInstance Win32_Processor |
>>     Format-List Virtualization*, VMMonitor*, SecondLevel*
VirtualizationFirmwareEnabled           : True
VMMonitorModeExtensions                 : True
SecondLevelAddressTranslationExtensions : True

PS> (Get-CimInstance Win32_ComputerSystem).HypervisorPresent
False

The key is the last line. The hardware is ready, but no hypervisor is loaded: nobody occupies the level below the kernel. Docker Desktop on Windows usually runs on WSL2, and WSL2 is a lightweight virtual machine that needs Windows to boot its own hypervisor. Without it, there are no containers.

In the previous post we went down from Ring 3 to Ring 0 and said the kernel is in charge. Today we need to qualify that: there can be someone else below the kernel. It's informally called Ring -1.

What a hypervisor is

A hypervisor is the software that creates and manages virtual machines: it shares the CPU, memory and devices among several operating systems and makes each one believe it has the machine to itself. Two types are usually distinguished:

Type 1 (bare metal) Type 2 (hosted)
Where it runs Directly on the hardware As an application inside another operating system
Examples Hyper-V, VMware ESXi, Xen VirtualBox, VMware Workstation
Typical use Servers, cloud, securing the OS itself Desktop, testing, development

The line isn't as sharp as it looks. KVM, for instance, is a kernel module that turns Linux itself into a hypervisor. And when you enable Hyper-V on Windows, something happens that surprises many people: your Windows stops running directly on the hardware and becomes the root partition, a virtual machine with special privileges running on top of Hyper-V.

RING 3 guest applications RING 0 guest kernel (Windows, Linux...) ▲ VMX non-root VM exit ▼ ▲ VM entry VMX root ▼ RING -1 hypervisor · VMCS/VMCB · EPT/NPT tables HARDWARE CPU with VT-x or AMD-V · memory · devices
Fig. 1 — The guest keeps its rings 3 and 0, but all of it lives in VMX non-root. The hypervisor sits below.

How the hardware does it: VT-x and AMD-V

Virtualizing x86 purely in software was slow and complicated, because the guest expects to run in Ring 0 and freely touch control registers, page tables and interrupts. Since 2005–2006, Intel (VT-x) and AMD (AMD-V, also called SVM) solve it in hardware with a new execution mode, independent of the rings:

  • VMX root: where the hypervisor lives. That's what we call Ring -1, although technically it isn't a new ring but a CPU mode.
  • VMX non-root: where the guest lives, with its Ring 0 and Ring 3 intact. The guest kernel still runs in Ring 0, but under supervision.

The hypervisor describes each virtual machine in a memory structure (the VMCS on Intel, the VMCB on AMD) holding the guest's register state and, above all, which guest operations it wants to intercept. When the guest performs one of them — accessing certain CPU registers, an I/O port, or memory that isn't its own — the CPU stops it and jumps to the hypervisor. That's a VM exit. The hypervisor decides what to do and hands control back with a VM entry.

The cost of a VM exit

Every VM exit saves the guest's state, loads the hypervisor's, and later makes the trip back. Depending on the CPU and the work the hypervisor has to do, that costs on the order of hundreds to a few thousand cycles, compared with the handful of cycles of a normal instruction. Some instructions, such as CPUID under Intel VT-x, always cause a VM exit. That's why much of a modern hypervisor's work is about avoiding exits: the less it intervenes, the faster the guest runs.

Memory of memory: EPT and NPT

In the previous post we saw that every process uses virtual addresses that the kernel translates to physical ones with its page tables. In a virtual machine that's no longer enough: the "physical memory" the guest sees isn't real either. A second translation is needed:

  1. Guest virtual address → guest physical address. Done by the guest kernel, with its own page tables, as usual.
  2. Guest physical address → real physical address. Done by the hypervisor, with a second table the guest can neither see nor touch.

Intel calls that second table EPT (Extended Page Tables) and AMD calls it NPT (Nested Page Tables); the generic name is SLAT. Before it existed, around 2007–2008, hypervisors maintained "shadow page tables" in software that had to be resynchronized every time the guest changed its own, and that was very expensive. With SLAT, the hardware walks both tables by itself. It isn't free: in the worst case, a TLB miss with four-level paging can take up to 24 memory accesses instead of 4. That's why translation caches matter so much in virtualization.

The interesting part for security is that every EPT entry has its own read, write and execute permissions, independent of whatever the guest sets. The hypervisor can decide a page can't be modified, even if the guest kernel, from its Ring 0, says otherwise.

The hypervisor as the kernel's guardian

That last idea is the foundation of Windows' Virtualization-Based Security (VBS). With VBS enabled, Hyper-V splits the system into two virtual trust levels: regular Windows, with its kernel, at level 0 (VTL0), and a small secure kernel at level 1 (VTL1), isolated by the hypervisor. Two of the most important defenses in today's Windows are built on top of it:

  • HVCI (Memory Integrity): kernel code signature checks happen in VTL1, and the hypervisor uses SLAT so that no kernel page is ever both writable and executable. Even if an attacker manages to run code in Ring 0, they can't inject new code into the kernel: the page won't allow it.
  • Credential Guard: authentication secrets are kept in an isolated process in VTL1, out of reach even of the Windows kernel itself.

It's the direct answer to the idea we closed the previous post with: if Ring 0 doesn't forgive, put someone below it to watch over Ring 0. On many new Windows 11 machines, VBS and HVCI are enabled out of the box.

Check it yourself with CPUID

CPUID is the instruction any program uses to ask the CPU what it is and what it can do. It needs no privileges, so we can use it from Ring 3. First, a small portable header for GCC, Clang and MSVC:

// cpuid.hpp  —  portable CPUID access (GCC/Clang and MSVC)
#pragma once
#include <cstdint>
#include <cstring>
#include <string>
#if defined(_MSC_VER)
#include <intrin.h>
#else
#include <cpuid.h>
#endif

struct CpuidRegs { std::uint32_t eax, ebx, ecx, edx; };

inline CpuidRegs cpuid(std::uint32_t leaf) {
    CpuidRegs r{};
#if defined(_MSC_VER)
    int out[4];
    __cpuid(out, static_cast<int>(leaf));
    std::memcpy(&r, out, sizeof out);
#else
    __cpuid(leaf, r.eax, r.ebx, r.ecx, r.edx);
#endif
    return r;
}

// Joins three registers into the 12-byte ASCII text they encode.
inline std::string cpuid_text(std::uint32_t a, std::uint32_t b,
                              std::uint32_t c) {
    char s[12];
    std::memcpy(s, &a, 4);
    std::memcpy(s + 4, &b, 4);
    std::memcpy(s + 8, &c, 4);
    std::string t(s, sizeof s);
    t.erase(t.find_last_not_of('\0') + 1);   // strip trailing padding
    return t;
}

Does your CPU support virtualization?

// virt.cpp  —  g++ -std=c++20 virt.cpp -o virt
#include "cpuid.hpp"
#include <format>
#include <iostream>

int main() {
    const CpuidRegs v = cpuid(0);                         // vendor
    const bool vmx = cpuid(1).ecx & (1u << 5);            // Intel VT-x
    const bool svm = cpuid(0x8000'0001).ecx & (1u << 2);  // AMD-V
    const std::string vendor = cpuid_text(v.ebx, v.edx, v.ecx);
    auto yes = [](bool b) { return b ? "supported" : "not supported"; };

    std::cout << std::format("CPU:         {}\n", vendor)
              << std::format("VT-x (VMX):  {}\n", yes(vmx))
              << std::format("AMD-V (SVM): {}\n", yes(svm));
}

These are the real outputs on my machine and inside a GitHub Actions runner, which is a virtual machine on Azure:

# Intel Core i9-14900KF (physical hardware)
CPU:         GenuineIntel
VT-x (VMX):  supported
AMD-V (SVM): not supported

# GitHub Actions runner (Azure VM, AMD EPYC 7763)
CPU:         AuthenticAMD
VT-x (VMX):  not supported
AMD-V (SVM): supported

Two important nuances. First: the CPU supporting virtualization doesn't mean it's enabled. That's decided by the BIOS in a special CPU register (an MSR) that can only be read from Ring 0; that's why Windows checks it from the kernel and shows it to you in Task Manager, under Performance → CPU → Virtualization. Second: inside a virtual machine, CPUID answers whatever the hypervisor chooses to expose. The Azure runner showing AMD-V means that hypervisor allows nested virtualization: creating virtual machines inside the virtual machine.

Is there a hypervisor beneath you?

// hv.cpp  —  g++ -std=c++20 hv.cpp -o hv
#include "cpuid.hpp"
#include <format>
#include <iostream>

int main() {
    // Leaf 1, ECX bit 31: reserved for a hypervisor to announce itself.
    if (!(cpuid(1).ecx & (1u << 31))) {
        std::cout << "No hypervisor announced: running on bare metal.\n";
        return 0;
    }
    // Leaf 0x40000000: the hypervisor's 12-character signature.
    const CpuidRegs h = cpuid(0x4000'0000);
    std::cout << std::format("Hypervisor: \"{}\"\n",
                             cpuid_text(h.ebx, h.ecx, h.edx));
}
# Intel Core i9-14900KF (physical hardware)
No hypervisor announced: running on bare metal.

# GitHub Actions runner (Azure VM)
Hypervisor: "Microsoft Hv"

"Microsoft Hv" is Hyper-V's signature: Azure runs on a customized version of Hyper-V. On Linux with KVM you'd see KVMKVMKVM, and on VMware, VMwareVMware. And if you run it on your own Windows with VBS enabled, you'll also see "Microsoft Hv": your "real" system is Hyper-V's root partition.

Notice that the program checks bit 31 first: without a hypervisor, leaf 0x40000000 is undefined and whatever it returns means nothing. That bit is also a voluntary declaration. Hypervisors set it so the guest knows it's virtualized and can use mechanisms designed for it (known as paravirtualization), such as clocks and devices that don't need to emulate legacy hardware.

This is what the cloud is

When you rent a machine in the cloud, your entire operating system lives in VMX non-root. Azure runs on Hyper-V, as we just saw; AWS uses Nitro, a KVM-based hypervisor, and Google Cloud also uses KVM. Your "server" is really a guest, and someone else's Ring -1 is what guarantees that your machine and your neighbor's can't see each other.

A rule you learn the hard way

Personal note

A while ago I wrote an academic hypervisor on Intel VT-x as a Windows kernel driver: it enables VMX on every core, virtualizes the very system that's already running, and places beneath it an EPT table that, for now, maps every address to itself. It's the approach of research projects like HyperPlatform and, at heart, the same one VBS uses, except Windows does it from boot. The lesson that took me longest to accept is in my own lab guide, in a warning box: only one hypervisor fits in VMX root. If Hyper-V, WSL2 or VBS are already active, yours can't get in; and if you switch Windows' off to make room (bcdedit /set hypervisorlaunchtype off), you lose WSL2 and Docker. It isn't a bug, it's a decision you have to make. That's why testing was done inside a virtual machine with nested virtualization.

If Docker or WSL2 say there's no virtualization
  1. Check in Task Manager (Performance → CPU) that virtualization shows as enabled. If it doesn't, turn it on in the BIOS/UEFI.
  2. Enable Windows' Virtual Machine Platform, for example with wsl --install from an administrator terminal, and reboot.
  3. If you ever turned off Windows' hypervisor, turn it back on with bcdedit /set hypervisorlaunchtype auto and reboot.

And there's still more below

Ring -1 isn't the bottom either. Below the hypervisor sits System Management Mode (SMM), sometimes called Ring -2: a CPU mode reserved for firmware, entered through special interrupts, that runs code in a memory region not even the hypervisor can see. And outside the main CPU live microcontrollers such as Intel ME or AMD PSP, which some call Ring -3. But that's another story.

Next time Docker tells you it can't find virtualization, you'll know what it means: the hardware is there, but nobody is occupying Ring -1.

¿Tu proyecto necesita ir por debajo de la capa de aplicación? Does your project need to go below the application layer?

Software de sistemas, rendimiento y seguridad en Windows y Linux: diagnóstico, herramientas a medida y revisión de código de bajo nivel. Systems software, performance and security on Windows and Linux: diagnostics, custom tooling and low-level code review.

Hablemos Let's talk