1#!/bin/bash
2# Copyright (C) 2017 Luis R. Rodriguez <mcgrof@kernel.org>
3#
4# This program is free software; you can redistribute it and/or modify it
5# under the terms of the GNU General Public License as published by the Free
6# Software Foundation; either version 2 of the License, or at your option any
7# later version; or, when distributed separately from the Linux kernel or
8# when incorporated into other software packages, subject to the following
9# license:
10#
11# This program is free software; you can redistribute it and/or modify it
12# under the terms of copyleft-next (version 0.3.1 or later) as published
13# at http://copyleft-next.org/.
14
15# This performs a series tests against the proc sysctl interface.
16
17TEST_NAME="sysctl"
18TEST_DRIVER="test_${TEST_NAME}"
19TEST_DIR=$(dirname $0)
20TEST_FILE=$(mktemp)
21
22# This represents
23#
24# TEST_ID:TEST_COUNT:ENABLED
25#
26# TEST_ID: is the test id number
27# TEST_COUNT: number of times we should run the test
28# ENABLED: 1 if enabled, 0 otherwise
29#
30# Once these are enabled please leave them as-is. Write your own test,
31# we have tons of space.
32ALL_TESTS="0001:1:1"
33ALL_TESTS="$ALL_TESTS 0002:1:1"
34ALL_TESTS="$ALL_TESTS 0003:1:1"
35ALL_TESTS="$ALL_TESTS 0004:1:1"
36ALL_TESTS="$ALL_TESTS 0005:3:1"
37
38test_modprobe()
39{
40       if [ ! -d $DIR ]; then
41               echo "$0: $DIR not present" >&2
42               echo "You must have the following enabled in your kernel:" >&2
43               cat $TEST_DIR/config >&2
44               exit 1
45       fi
46}
47
48function allow_user_defaults()
49{
50	if [ -z $DIR ]; then
51		DIR="/sys/module/test_sysctl/"
52	fi
53	if [ -z $DEFAULT_NUM_TESTS ]; then
54		DEFAULT_NUM_TESTS=50
55	fi
56	if [ -z $SYSCTL ]; then
57		SYSCTL="/proc/sys/debug/test_sysctl"
58	fi
59	if [ -z $PROD_SYSCTL ]; then
60		PROD_SYSCTL="/proc/sys"
61	fi
62	if [ -z $WRITES_STRICT ]; then
63		WRITES_STRICT="${PROD_SYSCTL}/kernel/sysctl_writes_strict"
64	fi
65}
66
67function check_production_sysctl_writes_strict()
68{
69	echo -n "Checking production write strict setting ... "
70	if [ ! -e ${WRITES_STRICT} ]; then
71		echo "FAIL, but skip in case of old kernel" >&2
72	else
73		old_strict=$(cat ${WRITES_STRICT})
74		if [ "$old_strict" = "1" ]; then
75			echo "ok"
76		else
77			echo "FAIL, strict value is 0 but force to 1 to continue" >&2
78			echo "1" > ${WRITES_STRICT}
79		fi
80	fi
81
82	if [ -z $PAGE_SIZE ]; then
83		PAGE_SIZE=$(getconf PAGESIZE)
84	fi
85	if [ -z $MAX_DIGITS ]; then
86		MAX_DIGITS=$(($PAGE_SIZE/8))
87	fi
88	if [ -z $INT_MAX ]; then
89		INT_MAX=$(getconf INT_MAX)
90	fi
91	if [ -z $UINT_MAX ]; then
92		UINT_MAX=$(getconf UINT_MAX)
93	fi
94}
95
96test_reqs()
97{
98	uid=$(id -u)
99	if [ $uid -ne 0 ]; then
100		echo $msg must be run as root >&2
101		exit 0
102	fi
103
104	if ! which perl 2> /dev/null > /dev/null; then
105		echo "$0: You need perl installed"
106		exit 1
107	fi
108	if ! which getconf 2> /dev/null > /dev/null; then
109		echo "$0: You need getconf installed"
110		exit 1
111	fi
112	if ! which diff 2> /dev/null > /dev/null; then
113		echo "$0: You need diff installed"
114		exit 1
115	fi
116}
117
118function load_req_mod()
119{
120	trap "test_modprobe" EXIT
121
122	if [ ! -d $DIR ]; then
123		modprobe $TEST_DRIVER
124		if [ $? -ne 0 ]; then
125			exit
126		fi
127	fi
128}
129
130reset_vals()
131{
132	VAL=""
133	TRIGGER=$(basename ${TARGET})
134	case "$TRIGGER" in
135		int_0001)
136			VAL="60"
137			;;
138		int_0002)
139			VAL="1"
140			;;
141		uint_0001)
142			VAL="314"
143			;;
144		string_0001)
145			VAL="(none)"
146			;;
147		*)
148			;;
149	esac
150	echo -n $VAL > $TARGET
151}
152
153set_orig()
154{
155	if [ ! -z $TARGET ]; then
156		echo "${ORIG}" > "${TARGET}"
157	fi
158}
159
160set_test()
161{
162	echo "${TEST_STR}" > "${TARGET}"
163}
164
165verify()
166{
167	local seen
168	seen=$(cat "$1")
169	if [ "${seen}" != "${TEST_STR}" ]; then
170		return 1
171	fi
172	return 0
173}
174
175verify_diff_w()
176{
177	echo "$TEST_STR" | diff -q -w -u - $1
178	return $?
179}
180
181test_rc()
182{
183	if [[ $rc != 0 ]]; then
184		echo "Failed test, return value: $rc" >&2
185		exit $rc
186	fi
187}
188
189test_finish()
190{
191	set_orig
192	rm -f "${TEST_FILE}"
193
194	if [ ! -z ${old_strict} ]; then
195		echo ${old_strict} > ${WRITES_STRICT}
196	fi
197	exit $rc
198}
199
200run_numerictests()
201{
202	echo "== Testing sysctl behavior against ${TARGET} =="
203
204	rc=0
205
206	echo -n "Writing test file ... "
207	echo "${TEST_STR}" > "${TEST_FILE}"
208	if ! verify "${TEST_FILE}"; then
209		echo "FAIL" >&2
210		exit 1
211	else
212		echo "ok"
213	fi
214
215	echo -n "Checking sysctl is not set to test value ... "
216	if verify "${TARGET}"; then
217		echo "FAIL" >&2
218		exit 1
219	else
220		echo "ok"
221	fi
222
223	echo -n "Writing sysctl from shell ... "
224	set_test
225	if ! verify "${TARGET}"; then
226		echo "FAIL" >&2
227		exit 1
228	else
229		echo "ok"
230	fi
231
232	echo -n "Resetting sysctl to original value ... "
233	set_orig
234	if verify "${TARGET}"; then
235		echo "FAIL" >&2
236		exit 1
237	else
238		echo "ok"
239	fi
240
241	# Now that we've validated the sanity of "set_test" and "set_orig",
242	# we can use those functions to set starting states before running
243	# specific behavioral tests.
244
245	echo -n "Writing entire sysctl in single write ... "
246	set_orig
247	dd if="${TEST_FILE}" of="${TARGET}" bs=4096 2>/dev/null
248	if ! verify "${TARGET}"; then
249		echo "FAIL" >&2
250		rc=1
251	else
252		echo "ok"
253	fi
254
255	echo -n "Writing middle of sysctl after synchronized seek ... "
256	set_test
257	dd if="${TEST_FILE}" of="${TARGET}" bs=1 seek=1 skip=1 2>/dev/null
258	if ! verify "${TARGET}"; then
259		echo "FAIL" >&2
260		rc=1
261	else
262		echo "ok"
263	fi
264
265	echo -n "Writing beyond end of sysctl ... "
266	set_orig
267	dd if="${TEST_FILE}" of="${TARGET}" bs=20 seek=2 2>/dev/null
268	if verify "${TARGET}"; then
269		echo "FAIL" >&2
270		rc=1
271	else
272		echo "ok"
273	fi
274
275	echo -n "Writing sysctl with multiple long writes ... "
276	set_orig
277	(perl -e 'print "A" x 50;'; echo "${TEST_STR}") | \
278		dd of="${TARGET}" bs=50 2>/dev/null
279	if verify "${TARGET}"; then
280		echo "FAIL" >&2
281		rc=1
282	else
283		echo "ok"
284	fi
285	test_rc
286}
287
288# Your test must accept digits 3 and 4 to use this
289run_limit_digit()
290{
291	echo -n "Checking ignoring spaces up to PAGE_SIZE works on write ..."
292	reset_vals
293
294	LIMIT=$((MAX_DIGITS -1))
295	TEST_STR="3"
296	(perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \
297		dd of="${TARGET}" 2>/dev/null
298
299	if ! verify "${TARGET}"; then
300		echo "FAIL" >&2
301		rc=1
302	else
303		echo "ok"
304	fi
305	test_rc
306
307	echo -n "Checking passing PAGE_SIZE of spaces fails on write ..."
308	reset_vals
309
310	LIMIT=$((MAX_DIGITS))
311	TEST_STR="4"
312	(perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \
313		dd of="${TARGET}" 2>/dev/null
314
315	if verify "${TARGET}"; then
316		echo "FAIL" >&2
317		rc=1
318	else
319		echo "ok"
320	fi
321	test_rc
322}
323
324# You are using an int
325run_limit_digit_int()
326{
327	echo -n "Testing INT_MAX works ..."
328	reset_vals
329	TEST_STR="$INT_MAX"
330	echo -n $TEST_STR > $TARGET
331
332	if ! verify "${TARGET}"; then
333		echo "FAIL" >&2
334		rc=1
335	else
336		echo "ok"
337	fi
338	test_rc
339
340	echo -n "Testing INT_MAX + 1 will fail as expected..."
341	reset_vals
342	let TEST_STR=$INT_MAX+1
343	echo -n $TEST_STR > $TARGET 2> /dev/null
344
345	if verify "${TARGET}"; then
346		echo "FAIL" >&2
347		rc=1
348	else
349		echo "ok"
350	fi
351	test_rc
352
353	echo -n "Testing negative values will work as expected..."
354	reset_vals
355	TEST_STR="-3"
356	echo -n $TEST_STR > $TARGET 2> /dev/null
357	if ! verify "${TARGET}"; then
358		echo "FAIL" >&2
359		rc=1
360	else
361		echo "ok"
362	fi
363	test_rc
364}
365
366# You used an int array
367run_limit_digit_int_array()
368{
369	echo -n "Testing array works as expected ... "
370	TEST_STR="4 3 2 1"
371	echo -n $TEST_STR > $TARGET
372
373	if ! verify_diff_w "${TARGET}"; then
374		echo "FAIL" >&2
375		rc=1
376	else
377		echo "ok"
378	fi
379	test_rc
380
381	echo -n "Testing skipping trailing array elements works ... "
382	# Do not reset_vals, carry on the values from the last test.
383	# If we only echo in two digits the last two are left intact
384	TEST_STR="100 101"
385	echo -n $TEST_STR > $TARGET
386	# After we echo in, to help diff we need to set on TEST_STR what
387	# we expect the result to be.
388	TEST_STR="100 101 2 1"
389
390	if ! verify_diff_w "${TARGET}"; then
391		echo "FAIL" >&2
392		rc=1
393	else
394		echo "ok"
395	fi
396	test_rc
397
398	echo -n "Testing PAGE_SIZE limit on array works ... "
399	# Do not reset_vals, carry on the values from the last test.
400	# Even if you use an int array, you are still restricted to
401	# MAX_DIGITS, this is a known limitation. Test limit works.
402	LIMIT=$((MAX_DIGITS -1))
403	TEST_STR="9"
404	(perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \
405		dd of="${TARGET}" 2>/dev/null
406
407	TEST_STR="9 101 2 1"
408	if ! verify_diff_w "${TARGET}"; then
409		echo "FAIL" >&2
410		rc=1
411	else
412		echo "ok"
413	fi
414	test_rc
415
416	echo -n "Testing exceeding PAGE_SIZE limit fails as expected ... "
417	# Do not reset_vals, carry on the values from the last test.
418	# Now go over limit.
419	LIMIT=$((MAX_DIGITS))
420	TEST_STR="7"
421	(perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \
422		dd of="${TARGET}" 2>/dev/null
423
424	TEST_STR="7 101 2 1"
425	if verify_diff_w "${TARGET}"; then
426		echo "FAIL" >&2
427		rc=1
428	else
429		echo "ok"
430	fi
431	test_rc
432}
433
434# You are using an unsigned int
435run_limit_digit_uint()
436{
437	echo -n "Testing UINT_MAX works ..."
438	reset_vals
439	TEST_STR="$UINT_MAX"
440	echo -n $TEST_STR > $TARGET
441
442	if ! verify "${TARGET}"; then
443		echo "FAIL" >&2
444		rc=1
445	else
446		echo "ok"
447	fi
448	test_rc
449
450	echo -n "Testing UINT_MAX + 1 will fail as expected..."
451	reset_vals
452	TEST_STR=$(($UINT_MAX+1))
453	echo -n $TEST_STR > $TARGET 2> /dev/null
454
455	if verify "${TARGET}"; then
456		echo "FAIL" >&2
457		rc=1
458	else
459		echo "ok"
460	fi
461	test_rc
462
463	echo -n "Testing negative values will not work as expected ..."
464	reset_vals
465	TEST_STR="-3"
466	echo -n $TEST_STR > $TARGET 2> /dev/null
467
468	if verify "${TARGET}"; then
469		echo "FAIL" >&2
470		rc=1
471	else
472		echo "ok"
473	fi
474	test_rc
475}
476
477run_stringtests()
478{
479	echo -n "Writing entire sysctl in short writes ... "
480	set_orig
481	dd if="${TEST_FILE}" of="${TARGET}" bs=1 2>/dev/null
482	if ! verify "${TARGET}"; then
483		echo "FAIL" >&2
484		rc=1
485	else
486		echo "ok"
487	fi
488
489	echo -n "Writing middle of sysctl after unsynchronized seek ... "
490	set_test
491	dd if="${TEST_FILE}" of="${TARGET}" bs=1 seek=1 2>/dev/null
492	if verify "${TARGET}"; then
493		echo "FAIL" >&2
494		rc=1
495	else
496		echo "ok"
497	fi
498
499	echo -n "Checking sysctl maxlen is at least $MAXLEN ... "
500	set_orig
501	perl -e 'print "A" x ('"${MAXLEN}"'-2), "B";' | \
502		dd of="${TARGET}" bs="${MAXLEN}" 2>/dev/null
503	if ! grep -q B "${TARGET}"; then
504		echo "FAIL" >&2
505		rc=1
506	else
507		echo "ok"
508	fi
509
510	echo -n "Checking sysctl keeps original string on overflow append ... "
511	set_orig
512	perl -e 'print "A" x ('"${MAXLEN}"'-1), "B";' | \
513		dd of="${TARGET}" bs=$(( MAXLEN - 1 )) 2>/dev/null
514	if grep -q B "${TARGET}"; then
515		echo "FAIL" >&2
516		rc=1
517	else
518		echo "ok"
519	fi
520
521	echo -n "Checking sysctl stays NULL terminated on write ... "
522	set_orig
523	perl -e 'print "A" x ('"${MAXLEN}"'-1), "B";' | \
524		dd of="${TARGET}" bs="${MAXLEN}" 2>/dev/null
525	if grep -q B "${TARGET}"; then
526		echo "FAIL" >&2
527		rc=1
528	else
529		echo "ok"
530	fi
531
532	echo -n "Checking sysctl stays NULL terminated on overwrite ... "
533	set_orig
534	perl -e 'print "A" x ('"${MAXLEN}"'-1), "BB";' | \
535		dd of="${TARGET}" bs=$(( $MAXLEN + 1 )) 2>/dev/null
536	if grep -q B "${TARGET}"; then
537		echo "FAIL" >&2
538		rc=1
539	else
540		echo "ok"
541	fi
542
543	test_rc
544}
545
546sysctl_test_0001()
547{
548	TARGET="${SYSCTL}/int_0001"
549	reset_vals
550	ORIG=$(cat "${TARGET}")
551	TEST_STR=$(( $ORIG + 1 ))
552
553	run_numerictests
554	run_limit_digit
555}
556
557sysctl_test_0002()
558{
559	TARGET="${SYSCTL}/string_0001"
560	reset_vals
561	ORIG=$(cat "${TARGET}")
562	TEST_STR="Testing sysctl"
563	# Only string sysctls support seeking/appending.
564	MAXLEN=65
565
566	run_numerictests
567	run_stringtests
568}
569
570sysctl_test_0003()
571{
572	TARGET="${SYSCTL}/int_0002"
573	reset_vals
574	ORIG=$(cat "${TARGET}")
575	TEST_STR=$(( $ORIG + 1 ))
576
577	run_numerictests
578	run_limit_digit
579	run_limit_digit_int
580}
581
582sysctl_test_0004()
583{
584	TARGET="${SYSCTL}/uint_0001"
585	reset_vals
586	ORIG=$(cat "${TARGET}")
587	TEST_STR=$(( $ORIG + 1 ))
588
589	run_numerictests
590	run_limit_digit
591	run_limit_digit_uint
592}
593
594sysctl_test_0005()
595{
596	TARGET="${SYSCTL}/int_0003"
597	reset_vals
598	ORIG=$(cat "${TARGET}")
599
600	run_limit_digit_int_array
601}
602
603list_tests()
604{
605	echo "Test ID list:"
606	echo
607	echo "TEST_ID x NUM_TEST"
608	echo "TEST_ID:   Test ID"
609	echo "NUM_TESTS: Number of recommended times to run the test"
610	echo
611	echo "0001 x $(get_test_count 0001) - tests proc_dointvec_minmax()"
612	echo "0002 x $(get_test_count 0002) - tests proc_dostring()"
613	echo "0003 x $(get_test_count 0003) - tests proc_dointvec()"
614	echo "0004 x $(get_test_count 0004) - tests proc_douintvec()"
615	echo "0005 x $(get_test_count 0005) - tests proc_douintvec() array"
616}
617
618test_reqs
619
620usage()
621{
622	NUM_TESTS=$(grep -o ' ' <<<"$ALL_TESTS" | grep -c .)
623	let NUM_TESTS=$NUM_TESTS+1
624	MAX_TEST=$(printf "%04d\n" $NUM_TESTS)
625	echo "Usage: $0 [ -t <4-number-digit> ] | [ -w <4-number-digit> ] |"
626	echo "		 [ -s <4-number-digit> ] | [ -c <4-number-digit> <test- count>"
627	echo "           [ all ] [ -h | --help ] [ -l ]"
628	echo ""
629	echo "Valid tests: 0001-$MAX_TEST"
630	echo ""
631	echo "    all     Runs all tests (default)"
632	echo "    -t      Run test ID the number amount of times is recommended"
633	echo "    -w      Watch test ID run until it runs into an error"
634	echo "    -c      Run test ID once"
635	echo "    -s      Run test ID x test-count number of times"
636	echo "    -l      List all test ID list"
637	echo " -h|--help  Help"
638	echo
639	echo "If an error every occurs execution will immediately terminate."
640	echo "If you are adding a new test try using -w <test-ID> first to"
641	echo "make sure the test passes a series of tests."
642	echo
643	echo Example uses:
644	echo
645	echo "$TEST_NAME.sh            -- executes all tests"
646	echo "$TEST_NAME.sh -t 0002    -- Executes test ID 0002 number of times is recomended"
647	echo "$TEST_NAME.sh -w 0002    -- Watch test ID 0002 run until an error occurs"
648	echo "$TEST_NAME.sh -s 0002    -- Run test ID 0002 once"
649	echo "$TEST_NAME.sh -c 0002 3  -- Run test ID 0002 three times"
650	echo
651	list_tests
652	exit 1
653}
654
655function test_num()
656{
657	re='^[0-9]+$'
658	if ! [[ $1 =~ $re ]]; then
659		usage
660	fi
661}
662
663function get_test_count()
664{
665	test_num $1
666	TEST_DATA=$(echo $ALL_TESTS | awk '{print $'$1'}')
667	LAST_TWO=${TEST_DATA#*:*}
668	echo ${LAST_TWO%:*}
669}
670
671function get_test_enabled()
672{
673	test_num $1
674	TEST_DATA=$(echo $ALL_TESTS | awk '{print $'$1'}')
675	echo ${TEST_DATA#*:*:}
676}
677
678function run_all_tests()
679{
680	for i in $ALL_TESTS ; do
681		TEST_ID=${i%:*:*}
682		ENABLED=$(get_test_enabled $TEST_ID)
683		TEST_COUNT=$(get_test_count $TEST_ID)
684		if [[ $ENABLED -eq "1" ]]; then
685			test_case $TEST_ID $TEST_COUNT
686		fi
687	done
688}
689
690function watch_log()
691{
692	if [ $# -ne 3 ]; then
693		clear
694	fi
695	date
696	echo "Running test: $2 - run #$1"
697}
698
699function watch_case()
700{
701	i=0
702	while [ 1 ]; do
703
704		if [ $# -eq 1 ]; then
705			test_num $1
706			watch_log $i ${TEST_NAME}_test_$1
707			${TEST_NAME}_test_$1
708		else
709			watch_log $i all
710			run_all_tests
711		fi
712		let i=$i+1
713	done
714}
715
716function test_case()
717{
718	NUM_TESTS=$DEFAULT_NUM_TESTS
719	if [ $# -eq 2 ]; then
720		NUM_TESTS=$2
721	fi
722
723	i=0
724	while [ $i -lt $NUM_TESTS ]; do
725		test_num $1
726		watch_log $i ${TEST_NAME}_test_$1 noclear
727		RUN_TEST=${TEST_NAME}_test_$1
728		$RUN_TEST
729		let i=$i+1
730	done
731}
732
733function parse_args()
734{
735	if [ $# -eq 0 ]; then
736		run_all_tests
737	else
738		if [[ "$1" = "all" ]]; then
739			run_all_tests
740		elif [[ "$1" = "-w" ]]; then
741			shift
742			watch_case $@
743		elif [[ "$1" = "-t" ]]; then
744			shift
745			test_num $1
746			test_case $1 $(get_test_count $1)
747		elif [[ "$1" = "-c" ]]; then
748			shift
749			test_num $1
750			test_num $2
751			test_case $1 $2
752		elif [[ "$1" = "-s" ]]; then
753			shift
754			test_case $1 1
755		elif [[ "$1" = "-l" ]]; then
756			list_tests
757		elif [[ "$1" = "-h" || "$1" = "--help" ]]; then
758			usage
759		else
760			usage
761		fi
762	fi
763}
764
765test_reqs
766allow_user_defaults
767check_production_sysctl_writes_strict
768load_req_mod
769
770trap "test_finish" EXIT
771
772parse_args $@
773
774exit 0
775