1#!/bin/bash
2# Copyright (C) 2017 Luis R. Rodriguez <[email protected]>
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"
35
36test_modprobe()
37{
38       if [ ! -d $DIR ]; then
39               echo "$0: $DIR not present" >&2
40               echo "You must have the following enabled in your kernel:" >&2
41               cat $TEST_DIR/config >&2
42               exit 1
43       fi
44}
45
46function allow_user_defaults()
47{
48	if [ -z $DIR ]; then
49		DIR="/sys/module/test_sysctl/"
50	fi
51	if [ -z $DEFAULT_NUM_TESTS ]; then
52		DEFAULT_NUM_TESTS=50
53	fi
54	if [ -z $SYSCTL ]; then
55		SYSCTL="/proc/sys/debug/test_sysctl"
56	fi
57	if [ -z $PROD_SYSCTL ]; then
58		PROD_SYSCTL="/proc/sys"
59	fi
60	if [ -z $WRITES_STRICT ]; then
61		WRITES_STRICT="${PROD_SYSCTL}/kernel/sysctl_writes_strict"
62	fi
63}
64
65function check_production_sysctl_writes_strict()
66{
67	echo -n "Checking production write strict setting ... "
68	if [ ! -e ${WRITES_STRICT} ]; then
69		echo "FAIL, but skip in case of old kernel" >&2
70	else
71		old_strict=$(cat ${WRITES_STRICT})
72		if [ "$old_strict" = "1" ]; then
73			echo "ok"
74		else
75			echo "FAIL, strict value is 0 but force to 1 to continue" >&2
76			echo "1" > ${WRITES_STRICT}
77		fi
78	fi
79
80	if [ -z $PAGE_SIZE ]; then
81		PAGE_SIZE=$(getconf PAGESIZE)
82	fi
83	if [ -z $MAX_DIGITS ]; then
84		MAX_DIGITS=$(($PAGE_SIZE/8))
85	fi
86	if [ -z $INT_MAX ]; then
87		INT_MAX=$(getconf INT_MAX)
88	fi
89}
90
91test_reqs()
92{
93	uid=$(id -u)
94	if [ $uid -ne 0 ]; then
95		echo $msg must be run as root >&2
96		exit 0
97	fi
98
99	if ! which perl 2> /dev/null > /dev/null; then
100		echo "$0: You need perl installed"
101		exit 1
102	fi
103	if ! which getconf 2> /dev/null > /dev/null; then
104		echo "$0: You need getconf installed"
105		exit 1
106	fi
107}
108
109function load_req_mod()
110{
111	trap "test_modprobe" EXIT
112
113	if [ ! -d $DIR ]; then
114		modprobe $TEST_DRIVER
115		if [ $? -ne 0 ]; then
116			exit
117		fi
118	fi
119}
120
121reset_vals()
122{
123	VAL=""
124	TRIGGER=$(basename ${TARGET})
125	case "$TRIGGER" in
126		int_0001)
127			VAL="60"
128			;;
129		int_0002)
130			VAL="1"
131			;;
132		string_0001)
133			VAL="(none)"
134			;;
135		*)
136			;;
137	esac
138	echo -n $VAL > $TARGET
139}
140
141set_orig()
142{
143	if [ ! -z $TARGET ]; then
144		echo "${ORIG}" > "${TARGET}"
145	fi
146}
147
148set_test()
149{
150	echo "${TEST_STR}" > "${TARGET}"
151}
152
153verify()
154{
155	local seen
156	seen=$(cat "$1")
157	if [ "${seen}" != "${TEST_STR}" ]; then
158		return 1
159	fi
160	return 0
161}
162
163test_rc()
164{
165	if [[ $rc != 0 ]]; then
166		echo "Failed test, return value: $rc" >&2
167		exit $rc
168	fi
169}
170
171test_finish()
172{
173	set_orig
174	rm -f "${TEST_FILE}"
175
176	if [ ! -z ${old_strict} ]; then
177		echo ${old_strict} > ${WRITES_STRICT}
178	fi
179	exit $rc
180}
181
182run_numerictests()
183{
184	echo "== Testing sysctl behavior against ${TARGET} =="
185
186	rc=0
187
188	echo -n "Writing test file ... "
189	echo "${TEST_STR}" > "${TEST_FILE}"
190	if ! verify "${TEST_FILE}"; then
191		echo "FAIL" >&2
192		exit 1
193	else
194		echo "ok"
195	fi
196
197	echo -n "Checking sysctl is not set to test value ... "
198	if verify "${TARGET}"; then
199		echo "FAIL" >&2
200		exit 1
201	else
202		echo "ok"
203	fi
204
205	echo -n "Writing sysctl from shell ... "
206	set_test
207	if ! verify "${TARGET}"; then
208		echo "FAIL" >&2
209		exit 1
210	else
211		echo "ok"
212	fi
213
214	echo -n "Resetting sysctl to original value ... "
215	set_orig
216	if verify "${TARGET}"; then
217		echo "FAIL" >&2
218		exit 1
219	else
220		echo "ok"
221	fi
222
223	# Now that we've validated the sanity of "set_test" and "set_orig",
224	# we can use those functions to set starting states before running
225	# specific behavioral tests.
226
227	echo -n "Writing entire sysctl in single write ... "
228	set_orig
229	dd if="${TEST_FILE}" of="${TARGET}" bs=4096 2>/dev/null
230	if ! verify "${TARGET}"; then
231		echo "FAIL" >&2
232		rc=1
233	else
234		echo "ok"
235	fi
236
237	echo -n "Writing middle of sysctl after synchronized seek ... "
238	set_test
239	dd if="${TEST_FILE}" of="${TARGET}" bs=1 seek=1 skip=1 2>/dev/null
240	if ! verify "${TARGET}"; then
241		echo "FAIL" >&2
242		rc=1
243	else
244		echo "ok"
245	fi
246
247	echo -n "Writing beyond end of sysctl ... "
248	set_orig
249	dd if="${TEST_FILE}" of="${TARGET}" bs=20 seek=2 2>/dev/null
250	if verify "${TARGET}"; then
251		echo "FAIL" >&2
252		rc=1
253	else
254		echo "ok"
255	fi
256
257	echo -n "Writing sysctl with multiple long writes ... "
258	set_orig
259	(perl -e 'print "A" x 50;'; echo "${TEST_STR}") | \
260		dd of="${TARGET}" bs=50 2>/dev/null
261	if verify "${TARGET}"; then
262		echo "FAIL" >&2
263		rc=1
264	else
265		echo "ok"
266	fi
267	test_rc
268}
269
270# Your test must accept digits 3 and 4 to use this
271run_limit_digit()
272{
273	echo -n "Checking ignoring spaces up to PAGE_SIZE works on write ..."
274	reset_vals
275
276	LIMIT=$((MAX_DIGITS -1))
277	TEST_STR="3"
278	(perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \
279		dd of="${TARGET}" 2>/dev/null
280
281	if ! verify "${TARGET}"; then
282		echo "FAIL" >&2
283		rc=1
284	else
285		echo "ok"
286	fi
287	test_rc
288
289	echo -n "Checking passing PAGE_SIZE of spaces fails on write ..."
290	reset_vals
291
292	LIMIT=$((MAX_DIGITS))
293	TEST_STR="4"
294	(perl -e 'print " " x '$LIMIT';'; echo "${TEST_STR}") | \
295		dd of="${TARGET}" 2>/dev/null
296
297	if verify "${TARGET}"; then
298		echo "FAIL" >&2
299		rc=1
300	else
301		echo "ok"
302	fi
303	test_rc
304}
305
306# You are using an int
307run_limit_digit_int()
308{
309	echo -n "Testing INT_MAX works ..."
310	reset_vals
311	TEST_STR="$INT_MAX"
312	echo -n $TEST_STR > $TARGET
313
314	if ! verify "${TARGET}"; then
315		echo "FAIL" >&2
316		rc=1
317	else
318		echo "ok"
319	fi
320	test_rc
321
322	echo -n "Testing INT_MAX + 1 will fail as expected..."
323	reset_vals
324	let TEST_STR=$INT_MAX+1
325	echo -n $TEST_STR > $TARGET 2> /dev/null
326
327	if verify "${TARGET}"; then
328		echo "FAIL" >&2
329		rc=1
330	else
331		echo "ok"
332	fi
333	test_rc
334
335	echo -n "Testing negative values will work as expected..."
336	reset_vals
337	TEST_STR="-3"
338	echo -n $TEST_STR > $TARGET 2> /dev/null
339	if ! verify "${TARGET}"; then
340		echo "FAIL" >&2
341		rc=1
342	else
343		echo "ok"
344	fi
345	test_rc
346}
347
348run_stringtests()
349{
350	echo -n "Writing entire sysctl in short writes ... "
351	set_orig
352	dd if="${TEST_FILE}" of="${TARGET}" bs=1 2>/dev/null
353	if ! verify "${TARGET}"; then
354		echo "FAIL" >&2
355		rc=1
356	else
357		echo "ok"
358	fi
359
360	echo -n "Writing middle of sysctl after unsynchronized seek ... "
361	set_test
362	dd if="${TEST_FILE}" of="${TARGET}" bs=1 seek=1 2>/dev/null
363	if verify "${TARGET}"; then
364		echo "FAIL" >&2
365		rc=1
366	else
367		echo "ok"
368	fi
369
370	echo -n "Checking sysctl maxlen is at least $MAXLEN ... "
371	set_orig
372	perl -e 'print "A" x ('"${MAXLEN}"'-2), "B";' | \
373		dd of="${TARGET}" bs="${MAXLEN}" 2>/dev/null
374	if ! grep -q B "${TARGET}"; then
375		echo "FAIL" >&2
376		rc=1
377	else
378		echo "ok"
379	fi
380
381	echo -n "Checking sysctl keeps original string on overflow append ... "
382	set_orig
383	perl -e 'print "A" x ('"${MAXLEN}"'-1), "B";' | \
384		dd of="${TARGET}" bs=$(( MAXLEN - 1 )) 2>/dev/null
385	if grep -q B "${TARGET}"; then
386		echo "FAIL" >&2
387		rc=1
388	else
389		echo "ok"
390	fi
391
392	echo -n "Checking sysctl stays NULL terminated on write ... "
393	set_orig
394	perl -e 'print "A" x ('"${MAXLEN}"'-1), "B";' | \
395		dd of="${TARGET}" bs="${MAXLEN}" 2>/dev/null
396	if grep -q B "${TARGET}"; then
397		echo "FAIL" >&2
398		rc=1
399	else
400		echo "ok"
401	fi
402
403	echo -n "Checking sysctl stays NULL terminated on overwrite ... "
404	set_orig
405	perl -e 'print "A" x ('"${MAXLEN}"'-1), "BB";' | \
406		dd of="${TARGET}" bs=$(( $MAXLEN + 1 )) 2>/dev/null
407	if grep -q B "${TARGET}"; then
408		echo "FAIL" >&2
409		rc=1
410	else
411		echo "ok"
412	fi
413
414	test_rc
415}
416
417sysctl_test_0001()
418{
419	TARGET="${SYSCTL}/int_0001"
420	reset_vals
421	ORIG=$(cat "${TARGET}")
422	TEST_STR=$(( $ORIG + 1 ))
423
424	run_numerictests
425	run_limit_digit
426}
427
428sysctl_test_0002()
429{
430	TARGET="${SYSCTL}/string_0001"
431	reset_vals
432	ORIG=$(cat "${TARGET}")
433	TEST_STR="Testing sysctl"
434	# Only string sysctls support seeking/appending.
435	MAXLEN=65
436
437	run_numerictests
438	run_stringtests
439}
440
441sysctl_test_0003()
442{
443	TARGET="${SYSCTL}/int_0002"
444	reset_vals
445	ORIG=$(cat "${TARGET}")
446	TEST_STR=$(( $ORIG + 1 ))
447
448	run_numerictests
449	run_limit_digit
450	run_limit_digit_int
451}
452
453list_tests()
454{
455	echo "Test ID list:"
456	echo
457	echo "TEST_ID x NUM_TEST"
458	echo "TEST_ID:   Test ID"
459	echo "NUM_TESTS: Number of recommended times to run the test"
460	echo
461	echo "0001 x $(get_test_count 0001) - tests proc_dointvec_minmax()"
462	echo "0002 x $(get_test_count 0002) - tests proc_dostring()"
463	echo "0003 x $(get_test_count 0003) - tests proc_dointvec()"
464}
465
466test_reqs
467
468usage()
469{
470	NUM_TESTS=$(grep -o ' ' <<<"$ALL_TESTS" | grep -c .)
471	let NUM_TESTS=$NUM_TESTS+1
472	MAX_TEST=$(printf "%04d\n" $NUM_TESTS)
473	echo "Usage: $0 [ -t <4-number-digit> ] | [ -w <4-number-digit> ] |"
474	echo "		 [ -s <4-number-digit> ] | [ -c <4-number-digit> <test- count>"
475	echo "           [ all ] [ -h | --help ] [ -l ]"
476	echo ""
477	echo "Valid tests: 0001-$MAX_TEST"
478	echo ""
479	echo "    all     Runs all tests (default)"
480	echo "    -t      Run test ID the number amount of times is recommended"
481	echo "    -w      Watch test ID run until it runs into an error"
482	echo "    -c      Run test ID once"
483	echo "    -s      Run test ID x test-count number of times"
484	echo "    -l      List all test ID list"
485	echo " -h|--help  Help"
486	echo
487	echo "If an error every occurs execution will immediately terminate."
488	echo "If you are adding a new test try using -w <test-ID> first to"
489	echo "make sure the test passes a series of tests."
490	echo
491	echo Example uses:
492	echo
493	echo "$TEST_NAME.sh            -- executes all tests"
494	echo "$TEST_NAME.sh -t 0002    -- Executes test ID 0002 number of times is recomended"
495	echo "$TEST_NAME.sh -w 0002    -- Watch test ID 0002 run until an error occurs"
496	echo "$TEST_NAME.sh -s 0002    -- Run test ID 0002 once"
497	echo "$TEST_NAME.sh -c 0002 3  -- Run test ID 0002 three times"
498	echo
499	list_tests
500	exit 1
501}
502
503function test_num()
504{
505	re='^[0-9]+$'
506	if ! [[ $1 =~ $re ]]; then
507		usage
508	fi
509}
510
511function get_test_count()
512{
513	test_num $1
514	TEST_DATA=$(echo $ALL_TESTS | awk '{print $'$1'}')
515	LAST_TWO=${TEST_DATA#*:*}
516	echo ${LAST_TWO%:*}
517}
518
519function get_test_enabled()
520{
521	test_num $1
522	TEST_DATA=$(echo $ALL_TESTS | awk '{print $'$1'}')
523	echo ${TEST_DATA#*:*:}
524}
525
526function run_all_tests()
527{
528	for i in $ALL_TESTS ; do
529		TEST_ID=${i%:*:*}
530		ENABLED=$(get_test_enabled $TEST_ID)
531		TEST_COUNT=$(get_test_count $TEST_ID)
532		if [[ $ENABLED -eq "1" ]]; then
533			test_case $TEST_ID $TEST_COUNT
534		fi
535	done
536}
537
538function watch_log()
539{
540	if [ $# -ne 3 ]; then
541		clear
542	fi
543	date
544	echo "Running test: $2 - run #$1"
545}
546
547function watch_case()
548{
549	i=0
550	while [ 1 ]; do
551
552		if [ $# -eq 1 ]; then
553			test_num $1
554			watch_log $i ${TEST_NAME}_test_$1
555			${TEST_NAME}_test_$1
556		else
557			watch_log $i all
558			run_all_tests
559		fi
560		let i=$i+1
561	done
562}
563
564function test_case()
565{
566	NUM_TESTS=$DEFAULT_NUM_TESTS
567	if [ $# -eq 2 ]; then
568		NUM_TESTS=$2
569	fi
570
571	i=0
572	while [ $i -lt $NUM_TESTS ]; do
573		test_num $1
574		watch_log $i ${TEST_NAME}_test_$1 noclear
575		RUN_TEST=${TEST_NAME}_test_$1
576		$RUN_TEST
577		let i=$i+1
578	done
579}
580
581function parse_args()
582{
583	if [ $# -eq 0 ]; then
584		run_all_tests
585	else
586		if [[ "$1" = "all" ]]; then
587			run_all_tests
588		elif [[ "$1" = "-w" ]]; then
589			shift
590			watch_case $@
591		elif [[ "$1" = "-t" ]]; then
592			shift
593			test_num $1
594			test_case $1 $(get_test_count $1)
595		elif [[ "$1" = "-c" ]]; then
596			shift
597			test_num $1
598			test_num $2
599			test_case $1 $2
600		elif [[ "$1" = "-s" ]]; then
601			shift
602			test_case $1 1
603		elif [[ "$1" = "-l" ]]; then
604			list_tests
605		elif [[ "$1" = "-h" || "$1" = "--help" ]]; then
606			usage
607		else
608			usage
609		fi
610	fi
611}
612
613test_reqs
614allow_user_defaults
615check_production_sysctl_writes_strict
616load_req_mod
617
618trap "test_finish" EXIT
619
620parse_args $@
621
622exit 0
623