// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (c) 2021-2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cmd.h" /* Device specification max LOAD size */ #define MAX_LOAD_SIZE (BIT_ULL(__mlx5_bit_sz(load_vhca_state_in, size)) - 1) static struct mlx5vf_pci_core_device *mlx5vf_drvdata(struct pci_dev *pdev) { struct vfio_pci_core_device *core_device = dev_get_drvdata(&pdev->dev); return container_of(core_device, struct mlx5vf_pci_core_device, core_device); } struct page * mlx5vf_get_migration_page(struct mlx5_vhca_data_buffer *buf, unsigned long offset) { unsigned long cur_offset = 0; struct scatterlist *sg; unsigned int i; /* All accesses are sequential */ if (offset < buf->last_offset || !buf->last_offset_sg) { buf->last_offset = 0; buf->last_offset_sg = buf->table.sgt.sgl; buf->sg_last_entry = 0; } cur_offset = buf->last_offset; for_each_sg(buf->last_offset_sg, sg, buf->table.sgt.orig_nents - buf->sg_last_entry, i) { if (offset < sg->length + cur_offset) { buf->last_offset_sg = sg; buf->sg_last_entry += i; buf->last_offset = cur_offset; return nth_page(sg_page(sg), (offset - cur_offset) / PAGE_SIZE); } cur_offset += sg->length; } return NULL; } int mlx5vf_add_migration_pages(struct mlx5_vhca_data_buffer *buf, unsigned int npages) { unsigned int to_alloc = npages; struct page **page_list; unsigned long filled; unsigned int to_fill; int ret; to_fill = min_t(unsigned int, npages, PAGE_SIZE / sizeof(*page_list)); page_list = kvzalloc(to_fill * sizeof(*page_list), GFP_KERNEL_ACCOUNT); if (!page_list) return -ENOMEM; do { filled = alloc_pages_bulk_array(GFP_KERNEL_ACCOUNT, to_fill, page_list); if (!filled) { ret = -ENOMEM; goto err; } to_alloc -= filled; ret = sg_alloc_append_table_from_pages( &buf->table, page_list, filled, 0, filled << PAGE_SHIFT, UINT_MAX, SG_MAX_SINGLE_ALLOC, GFP_KERNEL_ACCOUNT); if (ret) goto err; buf->allocated_length += filled * PAGE_SIZE; /* clean input for another bulk allocation */ memset(page_list, 0, filled * sizeof(*page_list)); to_fill = min_t(unsigned int, to_alloc, PAGE_SIZE / sizeof(*page_list)); } while (to_alloc > 0); kvfree(page_list); return 0; err: kvfree(page_list); return ret; } static void mlx5vf_disable_fd(struct mlx5_vf_migration_file *migf) { mutex_lock(&migf->lock); migf->state = MLX5_MIGF_STATE_ERROR; migf->filp->f_pos = 0; mutex_unlock(&migf->lock); } static int mlx5vf_release_file(struct inode *inode, struct file *filp) { struct mlx5_vf_migration_file *migf = filp->private_data; mlx5vf_disable_fd(migf); mutex_destroy(&migf->lock); kfree(migf); return 0; } static struct mlx5_vhca_data_buffer * mlx5vf_get_data_buff_from_pos(struct mlx5_vf_migration_file *migf, loff_t pos, bool *end_of_data) { struct mlx5_vhca_data_buffer *buf; bool found = false; *end_of_data = false; spin_lock_irq(&migf->list_lock); if (list_empty(&migf->buf_list)) { *end_of_data = true; goto end; } buf = list_first_entry(&migf->buf_list, struct mlx5_vhca_data_buffer, buf_elm); if (pos >= buf->start_pos && pos < buf->start_pos + buf->length) { found = true; goto end; } /* * As we use a stream based FD we may expect having the data always * on first chunk */ migf->state = MLX5_MIGF_STATE_ERROR; end: spin_unlock_irq(&migf->list_lock); return found ? buf : NULL; } static ssize_t mlx5vf_buf_read(struct mlx5_vhca_data_buffer *vhca_buf, char __user **buf, size_t *len, loff_t *pos) { unsigned long offset; ssize_t done = 0; size_t copy_len; copy_len = min_t(size_t, vhca_buf->start_pos + vhca_buf->length - *pos, *len); while (copy_len) { size_t page_offset; struct page *page; size_t page_len; u8 *from_buff; int ret; offset = *pos - vhca_buf->start_pos; page_offset = offset % PAGE_SIZE; offset -= page_offset; page = mlx5vf_get_migration_page(vhca_buf, offset); if (!page) return -EINVAL; page_len = min_t(size_t, copy_len, PAGE_SIZE - page_offset); from_buff = kmap_local_page(page); ret = copy_to_user(*buf, from_buff + page_offset, page_len); kunmap_local(from_buff); if (ret) return -EFAULT; *pos += page_len; *len -= page_len; *buf += page_len; done += page_len; copy_len -= page_len; } if (*pos >= vhca_buf->start_pos + vhca_buf->length) { spin_lock_irq(&vhca_buf->migf->list_lock); list_del_init(&vhca_buf->buf_elm); list_add_tail(&vhca_buf->buf_elm, &vhca_buf->migf->avail_list); spin_unlock_irq(&vhca_buf->migf->list_lock); } return done; } static ssize_t mlx5vf_save_read(struct file *filp, char __user *buf, size_t len, loff_t *pos) { struct mlx5_vf_migration_file *migf = filp->private_data; struct mlx5_vhca_data_buffer *vhca_buf; bool first_loop_call = true; bool end_of_data; ssize_t done = 0; if (pos) return -ESPIPE; pos = &filp->f_pos; if (!(filp->f_flags & O_NONBLOCK)) { if (wait_event_interruptible(migf->poll_wait, !list_empty(&migf->buf_list) || migf->state == MLX5_MIGF_STATE_ERROR || migf->state == MLX5_MIGF_STATE_PRE_COPY_ERROR || migf->state == MLX5_MIGF_STATE_PRE_COPY || migf->state == MLX5_MIGF_STATE_COMPLETE)) return -ERESTARTSYS; } mutex_lock(&migf->lock); if (migf->state == MLX5_MIGF_STATE_ERROR) { done = -ENODEV; goto out_unlock; } while (len) { ssize_t count; vhca_buf = mlx5vf_get_data_buff_from_pos(migf, *pos, &end_of_data); if (first_loop_call) { first_loop_call = false; /* Temporary end of file as part of PRE_COPY */ if (end_of_data && (migf->state == MLX5_MIGF_STATE_PRE_COPY || migf->state == MLX5_MIGF_STATE_PRE_COPY_ERROR)) { done = -ENOMSG; goto out_unlock; } if (end_of_data && migf->state != MLX5_MIGF_STATE_COMPLETE) { if (filp->f_flags & O_NONBLOCK) { done = -EAGAIN; goto out_unlock; } } } if (end_of_data) goto out_unlock; if (!vhca_buf) { done = -EINVAL; goto out_unlock; } count = mlx5vf_buf_read(vhca_buf, &buf, &len, pos); if (count < 0) { done = count; goto out_unlock; } done += count; } out_unlock: mutex_unlock(&migf->lock); return done; } static __poll_t mlx5vf_save_poll(struct file *filp, struct poll_table_struct *wait) { struct mlx5_vf_migration_file *migf = filp->private_data; __poll_t pollflags = 0; poll_wait(filp, &migf->poll_wait, wait); mutex_lock(&migf->lock); if (migf->state == MLX5_MIGF_STATE_ERROR) pollflags = EPOLLIN | EPOLLRDNORM | EPOLLRDHUP; else if (!list_empty(&migf->buf_list) || migf->state == MLX5_MIGF_STATE_COMPLETE) pollflags = EPOLLIN | EPOLLRDNORM; mutex_unlock(&migf->lock); return pollflags; } /* * FD is exposed and user can use it after receiving an error. * Mark migf in error, and wake the user. */ static void mlx5vf_mark_err(struct mlx5_vf_migration_file *migf) { migf->state = MLX5_MIGF_STATE_ERROR; wake_up_interruptible(&migf->poll_wait); } static int mlx5vf_add_stop_copy_header(struct mlx5_vf_migration_file *migf) { size_t size = sizeof(struct mlx5_vf_migration_header) + sizeof(struct mlx5_vf_migration_tag_stop_copy_data); struct mlx5_vf_migration_tag_stop_copy_data data = {}; struct mlx5_vhca_data_buffer *header_buf = NULL; struct mlx5_vf_migration_header header = {}; unsigned long flags; struct page *page; u8 *to_buff; int ret; header_buf = mlx5vf_get_data_buffer(migf, size, DMA_NONE); if (IS_ERR(header_buf)) return PTR_ERR(header_buf); header.record_size = cpu_to_le64(sizeof(data)); header.flags = cpu_to_le32(MLX5_MIGF_HEADER_FLAGS_TAG_OPTIONAL); header.tag = cpu_to_le32(MLX5_MIGF_HEADER_TAG_STOP_COPY_SIZE); page = mlx5vf_get_migration_page(header_buf, 0); if (!page) { ret = -EINVAL; goto err; } to_buff = kmap_local_page(page); memcpy(to_buff, &header, sizeof(header)); header_buf->length = sizeof(header); data.stop_copy_size = cpu_to_le64(migf->buf->allocated_length); memcpy(to_buff + sizeof(header), &data, sizeof(data)); header_buf->length += sizeof(data); kunmap_local(to_buff); header_buf->start_pos = header_buf->migf->max_pos; migf->max_pos += header_buf->length; spin_lock_irqsave(&migf->list_lock, flags); list_add_tail(&header_buf->buf_elm, &migf->buf_list); spin_unlock_irqrestore(&migf->list_lock, flags); migf->pre_copy_initial_bytes = size; return 0; err: mlx5vf_put_data_buffer(header_buf); return ret; } static int mlx5vf_prep_stop_copy(struct mlx5_vf_migration_file *migf, size_t state_size) { struct mlx5_vhca_data_buffer *buf; size_t inc_state_size; int ret; /* let's be ready for stop_copy size that might grow by 10 percents */ if (check_add_overflow(state_size, state_size / 10, &inc_state_size)) inc_state_size = state_size; buf = mlx5vf_get_data_buffer(migf, inc_state_size, DMA_FROM_DEVICE); if (IS_ERR(buf)) return PTR_ERR(buf); migf->buf = buf; buf = mlx5vf_get_data_buffer(migf, sizeof(struct mlx5_vf_migration_header), DMA_NONE); if (IS_ERR(buf)) { ret = PTR_ERR(buf); goto err; } migf->buf_header = buf; ret = mlx5vf_add_stop_copy_header(migf); if (ret) goto err_header; return 0; err_header: mlx5vf_put_data_buffer(migf->buf_header); migf->buf_header = NULL; err: mlx5vf_put_data_buffer(migf->buf); migf->buf = NULL; return ret; } static long mlx5vf_precopy_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct mlx5_vf_migration_file *migf = filp->private_data; struct mlx5vf_pci_core_device *mvdev = migf->mvdev; struct mlx5_vhca_data_buffer *buf; struct vfio_precopy_info info = {}; loff_t *pos = &filp->f_pos; unsigned long minsz; size_t inc_length = 0; bool end_of_data = false; int ret; if (cmd != VFIO_MIG_GET_PRECOPY_INFO) return -ENOTTY; minsz = offsetofend(struct vfio_precopy_info, dirty_bytes); if (copy_from_user(&info, (void __user *)arg, minsz)) return -EFAULT; if (info.argsz < minsz) return -EINVAL; mutex_lock(&mvdev->state_mutex); if (mvdev->mig_state != VFIO_DEVICE_STATE_PRE_COPY && mvdev->mig_state != VFIO_DEVICE_STATE_PRE_COPY_P2P) { ret = -EINVAL; goto err_state_unlock; } /* * We can't issue a SAVE command when the device is suspended, so as * part of VFIO_DEVICE_STATE_PRE_COPY_P2P no reason to query for extra * bytes that can't be read. */ if (mvdev->mig_state == VFIO_DEVICE_STATE_PRE_COPY) { /* * Once the query returns it's guaranteed that there is no * active SAVE command. * As so, the other code below is safe with the proper locks. */ ret = mlx5vf_cmd_query_vhca_migration_state(mvdev, &inc_length, MLX5VF_QUERY_INC); if (ret) goto err_state_unlock; } mutex_lock(&migf->lock); if (migf->state == MLX5_MIGF_STATE_ERROR) { ret = -ENODEV; goto err_migf_unlock; } if (migf->pre_copy_initial_bytes > *pos) { info.initial_bytes = migf->pre_copy_initial_bytes - *pos; } else { info.dirty_bytes = migf->max_pos - *pos; if (!info.dirty_bytes) end_of_data = true; info.dirty_bytes += inc_length; } if (!end_of_data || !inc_length) { mutex_unlock(&migf->lock); goto done; } mutex_unlock(&migf->lock); /* * We finished transferring the current state and the device has a * dirty state, save a new state to be ready for. */ buf = mlx5vf_get_data_buffer(migf, inc_length, DMA_FROM_DEVICE); if (IS_ERR(buf)) { ret = PTR_ERR(buf); mlx5vf_mark_err(migf); goto err_state_unlock; } ret = mlx5vf_cmd_save_vhca_state(mvdev, migf, buf, true, true); if (ret) { mlx5vf_mark_err(migf); mlx5vf_put_data_buffer(buf); goto err_state_unlock; } done: mlx5vf_state_mutex_unlock(mvdev); if (copy_to_user((void __user *)arg, &info, minsz)) return -EFAULT; return 0; err_migf_unlock: mutex_unlock(&migf->lock); err_state_unlock: mlx5vf_state_mutex_unlock(mvdev); return ret; } static const struct file_operations mlx5vf_save_fops = { .owner = THIS_MODULE, .read = mlx5vf_save_read, .poll = mlx5vf_save_poll, .unlocked_ioctl = mlx5vf_precopy_ioctl, .compat_ioctl = compat_ptr_ioctl, .release = mlx5vf_release_file, .llseek = no_llseek, }; static int mlx5vf_pci_save_device_inc_data(struct mlx5vf_pci_core_device *mvdev) { struct mlx5_vf_migration_file *migf = mvdev->saving_migf; struct mlx5_vhca_data_buffer *buf; size_t length; int ret; if (migf->state == MLX5_MIGF_STATE_ERROR) return -ENODEV; ret = mlx5vf_cmd_query_vhca_migration_state(mvdev, &length, MLX5VF_QUERY_INC | MLX5VF_QUERY_FINAL); if (ret) goto err; /* Checking whether we have a matching pre-allocated buffer that can fit */ if (migf->buf && migf->buf->allocated_length >= length) { buf = migf->buf; migf->buf = NULL; } else { buf = mlx5vf_get_data_buffer(migf, length, DMA_FROM_DEVICE); if (IS_ERR(buf)) { ret = PTR_ERR(buf); goto err; } } ret = mlx5vf_cmd_save_vhca_state(mvdev, migf, buf, true, false); if (ret) goto err_save; return 0; err_save: mlx5vf_put_data_buffer(buf); err: mlx5vf_mark_err(migf); return ret; } static struct mlx5_vf_migration_file * mlx5vf_pci_save_device_data(struct mlx5vf_pci_core_device *mvdev, bool track) { struct mlx5_vf_migration_file *migf; struct mlx5_vhca_data_buffer *buf; size_t length; int ret; migf = kzalloc(sizeof(*migf), GFP_KERNEL_ACCOUNT); if (!migf) return ERR_PTR(-ENOMEM); migf->filp = anon_inode_getfile("mlx5vf_mig", &mlx5vf_save_fops, migf, O_RDONLY); if (IS_ERR(migf->filp)) { ret = PTR_ERR(migf->filp); goto end; } migf->mvdev = mvdev; ret = mlx5vf_cmd_alloc_pd(migf); if (ret) goto out_free; stream_open(migf->filp->f_inode, migf->filp); mutex_init(&migf->lock); init_waitqueue_head(&migf->poll_wait); init_completion(&migf->save_comp); /* * save_comp is being used as a binary semaphore built from * a completion. A normal mutex cannot be used because the lock is * passed between kernel threads and lockdep can't model this. */ complete(&migf->save_comp); mlx5_cmd_init_async_ctx(mvdev->mdev, &migf->async_ctx); INIT_WORK(&migf->async_data.work, mlx5vf_mig_file_cleanup_cb); INIT_LIST_HEAD(&migf->buf_list); INIT_LIST_HEAD(&migf->avail_list); spin_lock_init(&migf->list_lock); ret = mlx5vf_cmd_query_vhca_migration_state(mvdev, &length, 0); if (ret) goto out_pd; if (track) { ret = mlx5vf_prep_stop_copy(migf, length); if (ret) goto out_pd; } buf = mlx5vf_alloc_data_buffer(migf, length, DMA_FROM_DEVICE); if (IS_ERR(buf)) { ret = PTR_ERR(buf); goto out_pd; } ret = mlx5vf_cmd_save_vhca_state(mvdev, migf, buf, false, track); if (ret) goto out_save; return migf; out_save: mlx5vf_free_data_buffer(buf); out_pd: mlx5fv_cmd_clean_migf_resources(migf); out_free: fput(migf->filp); end: kfree(migf); return ERR_PTR(ret); } static int mlx5vf_append_page_to_mig_buf(struct mlx5_vhca_data_buffer *vhca_buf, const char __user **buf, size_t *len, loff_t *pos, ssize_t *done) { unsigned long offset; size_t page_offset; struct page *page; size_t page_len; u8 *to_buff; int ret; offset = *pos - vhca_buf->start_pos; page_offset = offset % PAGE_SIZE; page = mlx5vf_get_migration_page(vhca_buf, offset - page_offset); if (!page) return -EINVAL; page_len = min_t(size_t, *len, PAGE_SIZE - page_offset); to_buff = kmap_local_page(page); ret = copy_from_user(to_buff + page_offset, *buf, page_len); kunmap_local(to_buff); if (ret) return -EFAULT; *pos += page_len; *done += page_len; *buf += page_len; *len -= page_len; vhca_buf->length += page_len; return 0; } static int mlx5vf_resume_read_image_no_header(struct mlx5_vhca_data_buffer *vhca_buf, loff_t requested_length, const char __user **buf, size_t *len, loff_t *pos, ssize_t *done) { int ret; if (requested_length > MAX_LOAD_SIZE) return -ENOMEM; if (vhca_buf->allocated_length < requested_length) { ret = mlx5vf_add_migration_pages( vhca_buf, DIV_ROUND_UP(requested_length - vhca_buf->allocated_length, PAGE_SIZE)); if (ret) return ret; } while (*len) { ret = mlx5vf_append_page_to_mig_buf(vhca_buf, buf, len, pos, done); if (ret) return ret; } return 0; } static ssize_t mlx5vf_resume_read_image(struct mlx5_vf_migration_file *migf, struct mlx5_vhca_data_buffer *vhca_buf, size_t image_size, const char __user **buf, size_t *len, loff_t *pos, ssize_t *done, bool *has_work) { size_t copy_len, to_copy; int ret; to_copy = min_t(size_t, *len, image_size - vhca_buf->length); copy_len = to_copy; while (to_copy) { ret = mlx5vf_append_page_to_mig_buf(vhca_buf, buf, &to_copy, pos, done); if (ret) return ret; } *len -= copy_len; if (vhca_buf->length == image_size) { migf->load_state = MLX5_VF_LOAD_STATE_LOAD_IMAGE; migf->max_pos += image_size; *has_work = true; } return 0; } static int mlx5vf_resume_read_header_data(struct mlx5_vf_migration_file *migf, struct mlx5_vhca_data_buffer *vhca_buf, const char __user **buf, size_t *len, loff_t *pos, ssize_t *done) { size_t copy_len, to_copy; size_t required_data; u8 *to_buff; int ret; required_data = migf->record_size - vhca_buf->length; to_copy = min_t(size_t, *len, required_data); copy_len = to_copy; while (to_copy) { ret = mlx5vf_append_page_to_mig_buf(vhca_buf, buf, &to_copy, pos, done); if (ret) return ret; } *len -= copy_len; if (vhca_buf->length == migf->record_size) { switch (migf->record_tag) { case MLX5_MIGF_HEADER_TAG_STOP_COPY_SIZE: { struct page *page; page = mlx5vf_get_migration_page(vhca_buf, 0); if (!page) return -EINVAL; to_buff = kmap_local_page(page); migf->stop_copy_prep_size = min_t(u64, le64_to_cpup((__le64 *)to_buff), MAX_LOAD_SIZE); kunmap_local(to_buff); break; } default: /* Optional tag */ break; } migf->load_state = MLX5_VF_LOAD_STATE_READ_HEADER; migf->max_pos += migf->record_size; vhca_buf->length = 0; } return 0; } static int mlx5vf_resume_read_header(struct mlx5_vf_migration_file *migf, struct mlx5_vhca_data_buffer *vhca_buf, const char __user **buf, size_t *len, loff_t *pos, ssize_t *done, bool *has_work) { struct page *page; size_t copy_len; u8 *to_buff; int ret; copy_len = min_t(size_t, *len, sizeof(struct mlx5_vf_migration_header) - vhca_buf->length); page = mlx5vf_get_migration_page(vhca_buf, 0); if (!page) return -EINVAL; to_buff = kmap_local_page(page); ret = copy_from_user(to_buff + vhca_buf->length, *buf, copy_len); if (ret) { ret = -EFAULT; goto end; } *buf += copy_len; *pos += copy_len; *done += copy_len; *len -= copy_len; vhca_buf->length += copy_len; if (vhca_buf->length == sizeof(struct mlx5_vf_migration_header)) { u64 record_size; u32 flags; record_size = le64_to_cpup((__le64 *)to_buff); if (record_size > MAX_LOAD_SIZE) { ret = -ENOMEM; goto end; } migf->record_size = record_size; flags = le32_to_cpup((__le32 *)(to_buff + offsetof(struct mlx5_vf_migration_header, flags))); migf->record_tag = le32_to_cpup((__le32 *)(to_buff + offsetof(struct mlx5_vf_migration_header, tag))); switch (migf->record_tag) { case MLX5_MIGF_HEADER_TAG_FW_DATA: migf->load_state = MLX5_VF_LOAD_STATE_PREP_IMAGE; break; case MLX5_MIGF_HEADER_TAG_STOP_COPY_SIZE: migf->load_state = MLX5_VF_LOAD_STATE_PREP_HEADER_DATA; break; default: if (!(flags & MLX5_MIGF_HEADER_FLAGS_TAG_OPTIONAL)) { ret = -EOPNOTSUPP; goto end; } /* We may read and skip this optional record data */ migf->load_state = MLX5_VF_LOAD_STATE_PREP_HEADER_DATA; } migf->max_pos += vhca_buf->length; vhca_buf->length = 0; *has_work = true; } end: kunmap_local(to_buff); return ret; } static ssize_t mlx5vf_resume_write(struct file *filp, const char __user *buf, size_t len, loff_t *pos) { struct mlx5_vf_migration_file *migf = filp->private_data; struct mlx5_vhca_data_buffer *vhca_buf = migf->buf; struct mlx5_vhca_data_buffer *vhca_buf_header = migf->buf_header; loff_t requested_length; bool has_work = false; ssize_t done = 0; int ret = 0; if (pos) return -ESPIPE; pos = &filp->f_pos; if (*pos < 0 || check_add_overflow((loff_t)len, *pos, &requested_length)) return -EINVAL; mutex_lock(&migf->mvdev->state_mutex); mutex_lock(&migf->lock); if (migf->state == MLX5_MIGF_STATE_ERROR) { ret = -ENODEV; goto out_unlock; } while (len || has_work) { has_work = false; switch (migf->load_state) { case MLX5_VF_LOAD_STATE_READ_HEADER: ret = mlx5vf_resume_read_header(migf, vhca_buf_header, &buf, &len, pos, &done, &has_work); if (ret) goto out_unlock; break; case MLX5_VF_LOAD_STATE_PREP_HEADER_DATA: if (vhca_buf_header->allocated_length < migf->record_size) { mlx5vf_free_data_buffer(vhca_buf_header); migf->buf_header = mlx5vf_alloc_data_buffer(migf, migf->record_size, DMA_NONE); if (IS_ERR(migf->buf_header)) { ret = PTR_ERR(migf->buf_header); migf->buf_header = NULL; goto out_unlock; } vhca_buf_header = migf->buf_header; } vhca_buf_header->start_pos = migf->max_pos; migf->load_state = MLX5_VF_LOAD_STATE_READ_HEADER_DATA; break; case MLX5_VF_LOAD_STATE_READ_HEADER_DATA: ret = mlx5vf_resume_read_header_data(migf, vhca_buf_header, &buf, &len, pos, &done); if (ret) goto out_unlock; break; case MLX5_VF_LOAD_STATE_PREP_IMAGE: { u64 size = max(migf->record_size, migf->stop_copy_prep_size); if (vhca_buf->allocated_length < size) { mlx5vf_free_data_buffer(vhca_buf); migf->buf = mlx5vf_alloc_data_buffer(migf, size, DMA_TO_DEVICE); if (IS_ERR(migf->buf)) { ret = PTR_ERR(migf->buf); migf->buf = NULL; goto out_unlock; } vhca_buf = migf->buf; } vhca_buf->start_pos = migf->max_pos; migf->load_state = MLX5_VF_LOAD_STATE_READ_IMAGE; break; } case MLX5_VF_LOAD_STATE_READ_IMAGE_NO_HEADER: ret = mlx5vf_resume_read_image_no_header(vhca_buf, requested_length, &buf, &len, pos, &done); if (ret) goto out_unlock; break; case MLX5_VF_LOAD_STATE_READ_IMAGE: ret = mlx5vf_resume_read_image(migf, vhca_buf, migf->record_size, &buf, &len, pos, &done, &has_work); if (ret) goto out_unlock; break; case MLX5_VF_LOAD_STATE_LOAD_IMAGE: ret = mlx5vf_cmd_load_vhca_state(migf->mvdev, migf, vhca_buf); if (ret) goto out_unlock; migf->load_state = MLX5_VF_LOAD_STATE_READ_HEADER; /* prep header buf for next image */ vhca_buf_header->length = 0; /* prep data buf for next image */ vhca_buf->length = 0; break; default: break; } } out_unlock: if (ret) migf->state = MLX5_MIGF_STATE_ERROR; mutex_unlock(&migf->lock); mlx5vf_state_mutex_unlock(migf->mvdev); return ret ? ret : done; } static const struct file_operations mlx5vf_resume_fops = { .owner = THIS_MODULE, .write = mlx5vf_resume_write, .release = mlx5vf_release_file, .llseek = no_llseek, }; static struct mlx5_vf_migration_file * mlx5vf_pci_resume_device_data(struct mlx5vf_pci_core_device *mvdev) { struct mlx5_vf_migration_file *migf; struct mlx5_vhca_data_buffer *buf; int ret; migf = kzalloc(sizeof(*migf), GFP_KERNEL_ACCOUNT); if (!migf) return ERR_PTR(-ENOMEM); migf->filp = anon_inode_getfile("mlx5vf_mig", &mlx5vf_resume_fops, migf, O_WRONLY); if (IS_ERR(migf->filp)) { ret = PTR_ERR(migf->filp); goto end; } migf->mvdev = mvdev; ret = mlx5vf_cmd_alloc_pd(migf); if (ret) goto out_free; buf = mlx5vf_alloc_data_buffer(migf, 0, DMA_TO_DEVICE); if (IS_ERR(buf)) { ret = PTR_ERR(buf); goto out_pd; } migf->buf = buf; if (MLX5VF_PRE_COPY_SUPP(mvdev)) { buf = mlx5vf_alloc_data_buffer(migf, sizeof(struct mlx5_vf_migration_header), DMA_NONE); if (IS_ERR(buf)) { ret = PTR_ERR(buf); goto out_buf; } migf->buf_header = buf; migf->load_state = MLX5_VF_LOAD_STATE_READ_HEADER; } else { /* Initial state will be to read the image */ migf->load_state = MLX5_VF_LOAD_STATE_READ_IMAGE_NO_HEADER; } stream_open(migf->filp->f_inode, migf->filp); mutex_init(&migf->lock); INIT_LIST_HEAD(&migf->buf_list); INIT_LIST_HEAD(&migf->avail_list); spin_lock_init(&migf->list_lock); return migf; out_buf: mlx5vf_free_data_buffer(migf->buf); out_pd: mlx5vf_cmd_dealloc_pd(migf); out_free: fput(migf->filp); end: kfree(migf); return ERR_PTR(ret); } void mlx5vf_disable_fds(struct mlx5vf_pci_core_device *mvdev) { if (mvdev->resuming_migf) { mlx5vf_disable_fd(mvdev->resuming_migf); mlx5fv_cmd_clean_migf_resources(mvdev->resuming_migf); fput(mvdev->resuming_migf->filp); mvdev->resuming_migf = NULL; } if (mvdev->saving_migf) { mlx5_cmd_cleanup_async_ctx(&mvdev->saving_migf->async_ctx); cancel_work_sync(&mvdev->saving_migf->async_data.work); mlx5vf_disable_fd(mvdev->saving_migf); mlx5fv_cmd_clean_migf_resources(mvdev->saving_migf); fput(mvdev->saving_migf->filp); mvdev->saving_migf = NULL; } } static struct file * mlx5vf_pci_step_device_state_locked(struct mlx5vf_pci_core_device *mvdev, u32 new) { u32 cur = mvdev->mig_state; int ret; if (cur == VFIO_DEVICE_STATE_RUNNING_P2P && new == VFIO_DEVICE_STATE_STOP) { ret = mlx5vf_cmd_suspend_vhca(mvdev, MLX5_SUSPEND_VHCA_IN_OP_MOD_SUSPEND_RESPONDER); if (ret) return ERR_PTR(ret); return NULL; } if (cur == VFIO_DEVICE_STATE_STOP && new == VFIO_DEVICE_STATE_RUNNING_P2P) { ret = mlx5vf_cmd_resume_vhca(mvdev, MLX5_RESUME_VHCA_IN_OP_MOD_RESUME_RESPONDER); if (ret) return ERR_PTR(ret); return NULL; } if ((cur == VFIO_DEVICE_STATE_RUNNING && new == VFIO_DEVICE_STATE_RUNNING_P2P) || (cur == VFIO_DEVICE_STATE_PRE_COPY && new == VFIO_DEVICE_STATE_PRE_COPY_P2P)) { ret = mlx5vf_cmd_suspend_vhca(mvdev, MLX5_SUSPEND_VHCA_IN_OP_MOD_SUSPEND_INITIATOR); if (ret) return ERR_PTR(ret); return NULL; } if ((cur == VFIO_DEVICE_STATE_RUNNING_P2P && new == VFIO_DEVICE_STATE_RUNNING) || (cur == VFIO_DEVICE_STATE_PRE_COPY_P2P && new == VFIO_DEVICE_STATE_PRE_COPY)) { ret = mlx5vf_cmd_resume_vhca(mvdev, MLX5_RESUME_VHCA_IN_OP_MOD_RESUME_INITIATOR); if (ret) return ERR_PTR(ret); return NULL; } if (cur == VFIO_DEVICE_STATE_STOP && new == VFIO_DEVICE_STATE_STOP_COPY) { struct mlx5_vf_migration_file *migf; migf = mlx5vf_pci_save_device_data(mvdev, false); if (IS_ERR(migf)) return ERR_CAST(migf); get_file(migf->filp); mvdev->saving_migf = migf; return migf->filp; } if ((cur == VFIO_DEVICE_STATE_STOP_COPY && new == VFIO_DEVICE_STATE_STOP) || (cur == VFIO_DEVICE_STATE_PRE_COPY && new == VFIO_DEVICE_STATE_RUNNING) || (cur == VFIO_DEVICE_STATE_PRE_COPY_P2P && new == VFIO_DEVICE_STATE_RUNNING_P2P)) { mlx5vf_disable_fds(mvdev); return NULL; } if (cur == VFIO_DEVICE_STATE_STOP && new == VFIO_DEVICE_STATE_RESUMING) { struct mlx5_vf_migration_file *migf; migf = mlx5vf_pci_resume_device_data(mvdev); if (IS_ERR(migf)) return ERR_CAST(migf); get_file(migf->filp); mvdev->resuming_migf = migf; return migf->filp; } if (cur == VFIO_DEVICE_STATE_RESUMING && new == VFIO_DEVICE_STATE_STOP) { if (!MLX5VF_PRE_COPY_SUPP(mvdev)) { ret = mlx5vf_cmd_load_vhca_state(mvdev, mvdev->resuming_migf, mvdev->resuming_migf->buf); if (ret) return ERR_PTR(ret); } mlx5vf_disable_fds(mvdev); return NULL; } if ((cur == VFIO_DEVICE_STATE_RUNNING && new == VFIO_DEVICE_STATE_PRE_COPY) || (cur == VFIO_DEVICE_STATE_RUNNING_P2P && new == VFIO_DEVICE_STATE_PRE_COPY_P2P)) { struct mlx5_vf_migration_file *migf; migf = mlx5vf_pci_save_device_data(mvdev, true); if (IS_ERR(migf)) return ERR_CAST(migf); get_file(migf->filp); mvdev->saving_migf = migf; return migf->filp; } if (cur == VFIO_DEVICE_STATE_PRE_COPY_P2P && new == VFIO_DEVICE_STATE_STOP_COPY) { ret = mlx5vf_cmd_suspend_vhca(mvdev, MLX5_SUSPEND_VHCA_IN_OP_MOD_SUSPEND_RESPONDER); if (ret) return ERR_PTR(ret); ret = mlx5vf_pci_save_device_inc_data(mvdev); return ret ? ERR_PTR(ret) : NULL; } /* * vfio_mig_get_next_state() does not use arcs other than the above */ WARN_ON(true); return ERR_PTR(-EINVAL); } /* * This function is called in all state_mutex unlock cases to * handle a 'deferred_reset' if exists. */ void mlx5vf_state_mutex_unlock(struct mlx5vf_pci_core_device *mvdev) { again: spin_lock(&mvdev->reset_lock); if (mvdev->deferred_reset) { mvdev->deferred_reset = false; spin_unlock(&mvdev->reset_lock); mvdev->mig_state = VFIO_DEVICE_STATE_RUNNING; mlx5vf_disable_fds(mvdev); goto again; } mutex_unlock(&mvdev->state_mutex); spin_unlock(&mvdev->reset_lock); } static struct file * mlx5vf_pci_set_device_state(struct vfio_device *vdev, enum vfio_device_mig_state new_state) { struct mlx5vf_pci_core_device *mvdev = container_of( vdev, struct mlx5vf_pci_core_device, core_device.vdev); enum vfio_device_mig_state next_state; struct file *res = NULL; int ret; mutex_lock(&mvdev->state_mutex); while (new_state != mvdev->mig_state) { ret = vfio_mig_get_next_state(vdev, mvdev->mig_state, new_state, &next_state); if (ret) { res = ERR_PTR(ret); break; } res = mlx5vf_pci_step_device_state_locked(mvdev, next_state); if (IS_ERR(res)) break; mvdev->mig_state = next_state; if (WARN_ON(res && new_state != mvdev->mig_state)) { fput(res); res = ERR_PTR(-EINVAL); break; } } mlx5vf_state_mutex_unlock(mvdev); return res; } static int mlx5vf_pci_get_data_size(struct vfio_device *vdev, unsigned long *stop_copy_length) { struct mlx5vf_pci_core_device *mvdev = container_of( vdev, struct mlx5vf_pci_core_device, core_device.vdev); size_t state_size; int ret; mutex_lock(&mvdev->state_mutex); ret = mlx5vf_cmd_query_vhca_migration_state(mvdev, &state_size, 0); if (!ret) *stop_copy_length = state_size; mlx5vf_state_mutex_unlock(mvdev); return ret; } static int mlx5vf_pci_get_device_state(struct vfio_device *vdev, enum vfio_device_mig_state *curr_state) { struct mlx5vf_pci_core_device *mvdev = container_of( vdev, struct mlx5vf_pci_core_device, core_device.vdev); mutex_lock(&mvdev->state_mutex); *curr_state = mvdev->mig_state; mlx5vf_state_mutex_unlock(mvdev); return 0; } static void mlx5vf_pci_aer_reset_done(struct pci_dev *pdev) { struct mlx5vf_pci_core_device *mvdev = mlx5vf_drvdata(pdev); if (!mvdev->migrate_cap) return; /* * As the higher VFIO layers are holding locks across reset and using * those same locks with the mm_lock we need to prevent ABBA deadlock * with the state_mutex and mm_lock. * In case the state_mutex was taken already we defer the cleanup work * to the unlock flow of the other running context. */ spin_lock(&mvdev->reset_lock); mvdev->deferred_reset = true; if (!mutex_trylock(&mvdev->state_mutex)) { spin_unlock(&mvdev->reset_lock); return; } spin_unlock(&mvdev->reset_lock); mlx5vf_state_mutex_unlock(mvdev); } static int mlx5vf_pci_open_device(struct vfio_device *core_vdev) { struct mlx5vf_pci_core_device *mvdev = container_of( core_vdev, struct mlx5vf_pci_core_device, core_device.vdev); struct vfio_pci_core_device *vdev = &mvdev->core_device; int ret; ret = vfio_pci_core_enable(vdev); if (ret) return ret; if (mvdev->migrate_cap) mvdev->mig_state = VFIO_DEVICE_STATE_RUNNING; vfio_pci_core_finish_enable(vdev); return 0; } static void mlx5vf_pci_close_device(struct vfio_device *core_vdev) { struct mlx5vf_pci_core_device *mvdev = container_of( core_vdev, struct mlx5vf_pci_core_device, core_device.vdev); mlx5vf_cmd_close_migratable(mvdev); vfio_pci_core_close_device(core_vdev); } static const struct vfio_migration_ops mlx5vf_pci_mig_ops = { .migration_set_state = mlx5vf_pci_set_device_state, .migration_get_state = mlx5vf_pci_get_device_state, .migration_get_data_size = mlx5vf_pci_get_data_size, }; static const struct vfio_log_ops mlx5vf_pci_log_ops = { .log_start = mlx5vf_start_page_tracker, .log_stop = mlx5vf_stop_page_tracker, .log_read_and_clear = mlx5vf_tracker_read_and_clear, }; static int mlx5vf_pci_init_dev(struct vfio_device *core_vdev) { struct mlx5vf_pci_core_device *mvdev = container_of(core_vdev, struct mlx5vf_pci_core_device, core_device.vdev); int ret; ret = vfio_pci_core_init_dev(core_vdev); if (ret) return ret; mlx5vf_cmd_set_migratable(mvdev, &mlx5vf_pci_mig_ops, &mlx5vf_pci_log_ops); return 0; } static void mlx5vf_pci_release_dev(struct vfio_device *core_vdev) { struct mlx5vf_pci_core_device *mvdev = container_of(core_vdev, struct mlx5vf_pci_core_device, core_device.vdev); mlx5vf_cmd_remove_migratable(mvdev); vfio_pci_core_release_dev(core_vdev); } static const struct vfio_device_ops mlx5vf_pci_ops = { .name = "mlx5-vfio-pci", .init = mlx5vf_pci_init_dev, .release = mlx5vf_pci_release_dev, .open_device = mlx5vf_pci_open_device, .close_device = mlx5vf_pci_close_device, .ioctl = vfio_pci_core_ioctl, .device_feature = vfio_pci_core_ioctl_feature, .read = vfio_pci_core_read, .write = vfio_pci_core_write, .mmap = vfio_pci_core_mmap, .request = vfio_pci_core_request, .match = vfio_pci_core_match, .bind_iommufd = vfio_iommufd_physical_bind, .unbind_iommufd = vfio_iommufd_physical_unbind, .attach_ioas = vfio_iommufd_physical_attach_ioas, .detach_ioas = vfio_iommufd_physical_detach_ioas, }; static int mlx5vf_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct mlx5vf_pci_core_device *mvdev; int ret; mvdev = vfio_alloc_device(mlx5vf_pci_core_device, core_device.vdev, &pdev->dev, &mlx5vf_pci_ops); if (IS_ERR(mvdev)) return PTR_ERR(mvdev); dev_set_drvdata(&pdev->dev, &mvdev->core_device); ret = vfio_pci_core_register_device(&mvdev->core_device); if (ret) goto out_put_vdev; return 0; out_put_vdev: vfio_put_device(&mvdev->core_device.vdev); return ret; } static void mlx5vf_pci_remove(struct pci_dev *pdev) { struct mlx5vf_pci_core_device *mvdev = mlx5vf_drvdata(pdev); vfio_pci_core_unregister_device(&mvdev->core_device); vfio_put_device(&mvdev->core_device.vdev); } static const struct pci_device_id mlx5vf_pci_table[] = { { PCI_DRIVER_OVERRIDE_DEVICE_VFIO(PCI_VENDOR_ID_MELLANOX, 0x101e) }, /* ConnectX Family mlx5Gen Virtual Function */ {} }; MODULE_DEVICE_TABLE(pci, mlx5vf_pci_table); static const struct pci_error_handlers mlx5vf_err_handlers = { .reset_done = mlx5vf_pci_aer_reset_done, .error_detected = vfio_pci_core_aer_err_detected, }; static struct pci_driver mlx5vf_pci_driver = { .name = KBUILD_MODNAME, .id_table = mlx5vf_pci_table, .probe = mlx5vf_pci_probe, .remove = mlx5vf_pci_remove, .err_handler = &mlx5vf_err_handlers, .driver_managed_dma = true, }; module_pci_driver(mlx5vf_pci_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Max Gurtovoy "); MODULE_AUTHOR("Yishai Hadas "); MODULE_DESCRIPTION( "MLX5 VFIO PCI - User Level meta-driver for MLX5 device family");