| /* |
| * Copyright 2017, Data61 |
| * Commonwealth Scientific and Industrial Research Organisation (CSIRO) |
| * ABN 41 687 119 230. |
| * |
| * This software may be distributed and modified according to the terms of |
| * the GNU General Public License version 2. Note that NO WARRANTY is provided. |
| * See "LICENSE_GPLv2.txt" for details. |
| * |
| * @TAG(DATA61_GPL) |
| */ |
| |
| #include <autoconf.h> |
| #include <sel4vmm/gen_config.h> |
| |
| #include <string.h> |
| |
| #include <vmm/driver/virtio_emul.h> |
| #include <ethdrivers/virtio/virtio_pci.h> |
| #include <ethdrivers/virtio/virtio_net.h> |
| #include <ethdrivers/virtio/virtio_ring.h> |
| #include <ethdrivers/virtio/virtio_config.h> |
| |
| #define RX_QUEUE 0 |
| #define TX_QUEUE 1 |
| |
| #define BUF_SIZE 2048 |
| |
| typedef struct ethif_virtio_emul_internal { |
| struct eth_driver driver; |
| int status; |
| uint8_t mac[6]; |
| uint16_t queue; |
| struct vring vring[2]; |
| uint16_t queue_size[2]; |
| uint32_t queue_pfn[2]; |
| uint16_t last_idx[2]; |
| vspace_t guest_vspace; |
| ps_dma_man_t dma_man; |
| } ethif_virtio_emul_internal_t; |
| |
| typedef struct emul_tx_cookie { |
| uint16_t desc_head; |
| void *vaddr; |
| } emul_tx_cookie_t; |
| |
| static int read_guest_mem(uintptr_t phys, void *vaddr, size_t size, size_t offset, void *cookie) |
| { |
| memcpy(cookie + offset, vaddr, size); |
| return 0; |
| } |
| |
| static int write_guest_mem(uintptr_t phys, void *vaddr, size_t size, size_t offset, void *cookie) |
| { |
| memcpy(vaddr, cookie + offset, size); |
| return 0; |
| } |
| |
| static uint16_t ring_avail_idx(ethif_virtio_emul_t *emul, struct vring *vring) |
| { |
| uint16_t idx; |
| vmm_guest_vspace_touch(&emul->internal->guest_vspace, (uintptr_t)&vring->avail->idx, sizeof(vring->avail->idx), |
| read_guest_mem, &idx); |
| return idx; |
| } |
| |
| static uint16_t ring_avail(ethif_virtio_emul_t *emul, struct vring *vring, uint16_t idx) |
| { |
| uint16_t elem; |
| vmm_guest_vspace_touch(&emul->internal->guest_vspace, (uintptr_t) & (vring->avail->ring[idx % vring->num]), |
| sizeof(elem), read_guest_mem, &elem); |
| return elem; |
| } |
| |
| static struct vring_desc ring_desc(ethif_virtio_emul_t *emul, struct vring *vring, uint16_t idx) |
| { |
| struct vring_desc desc; |
| vmm_guest_vspace_touch(&emul->internal->guest_vspace, (uintptr_t) & (vring->desc[idx]), sizeof(desc), read_guest_mem, |
| &desc); |
| return desc; |
| } |
| |
| static void ring_used_add(ethif_virtio_emul_t *emul, struct vring *vring, struct vring_used_elem elem) |
| { |
| uint16_t guest_idx; |
| vmm_guest_vspace_touch(&emul->internal->guest_vspace, (uintptr_t)&vring->used->idx, sizeof(vring->used->idx), |
| read_guest_mem, &guest_idx); |
| vmm_guest_vspace_touch(&emul->internal->guest_vspace, (uintptr_t)&vring->used->ring[guest_idx % vring->num], |
| sizeof(elem), write_guest_mem, &elem); |
| guest_idx++; |
| vmm_guest_vspace_touch(&emul->internal->guest_vspace, (uintptr_t)&vring->used->idx, sizeof(vring->used->idx), |
| write_guest_mem, &guest_idx); |
| } |
| |
| static uintptr_t emul_allocate_rx_buf(void *iface, size_t buf_size, void **cookie) |
| { |
| ethif_virtio_emul_t *emul = (ethif_virtio_emul_t *)iface; |
| ethif_virtio_emul_internal_t *net = emul->internal; |
| if (buf_size > BUF_SIZE) { |
| return 0; |
| } |
| void *vaddr = ps_dma_alloc(&net->dma_man, BUF_SIZE, net->driver.dma_alignment, 1, PS_MEM_NORMAL); |
| if (!vaddr) { |
| return 0; |
| } |
| uintptr_t phys = ps_dma_pin(&net->dma_man, vaddr, BUF_SIZE); |
| *cookie = vaddr; |
| return phys; |
| } |
| |
| static void emul_rx_complete(void *iface, unsigned int num_bufs, void **cookies, unsigned int *lens) |
| { |
| ethif_virtio_emul_t *emul = (ethif_virtio_emul_t *)iface; |
| ethif_virtio_emul_internal_t *net = emul->internal; |
| unsigned int tot_len = 0; |
| int i; |
| struct vring *vring = &net->vring[RX_QUEUE]; |
| for (i = 0; i < num_bufs; i++) { |
| tot_len += lens[i]; |
| } |
| /* grab the next receive chain */ |
| struct virtio_net_hdr virtio_hdr; |
| memset(&virtio_hdr, 0, sizeof(virtio_hdr)); |
| uint16_t guest_idx = ring_avail_idx(emul, vring); |
| uint16_t idx = net->last_idx[RX_QUEUE]; |
| if (idx != guest_idx) { |
| /* total length of the written packet so far */ |
| size_t tot_written = 0; |
| /* amount of the current descriptor written */ |
| size_t desc_written = 0; |
| /* how much we have written of the current buffer */ |
| size_t buf_written = 0; |
| /* the current buffer. -1 indicates the virtio net buffer */ |
| int current_buf = -1; |
| uint16_t desc_head = ring_avail(emul, vring, idx); |
| /* start walking the descriptors */ |
| struct vring_desc desc; |
| uint16_t desc_idx = desc_head; |
| do { |
| desc = ring_desc(emul, vring, desc_idx); |
| /* determine how much we can copy */ |
| uint32_t copy; |
| void *buf_base = NULL; |
| if (current_buf == -1) { |
| copy = sizeof(struct virtio_net_hdr) - buf_written; |
| buf_base = &virtio_hdr; |
| } else { |
| copy = lens[current_buf] - buf_written; |
| buf_base = cookies[current_buf]; |
| } |
| copy = MIN(copy, desc.len - desc_written); |
| /* copy it */ |
| vmm_guest_vspace_touch(&net->guest_vspace, (uintptr_t)desc.addr + desc_written, copy, write_guest_mem, |
| buf_base + buf_written); |
| /* update amounts */ |
| tot_written += copy; |
| desc_written += copy; |
| buf_written += copy; |
| /* see what's gone over */ |
| if (desc_written == desc.len) { |
| if (!desc.flags & VRING_DESC_F_NEXT) { |
| /* descriptor chain is too short to hold the whole packet. |
| * just truncate */ |
| break; |
| } |
| desc_idx = desc.next; |
| desc_written = 0; |
| } |
| if (current_buf == -1) { |
| if (buf_written == sizeof(struct virtio_net_hdr)) { |
| current_buf++; |
| buf_written = 0; |
| } |
| } else { |
| if (buf_written == lens[current_buf]) { |
| current_buf++; |
| buf_written = 0; |
| } |
| } |
| } while (current_buf < num_bufs); |
| /* now put it in the used ring */ |
| struct vring_used_elem used_elem = {desc_head, tot_written}; |
| ring_used_add(emul, vring, used_elem); |
| |
| /* record that we've used this descriptor chain now */ |
| net->last_idx[RX_QUEUE]++; |
| /* notify the guest that there is something in its used ring */ |
| net->driver.i_fn.raw_handleIRQ(&net->driver, 0); |
| } |
| for (i = 0; i < num_bufs; i++) { |
| ps_dma_unpin(&net->dma_man, cookies[i], BUF_SIZE); |
| ps_dma_free(&net->dma_man, cookies[i], BUF_SIZE); |
| } |
| } |
| |
| static void emul_tx_complete(void *iface, void *cookie) |
| { |
| ethif_virtio_emul_t *emul = (ethif_virtio_emul_t *)iface; |
| ethif_virtio_emul_internal_t *net = emul->internal; |
| emul_tx_cookie_t *tx_cookie = (emul_tx_cookie_t *)cookie; |
| /* free the dma memory */ |
| ps_dma_unpin(&net->dma_man, tx_cookie->vaddr, BUF_SIZE); |
| ps_dma_free(&net->dma_man, tx_cookie->vaddr, BUF_SIZE); |
| /* put the descriptor chain into the used list */ |
| struct vring_used_elem used_elem = {tx_cookie->desc_head, 0}; |
| ring_used_add(emul, &net->vring[TX_QUEUE], used_elem); |
| free(tx_cookie); |
| /* notify the guest that we have completed some of its buffers */ |
| net->driver.i_fn.raw_handleIRQ(&net->driver, 0); |
| } |
| |
| static void emul_notify_tx(ethif_virtio_emul_t *emul) |
| { |
| ethif_virtio_emul_internal_t *net = emul->internal; |
| struct vring *vring = &net->vring[TX_QUEUE]; |
| /* read the index */ |
| uint16_t guest_idx = ring_avail_idx(emul, vring); |
| /* process what we can of the ring */ |
| uint16_t idx = net->last_idx[TX_QUEUE]; |
| while (idx != guest_idx) { |
| uint16_t desc_head; |
| /* read the head of the descriptor chain */ |
| desc_head = ring_avail(emul, vring, idx); |
| /* allocate a packet */ |
| void *vaddr = ps_dma_alloc(&net->dma_man, BUF_SIZE, net->driver.dma_alignment, 1, PS_MEM_NORMAL); |
| if (!vaddr) { |
| /* try again later */ |
| break; |
| } |
| uintptr_t phys = ps_dma_pin(&net->dma_man, vaddr, BUF_SIZE); |
| assert(phys); |
| /* length of the final packet to deliver */ |
| uint32_t len = 0; |
| /* we want to skip the initial virtio header, as this should |
| * not be sent to the actual ethernet driver. This records |
| * how much we have skipped so far. */ |
| uint32_t skipped = 0; |
| /* start walking the descriptors */ |
| struct vring_desc desc; |
| uint16_t desc_idx = desc_head; |
| do { |
| desc = ring_desc(emul, vring, desc_idx); |
| uint32_t skip = 0; |
| /* if we haven't yet skipped the full virtio net header, work |
| * out how much of this descriptor should be skipped */ |
| if (skipped < sizeof(struct virtio_net_hdr)) { |
| skip = MIN(sizeof(struct virtio_net_hdr) - skipped, desc.len); |
| skipped += skip; |
| } |
| /* truncate packets that are too large */ |
| uint32_t this_len = desc.len - skip; |
| this_len = MIN(BUF_SIZE - len, this_len); |
| vmm_guest_vspace_touch(&net->guest_vspace, (uintptr_t)desc.addr + skip, this_len, read_guest_mem, vaddr + len); |
| len += this_len; |
| desc_idx = desc.next; |
| } while (desc.flags & VRING_DESC_F_NEXT); |
| /* ship it */ |
| emul_tx_cookie_t *cookie = malloc(sizeof(*cookie)); |
| assert(cookie); |
| cookie->desc_head = desc_head; |
| cookie->vaddr = vaddr; |
| int result = net->driver.i_fn.raw_tx(&net->driver, 1, &phys, &len, cookie); |
| switch (result) { |
| case ETHIF_TX_COMPLETE: |
| emul_tx_complete(emul, cookie); |
| break; |
| case ETHIF_TX_FAILED: |
| ps_dma_unpin(&net->dma_man, vaddr, BUF_SIZE); |
| ps_dma_free(&net->dma_man, vaddr, BUF_SIZE); |
| free(cookie); |
| break; |
| } |
| /* next */ |
| idx++; |
| } |
| /* update which parts of the ring we have processed */ |
| net->last_idx[TX_QUEUE] = idx; |
| } |
| |
| static void emul_tx_complete_external(void *iface, void *cookie) |
| { |
| emul_tx_complete(iface, cookie); |
| /* space may have cleared for additional transmits */ |
| emul_notify_tx(iface); |
| } |
| |
| static struct raw_iface_callbacks emul_callbacks = { |
| .tx_complete = emul_tx_complete_external, |
| .rx_complete = emul_rx_complete, |
| .allocate_rx_buf = emul_allocate_rx_buf |
| }; |
| |
| static int emul_io_in(struct ethif_virtio_emul *emul, unsigned int offset, unsigned int size, unsigned int *result) |
| { |
| switch (offset) { |
| case VIRTIO_PCI_HOST_FEATURES: |
| assert(size == 4); |
| *result = BIT(VIRTIO_NET_F_MAC); |
| break; |
| case VIRTIO_PCI_STATUS: |
| assert(size == 1); |
| *result = emul->internal->status; |
| break; |
| case VIRTIO_PCI_QUEUE_NUM: |
| assert(size == 2); |
| *result = emul->internal->queue_size[emul->internal->queue]; |
| break; |
| case 0x14 ... 0x19: |
| assert(size == 1); |
| *result = emul->internal->mac[offset - 0x14]; |
| break; |
| case VIRTIO_PCI_QUEUE_PFN: |
| assert(size == 4); |
| *result = emul->internal->queue_pfn[emul->internal->queue]; |
| break; |
| case VIRTIO_PCI_ISR: |
| assert(size == 1); |
| *result = 1; |
| break; |
| default: |
| printf("Unhandled offset of 0x%x of size %d, reading\n", offset, size); |
| assert(!"panic"); |
| } |
| return 0; |
| } |
| |
| static int emul_io_out(struct ethif_virtio_emul *emul, unsigned int offset, unsigned int size, unsigned int value) |
| { |
| switch (offset) { |
| case VIRTIO_PCI_GUEST_FEATURES: |
| assert(size == 4); |
| assert(value == BIT(VIRTIO_NET_F_MAC)); |
| break; |
| case VIRTIO_PCI_STATUS: |
| assert(size == 1); |
| emul->internal->status = value & 0xff; |
| break; |
| case VIRTIO_PCI_QUEUE_SEL: |
| assert(size == 2); |
| emul->internal->queue = (value & 0xffff); |
| assert(emul->internal->queue == 0 || emul->internal->queue == 1); |
| break; |
| case VIRTIO_PCI_QUEUE_PFN: { |
| assert(size == 4); |
| int queue = emul->internal->queue; |
| emul->internal->queue_pfn[queue] = value; |
| vring_init(&emul->internal->vring[queue], emul->internal->queue_size[queue], (void *)(uintptr_t)(value << 12), |
| VIRTIO_PCI_VRING_ALIGN); |
| break; |
| } |
| case VIRTIO_PCI_QUEUE_NOTIFY: |
| if (value == RX_QUEUE) { |
| /* Currently RX packets will just get dropped if there was no space |
| * so we will never have work to do if the client suddenly adds |
| * more buffers */ |
| } else if (value == TX_QUEUE) { |
| emul_notify_tx(emul); |
| } |
| break; |
| default: |
| printf("Unhandled offset of 0x%x of size %d, writing 0x%x\n", offset, size, value); |
| assert(!"panic"); |
| } |
| return 0; |
| } |
| |
| static int emul_notify(ethif_virtio_emul_t *emul) |
| { |
| if (emul->internal->status != VIRTIO_CONFIG_S_DRIVER_OK) { |
| return -1; |
| } |
| emul_notify_tx(emul); |
| return 0; |
| } |
| |
| ethif_virtio_emul_t *ethif_virtio_emul_init(ps_io_ops_t io_ops, int queue_size, vspace_t *guest_vspace, |
| ethif_driver_init driver, void *config) |
| { |
| ethif_virtio_emul_t *emul = NULL; |
| ethif_virtio_emul_internal_t *internal = NULL; |
| int err; |
| emul = malloc(sizeof(*emul)); |
| internal = malloc(sizeof(*internal)); |
| if (!emul || !internal) { |
| goto error; |
| } |
| memset(emul, 0, sizeof(*emul)); |
| memset(internal, 0, sizeof(*internal)); |
| emul->internal = internal; |
| emul->io_in = emul_io_in; |
| emul->io_out = emul_io_out; |
| emul->notify = emul_notify; |
| internal->queue_size[RX_QUEUE] = queue_size; |
| internal->queue_size[TX_QUEUE] = queue_size; |
| /* create dummy rings. we never actually dereference the rings so they can be null */ |
| vring_init(&internal->vring[RX_QUEUE], emul->internal->queue_size[RX_QUEUE], 0, VIRTIO_PCI_VRING_ALIGN); |
| vring_init(&internal->vring[TX_QUEUE], emul->internal->queue_size[RX_QUEUE], 0, VIRTIO_PCI_VRING_ALIGN); |
| internal->driver.cb_cookie = emul; |
| internal->driver.i_cb = emul_callbacks; |
| internal->guest_vspace = *guest_vspace; |
| internal->dma_man = io_ops.dma_manager; |
| err = driver(&internal->driver, io_ops, config); |
| if (err) { |
| ZF_LOGE("Fafiled to initialize driver"); |
| goto error; |
| } |
| int mtu; |
| internal->driver.i_fn.low_level_init(&internal->driver, internal->mac, &mtu); |
| return emul; |
| error: |
| if (emul) { |
| free(emul); |
| } |
| if (internal) { |
| free(internal); |
| } |
| return NULL; |
| } |