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