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