blob: d423119ec44cee11b2741bdf7f8259770e551689 [file] [log] [blame]
#
# Copyright (c) 2023 Nutanix Inc. All rights reserved.
#
# Authors: Thanos Makatos <thanos@nutanix.com>
# William Henderson <william.henderson@nutanix.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of Nutanix nor the names of its contributors may be
# used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
#
from libvfio_user import *
from collections import deque
import ctypes
import errno
ctx = None
client = None
current_state = None # the current migration state on the server
path = [] # the server transition path (each transition appends the new state)
read_data = None
write_data = None
callbacks_errno = 0
STATES = {
VFIO_USER_DEVICE_STATE_STOP,
VFIO_USER_DEVICE_STATE_RUNNING,
VFIO_USER_DEVICE_STATE_STOP_COPY,
VFIO_USER_DEVICE_STATE_RESUMING,
VFIO_USER_DEVICE_STATE_PRE_COPY
}
UNREACHABLE_STATES = {
VFIO_USER_DEVICE_STATE_ERROR,
VFIO_USER_DEVICE_STATE_PRE_COPY_P2P,
VFIO_USER_DEVICE_STATE_RUNNING_P2P
}
VFU_TO_VFIO_MIGR_STATE = {
VFU_MIGR_STATE_STOP: VFIO_USER_DEVICE_STATE_STOP,
VFU_MIGR_STATE_RUNNING: VFIO_USER_DEVICE_STATE_RUNNING,
VFU_MIGR_STATE_STOP_AND_COPY: VFIO_USER_DEVICE_STATE_STOP_COPY,
VFU_MIGR_STATE_RESUME: VFIO_USER_DEVICE_STATE_RESUMING,
VFU_MIGR_STATE_PRE_COPY: VFIO_USER_DEVICE_STATE_PRE_COPY
}
# Set a very small maximum transfer size for later tests.
MAX_DATA_XFER_SIZE = 4
@transition_cb_t
def migr_trans_cb(_ctx, state):
global current_state, path
if callbacks_errno != 0:
set_real_errno(callbacks_errno)
return -1
if state in VFU_TO_VFIO_MIGR_STATE:
state = VFU_TO_VFIO_MIGR_STATE[state]
else:
assert False
current_state = state
path.append(state)
return 0
@read_data_cb_t
def migr_read_data_cb(_ctx, buf, count):
global read_data
if callbacks_errno != 0:
set_real_errno(callbacks_errno)
return -1
length = min(count, len(read_data))
ctypes.memmove(buf, read_data, length)
read_data = None
return length
@write_data_cb_t
def migr_write_data_cb(_ctx, buf, count):
global write_data
if callbacks_errno != 0:
set_real_errno(callbacks_errno)
return -1
write_data = bytes(count)
ctypes.memmove(write_data, buf, count)
return count
def setup_fail_callbacks(errno):
global callbacks_errno
callbacks_errno = errno
def teardown_fail_callbacks():
global callbacks_errno
callbacks_errno = 0
c.set_errno(0)
def teardown_function(function):
teardown_fail_callbacks()
def transition_to_migr_state(state, expect=0, rsp=True, busy=False):
return transition_to_state(ctx, client.sock, state, expect, rsp, busy)
def mig_data_payload(data):
argsz = len(vfio_user_mig_data()) + len(data)
return vfio_user_mig_data(
argsz=argsz,
size=len(data)
)
def test_migration_setup():
global ctx, client
ctx = vfu_create_ctx(flags=LIBVFIO_USER_FLAG_ATTACH_NB)
assert ctx is not None
cbs = vfu_migration_callbacks_t()
cbs.version = 1 # old callbacks version
cbs.transition = migr_trans_cb
cbs.read_data = migr_read_data_cb
cbs.write_data = migr_write_data_cb
ret = vfu_setup_device_migration_callbacks(ctx, cbs)
assert ret < 0, "do not allow old callbacks version"
cbs.version = VFU_MIGR_CALLBACKS_VERS # new callbacks version
ret = vfu_setup_device_migration_callbacks(ctx, cbs)
assert ret == 0
vfu_setup_device_quiesce_cb(ctx)
ret = vfu_realize_ctx(ctx)
assert ret == 0
caps = {
"capabilities": {
"max_data_xfer_size": MAX_DATA_XFER_SIZE,
}
}
client = connect_client(ctx, caps)
def server_transition_track_path(a, b, expectA=0, expectB=0):
"""
Carry out the state transition from a to b on the server, keeping track of
and returning the transition path taken.
"""
global path
if current_state == VFIO_USER_DEVICE_STATE_STOP_COPY and \
a == VFIO_USER_DEVICE_STATE_PRE_COPY:
# The transition STOP_COPY -> PRE_COPY is explicitly blocked so we
# advance one state to get around this in order to set up the test.
transition_to_migr_state(VFIO_USER_DEVICE_STATE_STOP)
transition_to_migr_state(a, expect=expectA)
if expectA != 0:
return None
path = []
transition_to_migr_state(b, expect=expectB)
return path.copy()
def test_migration_shortest_state_transition_paths():
"""
The spec dictates that complex state transitions are to be implemented as
combinations of the defined direct transitions, with the path selected
according to the following rules:
- Select the shortest path.
- The path cannot have saving group states as interior arcs, only start/end
states.
This test implements a breadth-first search to ensure that the paths taken
by the implementation correctly follow these rules.
"""
# allowed direct transitions (edges)
E = {
VFIO_USER_DEVICE_STATE_ERROR: set(),
VFIO_USER_DEVICE_STATE_STOP: {
VFIO_USER_DEVICE_STATE_RUNNING,
VFIO_USER_DEVICE_STATE_STOP_COPY,
VFIO_USER_DEVICE_STATE_RESUMING
},
VFIO_USER_DEVICE_STATE_RUNNING: {
VFIO_USER_DEVICE_STATE_STOP,
VFIO_USER_DEVICE_STATE_PRE_COPY
},
VFIO_USER_DEVICE_STATE_STOP_COPY: {VFIO_USER_DEVICE_STATE_STOP},
VFIO_USER_DEVICE_STATE_RESUMING: {VFIO_USER_DEVICE_STATE_STOP},
VFIO_USER_DEVICE_STATE_RUNNING_P2P: set(),
VFIO_USER_DEVICE_STATE_PRE_COPY: {
VFIO_USER_DEVICE_STATE_RUNNING,
VFIO_USER_DEVICE_STATE_STOP_COPY
},
VFIO_USER_DEVICE_STATE_PRE_COPY_P2P: set()
}
# states (vertices)
V = E.keys()
# "saving states" which cannot be internal arcs
saving_states = {VFIO_USER_DEVICE_STATE_PRE_COPY,
VFIO_USER_DEVICE_STATE_STOP_COPY}
# Consider each vertex in turn to be the start state, that is, the state
# we are transitioning from.
for source in V:
# The previous node in the shortest path for each node, e.g. for
# shortest path `source -> node -> target`, `back[node] == source`.
back = {v: None for v in V}
queue = deque([(source, None)])
# Use BFS to calculate the shortest path from the start state to every
# other state, following the rule that no intermediate states can be
# saving states.
while len(queue) > 0:
(curr, prev) = queue.popleft()
back[curr] = prev
# Intermediate states cannot be saving states, so if our current
# node is not the start state and it is a saving state, it is only
# allowed to be an end state so we don't explore its neighbours.
if curr != source and curr in saving_states:
continue
for nxt in E[curr]:
if back[nxt] is None:
queue.append((nxt, curr))
# Iterate over the states
for target in V:
if source == VFIO_USER_DEVICE_STATE_STOP_COPY \
and target == VFIO_USER_DEVICE_STATE_PRE_COPY:
# test for this transition being blocked in a separate test
continue
# If BFS found a path to that state, follow the backpointers to
# calculate the path, and check that it's equal to the path taken
# by the server.
if back[target] is not None:
seq = deque([])
curr = target
while curr != source:
seq.appendleft(curr)
curr = back[curr]
server_seq = server_transition_track_path(source, target)
assert len(seq) == len(server_seq)
assert all(seq[i] == server_seq[i] for i in range(len(seq)))
# If BFS couldn't find a path to that state, check that the server
# doesn't allow that transition either.
else:
# If the start state is an unreachable state, we won't be able
# to transition into it in order to try and calculate a path on
# the server, so we expect that transition to fail.
expectA = errno.EINVAL if source in UNREACHABLE_STATES else 0
# No matter what, we expect transitioning to the target state
# to fail.
server_transition_track_path(source, target, expectA=expectA,
expectB=errno.EINVAL)
def test_migration_stop_copy_to_pre_copy_rejected():
transition_to_migr_state(VFIO_USER_DEVICE_STATE_STOP_COPY)
transition_to_migr_state(VFIO_USER_DEVICE_STATE_PRE_COPY,
expect=errno.EINVAL)
def test_migration_nonexistent_state():
transition_to_migr_state(0xabcd, expect=errno.EINVAL)
def test_migration_failed_callback():
setup_fail_callbacks(0xbeef)
transition_to_migr_state(VFIO_USER_DEVICE_STATE_RUNNING, expect=0xbeef)
assert c.get_errno() == 0xbeef
teardown_fail_callbacks()
def test_migration_get_state():
transition_to_migr_state(VFIO_USER_DEVICE_STATE_RUNNING)
feature = vfio_user_device_feature(
argsz=len(vfio_user_device_feature()) +
len(vfio_user_device_feature_mig_state()),
flags=VFIO_DEVICE_FEATURE_GET | VFIO_DEVICE_FEATURE_MIG_DEVICE_STATE
)
result = msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, feature)
_, result = vfio_user_device_feature.pop_from_buffer(result)
state, _ = vfio_user_device_feature_mig_state.pop_from_buffer(result)
assert state.device_state == VFIO_USER_DEVICE_STATE_RUNNING
def test_handle_mig_data_read():
global read_data
transition_to_migr_state(VFIO_USER_DEVICE_STATE_RUNNING)
data = bytes([0, 1, 2, 3])
payload = mig_data_payload(data)
VALID_STATES = {VFIO_USER_DEVICE_STATE_PRE_COPY,
VFIO_USER_DEVICE_STATE_STOP_COPY}
for state in STATES:
transition_to_migr_state(state)
read_data = data
expect = 0 if state in VALID_STATES else errno.EINVAL
result = msg(ctx, client.sock, VFIO_USER_MIG_DATA_READ, payload,
expect=expect)
if state in VALID_STATES:
assert len(result) == len(payload) + len(data)
assert result[len(vfio_user_mig_data()):] == data
def test_handle_mig_data_read_too_long():
"""
When we set up the tests at the top of this file we specify that the max
data transfer size is 4 bytes. Here we test to check that a transfer of too
many bytes fails.
"""
global read_data
transition_to_migr_state(VFIO_USER_DEVICE_STATE_RUNNING)
transition_to_migr_state(VFIO_USER_DEVICE_STATE_PRE_COPY)
# Create a payload reading with length 1 byte longer than the max.
read_data = bytes([i for i in range(MAX_DATA_XFER_SIZE + 1)])
payload = mig_data_payload(read_data)
msg(ctx, client.sock, VFIO_USER_MIG_DATA_READ, payload,
expect=errno.EINVAL)
def test_handle_mig_data_read_failed_callback():
transition_to_migr_state(VFIO_USER_DEVICE_STATE_PRE_COPY)
read_data = bytes([1, 2, 3, 4])
payload = mig_data_payload(read_data)
setup_fail_callbacks(0xbeef)
msg(ctx, client.sock, VFIO_USER_MIG_DATA_READ, payload, expect=0xbeef)
assert c.get_errno() == 0xbeef
def test_handle_mig_data_read_short_write():
data = bytes([1, 2, 3, 4])
payload = bytes(mig_data_payload(data))
# don't send the last byte
msg(ctx, client.sock, VFIO_USER_MIG_DATA_READ, payload[:-1],
expect=errno.EINVAL)
def test_handle_mig_data_write():
data = bytes([1, 2, 3, 4])
payload = mig_data_payload(data)
transition_to_migr_state(VFIO_USER_DEVICE_STATE_RESUMING)
msg(ctx, client.sock, VFIO_USER_MIG_DATA_WRITE, bytes(payload) + data)
assert write_data == data
def test_handle_mig_data_write_invalid_state():
data = bytes([1, 2, 3, 4])
payload = mig_data_payload(data)
transition_to_migr_state(VFIO_USER_DEVICE_STATE_RUNNING)
msg(ctx, client.sock, VFIO_USER_MIG_DATA_WRITE, bytes(payload) + data,
expect=errno.EINVAL)
def test_handle_mig_data_write_too_long():
"""
When we set up the tests at the top of this file we specify that the max
data transfer size is 4 bytes. Here we test to check that a transfer of too
many bytes fails.
"""
# Create a payload writing with length 1 byte longer than the max.
data = bytes([i for i in range(MAX_DATA_XFER_SIZE + 1)])
payload = mig_data_payload(data)
transition_to_migr_state(VFIO_USER_DEVICE_STATE_RESUMING)
msg(ctx, client.sock, VFIO_USER_MIG_DATA_WRITE, bytes(payload) + data,
expect=errno.EINVAL)
def test_handle_mig_data_write_failed_callback():
transition_to_migr_state(VFIO_USER_DEVICE_STATE_RESUMING)
data = bytes([1, 2, 3, 4])
payload = mig_data_payload(data)
setup_fail_callbacks(0xbeef)
msg(ctx, client.sock, VFIO_USER_MIG_DATA_WRITE, bytes(payload) + data,
expect=0xbeef)
assert c.get_errno() == 0xbeef
def test_handle_mig_data_write_short_write():
data = bytes([1, 2, 3, 4])
payload = mig_data_payload(data)
msg(ctx, client.sock, VFIO_USER_MIG_DATA_WRITE, payload,
expect=errno.EINVAL)
def test_device_feature_migration_get():
payload = vfio_user_device_feature(
argsz=len(vfio_user_device_feature()) +
len(vfio_user_device_feature_migration()),
flags=VFIO_DEVICE_FEATURE_GET | VFIO_DEVICE_FEATURE_MIGRATION
)
result = msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, payload)
_, result = vfio_user_device_feature.pop_from_buffer(result)
flags, _ = vfio_user_device_feature_migration.pop_from_buffer(result)
flags = flags.flags
assert flags == VFIO_MIGRATION_STOP_COPY | VFIO_MIGRATION_PRE_COPY
def test_device_feature_short_write():
payload = vfio_user_device_feature(
argsz=len(vfio_user_device_feature()) +
len(vfio_user_device_feature_migration()),
flags=VFIO_DEVICE_FEATURE_GET | VFIO_DEVICE_FEATURE_MIGRATION
)
payload = bytes(payload)
# don't send the last byte
msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, payload[:-1],
expect=errno.EINVAL)
def test_device_feature_unsupported_operation():
payload = vfio_user_device_feature(
argsz=len(vfio_user_device_feature()) +
len(vfio_user_device_feature_migration()),
flags=VFIO_DEVICE_FEATURE_SET | VFIO_DEVICE_FEATURE_MIGRATION
)
msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, payload,
expect=errno.EINVAL)
def test_device_feature_bad_argsz_probe():
payload = vfio_user_device_feature(
argsz=2,
flags=VFIO_DEVICE_FEATURE_PROBE | VFIO_DEVICE_FEATURE_MIGRATION
)
msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, payload,
expect=errno.EINVAL)
def test_device_feature_bad_argsz_get_migration():
payload = vfio_user_device_feature(
argsz=len(vfio_user_device_feature()),
flags=VFIO_DEVICE_FEATURE_GET | VFIO_DEVICE_FEATURE_MIGRATION
)
msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, payload,
expect=errno.EINVAL)
def test_device_feature_bad_argsz_get_dma():
argsz = len(vfio_user_device_feature()) + \
len(vfio_user_device_feature_dma_logging_report()) + \
get_bitmap_size(0x20 << PAGE_SHIFT, PAGE_SIZE)
feature = vfio_user_device_feature(
argsz=argsz - 1, # not big enough
flags=VFIO_DEVICE_FEATURE_DMA_LOGGING_REPORT | VFIO_DEVICE_FEATURE_GET
)
report = vfio_user_device_feature_dma_logging_report(
iova=0x10 << PAGE_SHIFT,
length=0x20 << PAGE_SHIFT,
page_size=PAGE_SIZE
)
msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, bytes(feature)
+ bytes(report), expect=errno.EINVAL)
def test_device_feature_bad_argsz_set():
feature = vfio_user_device_feature(
argsz=len(vfio_user_device_feature()), # no space for state data
flags=VFIO_DEVICE_FEATURE_SET | VFIO_DEVICE_FEATURE_MIG_DEVICE_STATE
)
payload = vfio_user_device_feature_mig_state(
device_state=VFIO_USER_DEVICE_STATE_RUNNING
)
msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, bytes(feature)
+ bytes(payload), expect=errno.EINVAL)
def test_device_feature_probe():
payload = vfio_user_device_feature(
argsz=len(vfio_user_device_feature()),
flags=VFIO_DEVICE_FEATURE_PROBE | VFIO_DEVICE_FEATURE_MIGRATION
)
result = msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, payload)
assert bytes(payload) == result
payload = vfio_user_device_feature(
argsz=len(vfio_user_device_feature()),
flags=VFIO_DEVICE_FEATURE_PROBE | VFIO_DEVICE_FEATURE_SET |
VFIO_DEVICE_FEATURE_GET | VFIO_DEVICE_FEATURE_MIGRATION
)
msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, payload,
expect=errno.EINVAL)
def test_migration_cleanup():
client.disconnect(ctx)
vfu_destroy_ctx(ctx)
# ex: set tabstop=4 shiftwidth=4 softtabstop=4 expandtab: #