diff options
Diffstat (limited to 'tools/testing/selftests/firmware')
-rw-r--r-- | tools/testing/selftests/firmware/.gitignore | 2 | ||||
-rw-r--r-- | tools/testing/selftests/firmware/Makefile | 10 | ||||
-rw-r--r-- | tools/testing/selftests/firmware/config | 6 | ||||
-rwxr-xr-x | tools/testing/selftests/firmware/fw_fallback.sh | 283 | ||||
-rwxr-xr-x | tools/testing/selftests/firmware/fw_filesystem.sh | 546 | ||||
-rwxr-xr-x | tools/testing/selftests/firmware/fw_lib.sh | 236 | ||||
-rw-r--r-- | tools/testing/selftests/firmware/fw_namespace.c | 152 | ||||
-rwxr-xr-x | tools/testing/selftests/firmware/fw_run_tests.sh | 79 | ||||
-rwxr-xr-x | tools/testing/selftests/firmware/fw_upload.sh | 214 | ||||
-rw-r--r-- | tools/testing/selftests/firmware/settings | 8 |
10 files changed, 1536 insertions, 0 deletions
diff --git a/tools/testing/selftests/firmware/.gitignore b/tools/testing/selftests/firmware/.gitignore new file mode 100644 index 000000000..62abc92a9 --- /dev/null +++ b/tools/testing/selftests/firmware/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +fw_namespace diff --git a/tools/testing/selftests/firmware/Makefile b/tools/testing/selftests/firmware/Makefile new file mode 100644 index 000000000..7992969de --- /dev/null +++ b/tools/testing/selftests/firmware/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Makefile for firmware loading selftests +CFLAGS = -Wall \ + -O2 + +TEST_PROGS := fw_run_tests.sh +TEST_FILES := fw_fallback.sh fw_filesystem.sh fw_upload.sh fw_lib.sh +TEST_GEN_FILES := fw_namespace + +include ../lib.mk diff --git a/tools/testing/selftests/firmware/config b/tools/testing/selftests/firmware/config new file mode 100644 index 000000000..6e402519b --- /dev/null +++ b/tools/testing/selftests/firmware/config @@ -0,0 +1,6 @@ +CONFIG_TEST_FIRMWARE=y +CONFIG_FW_LOADER=y +CONFIG_FW_LOADER_USER_HELPER=y +CONFIG_IKCONFIG=y +CONFIG_IKCONFIG_PROC=y +CONFIG_FW_UPLOAD=y diff --git a/tools/testing/selftests/firmware/fw_fallback.sh b/tools/testing/selftests/firmware/fw_fallback.sh new file mode 100755 index 000000000..70d18be46 --- /dev/null +++ b/tools/testing/selftests/firmware/fw_fallback.sh @@ -0,0 +1,283 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# This validates that the kernel will fall back to using the fallback mechanism +# to load firmware it can't find on disk itself. We must request a firmware +# that the kernel won't find, and any installed helper (e.g. udev) also +# won't find so that we can do the load ourself manually. +set -e + +TEST_REQS_FW_SYSFS_FALLBACK="yes" +TEST_REQS_FW_SET_CUSTOM_PATH="no" +TEST_DIR=$(dirname $0) +source $TEST_DIR/fw_lib.sh + +check_mods +check_setup +verify_reqs +setup_tmp_file + +trap "test_finish" EXIT + +load_fw() +{ + local name="$1" + local file="$2" + + # This will block until our load (below) has finished. + echo -n "$name" >"$DIR"/trigger_request & + + # Give kernel a chance to react. + local timeout=10 + while [ ! -e "$DIR"/"$name"/loading ]; do + sleep 0.1 + timeout=$(( $timeout - 1 )) + if [ "$timeout" -eq 0 ]; then + echo "$0: firmware interface never appeared" >&2 + exit 1 + fi + done + + echo 1 >"$DIR"/"$name"/loading + cat "$file" >"$DIR"/"$name"/data + echo 0 >"$DIR"/"$name"/loading + + # Wait for request to finish. + wait +} + +load_fw_cancel() +{ + local name="$1" + local file="$2" + + # This will block until our load (below) has finished. + echo -n "$name" >"$DIR"/trigger_request 2>/dev/null & + + # Give kernel a chance to react. + local timeout=10 + while [ ! -e "$DIR"/"$name"/loading ]; do + sleep 0.1 + timeout=$(( $timeout - 1 )) + if [ "$timeout" -eq 0 ]; then + echo "$0: firmware interface never appeared" >&2 + exit 1 + fi + done + + echo -1 >"$DIR"/"$name"/loading + + # Wait for request to finish. + wait +} + +load_fw_custom() +{ + if [ ! -e "$DIR"/trigger_custom_fallback ]; then + echo "$0: custom fallback trigger not present, ignoring test" >&2 + exit $ksft_skip + fi + + local name="$1" + local file="$2" + + echo -n "$name" >"$DIR"/trigger_custom_fallback 2>/dev/null & + + # Give kernel a chance to react. + local timeout=10 + while [ ! -e "$DIR"/"$name"/loading ]; do + sleep 0.1 + timeout=$(( $timeout - 1 )) + if [ "$timeout" -eq 0 ]; then + echo "$0: firmware interface never appeared" >&2 + exit 1 + fi + done + + echo 1 >"$DIR"/"$name"/loading + cat "$file" >"$DIR"/"$name"/data + echo 0 >"$DIR"/"$name"/loading + + # Wait for request to finish. + wait + return 0 +} + + +load_fw_custom_cancel() +{ + if [ ! -e "$DIR"/trigger_custom_fallback ]; then + echo "$0: canceling custom fallback trigger not present, ignoring test" >&2 + exit $ksft_skip + fi + + local name="$1" + local file="$2" + + echo -n "$name" >"$DIR"/trigger_custom_fallback 2>/dev/null & + + # Give kernel a chance to react. + local timeout=10 + while [ ! -e "$DIR"/"$name"/loading ]; do + sleep 0.1 + timeout=$(( $timeout - 1 )) + if [ "$timeout" -eq 0 ]; then + echo "$0: firmware interface never appeared" >&2 + exit 1 + fi + done + + echo -1 >"$DIR"/"$name"/loading + + # Wait for request to finish. + wait + return 0 +} + +load_fw_fallback_with_child() +{ + local name="$1" + local file="$2" + + # This is the value already set but we want to be explicit + echo 4 >/sys/class/firmware/timeout + + sleep 1 & + SECONDS_BEFORE=$(date +%s) + echo -n "$name" >"$DIR"/trigger_request 2>/dev/null + SECONDS_AFTER=$(date +%s) + SECONDS_DELTA=$(($SECONDS_AFTER - $SECONDS_BEFORE)) + if [ "$SECONDS_DELTA" -lt 4 ]; then + RET=1 + else + RET=0 + fi + wait + return $RET +} + +test_syfs_timeout() +{ + DEVPATH="$DIR"/"nope-$NAME"/loading + + # Test failure when doing nothing (timeout works). + echo -n 2 >/sys/class/firmware/timeout + echo -n "nope-$NAME" >"$DIR"/trigger_request 2>/dev/null & + + # Give the kernel some time to load the loading file, must be less + # than the timeout above. + sleep 1 + if [ ! -f $DEVPATH ]; then + echo "$0: fallback mechanism immediately cancelled" + echo "" + echo "The file never appeared: $DEVPATH" + echo "" + echo "This might be a distribution udev rule setup by your distribution" + echo "to immediately cancel all fallback requests, this must be" + echo "removed before running these tests. To confirm look for" + echo "a firmware rule like /lib/udev/rules.d/50-firmware.rules" + echo "and see if you have something like this:" + echo "" + echo "SUBSYSTEM==\"firmware\", ACTION==\"add\", ATTR{loading}=\"-1\"" + echo "" + echo "If you do remove this file or comment out this line before" + echo "proceeding with these tests." + exit 1 + fi + + if diff -q "$FW" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was not expected to match" >&2 + exit 1 + else + echo "$0: timeout works" + fi +} + +run_sysfs_main_tests() +{ + test_syfs_timeout + # Put timeout high enough for us to do work but not so long that failures + # slow down this test too much. + echo 4 >/sys/class/firmware/timeout + + # Load this script instead of the desired firmware. + load_fw "$NAME" "$0" + if diff -q "$FW" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was not expected to match" >&2 + exit 1 + else + echo "$0: firmware comparison works" + fi + + # Do a proper load, which should work correctly. + load_fw "$NAME" "$FW" + if ! diff -q "$FW" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was not loaded" >&2 + exit 1 + else + echo "$0: fallback mechanism works" + fi + + load_fw_cancel "nope-$NAME" "$FW" + if diff -q "$FW" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was expected to be cancelled" >&2 + exit 1 + else + echo "$0: cancelling fallback mechanism works" + fi + + set +e + load_fw_fallback_with_child "nope-signal-$NAME" "$FW" + if [ "$?" -eq 0 ]; then + echo "$0: SIGCHLD on sync ignored as expected" >&2 + else + echo "$0: error - sync firmware request cancelled due to SIGCHLD" >&2 + exit 1 + fi + set -e +} + +run_sysfs_custom_load_tests() +{ + RANDOM_FILE_PATH=$(setup_random_file) + RANDOM_FILE="$(basename $RANDOM_FILE_PATH)" + if load_fw_custom "$RANDOM_FILE" "$RANDOM_FILE_PATH" ; then + if ! diff -q "$RANDOM_FILE_PATH" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was not loaded" >&2 + exit 1 + else + echo "$0: custom fallback loading mechanism works" + fi + fi + + RANDOM_FILE_PATH=$(setup_random_file) + RANDOM_FILE="$(basename $RANDOM_FILE_PATH)" + if load_fw_custom "$RANDOM_FILE" "$RANDOM_FILE_PATH" ; then + if ! diff -q "$RANDOM_FILE_PATH" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was not loaded" >&2 + exit 1 + else + echo "$0: custom fallback loading mechanism works" + fi + fi + + RANDOM_FILE_REAL="$RANDOM_FILE_PATH" + FAKE_RANDOM_FILE_PATH=$(setup_random_file_fake) + FAKE_RANDOM_FILE="$(basename $FAKE_RANDOM_FILE_PATH)" + + if load_fw_custom_cancel "$FAKE_RANDOM_FILE" "$RANDOM_FILE_REAL" ; then + if diff -q "$RANDOM_FILE_PATH" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was expected to be cancelled" >&2 + exit 1 + else + echo "$0: cancelling custom fallback mechanism works" + fi + fi +} + +if [ "$HAS_FW_LOADER_USER_HELPER_FALLBACK" = "yes" ]; then + run_sysfs_main_tests +fi + +run_sysfs_custom_load_tests + +exit 0 diff --git a/tools/testing/selftests/firmware/fw_filesystem.sh b/tools/testing/selftests/firmware/fw_filesystem.sh new file mode 100755 index 000000000..1a99aea05 --- /dev/null +++ b/tools/testing/selftests/firmware/fw_filesystem.sh @@ -0,0 +1,546 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# This validates that the kernel will load firmware out of its list of +# firmware locations on disk. Since the user helper does similar work, +# we reset the custom load directory to a location the user helper doesn't +# know so we can be sure we're not accidentally testing the user helper. +set -e + +TEST_REQS_FW_SYSFS_FALLBACK="no" +TEST_REQS_FW_SET_CUSTOM_PATH="yes" +TEST_DIR=$(dirname $0) +source $TEST_DIR/fw_lib.sh + +RUN_XZ="xz -C crc32 --lzma2=dict=2MiB" +RUN_ZSTD="zstd -q" + +check_mods +check_setup +verify_reqs +setup_tmp_file + +trap "test_finish" EXIT + +if [ "$HAS_FW_LOADER_USER_HELPER" = "yes" ]; then + # Turn down the timeout so failures don't take so long. + echo 1 >/sys/class/firmware/timeout +fi + +if printf '\000' >"$DIR"/trigger_request 2> /dev/null; then + echo "$0: empty filename should not succeed" >&2 + exit 1 +fi + +if [ ! -e "$DIR"/trigger_async_request ]; then + echo "$0: empty filename: async trigger not present, ignoring test" >&2 + exit $ksft_skip +else + if printf '\000' >"$DIR"/trigger_async_request 2> /dev/null; then + echo "$0: empty filename should not succeed (async)" >&2 + exit 1 + fi +fi + +# Request a firmware that doesn't exist, it should fail. +if echo -n "nope-$NAME" >"$DIR"/trigger_request 2> /dev/null; then + echo "$0: firmware shouldn't have loaded" >&2 + exit 1 +fi +if diff -q "$FW" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was not expected to match" >&2 + exit 1 +else + if [ "$HAS_FW_LOADER_USER_HELPER" = "yes" ]; then + echo "$0: timeout works" + fi +fi + +# This should succeed via kernel load or will fail after 1 second after +# being handed over to the user helper, which won't find the fw either. +if ! echo -n "$NAME" >"$DIR"/trigger_request ; then + echo "$0: could not trigger request" >&2 + exit 1 +fi + +# Verify the contents are what we expect. +if ! diff -q "$FW" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was not loaded" >&2 + exit 1 +else + echo "$0: filesystem loading works" +fi + +# Try the asynchronous version too +if [ ! -e "$DIR"/trigger_async_request ]; then + echo "$0: firmware loading: async trigger not present, ignoring test" >&2 + exit $ksft_skip +else + if ! echo -n "$NAME" >"$DIR"/trigger_async_request ; then + echo "$0: could not trigger async request" >&2 + exit 1 + fi + + # Verify the contents are what we expect. + if ! diff -q "$FW" /dev/test_firmware >/dev/null ; then + echo "$0: firmware was not loaded (async)" >&2 + exit 1 + else + echo "$0: async filesystem loading works" + fi +fi + +# Try platform (EFI embedded fw) loading too +if [ ! -e "$DIR"/trigger_request_platform ]; then + echo "$0: firmware loading: platform trigger not present, ignoring test" >&2 +else + if printf '\000' >"$DIR"/trigger_request_platform 2> /dev/null; then + echo "$0: empty filename should not succeed (platform)" >&2 + exit 1 + fi + + # Note we echo a non-existing name, since files on the file-system + # are preferred over firmware embedded inside the platform's firmware + # The test adds a fake entry with the requested name to the platform's + # fw list, so the name does not matter as long as it does not exist + if ! echo -n "nope-$NAME" >"$DIR"/trigger_request_platform ; then + echo "$0: could not trigger request platform" >&2 + exit 1 + fi + + # The test verifies itself that the loaded firmware contents matches + # the contents for the fake platform fw entry it added. + echo "$0: platform loading works" +fi + +### Batched requests tests +test_config_present() +{ + if [ ! -f $DIR/reset ]; then + echo "Configuration triggers not present, ignoring test" + exit $ksft_skip + fi +} + +# Defaults : +# +# send_uevent: 1 +# sync_direct: 0 +# name: test-firmware.bin +# num_requests: 4 +config_reset() +{ + echo 1 > $DIR/reset +} + +release_all_firmware() +{ + echo 1 > $DIR/release_all_firmware +} + +config_set_name() +{ + echo -n $1 > $DIR/config_name +} + +config_set_into_buf() +{ + echo 1 > $DIR/config_into_buf +} + +config_unset_into_buf() +{ + echo 0 > $DIR/config_into_buf +} + +config_set_buf_size() +{ + echo $1 > $DIR/config_buf_size +} + +config_set_file_offset() +{ + echo $1 > $DIR/config_file_offset +} + +config_set_partial() +{ + echo 1 > $DIR/config_partial +} + +config_unset_partial() +{ + echo 0 > $DIR/config_partial +} + +config_set_sync_direct() +{ + echo 1 > $DIR/config_sync_direct +} + +config_unset_sync_direct() +{ + echo 0 > $DIR/config_sync_direct +} + +config_set_uevent() +{ + echo 1 > $DIR/config_send_uevent +} + +config_unset_uevent() +{ + echo 0 > $DIR/config_send_uevent +} + +config_trigger_sync() +{ + echo -n 1 > $DIR/trigger_batched_requests 2>/dev/null +} + +config_trigger_async() +{ + echo -n 1 > $DIR/trigger_batched_requests_async 2> /dev/null +} + +config_set_read_fw_idx() +{ + echo -n $1 > $DIR/config_read_fw_idx 2> /dev/null +} + +read_firmwares() +{ + if [ "$(cat $DIR/config_into_buf)" == "1" ]; then + fwfile="$FW_INTO_BUF" + else + fwfile="$FW" + fi + if [ "$1" = "componly" ]; then + fwfile="${fwfile}-orig" + fi + for i in $(seq 0 3); do + config_set_read_fw_idx $i + # Verify the contents are what we expect. + # -Z required for now -- check for yourself, md5sum + # on $FW and DIR/read_firmware will yield the same. Even + # cmp agrees, so something is off. + if ! diff -q -Z "$fwfile" $DIR/read_firmware 2>/dev/null ; then + echo "request #$i: firmware was not loaded" >&2 + exit 1 + fi + done +} + +read_partial_firmwares() +{ + if [ "$(cat $DIR/config_into_buf)" == "1" ]; then + fwfile="${FW_INTO_BUF}" + else + fwfile="${FW}" + fi + + if [ "$1" = "componly" ]; then + fwfile="${fwfile}-orig" + fi + + # Strip fwfile down to match partial offset and length + partial_data="$(cat $fwfile)" + partial_data="${partial_data:$2:$3}" + + for i in $(seq 0 3); do + config_set_read_fw_idx $i + + read_firmware="$(cat $DIR/read_firmware)" + + # Verify the contents are what we expect. + if [ $read_firmware != $partial_data ]; then + echo "request #$i: partial firmware was not loaded" >&2 + exit 1 + fi + done +} + +read_firmwares_expect_nofile() +{ + for i in $(seq 0 3); do + config_set_read_fw_idx $i + # Ensures contents differ + if diff -q -Z "$FW" $DIR/read_firmware 2>/dev/null ; then + echo "request $i: file was not expected to match" >&2 + exit 1 + fi + done +} + +test_batched_request_firmware_nofile() +{ + echo -n "Batched request_firmware() nofile try #$1: " + config_reset + config_set_name nope-test-firmware.bin + config_trigger_sync + read_firmwares_expect_nofile + release_all_firmware + echo "OK" +} + +test_batched_request_firmware_into_buf_nofile() +{ + echo -n "Batched request_firmware_into_buf() nofile try #$1: " + config_reset + config_set_name nope-test-firmware.bin + config_set_into_buf + config_trigger_sync + read_firmwares_expect_nofile + release_all_firmware + echo "OK" +} + +test_request_partial_firmware_into_buf_nofile() +{ + echo -n "Test request_partial_firmware_into_buf() off=$1 size=$2 nofile: " + config_reset + config_set_name nope-test-firmware.bin + config_set_into_buf + config_set_partial + config_set_buf_size $2 + config_set_file_offset $1 + config_trigger_sync + read_firmwares_expect_nofile + release_all_firmware + echo "OK" +} + +test_batched_request_firmware_direct_nofile() +{ + echo -n "Batched request_firmware_direct() nofile try #$1: " + config_reset + config_set_name nope-test-firmware.bin + config_set_sync_direct + config_trigger_sync + release_all_firmware + echo "OK" +} + +test_request_firmware_nowait_uevent_nofile() +{ + echo -n "Batched request_firmware_nowait(uevent=true) nofile try #$1: " + config_reset + config_set_name nope-test-firmware.bin + config_trigger_async + release_all_firmware + echo "OK" +} + +test_wait_and_cancel_custom_load() +{ + if [ "$HAS_FW_LOADER_USER_HELPER" != "yes" ]; then + return + fi + local timeout=10 + name=$1 + while [ ! -e "$DIR"/"$name"/loading ]; do + sleep 0.1 + timeout=$(( $timeout - 1 )) + if [ "$timeout" -eq 0 ]; then + echo "firmware interface never appeared:" >&2 + echo "$DIR/$name/loading" >&2 + exit 1 + fi + done + echo -1 >"$DIR"/"$name"/loading +} + +test_request_firmware_nowait_custom_nofile() +{ + echo -n "Batched request_firmware_nowait(uevent=false) nofile try #$1: " + config_reset + config_unset_uevent + RANDOM_FILE_PATH=$(setup_random_file_fake) + RANDOM_FILE="$(basename $RANDOM_FILE_PATH)" + config_set_name $RANDOM_FILE + config_trigger_async & + test_wait_and_cancel_custom_load $RANDOM_FILE + wait + release_all_firmware + echo "OK" +} + +test_batched_request_firmware() +{ + echo -n "Batched request_firmware() $2 try #$1: " + config_reset + config_trigger_sync + read_firmwares $2 + release_all_firmware + echo "OK" +} + +test_batched_request_firmware_into_buf() +{ + echo -n "Batched request_firmware_into_buf() $2 try #$1: " + config_reset + config_set_name $TEST_FIRMWARE_INTO_BUF_FILENAME + config_set_into_buf + config_trigger_sync + read_firmwares $2 + release_all_firmware + echo "OK" +} + +test_batched_request_firmware_direct() +{ + echo -n "Batched request_firmware_direct() $2 try #$1: " + config_reset + config_set_sync_direct + config_trigger_sync + release_all_firmware + echo "OK" +} + +test_request_firmware_nowait_uevent() +{ + echo -n "Batched request_firmware_nowait(uevent=true) $2 try #$1: " + config_reset + config_trigger_async + release_all_firmware + echo "OK" +} + +test_request_firmware_nowait_custom() +{ + echo -n "Batched request_firmware_nowait(uevent=false) $2 try #$1: " + config_reset + config_unset_uevent + RANDOM_FILE_PATH=$(setup_random_file) + RANDOM_FILE="$(basename $RANDOM_FILE_PATH)" + if [ -n "$2" -a "$2" != "normal" ]; then + compress_"$2"_"$COMPRESS_FORMAT" $RANDOM_FILE_PATH + fi + config_set_name $RANDOM_FILE + config_trigger_async + release_all_firmware + echo "OK" +} + +test_request_partial_firmware_into_buf() +{ + echo -n "Test request_partial_firmware_into_buf() off=$1 size=$2: " + config_reset + config_set_name $TEST_FIRMWARE_INTO_BUF_FILENAME + config_set_into_buf + config_set_partial + config_set_buf_size $2 + config_set_file_offset $1 + config_trigger_sync + read_partial_firmwares normal $1 $2 + release_all_firmware + echo "OK" +} + +do_tests () +{ + mode="$1" + suffix="$2" + + for i in $(seq 1 5); do + test_batched_request_firmware$suffix $i $mode + done + + for i in $(seq 1 5); do + test_batched_request_firmware_into_buf$suffix $i $mode + done + + for i in $(seq 1 5); do + test_batched_request_firmware_direct$suffix $i $mode + done + + for i in $(seq 1 5); do + test_request_firmware_nowait_uevent$suffix $i $mode + done + + for i in $(seq 1 5); do + test_request_firmware_nowait_custom$suffix $i $mode + done +} + +# Only continue if batched request triggers are present on the +# test-firmware driver +test_config_present + +# test with the file present +echo +echo "Testing with the file present..." +do_tests normal + +# Partial loads cannot use fallback, so do not repeat tests. +test_request_partial_firmware_into_buf 0 10 +test_request_partial_firmware_into_buf 0 5 +test_request_partial_firmware_into_buf 1 6 +test_request_partial_firmware_into_buf 2 10 + +# Test for file not found, errors are expected, the failure would be +# a hung task, which would require a hard reset. +echo +echo "Testing with the file missing..." +do_tests nofile _nofile + +# Partial loads cannot use fallback, so do not repeat tests. +test_request_partial_firmware_into_buf_nofile 0 10 +test_request_partial_firmware_into_buf_nofile 0 5 +test_request_partial_firmware_into_buf_nofile 1 6 +test_request_partial_firmware_into_buf_nofile 2 10 + +test_request_firmware_compressed () +{ + export COMPRESS_FORMAT="$1" + + # test with both files present + compress_both_"$COMPRESS_FORMAT" $FW + compress_both_"$COMPRESS_FORMAT" $FW_INTO_BUF + + config_set_name $NAME + echo + echo "Testing with both plain and $COMPRESS_FORMAT files present..." + do_tests both + + # test with only compressed file present + mv "$FW" "${FW}-orig" + mv "$FW_INTO_BUF" "${FW_INTO_BUF}-orig" + + config_set_name $NAME + echo + echo "Testing with only $COMPRESS_FORMAT file present..." + do_tests componly + + mv "${FW}-orig" "$FW" + mv "${FW_INTO_BUF}-orig" "$FW_INTO_BUF" +} + +compress_both_XZ () +{ + $RUN_XZ -k "$@" +} + +compress_componly_XZ () +{ + $RUN_XZ "$@" +} + +compress_both_ZSTD () +{ + $RUN_ZSTD -k "$@" +} + +compress_componly_ZSTD () +{ + $RUN_ZSTD --rm "$@" +} + +if test "$HAS_FW_LOADER_COMPRESS_XZ" = "yes"; then + test_request_firmware_compressed XZ +fi + +if test "$HAS_FW_LOADER_COMPRESS_ZSTD" = "yes"; then + test_request_firmware_compressed ZSTD +fi + +exit 0 diff --git a/tools/testing/selftests/firmware/fw_lib.sh b/tools/testing/selftests/firmware/fw_lib.sh new file mode 100755 index 000000000..7bffd6780 --- /dev/null +++ b/tools/testing/selftests/firmware/fw_lib.sh @@ -0,0 +1,236 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +# Library of helpers for test scripts. +set -e + +DIR=/sys/devices/virtual/misc/test_firmware + +PROC_CONFIG="/proc/config.gz" +TEST_DIR=$(dirname $0) + +# We need to load a different file to test request_firmware_into_buf +# I believe the issue is firmware loaded cached vs. non-cached +# with same filename is bungled. +# To reproduce rename this to test-firmware.bin +TEST_FIRMWARE_INTO_BUF_FILENAME=test-firmware-into-buf.bin + +# Kselftest framework requirement - SKIP code is 4. +ksft_skip=4 + +print_reqs_exit() +{ + echo "You must have the following enabled in your kernel:" >&2 + cat $TEST_DIR/config >&2 + exit $ksft_skip +} + +test_modprobe() +{ + if [ ! -d $DIR ]; then + print_reqs_exit + fi +} + +check_mods() +{ + local uid=$(id -u) + if [ $uid -ne 0 ]; then + echo "skip all tests: must be run as root" >&2 + exit $ksft_skip + fi + + trap "test_modprobe" EXIT + if [ ! -d $DIR ]; then + modprobe test_firmware + fi + if [ ! -f $PROC_CONFIG ]; then + if modprobe configs 2>/dev/null; then + echo "Loaded configs module" + if [ ! -f $PROC_CONFIG ]; then + echo "You must have the following enabled in your kernel:" >&2 + cat $TEST_DIR/config >&2 + echo "Resorting to old heuristics" >&2 + fi + else + echo "Failed to load configs module, using old heuristics" >&2 + fi + fi +} + +check_setup() +{ + HAS_FW_LOADER_USER_HELPER="$(kconfig_has CONFIG_FW_LOADER_USER_HELPER=y)" + HAS_FW_LOADER_USER_HELPER_FALLBACK="$(kconfig_has CONFIG_FW_LOADER_USER_HELPER_FALLBACK=y)" + HAS_FW_LOADER_COMPRESS_XZ="$(kconfig_has CONFIG_FW_LOADER_COMPRESS_XZ=y)" + HAS_FW_LOADER_COMPRESS_ZSTD="$(kconfig_has CONFIG_FW_LOADER_COMPRESS_ZSTD=y)" + HAS_FW_UPLOAD="$(kconfig_has CONFIG_FW_UPLOAD=y)" + PROC_FW_IGNORE_SYSFS_FALLBACK="0" + PROC_FW_FORCE_SYSFS_FALLBACK="0" + + if [ -z $PROC_SYS_DIR ]; then + PROC_SYS_DIR="/proc/sys/kernel" + fi + + FW_PROC="${PROC_SYS_DIR}/firmware_config" + FW_FORCE_SYSFS_FALLBACK="$FW_PROC/force_sysfs_fallback" + FW_IGNORE_SYSFS_FALLBACK="$FW_PROC/ignore_sysfs_fallback" + + if [ -f $FW_FORCE_SYSFS_FALLBACK ]; then + PROC_FW_FORCE_SYSFS_FALLBACK="$(cat $FW_FORCE_SYSFS_FALLBACK)" + fi + + if [ -f $FW_IGNORE_SYSFS_FALLBACK ]; then + PROC_FW_IGNORE_SYSFS_FALLBACK="$(cat $FW_IGNORE_SYSFS_FALLBACK)" + fi + + if [ "$PROC_FW_FORCE_SYSFS_FALLBACK" = "1" ]; then + HAS_FW_LOADER_USER_HELPER="yes" + HAS_FW_LOADER_USER_HELPER_FALLBACK="yes" + fi + + if [ "$PROC_FW_IGNORE_SYSFS_FALLBACK" = "1" ]; then + HAS_FW_LOADER_USER_HELPER_FALLBACK="no" + HAS_FW_LOADER_USER_HELPER="no" + fi + + if [ "$HAS_FW_LOADER_USER_HELPER" = "yes" ]; then + OLD_TIMEOUT="$(cat /sys/class/firmware/timeout)" + fi + + OLD_FWPATH="$(cat /sys/module/firmware_class/parameters/path)" + + if [ "$HAS_FW_LOADER_COMPRESS_XZ" = "yes" ]; then + if ! which xz 2> /dev/null > /dev/null; then + HAS_FW_LOADER_COMPRESS_XZ="" + fi + fi + if [ "$HAS_FW_LOADER_COMPRESS_ZSTD" = "yes" ]; then + if ! which zstd 2> /dev/null > /dev/null; then + HAS_FW_LOADER_COMPRESS_ZSTD="" + fi + fi +} + +verify_reqs() +{ + if [ "$TEST_REQS_FW_SYSFS_FALLBACK" = "yes" ]; then + if [ ! "$HAS_FW_LOADER_USER_HELPER" = "yes" ]; then + echo "usermode helper disabled so ignoring test" + exit 0 + fi + fi + if [ "$TEST_REQS_FW_UPLOAD" = "yes" ]; then + if [ ! "$HAS_FW_UPLOAD" = "yes" ]; then + echo "firmware upload disabled so ignoring test" + exit 0 + fi + fi +} + +setup_tmp_file() +{ + FWPATH=$(mktemp -d) + FW="$FWPATH/test-firmware.bin" + echo "ABCD0123" >"$FW" + FW_INTO_BUF="$FWPATH/$TEST_FIRMWARE_INTO_BUF_FILENAME" + echo "EFGH4567" >"$FW_INTO_BUF" + NAME=$(basename "$FW") + if [ "$TEST_REQS_FW_SET_CUSTOM_PATH" = "yes" ]; then + echo -n "$FWPATH" >/sys/module/firmware_class/parameters/path + fi +} + +__setup_random_file() +{ + RANDOM_FILE_PATH="$(mktemp -p $FWPATH)" + # mktemp says dry-run -n is unsafe, so... + if [[ "$1" = "fake" ]]; then + rm -rf $RANDOM_FILE_PATH + sync + else + echo "ABCD0123" >"$RANDOM_FILE_PATH" + fi + echo $RANDOM_FILE_PATH +} + +setup_random_file() +{ + echo $(__setup_random_file) +} + +setup_random_file_fake() +{ + echo $(__setup_random_file fake) +} + +proc_set_force_sysfs_fallback() +{ + if [ -f $FW_FORCE_SYSFS_FALLBACK ]; then + echo -n $1 > $FW_FORCE_SYSFS_FALLBACK + check_setup + fi +} + +proc_set_ignore_sysfs_fallback() +{ + if [ -f $FW_IGNORE_SYSFS_FALLBACK ]; then + echo -n $1 > $FW_IGNORE_SYSFS_FALLBACK + check_setup + fi +} + +proc_restore_defaults() +{ + proc_set_force_sysfs_fallback 0 + proc_set_ignore_sysfs_fallback 0 +} + +test_finish() +{ + if [ "$HAS_FW_LOADER_USER_HELPER" = "yes" ]; then + echo "$OLD_TIMEOUT" >/sys/class/firmware/timeout + fi + if [ "$TEST_REQS_FW_SET_CUSTOM_PATH" = "yes" ]; then + if [ "$OLD_FWPATH" = "" ]; then + # A zero-length write won't work; write a null byte + printf '\000' >/sys/module/firmware_class/parameters/path + else + echo -n "$OLD_FWPATH" >/sys/module/firmware_class/parameters/path + fi + fi + if [ -f $FW ]; then + rm -f "$FW" + fi + if [ -f $FW_INTO_BUF ]; then + rm -f "$FW_INTO_BUF" + fi + if [ -d $FWPATH ]; then + rm -rf "$FWPATH" + fi + proc_restore_defaults +} + +kconfig_has() +{ + if [ -f $PROC_CONFIG ]; then + if zgrep -q $1 $PROC_CONFIG 2>/dev/null; then + echo "yes" + else + echo "no" + fi + else + # We currently don't have easy heuristics to infer this + # so best we can do is just try to use the kernel assuming + # you had enabled it. This matches the old behaviour. + if [ "$1" = "CONFIG_FW_LOADER_USER_HELPER_FALLBACK=y" ]; then + echo "yes" + elif [ "$1" = "CONFIG_FW_LOADER_USER_HELPER=y" ]; then + if [ -d /sys/class/firmware/ ]; then + echo yes + else + echo no + fi + fi + fi +} diff --git a/tools/testing/selftests/firmware/fw_namespace.c b/tools/testing/selftests/firmware/fw_namespace.c new file mode 100644 index 000000000..4c6f0cd83 --- /dev/null +++ b/tools/testing/selftests/firmware/fw_namespace.c @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Test triggering of loading of firmware from different mount + * namespaces. Expect firmware to be always loaded from the mount + * namespace of PID 1. */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <sched.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#ifndef CLONE_NEWNS +# define CLONE_NEWNS 0x00020000 +#endif + +static char *fw_path = NULL; + +static void die(char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + if (fw_path) + unlink(fw_path); + umount("/lib/firmware"); + exit(EXIT_FAILURE); +} + +static void trigger_fw(const char *fw_name, const char *sys_path) +{ + int fd; + + fd = open(sys_path, O_WRONLY); + if (fd < 0) + die("open failed: %s\n", + strerror(errno)); + if (write(fd, fw_name, strlen(fw_name)) != strlen(fw_name)) + exit(EXIT_FAILURE); + close(fd); +} + +static void setup_fw(const char *fw_path) +{ + int fd; + const char fw[] = "ABCD0123"; + + fd = open(fw_path, O_WRONLY | O_CREAT, 0600); + if (fd < 0) + die("open failed: %s\n", + strerror(errno)); + if (write(fd, fw, sizeof(fw) -1) != sizeof(fw) -1) + die("write failed: %s\n", + strerror(errno)); + close(fd); +} + +static bool test_fw_in_ns(const char *fw_name, const char *sys_path, bool block_fw_in_parent_ns) +{ + pid_t child; + + if (block_fw_in_parent_ns) + if (mount("test", "/lib/firmware", "tmpfs", MS_RDONLY, NULL) == -1) + die("blocking firmware in parent ns failed\n"); + + child = fork(); + if (child == -1) { + die("fork failed: %s\n", + strerror(errno)); + } + if (child != 0) { /* parent */ + pid_t pid; + int status; + + pid = waitpid(child, &status, 0); + if (pid == -1) { + die("waitpid failed: %s\n", + strerror(errno)); + } + if (pid != child) { + die("waited for %d got %d\n", + child, pid); + } + if (!WIFEXITED(status)) { + die("child did not terminate cleanly\n"); + } + if (block_fw_in_parent_ns) + umount("/lib/firmware"); + return WEXITSTATUS(status) == EXIT_SUCCESS; + } + + if (unshare(CLONE_NEWNS) != 0) { + die("unshare(CLONE_NEWNS) failed: %s\n", + strerror(errno)); + } + if (mount(NULL, "/", NULL, MS_SLAVE|MS_REC, NULL) == -1) + die("remount root in child ns failed\n"); + + if (!block_fw_in_parent_ns) { + if (mount("test", "/lib/firmware", "tmpfs", MS_RDONLY, NULL) == -1) + die("blocking firmware in child ns failed\n"); + } else + umount("/lib/firmware"); + + trigger_fw(fw_name, sys_path); + + exit(EXIT_SUCCESS); +} + +int main(int argc, char **argv) +{ + const char *fw_name = "test-firmware.bin"; + char *sys_path; + if (argc != 2) + die("usage: %s sys_path\n", argv[0]); + + /* Mount tmpfs to /lib/firmware so we don't have to assume + that it is writable for us.*/ + if (mount("test", "/lib/firmware", "tmpfs", 0, NULL) == -1) + die("mounting tmpfs to /lib/firmware failed\n"); + + sys_path = argv[1]; + if (asprintf(&fw_path, "/lib/firmware/%s", fw_name) < 0) + die("error: failed to build full fw_path\n"); + + setup_fw(fw_path); + + setvbuf(stdout, NULL, _IONBF, 0); + /* Positive case: firmware in PID1 mount namespace */ + printf("Testing with firmware in parent namespace (assumed to be same file system as PID1)\n"); + if (!test_fw_in_ns(fw_name, sys_path, false)) + die("error: failed to access firmware\n"); + + /* Negative case: firmware in child mount namespace, expected to fail */ + printf("Testing with firmware in child namespace\n"); + if (test_fw_in_ns(fw_name, sys_path, true)) + die("error: firmware access did not fail\n"); + + unlink(fw_path); + free(fw_path); + umount("/lib/firmware"); + exit(EXIT_SUCCESS); +} diff --git a/tools/testing/selftests/firmware/fw_run_tests.sh b/tools/testing/selftests/firmware/fw_run_tests.sh new file mode 100755 index 000000000..f6d95a2d5 --- /dev/null +++ b/tools/testing/selftests/firmware/fw_run_tests.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +# This runs all known tests across all known possible configurations we could +# emulate in one run. + +set -e + +TEST_DIR=$(dirname $0) +source $TEST_DIR/fw_lib.sh + +export HAS_FW_LOADER_USER_HELPER="" +export HAS_FW_LOADER_USER_HELPER_FALLBACK="" +export HAS_FW_LOADER_COMPRESS="" + +run_tests() +{ + proc_set_force_sysfs_fallback $1 + proc_set_ignore_sysfs_fallback $2 + $TEST_DIR/fw_filesystem.sh + + proc_set_force_sysfs_fallback $1 + proc_set_ignore_sysfs_fallback $2 + $TEST_DIR/fw_fallback.sh + + proc_set_force_sysfs_fallback $1 + proc_set_ignore_sysfs_fallback $2 + $TEST_DIR/fw_upload.sh +} + +run_test_config_0001() +{ + echo "-----------------------------------------------------" + echo "Running kernel configuration test 1 -- rare" + echo "Emulates:" + echo "CONFIG_FW_LOADER=y" + echo "CONFIG_FW_LOADER_USER_HELPER=n" + echo "CONFIG_FW_LOADER_USER_HELPER_FALLBACK=n" + run_tests 0 1 +} + +run_test_config_0002() +{ + echo "-----------------------------------------------------" + echo "Running kernel configuration test 2 -- distro" + echo "Emulates:" + echo "CONFIG_FW_LOADER=y" + echo "CONFIG_FW_LOADER_USER_HELPER=y" + echo "CONFIG_FW_LOADER_USER_HELPER_FALLBACK=n" + proc_set_ignore_sysfs_fallback 0 + run_tests 0 0 +} + +run_test_config_0003() +{ + echo "-----------------------------------------------------" + echo "Running kernel configuration test 3 -- android" + echo "Emulates:" + echo "CONFIG_FW_LOADER=y" + echo "CONFIG_FW_LOADER_USER_HELPER=y" + echo "CONFIG_FW_LOADER_USER_HELPER_FALLBACK=y" + run_tests 1 0 +} + +check_mods +check_setup + +echo "Running namespace test: " +$TEST_DIR/fw_namespace $DIR/trigger_request +echo "OK" + +if [ -f $FW_FORCE_SYSFS_FALLBACK ]; then + run_test_config_0001 + run_test_config_0002 + run_test_config_0003 +else + echo "Running basic kernel configuration, working with your config" + run_tests +fi diff --git a/tools/testing/selftests/firmware/fw_upload.sh b/tools/testing/selftests/firmware/fw_upload.sh new file mode 100755 index 000000000..c7a6f06c9 --- /dev/null +++ b/tools/testing/selftests/firmware/fw_upload.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# This validates the user-initiated fw upload mechanism of the firmware +# loader. It verifies that one or more firmware devices can be created +# for a device driver. It also verifies the data transfer, the +# cancellation support, and the error flows. +set -e + +TEST_REQS_FW_UPLOAD="yes" +TEST_DIR=$(dirname $0) + +progress_states="preparing transferring programming" +errors="hw-error + timeout + device-busy + invalid-file-size + read-write-error + flash-wearout" +error_abort="user-abort" +fwname1=fw1 +fwname2=fw2 +fwname3=fw3 + +source $TEST_DIR/fw_lib.sh + +check_mods +check_setup +verify_reqs + +trap "upload_finish" EXIT + +upload_finish() { + local fwdevs="$fwname1 $fwname2 $fwname3" + + for name in $fwdevs; do + if [ -e "$DIR/$name" ]; then + echo -n "$name" > "$DIR"/upload_unregister + fi + done +} + +upload_fw() { + local name="$1" + local file="$2" + + echo 1 > "$DIR"/"$name"/loading + cat "$file" > "$DIR"/"$name"/data + echo 0 > "$DIR"/"$name"/loading +} + +verify_fw() { + local name="$1" + local file="$2" + + echo -n "$name" > "$DIR"/config_upload_name + if ! cmp "$file" "$DIR"/upload_read > /dev/null 2>&1; then + echo "$0: firmware compare for $name did not match" >&2 + exit 1 + fi + + echo "$0: firmware upload for $name works" >&2 + return 0 +} + +inject_error() { + local name="$1" + local status="$2" + local error="$3" + + echo 1 > "$DIR"/"$name"/loading + echo -n "inject":"$status":"$error" > "$DIR"/"$name"/data + echo 0 > "$DIR"/"$name"/loading +} + +await_status() { + local name="$1" + local expected="$2" + local status + local i + + let i=0 + while [ $i -lt 50 ]; do + status=$(cat "$DIR"/"$name"/status) + if [ "$status" = "$expected" ]; then + return 0; + fi + sleep 1e-03 + let i=$i+1 + done + + echo "$0: Invalid status: Expected $expected, Actual $status" >&2 + return 1; +} + +await_idle() { + local name="$1" + + await_status "$name" "idle" + return $? +} + +expect_error() { + local name="$1" + local expected="$2" + local error=$(cat "$DIR"/"$name"/error) + + if [ "$error" != "$expected" ]; then + echo "Invalid error: Expected $expected, Actual $error" >&2 + return 1 + fi + + return 0 +} + +random_firmware() { + local bs="$1" + local count="$2" + local file=$(mktemp -p /tmp uploadfwXXX.bin) + + dd if=/dev/urandom of="$file" bs="$bs" count="$count" > /dev/null 2>&1 + echo "$file" +} + +test_upload_cancel() { + local name="$1" + local status + + for status in $progress_states; do + inject_error $name $status $error_abort + if ! await_status $name $status; then + exit 1 + fi + + echo 1 > "$DIR"/"$name"/cancel + + if ! await_idle $name; then + exit 1 + fi + + if ! expect_error $name "$status":"$error_abort"; then + exit 1 + fi + done + + echo "$0: firmware upload cancellation works" + return 0 +} + +test_error_handling() { + local name=$1 + local status + local error + + for status in $progress_states; do + for error in $errors; do + inject_error $name $status $error + + if ! await_idle $name; then + exit 1 + fi + + if ! expect_error $name "$status":"$error"; then + exit 1 + fi + + done + done + echo "$0: firmware upload error handling works" +} + +test_fw_too_big() { + local name=$1 + local fw_too_big=`random_firmware 512 5` + local expected="preparing:invalid-file-size" + + upload_fw $name $fw_too_big + rm -f $fw_too_big + + if ! await_idle $name; then + exit 1 + fi + + if ! expect_error $name $expected; then + exit 1 + fi + + echo "$0: oversized firmware error handling works" +} + +echo -n "$fwname1" > "$DIR"/upload_register +echo -n "$fwname2" > "$DIR"/upload_register +echo -n "$fwname3" > "$DIR"/upload_register + +test_upload_cancel $fwname1 +test_error_handling $fwname1 +test_fw_too_big $fwname1 + +fw_file1=`random_firmware 512 4` +fw_file2=`random_firmware 512 3` +fw_file3=`random_firmware 512 2` + +upload_fw $fwname1 $fw_file1 +upload_fw $fwname2 $fw_file2 +upload_fw $fwname3 $fw_file3 + +verify_fw ${fwname1} ${fw_file1} +verify_fw ${fwname2} ${fw_file2} +verify_fw ${fwname3} ${fw_file3} + +echo -n "$fwname1" > "$DIR"/upload_unregister +echo -n "$fwname2" > "$DIR"/upload_unregister +echo -n "$fwname3" > "$DIR"/upload_unregister + +exit 0 diff --git a/tools/testing/selftests/firmware/settings b/tools/testing/selftests/firmware/settings new file mode 100644 index 000000000..085e664ee --- /dev/null +++ b/tools/testing/selftests/firmware/settings @@ -0,0 +1,8 @@ +# The async firmware timeout is set to 1 second (but ends up being effectively +# 2 seconds). There are 3 test configs, each done with and without firmware +# present, each with 2 "nowait" functions tested 5 times. Expected time for a +# normal execution should be 2 * 3 * 2 * 2 * 5 = 120 seconds for those alone. +# Additionally, fw_fallback may take 5 seconds for internal timeouts in each +# of the 3 configs, so at least another 15 seconds are needed. Add another +# 10 seconds for each testing config: 120 + 15 + 30 +timeout=165 |