1#!/bin/bash 2# SPDX-License-Identifier: GPL-2.0 3# 4# Copyright (c) 2025 Meta Platforms, Inc. and affiliates 5# 6# Dependencies: 7# * virtme-ng 8# * busybox-static (used by virtme-ng) 9# * qemu (used by virtme-ng) 10# 11# shellcheck disable=SC2317,SC2119 12 13readonly SCRIPT_DIR="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" 14readonly KERNEL_CHECKOUT=$(realpath "${SCRIPT_DIR}"/../../../../) 15 16source "${SCRIPT_DIR}"/../kselftest/ktap_helpers.sh 17 18readonly VSOCK_TEST="${SCRIPT_DIR}"/vsock_test 19readonly TEST_GUEST_PORT=51000 20readonly TEST_HOST_PORT=50000 21readonly TEST_HOST_PORT_LISTENER=50001 22readonly SSH_GUEST_PORT=22 23readonly SSH_HOST_PORT=2222 24readonly VSOCK_CID=1234 25readonly WAIT_PERIOD=3 26readonly WAIT_PERIOD_MAX=60 27readonly WAIT_QEMU=5 28readonly PIDFILE_TEMPLATE=/tmp/vsock_vmtest_XXXX.pid 29declare -A PIDFILES 30 31# virtme-ng offers a netdev for ssh when using "--ssh", but we also need a 32# control port forwarded for vsock_test. Because virtme-ng doesn't support 33# adding an additional port to forward to the device created from "--ssh" and 34# virtme-init mistakenly sets identical IPs to the ssh device and additional 35# devices, we instead opt out of using --ssh, add the device manually, and also 36# add the kernel cmdline options that virtme-init uses to setup the interface. 37readonly QEMU_TEST_PORT_FWD="hostfwd=tcp::${TEST_HOST_PORT}-:${TEST_GUEST_PORT}" 38readonly QEMU_SSH_PORT_FWD="hostfwd=tcp::${SSH_HOST_PORT}-:${SSH_GUEST_PORT}" 39readonly KERNEL_CMDLINE="\ 40 virtme.dhcp net.ifnames=0 biosdevname=0 \ 41 virtme.ssh virtme_ssh_channel=tcp virtme_ssh_user=$USER \ 42" 43readonly LOG=$(mktemp /tmp/vsock_vmtest_XXXX.log) 44readonly TEST_NAMES=(vm_server_host_client vm_client_host_server vm_loopback) 45readonly TEST_DESCS=( 46 "Run vsock_test in server mode on the VM and in client mode on the host." 47 "Run vsock_test in client mode on the VM and in server mode on the host." 48 "Run vsock_test using the loopback transport in the VM." 49) 50 51readonly USE_SHARED_VM=(vm_server_host_client vm_client_host_server vm_loopback) 52readonly NS_MODES=("local" "global") 53 54VERBOSE=0 55 56usage() { 57 local name 58 local desc 59 local i 60 61 echo 62 echo "$0 [OPTIONS] [TEST]..." 63 echo "If no TEST argument is given, all tests will be run." 64 echo 65 echo "Options" 66 echo " -b: build the kernel from the current source tree and use it for guest VMs" 67 echo " -q: set the path to or name of qemu binary" 68 echo " -v: verbose output" 69 echo 70 echo "Available tests" 71 72 for ((i = 0; i < ${#TEST_NAMES[@]}; i++)); do 73 name=${TEST_NAMES[${i}]} 74 desc=${TEST_DESCS[${i}]} 75 printf "\t%-35s%-35s\n" "${name}" "${desc}" 76 done 77 echo 78 79 exit 1 80} 81 82die() { 83 echo "$*" >&2 84 exit "${KSFT_FAIL}" 85} 86 87check_result() { 88 local rc arg 89 90 rc=$1 91 arg=$2 92 93 cnt_total=$(( cnt_total + 1 )) 94 95 if [[ ${rc} -eq ${KSFT_PASS} ]]; then 96 cnt_pass=$(( cnt_pass + 1 )) 97 echo "ok ${cnt_total} ${arg}" 98 elif [[ ${rc} -eq ${KSFT_SKIP} ]]; then 99 cnt_skip=$(( cnt_skip + 1 )) 100 echo "ok ${cnt_total} ${arg} # SKIP" 101 elif [[ ${rc} -eq ${KSFT_FAIL} ]]; then 102 cnt_fail=$(( cnt_fail + 1 )) 103 echo "not ok ${cnt_total} ${arg} # exit=${rc}" 104 fi 105} 106 107add_namespaces() { 108 local orig_mode 109 orig_mode=$(cat /proc/sys/net/vsock/child_ns_mode) 110 111 for mode in "${NS_MODES[@]}"; do 112 echo "${mode}" > /proc/sys/net/vsock/child_ns_mode 113 ip netns add "${mode}0" 2>/dev/null 114 ip netns add "${mode}1" 2>/dev/null 115 done 116 117 echo "${orig_mode}" > /proc/sys/net/vsock/child_ns_mode 118} 119 120init_namespaces() { 121 for mode in "${NS_MODES[@]}"; do 122 # we need lo for qemu port forwarding 123 ip netns exec "${mode}0" ip link set dev lo up 124 ip netns exec "${mode}1" ip link set dev lo up 125 done 126} 127 128del_namespaces() { 129 for mode in "${NS_MODES[@]}"; do 130 ip netns del "${mode}0" &>/dev/null 131 ip netns del "${mode}1" &>/dev/null 132 log_host "removed ns ${mode}0" 133 log_host "removed ns ${mode}1" 134 done 135} 136 137vm_ssh() { 138 local ns_exec 139 140 if [[ "${1}" == init_ns ]]; then 141 ns_exec="" 142 else 143 ns_exec="ip netns exec ${1}" 144 fi 145 146 shift 147 148 ${ns_exec} ssh -q -o UserKnownHostsFile=/dev/null -p "${SSH_HOST_PORT}" localhost "$@" 149 150 return $? 151} 152 153cleanup() { 154 terminate_pidfiles "${!PIDFILES[@]}" 155 del_namespaces 156} 157 158check_args() { 159 local found 160 161 for arg in "$@"; do 162 found=0 163 for name in "${TEST_NAMES[@]}"; do 164 if [[ "${name}" = "${arg}" ]]; then 165 found=1 166 break 167 fi 168 done 169 170 if [[ "${found}" -eq 0 ]]; then 171 echo "${arg} is not an available test" >&2 172 usage 173 fi 174 done 175 176 for arg in "$@"; do 177 if ! command -v > /dev/null "test_${arg}"; then 178 echo "Test ${arg} not found" >&2 179 usage 180 fi 181 done 182} 183 184check_deps() { 185 for dep in vng ${QEMU} busybox pkill ssh ss; do 186 if [[ ! -x $(command -v "${dep}") ]]; then 187 echo -e "skip: dependency ${dep} not found!\n" 188 exit "${KSFT_SKIP}" 189 fi 190 done 191 192 if [[ ! -x $(command -v "${VSOCK_TEST}") ]]; then 193 printf "skip: %s not found!" "${VSOCK_TEST}" 194 printf " Please build the kselftest vsock target.\n" 195 exit "${KSFT_SKIP}" 196 fi 197} 198 199check_vng() { 200 local tested_versions 201 local version 202 local ok 203 204 tested_versions=("1.33" "1.36" "1.37") 205 version="$(vng --version)" 206 207 ok=0 208 for tv in "${tested_versions[@]}"; do 209 if [[ "${version}" == *"${tv}"* ]]; then 210 ok=1 211 break 212 fi 213 done 214 215 if [[ ! "${ok}" -eq 1 ]]; then 216 printf "warning: vng version '%s' has not been tested and may " "${version}" >&2 217 printf "not function properly.\n\tThe following versions have been tested: " >&2 218 echo "${tested_versions[@]}" >&2 219 fi 220} 221 222handle_build() { 223 if [[ ! "${BUILD}" -eq 1 ]]; then 224 return 225 fi 226 227 if [[ ! -d "${KERNEL_CHECKOUT}" ]]; then 228 echo "-b requires vmtest.sh called from the kernel source tree" >&2 229 exit 1 230 fi 231 232 pushd "${KERNEL_CHECKOUT}" &>/dev/null 233 234 if ! vng --kconfig --config "${SCRIPT_DIR}"/config; then 235 die "failed to generate .config for kernel source tree (${KERNEL_CHECKOUT})" 236 fi 237 238 if ! make -j$(nproc); then 239 die "failed to build kernel from source tree (${KERNEL_CHECKOUT})" 240 fi 241 242 popd &>/dev/null 243} 244 245create_pidfile() { 246 local pidfile 247 248 pidfile=$(mktemp "${PIDFILE_TEMPLATE}") 249 PIDFILES["${pidfile}"]=1 250 251 echo "${pidfile}" 252} 253 254terminate_pidfiles() { 255 local pidfile 256 257 for pidfile in "$@"; do 258 if [[ -s "${pidfile}" ]]; then 259 pkill -SIGTERM -F "${pidfile}" > /dev/null 2>&1 260 fi 261 262 if [[ -e "${pidfile}" ]]; then 263 rm -f "${pidfile}" 264 fi 265 266 unset "PIDFILES[${pidfile}]" 267 done 268} 269 270vm_start() { 271 local pidfile=$1 272 local ns=$2 273 local logfile=/dev/null 274 local verbose_opt="" 275 local kernel_opt="" 276 local qemu_opts="" 277 local ns_exec="" 278 local qemu 279 280 qemu=$(command -v "${QEMU}") 281 282 if [[ "${VERBOSE}" -eq 1 ]]; then 283 verbose_opt="--verbose" 284 logfile=/dev/stdout 285 fi 286 287 qemu_opts="\ 288 -netdev user,id=n0,${QEMU_TEST_PORT_FWD},${QEMU_SSH_PORT_FWD} \ 289 -device virtio-net-pci,netdev=n0 \ 290 -device vhost-vsock-pci,guest-cid=${VSOCK_CID} \ 291 --pidfile ${pidfile} 292 " 293 294 if [[ "${BUILD}" -eq 1 ]]; then 295 kernel_opt="${KERNEL_CHECKOUT}" 296 fi 297 298 if [[ "${ns}" != "init_ns" ]]; then 299 ns_exec="ip netns exec ${ns}" 300 fi 301 302 ${ns_exec} vng \ 303 --run \ 304 ${kernel_opt} \ 305 ${verbose_opt} \ 306 --qemu-opts="${qemu_opts}" \ 307 --qemu="${qemu}" \ 308 --user root \ 309 --append "${KERNEL_CMDLINE}" \ 310 --rw &> ${logfile} & 311 312 timeout "${WAIT_QEMU}" \ 313 bash -c 'while [[ ! -s '"${pidfile}"' ]]; do sleep 1; done; exit 0' 314} 315 316vm_wait_for_ssh() { 317 local ns=$1 318 local i 319 320 i=0 321 while true; do 322 if [[ ${i} -gt ${WAIT_PERIOD_MAX} ]]; then 323 die "Timed out waiting for guest ssh" 324 fi 325 326 if vm_ssh "${ns}" -- true; then 327 break 328 fi 329 i=$(( i + 1 )) 330 sleep ${WAIT_PERIOD} 331 done 332} 333 334# derived from selftests/net/net_helper.sh 335wait_for_listener() 336{ 337 local port=$1 338 local interval=$2 339 local max_intervals=$3 340 local protocol=$4 341 local i 342 343 for i in $(seq "${max_intervals}"); do 344 case "${protocol}" in 345 tcp) 346 if ss --listening --tcp --numeric | grep -q ":${port} "; then 347 break 348 fi 349 ;; 350 vsock) 351 if ss --listening --vsock --numeric | grep -q ":${port} "; then 352 break 353 fi 354 ;; 355 unix) 356 # For unix sockets, port is actually the socket path 357 if ss --listening --unix | grep -q "${port}"; then 358 break 359 fi 360 ;; 361 *) 362 echo "Unknown protocol: ${protocol}" >&2 363 break 364 ;; 365 esac 366 sleep "${interval}" 367 done 368} 369 370vm_wait_for_listener() { 371 local ns=$1 372 local port=$2 373 local protocol=$3 374 375 vm_ssh "${ns}" <<EOF 376$(declare -f wait_for_listener) 377wait_for_listener ${port} ${WAIT_PERIOD} ${WAIT_PERIOD_MAX} ${protocol} 378EOF 379} 380 381host_wait_for_listener() { 382 local ns=$1 383 local port=$2 384 local protocol=$3 385 386 if [[ "${ns}" == "init_ns" ]]; then 387 wait_for_listener "${port}" "${WAIT_PERIOD}" "${WAIT_PERIOD_MAX}" "${protocol}" 388 else 389 ip netns exec "${ns}" bash <<-EOF 390 $(declare -f wait_for_listener) 391 wait_for_listener ${port} ${WAIT_PERIOD} ${WAIT_PERIOD_MAX} ${protocol} 392 EOF 393 fi 394} 395 396vm_dmesg_oops_count() { 397 local ns=$1 398 399 vm_ssh "${ns}" -- dmesg 2>/dev/null | grep -c -i 'Oops' 400} 401 402vm_dmesg_warn_count() { 403 local ns=$1 404 405 vm_ssh "${ns}" -- dmesg --level=warn 2>/dev/null | grep -c -i 'vsock' 406} 407 408vm_vsock_test() { 409 local ns=$1 410 local host=$2 411 local cid=$3 412 local port=$4 413 local rc 414 415 # log output and use pipefail to respect vsock_test errors 416 set -o pipefail 417 if [[ "${host}" != server ]]; then 418 vm_ssh "${ns}" -- "${VSOCK_TEST}" \ 419 --mode=client \ 420 --control-host="${host}" \ 421 --peer-cid="${cid}" \ 422 --control-port="${port}" \ 423 2>&1 | log_guest 424 rc=$? 425 else 426 vm_ssh "${ns}" -- "${VSOCK_TEST}" \ 427 --mode=server \ 428 --peer-cid="${cid}" \ 429 --control-port="${port}" \ 430 2>&1 | log_guest & 431 rc=$? 432 433 if [[ $rc -ne 0 ]]; then 434 set +o pipefail 435 return $rc 436 fi 437 438 vm_wait_for_listener "${ns}" "${port}" "tcp" 439 rc=$? 440 fi 441 set +o pipefail 442 443 return $rc 444} 445 446host_vsock_test() { 447 local ns=$1 448 local host=$2 449 local cid=$3 450 local port=$4 451 local rc 452 453 local cmd="${VSOCK_TEST}" 454 if [[ "${ns}" != "init_ns" ]]; then 455 cmd="ip netns exec ${ns} ${cmd}" 456 fi 457 458 # log output and use pipefail to respect vsock_test errors 459 set -o pipefail 460 if [[ "${host}" != server ]]; then 461 ${cmd} \ 462 --mode=client \ 463 --peer-cid="${cid}" \ 464 --control-host="${host}" \ 465 --control-port="${port}" 2>&1 | log_host 466 rc=$? 467 else 468 ${cmd} \ 469 --mode=server \ 470 --peer-cid="${cid}" \ 471 --control-port="${port}" 2>&1 | log_host & 472 rc=$? 473 474 if [[ $rc -ne 0 ]]; then 475 set +o pipefail 476 return $rc 477 fi 478 479 host_wait_for_listener "${ns}" "${port}" "tcp" 480 rc=$? 481 fi 482 set +o pipefail 483 484 return $rc 485} 486 487log() { 488 local redirect 489 local prefix 490 491 if [[ ${VERBOSE} -eq 0 ]]; then 492 redirect=/dev/null 493 else 494 redirect=/dev/stdout 495 fi 496 497 prefix="${LOG_PREFIX:-}" 498 499 if [[ "$#" -eq 0 ]]; then 500 if [[ -n "${prefix}" ]]; then 501 awk -v prefix="${prefix}" '{printf "%s: %s\n", prefix, $0}' 502 else 503 cat 504 fi 505 else 506 if [[ -n "${prefix}" ]]; then 507 echo "${prefix}: " "$@" 508 else 509 echo "$@" 510 fi 511 fi | tee -a "${LOG}" > "${redirect}" 512} 513 514log_host() { 515 LOG_PREFIX=host log "$@" 516} 517 518log_guest() { 519 LOG_PREFIX=guest log "$@" 520} 521 522test_vm_server_host_client() { 523 if ! vm_vsock_test "init_ns" "server" 2 "${TEST_GUEST_PORT}"; then 524 return "${KSFT_FAIL}" 525 fi 526 527 if ! host_vsock_test "init_ns" "127.0.0.1" "${VSOCK_CID}" "${TEST_HOST_PORT}"; then 528 return "${KSFT_FAIL}" 529 fi 530 531 return "${KSFT_PASS}" 532} 533 534test_vm_client_host_server() { 535 if ! host_vsock_test "init_ns" "server" "${VSOCK_CID}" "${TEST_HOST_PORT_LISTENER}"; then 536 return "${KSFT_FAIL}" 537 fi 538 539 if ! vm_vsock_test "init_ns" "10.0.2.2" 2 "${TEST_HOST_PORT_LISTENER}"; then 540 return "${KSFT_FAIL}" 541 fi 542 543 return "${KSFT_PASS}" 544} 545 546test_vm_loopback() { 547 local port=60000 # non-forwarded local port 548 549 vm_ssh "init_ns" -- modprobe vsock_loopback &> /dev/null || : 550 551 if ! vm_vsock_test "init_ns" "server" 1 "${port}"; then 552 return "${KSFT_FAIL}" 553 fi 554 555 556 if ! vm_vsock_test "init_ns" "127.0.0.1" 1 "${port}"; then 557 return "${KSFT_FAIL}" 558 fi 559 560 return "${KSFT_PASS}" 561} 562 563shared_vm_test() { 564 local tname 565 566 tname="${1}" 567 568 for testname in "${USE_SHARED_VM[@]}"; do 569 if [[ "${tname}" == "${testname}" ]]; then 570 return 0 571 fi 572 done 573 574 return 1 575} 576 577shared_vm_tests_requested() { 578 for arg in "$@"; do 579 if shared_vm_test "${arg}"; then 580 return 0 581 fi 582 done 583 584 return 1 585} 586 587run_shared_vm_tests() { 588 local arg 589 590 for arg in "$@"; do 591 if ! shared_vm_test "${arg}"; then 592 continue 593 fi 594 595 run_shared_vm_test "${arg}" 596 check_result "$?" "${arg}" 597 done 598} 599 600run_shared_vm_test() { 601 local host_oops_cnt_before 602 local host_warn_cnt_before 603 local vm_oops_cnt_before 604 local vm_warn_cnt_before 605 local host_oops_cnt_after 606 local host_warn_cnt_after 607 local vm_oops_cnt_after 608 local vm_warn_cnt_after 609 local name 610 local rc 611 612 host_oops_cnt_before=$(dmesg | grep -c -i 'Oops') 613 host_warn_cnt_before=$(dmesg --level=warn | grep -c -i 'vsock') 614 vm_oops_cnt_before=$(vm_dmesg_oops_count "init_ns") 615 vm_warn_cnt_before=$(vm_dmesg_warn_count "init_ns") 616 617 name=$(echo "${1}" | awk '{ print $1 }') 618 eval test_"${name}" 619 rc=$? 620 621 host_oops_cnt_after=$(dmesg | grep -i 'Oops' | wc -l) 622 if [[ ${host_oops_cnt_after} -gt ${host_oops_cnt_before} ]]; then 623 echo "FAIL: kernel oops detected on host" | log_host 624 rc=$KSFT_FAIL 625 fi 626 627 host_warn_cnt_after=$(dmesg --level=warn | grep -c -i 'vsock') 628 if [[ ${host_warn_cnt_after} -gt ${host_warn_cnt_before} ]]; then 629 echo "FAIL: kernel warning detected on host" | log_host 630 rc=$KSFT_FAIL 631 fi 632 633 vm_oops_cnt_after=$(vm_dmesg_oops_count "init_ns") 634 if [[ ${vm_oops_cnt_after} -gt ${vm_oops_cnt_before} ]]; then 635 echo "FAIL: kernel oops detected on vm" | log_host 636 rc=$KSFT_FAIL 637 fi 638 639 vm_warn_cnt_after=$(vm_dmesg_warn_count "init_ns") 640 if [[ ${vm_warn_cnt_after} -gt ${vm_warn_cnt_before} ]]; then 641 echo "FAIL: kernel warning detected on vm" | log_host 642 rc=$KSFT_FAIL 643 fi 644 645 return "${rc}" 646} 647 648BUILD=0 649QEMU="qemu-system-$(uname -m)" 650 651while getopts :hvsq:b o 652do 653 case $o in 654 v) VERBOSE=1;; 655 b) BUILD=1;; 656 q) QEMU=$OPTARG;; 657 h|*) usage;; 658 esac 659done 660shift $((OPTIND-1)) 661 662trap cleanup EXIT 663 664if [[ ${#} -eq 0 ]]; then 665 ARGS=("${TEST_NAMES[@]}") 666else 667 ARGS=("$@") 668fi 669 670check_args "${ARGS[@]}" 671check_deps 672check_vng 673handle_build 674 675echo "1..${#ARGS[@]}" 676 677cnt_pass=0 678cnt_fail=0 679cnt_skip=0 680cnt_total=0 681 682if shared_vm_tests_requested "${ARGS[@]}"; then 683 log_host "Booting up VM" 684 pidfile="$(create_pidfile)" 685 vm_start "${pidfile}" "init_ns" 686 vm_wait_for_ssh "init_ns" 687 log_host "VM booted up" 688 689 run_shared_vm_tests "${ARGS[@]}" 690 terminate_pidfiles "${pidfile}" 691fi 692 693echo "SUMMARY: PASS=${cnt_pass} SKIP=${cnt_skip} FAIL=${cnt_fail}" 694echo "Log: ${LOG}" 695 696if [ $((cnt_pass + cnt_skip)) -eq ${cnt_total} ]; then 697 exit "$KSFT_PASS" 698else 699 exit "$KSFT_FAIL" 700fi 701