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