1#!/bin/bash 2# SPDX-License-Identifier: GPL-2.0 3 4set -e 5 6# This script currently only works for the following platforms, 7# as it is based on the VM image used by the BPF CI, which is 8# available only for these architectures. We can also specify 9# the local rootfs image generated by the following script: 10# https://github.com/libbpf/ci/blob/main/rootfs/mkrootfs_debian.sh 11PLATFORM="${PLATFORM:-$(uname -m)}" 12case "${PLATFORM}" in 13s390x) 14 QEMU_BINARY=qemu-system-s390x 15 QEMU_CONSOLE="ttyS1" 16 HOST_FLAGS=(-smp 2 -enable-kvm) 17 CROSS_FLAGS=(-smp 2) 18 BZIMAGE="arch/s390/boot/vmlinux" 19 ARCH="s390" 20 ;; 21x86_64) 22 QEMU_BINARY=qemu-system-x86_64 23 QEMU_CONSOLE="ttyS0,115200" 24 HOST_FLAGS=(-cpu host -enable-kvm -smp 8) 25 CROSS_FLAGS=(-smp 8) 26 BZIMAGE="arch/x86/boot/bzImage" 27 ARCH="x86" 28 ;; 29aarch64) 30 QEMU_BINARY=qemu-system-aarch64 31 QEMU_CONSOLE="ttyAMA0,115200" 32 HOST_FLAGS=(-M virt,gic-version=3 -cpu host -enable-kvm -smp 8) 33 CROSS_FLAGS=(-M virt,gic-version=3 -cpu cortex-a76 -smp 8) 34 BZIMAGE="arch/arm64/boot/Image" 35 ARCH="arm64" 36 ;; 37riscv64) 38 # required qemu version v7.2.0+ 39 QEMU_BINARY=qemu-system-riscv64 40 QEMU_CONSOLE="ttyS0,115200" 41 HOST_FLAGS=(-M virt -cpu host -enable-kvm -smp 8) 42 CROSS_FLAGS=(-M virt -cpu rv64,sscofpmf=true -smp 8) 43 BZIMAGE="arch/riscv/boot/Image" 44 ARCH="riscv" 45 ;; 46*) 47 echo "Unsupported architecture" 48 exit 1 49 ;; 50esac 51DEFAULT_COMMAND="./test_progs" 52MOUNT_DIR="mnt" 53LOCAL_ROOTFS_IMAGE="" 54ROOTFS_IMAGE="root.img" 55OUTPUT_DIR="$HOME/.bpf_selftests" 56KCONFIG_REL_PATHS=("tools/testing/selftests/bpf/config" 57 "tools/testing/selftests/bpf/config.vm" 58 "tools/testing/selftests/bpf/config.${PLATFORM}") 59INDEX_URL="https://raw.githubusercontent.com/libbpf/ci/master/INDEX" 60NUM_COMPILE_JOBS="$(nproc)" 61LOG_FILE_BASE="$(date +"bpf_selftests.%Y-%m-%d_%H-%M-%S")" 62LOG_FILE="${LOG_FILE_BASE}.log" 63EXIT_STATUS_FILE="${LOG_FILE_BASE}.exit_status" 64 65usage() 66{ 67 cat <<EOF 68Usage: $0 [-i] [-s] [-d <output_dir>] -- [<command>] 69 70<command> is the command you would normally run when you are in 71tools/testing/selftests/bpf. e.g: 72 73 $0 -- ./test_progs -t test_lsm 74 75If no command is specified and a debug shell (-s) is not requested, 76"${DEFAULT_COMMAND}" will be run by default. 77 78Using PLATFORM= and CROSS_COMPILE= options will enable cross platform testing: 79 80 PLATFORM=<platform> CROSS_COMPILE=<toolchain> $0 -- ./test_progs -t test_lsm 81 82If you build your kernel using KBUILD_OUTPUT= or O= options, these 83can be passed as environment variables to the script: 84 85 O=<kernel_build_path> $0 -- ./test_progs -t test_lsm 86 87or 88 89 KBUILD_OUTPUT=<kernel_build_path> $0 -- ./test_progs -t test_lsm 90 91Options: 92 93 -l) Specify the path to the local rootfs image. 94 -i) Update the rootfs image with a newer version. 95 -d) Update the output directory (default: ${OUTPUT_DIR}) 96 -j) Number of jobs for compilation, similar to -j in make 97 (default: ${NUM_COMPILE_JOBS}) 98 -s) Instead of powering off the VM, start an interactive 99 shell. If <command> is specified, the shell runs after 100 the command finishes executing 101EOF 102} 103 104unset URLS 105populate_url_map() 106{ 107 if ! declare -p URLS &> /dev/null; then 108 # URLS contain the mapping from file names to URLs where 109 # those files can be downloaded from. 110 declare -gA URLS 111 while IFS=$'\t' read -r name url; do 112 URLS["$name"]="$url" 113 done < <(curl -Lsf ${INDEX_URL}) 114 fi 115} 116 117newest_rootfs_version() 118{ 119 { 120 for file in "${!URLS[@]}"; do 121 if [[ $file =~ ^"${PLATFORM}"/libbpf-vmtest-rootfs-(.*)\.tar\.zst$ ]]; then 122 echo "${BASH_REMATCH[1]}" 123 fi 124 done 125 } | sort -rV | head -1 126} 127 128download_rootfs() 129{ 130 populate_url_map 131 132 local rootfsversion="$(newest_rootfs_version)" 133 local file="${PLATFORM}/libbpf-vmtest-rootfs-$rootfsversion.tar.zst" 134 135 if [[ ! -v URLS[$file] ]]; then 136 echo "$file not found" >&2 137 return 1 138 fi 139 140 echo "Downloading $file..." >&2 141 curl -Lsf "${URLS[$file]}" "${@:2}" 142} 143 144load_rootfs() 145{ 146 local dir="$1" 147 148 if ! which zstd &> /dev/null; then 149 echo 'Could not find "zstd" on the system, please install zstd' 150 exit 1 151 fi 152 153 if [[ -n "${LOCAL_ROOTFS_IMAGE}" ]]; then 154 cat "${LOCAL_ROOTFS_IMAGE}" | zstd -d | sudo tar -C "$dir" -x 155 else 156 download_rootfs | zstd -d | sudo tar -C "$dir" -x 157 fi 158} 159 160recompile_kernel() 161{ 162 local kernel_checkout="$1" 163 local make_command="$2" 164 165 cd "${kernel_checkout}" 166 167 ${make_command} olddefconfig 168 ${make_command} 169} 170 171mount_image() 172{ 173 local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}" 174 local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}" 175 176 sudo mount -o loop "${rootfs_img}" "${mount_dir}" 177} 178 179unmount_image() 180{ 181 local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}" 182 183 sudo umount "${mount_dir}" &> /dev/null 184} 185 186update_selftests() 187{ 188 local kernel_checkout="$1" 189 local selftests_dir="${kernel_checkout}/tools/testing/selftests/bpf" 190 191 cd "${selftests_dir}" 192 ${make_command} 193 194 # Mount the image and copy the selftests to the image. 195 mount_image 196 sudo rm -rf "${mount_dir}/root/bpf" 197 sudo cp -r "${selftests_dir}" "${mount_dir}/root" 198 unmount_image 199} 200 201update_init_script() 202{ 203 local init_script_dir="${OUTPUT_DIR}/${MOUNT_DIR}/etc/rcS.d" 204 local init_script="${init_script_dir}/S50-startup" 205 local command="$1" 206 local exit_command="$2" 207 208 mount_image 209 210 if [[ ! -d "${init_script_dir}" ]]; then 211 cat <<EOF 212Could not find ${init_script_dir} in the mounted image. 213This likely indicates a bad rootfs image, Please download 214a new image by passing "-i" to the script 215EOF 216 exit 1 217 218 fi 219 220 sudo bash -c "echo '#!/bin/bash' > ${init_script}" 221 222 if [[ "${command}" != "" ]]; then 223 sudo bash -c "cat >>${init_script}" <<EOF 224# Have a default value in the exit status file 225# incase the VM is forcefully stopped. 226echo "130" > "/root/${EXIT_STATUS_FILE}" 227 228{ 229 cd /root/bpf 230 echo ${command} 231 stdbuf -oL -eL ${command} 232 echo "\$?" > "/root/${EXIT_STATUS_FILE}" 233} 2>&1 | tee "/root/${LOG_FILE}" 234# Ensure that the logs are written to disk 235sync 236EOF 237 fi 238 239 sudo bash -c "echo ${exit_command} >> ${init_script}" 240 sudo chmod a+x "${init_script}" 241 unmount_image 242} 243 244create_vm_image() 245{ 246 local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}" 247 local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}" 248 249 rm -rf "${rootfs_img}" 250 touch "${rootfs_img}" 251 chattr +C "${rootfs_img}" >/dev/null 2>&1 || true 252 253 truncate -s 2G "${rootfs_img}" 254 mkfs.ext4 -q "${rootfs_img}" 255 256 mount_image 257 load_rootfs "${mount_dir}" 258 unmount_image 259} 260 261run_vm() 262{ 263 local kernel_bzimage="$1" 264 local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}" 265 266 if ! which "${QEMU_BINARY}" &> /dev/null; then 267 cat <<EOF 268Could not find ${QEMU_BINARY} 269Please install qemu or set the QEMU_BINARY environment variable. 270EOF 271 exit 1 272 fi 273 274 if [[ "${PLATFORM}" != "$(uname -m)" ]]; then 275 QEMU_FLAGS=("${CROSS_FLAGS[@]}") 276 else 277 QEMU_FLAGS=("${HOST_FLAGS[@]}") 278 fi 279 280 ${QEMU_BINARY} \ 281 -nodefaults \ 282 -display none \ 283 -serial mon:stdio \ 284 "${QEMU_FLAGS[@]}" \ 285 -m 4G \ 286 -drive file="${rootfs_img}",format=raw,index=1,media=disk,if=virtio,cache=none \ 287 -kernel "${kernel_bzimage}" \ 288 -append "root=/dev/vda rw console=${QEMU_CONSOLE}" 289} 290 291copy_logs() 292{ 293 local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}" 294 local log_file="${mount_dir}/root/${LOG_FILE}" 295 local exit_status_file="${mount_dir}/root/${EXIT_STATUS_FILE}" 296 297 mount_image 298 sudo cp ${log_file} "${OUTPUT_DIR}" 299 sudo cp ${exit_status_file} "${OUTPUT_DIR}" 300 sudo rm -f ${log_file} 301 unmount_image 302} 303 304is_rel_path() 305{ 306 local path="$1" 307 308 [[ ${path:0:1} != "/" ]] 309} 310 311do_update_kconfig() 312{ 313 local kernel_checkout="$1" 314 local kconfig_file="$2" 315 316 rm -f "$kconfig_file" 2> /dev/null 317 318 for config in "${KCONFIG_REL_PATHS[@]}"; do 319 local kconfig_src="${kernel_checkout}/${config}" 320 cat "$kconfig_src" >> "$kconfig_file" 321 done 322} 323 324update_kconfig() 325{ 326 local kernel_checkout="$1" 327 local kconfig_file="$2" 328 329 if [[ -f "${kconfig_file}" ]]; then 330 local local_modified="$(stat -c %Y "${kconfig_file}")" 331 332 for config in "${KCONFIG_REL_PATHS[@]}"; do 333 local kconfig_src="${kernel_checkout}/${config}" 334 local src_modified="$(stat -c %Y "${kconfig_src}")" 335 # Only update the config if it has been updated after the 336 # previously cached config was created. This avoids 337 # unnecessarily compiling the kernel and selftests. 338 if [[ "${src_modified}" -gt "${local_modified}" ]]; then 339 do_update_kconfig "$kernel_checkout" "$kconfig_file" 340 # Once we have found one outdated configuration 341 # there is no need to check other ones. 342 break 343 fi 344 done 345 else 346 do_update_kconfig "$kernel_checkout" "$kconfig_file" 347 fi 348} 349 350catch() 351{ 352 local exit_code=$1 353 local exit_status_file="${OUTPUT_DIR}/${EXIT_STATUS_FILE}" 354 # This is just a cleanup and the directory may 355 # have already been unmounted. So, don't let this 356 # clobber the error code we intend to return. 357 unmount_image || true 358 if [[ -f "${exit_status_file}" ]]; then 359 exit_code="$(cat ${exit_status_file})" 360 fi 361 exit ${exit_code} 362} 363 364main() 365{ 366 local script_dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" 367 local kernel_checkout=$(realpath "${script_dir}"/../../../../) 368 # By default the script searches for the kernel in the checkout directory but 369 # it also obeys environment variables O= and KBUILD_OUTPUT= 370 local kernel_bzimage="${kernel_checkout}/${BZIMAGE}" 371 local command="${DEFAULT_COMMAND}" 372 local update_image="no" 373 local exit_command="poweroff -f" 374 local debug_shell="no" 375 376 while getopts ':hskl:id:j:' opt; do 377 case ${opt} in 378 l) 379 LOCAL_ROOTFS_IMAGE="$OPTARG" 380 ;; 381 i) 382 update_image="yes" 383 ;; 384 d) 385 OUTPUT_DIR="$OPTARG" 386 ;; 387 j) 388 NUM_COMPILE_JOBS="$OPTARG" 389 ;; 390 s) 391 command="" 392 debug_shell="yes" 393 exit_command="bash" 394 ;; 395 h) 396 usage 397 exit 0 398 ;; 399 \? ) 400 echo "Invalid Option: -$OPTARG" 401 usage 402 exit 1 403 ;; 404 : ) 405 echo "Invalid Option: -$OPTARG requires an argument" 406 usage 407 exit 1 408 ;; 409 esac 410 done 411 shift $((OPTIND -1)) 412 413 trap 'catch "$?"' EXIT 414 415 if [[ "${PLATFORM}" != "$(uname -m)" ]] && [[ -z "${CROSS_COMPILE}" ]]; then 416 echo "Cross-platform testing needs to specify CROSS_COMPILE" 417 exit 1 418 fi 419 420 if [[ $# -eq 0 && "${debug_shell}" == "no" ]]; then 421 echo "No command specified, will run ${DEFAULT_COMMAND} in the vm" 422 else 423 command="$@" 424 fi 425 426 local kconfig_file="${OUTPUT_DIR}/latest.config" 427 local make_command="make ARCH=${ARCH} CROSS_COMPILE=${CROSS_COMPILE} \ 428 -j ${NUM_COMPILE_JOBS} KCONFIG_CONFIG=${kconfig_file}" 429 430 # Figure out where the kernel is being built. 431 # O takes precedence over KBUILD_OUTPUT. 432 if [[ "${O:=""}" != "" ]]; then 433 if is_rel_path "${O}"; then 434 O="$(realpath "${PWD}/${O}")" 435 fi 436 kernel_bzimage="${O}/${BZIMAGE}" 437 make_command="${make_command} O=${O}" 438 elif [[ "${KBUILD_OUTPUT:=""}" != "" ]]; then 439 if is_rel_path "${KBUILD_OUTPUT}"; then 440 KBUILD_OUTPUT="$(realpath "${PWD}/${KBUILD_OUTPUT}")" 441 fi 442 kernel_bzimage="${KBUILD_OUTPUT}/${BZIMAGE}" 443 make_command="${make_command} KBUILD_OUTPUT=${KBUILD_OUTPUT}" 444 fi 445 446 local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}" 447 local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}" 448 449 echo "Output directory: ${OUTPUT_DIR}" 450 451 mkdir -p "${OUTPUT_DIR}" 452 mkdir -p "${mount_dir}" 453 update_kconfig "${kernel_checkout}" "${kconfig_file}" 454 455 recompile_kernel "${kernel_checkout}" "${make_command}" 456 457 if [[ "${update_image}" == "no" && ! -f "${rootfs_img}" ]]; then 458 echo "rootfs image not found in ${rootfs_img}" 459 update_image="yes" 460 fi 461 462 if [[ "${update_image}" == "yes" ]]; then 463 create_vm_image 464 fi 465 466 update_selftests "${kernel_checkout}" "${make_command}" 467 update_init_script "${command}" "${exit_command}" 468 run_vm "${kernel_bzimage}" 469 if [[ "${command}" != "" ]]; then 470 copy_logs 471 echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}" 472 fi 473} 474 475main "$@" 476