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