From: Bobby Eshleman <[email protected]> Add six tests covering basic cases and hopefully most edge cases: normal transfer in both directions, namespaces being deleted, device movement between namespaces mid-socket-lifetime.
The namespaces are created with "unshare -n" and held open by a sleeping process rather than by ip netns because bind-mounting namespaces via ip netns is incompatible with the guest 9p rootfs. Signed-off-by: Bobby Eshleman <[email protected]> --- tools/testing/selftests/vsock/vmtest.sh | 404 +++++++++++++++++++++++++++++++- 1 file changed, 394 insertions(+), 10 deletions(-) diff --git a/tools/testing/selftests/vsock/vmtest.sh b/tools/testing/selftests/vsock/vmtest.sh index 310dfc2a39ad..53591fa07f1a 100755 --- a/tools/testing/selftests/vsock/vmtest.sh +++ b/tools/testing/selftests/vsock/vmtest.sh @@ -17,6 +17,7 @@ readonly KERNEL_CHECKOUT=$(realpath "${SCRIPT_DIR}"/../../../../) source "${SCRIPT_DIR}"/../kselftest/ktap_helpers.sh readonly VSOCK_TEST="${SCRIPT_DIR}"/vsock_test +readonly VSOCK_ASSIGN_G2H_NETNS="${SCRIPT_DIR}"/vsock_assign_g2h_netns readonly TEST_GUEST_PORT=51000 readonly TEST_HOST_PORT=50000 readonly TEST_HOST_PORT_LISTENER=50001 @@ -73,6 +74,12 @@ readonly TEST_NAMES=( ns_delete_vm_ok ns_delete_host_ok ns_delete_both_ok + ns_guest_local_connect_to_host_fails + ns_guest_assign_g2h_netns_connect_to_host_ok + ns_guest_assign_g2h_netns_init_ns_connect_fails + ns_guest_assign_g2h_netns_host_connect_ok + ns_guest_assign_g2h_netns_reset_on_ns_delete_ok + ns_guest_assign_g2h_netns_old_conn_send_fails ) readonly TEST_DESCS=( # vm_server_host_client @@ -149,12 +156,36 @@ readonly TEST_DESCS=( # ns_delete_both_ok "Check that deleting the VM and host's namespaces does not break the socket connection" + + # ns_guest_local_connect_to_host_fails + "Check a guest process in a local ns cannot reach the host without the assign ioctl." + + # ns_guest_assign_g2h_netns_connect_to_host_ok + "Check a guest process in a local ns reaches the host once the vsock device is assigned to it." + + # ns_guest_assign_g2h_netns_init_ns_connect_fails + "Check the guest's initial ns loses vsock once the device is assigned to another ns." + + # ns_guest_assign_g2h_netns_host_connect_ok + "Check the host reaches a guest listener in the ns the vsock device is assigned to." + + # ns_guest_assign_g2h_netns_reset_on_ns_delete_ok + "Check the guest's vsock device returns to the initial ns when its ns is deleted." + + # ns_guest_assign_g2h_netns_old_conn_send_fails + "Check connections made before the assign stop sending once they lose the device." ) readonly USE_SHARED_VM=( vm_server_host_client vm_client_host_server vm_loopback + ns_guest_local_connect_to_host_fails + ns_guest_assign_g2h_netns_connect_to_host_ok + ns_guest_assign_g2h_netns_init_ns_connect_fails + ns_guest_assign_g2h_netns_host_connect_ok + ns_guest_assign_g2h_netns_reset_on_ns_delete_ok + ns_guest_assign_g2h_netns_old_conn_send_fails ) readonly NS_MODES=("local" "global") @@ -302,18 +333,20 @@ check_args() { } check_deps() { - for dep in vng ${QEMU} busybox pkill ssh ss socat nsenter; do + for dep in vng ${QEMU} busybox pkill ssh ss socat nsenter unshare; do if [[ ! -x $(command -v "${dep}") ]]; then echo -e "skip: dependency ${dep} not found!\n" exit "${KSFT_SKIP}" fi done - if [[ ! -x $(command -v "${VSOCK_TEST}") ]]; then - printf "skip: %s not found!" "${VSOCK_TEST}" - printf " Please build the kselftest vsock target.\n" - exit "${KSFT_SKIP}" - fi + for prog in "${VSOCK_TEST}" "${VSOCK_ASSIGN_G2H_NETNS}"; do + if [[ ! -x $(command -v "${prog}") ]]; then + printf "skip: %s not found!" "${prog}" + printf " Please build the kselftest vsock target.\n" + exit "${KSFT_SKIP}" + fi + done } check_netns() { @@ -401,6 +434,7 @@ setup_home() { mkdir -p "$(dirname "${SSH_KEY_PATH}")" ssh-keygen -t ed25519 -f "${SSH_KEY_PATH}" -N "" -q cp "${VSOCK_TEST}" "${TEST_HOME}"/vsock_test + cp "${VSOCK_ASSIGN_G2H_NETNS}" "${TEST_HOME}"/vsock_assign_g2h_netns } create_pidfile() { @@ -528,6 +562,58 @@ vm_wait_for_ssh() { done } +# Create a local mode namespace in the VM and echo the pid holding it open. +vm_ns_start() { + local ns=$1 + + vm_ssh "${ns}" -- \ + "echo local > /proc/sys/net/vsock/child_ns_mode" &>/dev/null + + vm_ssh "${ns}" -- "unshare -n sleep infinity" \ + '>/dev/null 2>&1 & echo $!' +} + +# Returns once the holder is gone, so that the namespace is unreferenced and +# the kernel can start tearing it down. +vm_ns_stop() { + local ns=$1 + local nspid=$2 + + vm_ssh "${ns}" <<-EOF &>/dev/null + kill ${nspid} + for ((i = 0; i < ${WAIT_PERIOD_MAX}; i++)); do + kill -0 ${nspid} 2>/dev/null || break + sleep 1 + done + EOF +} + +# Runs in the guest's initial namespace when <nspid> is empty. The command must +# not contain single quotes. +vm_ns_exec() { + local ns=$1 + local nspid=$2 + local cmd=$3 + + if [[ -z "${nspid}" ]]; then + vm_ssh "${ns}" -- "${cmd}" + return + fi + + vm_ssh "${ns}" -- nsenter -t "${nspid}" -n sh -c "'${cmd}'" +} + +vm_ns_assign_g2h() { + local ns=$1 + local nspid=$2 + + vm_ns_exec "${ns}" "${nspid}" ./vsock_assign_g2h_netns +} + +vm_reset_g2h() { + vm_ns_assign_g2h "init_ns" "" &>/dev/null +} + # derived from selftests/net/net_helper.sh wait_for_listener() { @@ -564,17 +650,31 @@ wait_for_listener() done } -vm_wait_for_listener() { +# Runs in the guest's initial namespace when <nspid> is empty. +vm_ns_wait_for_listener() { local ns=$1 - local port=$2 - local protocol=$3 + local nspid=$2 + local port=$3 + local protocol=$4 + local nsenter= + + [[ -n "${nspid}" ]] && nsenter="nsenter -t ${nspid} -n" vm_ssh "${ns}" <<EOF $(declare -f wait_for_listener) -wait_for_listener ${port} ${WAIT_PERIOD} ${WAIT_PERIOD_MAX} ${protocol} +export -f wait_for_listener +${nsenter} bash -c "wait_for_listener ${port} ${WAIT_PERIOD} ${WAIT_PERIOD_MAX} ${protocol}" EOF } +vm_wait_for_listener() { + local ns=$1 + local port=$2 + local protocol=$3 + + vm_ns_wait_for_listener "${ns}" "" "${port}" "${protocol}" +} + host_wait_for_listener() { local ns=$1 local port=$2 @@ -1421,6 +1521,290 @@ test_ns_delete_both_ok() { check_ns_delete_doesnt_break_connection "both" } +# Send a string from the guest to a host listener and leave what the host +# received in <outfile>. +guest_send_to_host() { + local ns=$1 + local nspid=$2 + local port=$3 + local outfile=$4 + local cmd="echo TEST | socat -u STDIN VSOCK-CONNECT:2:${port}" + local pid + + socat -u VSOCK-LISTEN:"${port}" STDOUT > "${outfile}" 2>/dev/null & + pid=$! + host_wait_for_listener "${ns}" "${port}" "vsock" + + vm_ns_exec "${ns}" "${nspid}" "${cmd}" 2>/dev/null + + timeout "${WAIT_PERIOD}" \ + bash -c 'while [[ ! -s '"${outfile}"' ]]; do sleep 1; done' + + terminate_pids "${pid}" +} + +# Send a string from the host to a listener in the guest and leave what the +# guest received in <outfile>. +host_send_to_guest() { + local ns=$1 + local nspid=$2 + local port=$3 + local outfile=$4 + local cmd="socat -u VSOCK-LISTEN:${port} STDOUT" + local dst="VSOCK-CONNECT:${VSOCK_CID}:${port}" + local pid + + vm_ns_exec "${ns}" "${nspid}" "${cmd}" > "${outfile}" 2>/dev/null & + pid=$! + vm_ns_wait_for_listener "${ns}" "${nspid}" "${port}" "vsock" + + echo TEST | socat -u STDIN "${dst}" 2>/dev/null + + timeout "${WAIT_PERIOD}" \ + bash -c 'while [[ ! -s '"${outfile}"' ]]; do sleep 1; done' + + terminate_pids "${pid}" +} + +test_ns_guest_assign_g2h_netns_old_conn_send_fails() { + local gap=$(( WAIT_PERIOD * 3 )) + local port=12346 + local outfile + local result + local nspid + local pid + + nspid=$(vm_ns_start "init_ns") + if [[ -z "${nspid}" ]]; then + log_host "failed to create a namespace inside the guest" + return "${KSFT_FAIL}" + fi + + outfile=$(mktemp) + socat -u VSOCK-LISTEN:"${port}" STDOUT > "${outfile}" 2>/dev/null & + pid=$! + host_wait_for_listener "init_ns" "${port}" "vsock" + + # Send a message, wait, then send another. While waiting, assign the + # device to a namespace. Confirm the second message does not arrive. + vm_ssh "init_ns" -- \ + "(echo FIRST; sleep ${gap}; echo SECOND) |" \ + "socat -u STDIN VSOCK-CONNECT:2:${port}" &>/dev/null & + + sleep "${WAIT_PERIOD}" + + if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then + log_host "failed to assign the vsock device to the guest ns" + terminate_pids "${pid}" + rm -f "${outfile}" + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + return "${KSFT_FAIL}" + fi + + # Let the second write happen and land, if it is going to. + sleep $(( gap + WAIT_PERIOD )) + + terminate_pids "${pid}" + result=$(cat "${outfile}") + rm -f "${outfile}" + + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + + if [[ "${result}" != *FIRST* ]]; then + log_host "connection did not work before the assign: [${result}]" + return "${KSFT_FAIL}" + fi + + if [[ "${result}" == *SECOND* ]]; then + log_host "old connection still delivered after the assign" + return "${KSFT_FAIL}" + fi + + return "${KSFT_PASS}" +} + +test_ns_guest_local_connect_to_host_fails() { + local port=12345 + local outfile + local result + local nspid + + nspid=$(vm_ns_start "init_ns") + if [[ -z "${nspid}" ]]; then + log_host "failed to create a namespace inside the guest" + return "${KSFT_FAIL}" + fi + + outfile=$(mktemp) + guest_send_to_host "init_ns" "${nspid}" "${port}" "${outfile}" + + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + + result=$(cat "${outfile}") + rm -f "${outfile}" + + if [[ "${result}" == TEST ]]; then + return "${KSFT_FAIL}" + fi + + return "${KSFT_PASS}" +} + +test_ns_guest_assign_g2h_netns_connect_to_host_ok() { + local port=12345 + local outfile + local result + local nspid + + nspid=$(vm_ns_start "init_ns") + if [[ -z "${nspid}" ]]; then + log_host "failed to create a namespace inside the guest" + return "${KSFT_FAIL}" + fi + + if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then + log_host "failed to assign the vsock device to the guest ns" + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + return "${KSFT_FAIL}" + fi + + outfile=$(mktemp) + guest_send_to_host "init_ns" "${nspid}" "${port}" "${outfile}" + + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + + result=$(cat "${outfile}") + rm -f "${outfile}" + + if [[ "${result}" != TEST ]]; then + return "${KSFT_FAIL}" + fi + + return "${KSFT_PASS}" +} + +test_ns_guest_assign_g2h_netns_init_ns_connect_fails() { + local port=12345 + local outfile + local result + local nspid + + nspid=$(vm_ns_start "init_ns") + if [[ -z "${nspid}" ]]; then + log_host "failed to create a namespace inside the guest" + return "${KSFT_FAIL}" + fi + + if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then + log_host "failed to assign the vsock device to the guest ns" + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + return "${KSFT_FAIL}" + fi + + # The device now belongs to a local-mode namespace, so the guest's + # initial namespace must no longer reach the host. + outfile=$(mktemp) + guest_send_to_host "init_ns" "" "${port}" "${outfile}" + + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + + result=$(cat "${outfile}") + rm -f "${outfile}" + + if [[ "${result}" == TEST ]]; then + return "${KSFT_FAIL}" + fi + + return "${KSFT_PASS}" +} + +test_ns_guest_assign_g2h_netns_host_connect_ok() { + local port=12345 + local outfile + local result + local nspid + + nspid=$(vm_ns_start "init_ns") + if [[ -z "${nspid}" ]]; then + log_host "failed to create a namespace inside the guest" + return "${KSFT_FAIL}" + fi + + if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then + log_host "failed to assign the vsock device to the guest ns" + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + return "${KSFT_FAIL}" + fi + + outfile=$(mktemp) + host_send_to_guest "init_ns" "${nspid}" "${port}" "${outfile}" + + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + + result=$(cat "${outfile}") + rm -f "${outfile}" + + if [[ "${result}" != TEST ]]; then + return "${KSFT_FAIL}" + fi + + return "${KSFT_PASS}" +} + +test_ns_guest_assign_g2h_netns_reset_on_ns_delete_ok() { + local port=12345 + local outfile + local result + local nspid + local i + + nspid=$(vm_ns_start "init_ns") + if [[ -z "${nspid}" ]]; then + log_host "failed to create a namespace inside the guest" + return "${KSFT_FAIL}" + fi + + if ! vm_ns_assign_g2h "init_ns" "${nspid}"; then + log_host "failed to assign the vsock device to the guest ns" + vm_ns_stop "init_ns" "${nspid}" + vm_reset_g2h + return "${KSFT_FAIL}" + fi + + vm_ns_stop "init_ns" "${nspid}" + + # The holder is gone, but the namespace itself is dismantled from a + # workqueue, so the device does not come back the same instant. Retry + # until it does, rather than expecting the first send to succeed. + outfile=$(mktemp) + for ((i = 0; i < 5; i++)); do + sleep "${WAIT_PERIOD}" + guest_send_to_host "init_ns" "" "$(( port + i ))" "${outfile}" + result=$(cat "${outfile}") + if [[ "${result}" == TEST ]]; then + break + fi + done + + rm -f "${outfile}" + vm_reset_g2h + + if [[ "${result}" != TEST ]]; then + return "${KSFT_FAIL}" + fi + + return "${KSFT_PASS}" +} + shared_vm_test() { local tname -- 2.53.0-Meta

