xref: /freebsd/usr.bin/truss/tests/truss_test.sh (revision af25f24762307bf606690c0aca6a3205a0515914)
1#
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
5#
6
7# The system calls a program makes vary with the machine and with the
8# run-time linker, so these tests assert which system calls -t may and
9# may not report rather than the exact sequence of them.
10
11require_truss()
12{
13	truss -o /dev/null /usr/bin/true >/dev/null 2>&1 ||
14	    atf_skip "unable to trace a child process here"
15}
16
17# Write the sorted, unique names of the system calls reported in a truss
18# output file to another file.
19syscall_names()
20{
21	sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_.]*\)(.*$/\1/p' "$1" | sort -u > "$2"
22}
23
24# Fail unless every name in a file matches an extended regular expression.
25only_names()
26{
27	if grep -Ev "$1" "$2" > unexpected; then
28		atf_fail "reported system calls not selected by the filter:" \
29		    "$(tr '\n' ' ' < unexpected)"
30	fi
31}
32
33# Fail if any name in a file matches an extended regular expression.
34no_name()
35{
36	if grep -E "$1" "$2" > unexpected; then
37		atf_fail "system calls excluded by the filter were reported:" \
38		    "$(tr '\n' ' ' < unexpected)"
39	fi
40}
41
42# Fail unless at least one name in a file matches.
43some_name()
44{
45	grep -Eq "$1" "$2" ||
46	    atf_fail "no system call matching $1 was reported"
47}
48
49atf_test_case list
50list_head()
51{
52	atf_set descr "-t with no expression prints the available groups"
53}
54list_body()
55{
56	atf_check -s exit:2 -o match:'@all' -o match:'@none' \
57	    -o match:'@read' -o match:'@write' -o match:'@file' \
58	    -o match:'@net' \
59	    truss -t
60}
61
62atf_test_case unknown_group
63unknown_group_head()
64{
65	atf_set descr "an unknown @group is rejected"
66}
67unknown_group_body()
68{
69	atf_check -s exit:1 -e match:'unknown system call group @nosuch' \
70	    truss -t @nosuch /usr/bin/true
71}
72
73atf_test_case empty_term
74empty_term_head()
75{
76	atf_set descr "an empty term adds nothing to the expression"
77}
78empty_term_body()
79{
80	require_truss
81	printf 'hello\n' > input
82
83	# An empty expression filters nothing, as if -t were absent.
84	atf_check -s exit:0 -o inline:"hello\n" truss -o out -t '' cat input
85	syscall_names out names
86	some_name '^read$' names
87	some_name '^openat$' names
88
89	# A stray comma is ignored rather than being an error.
90	atf_check -s exit:0 -o inline:"hello\n" \
91	    truss -o out2 -t ',read,,write,' cat input
92	syscall_names out2 names2
93	only_names '^(read|write)$' names2
94	some_name '^read$' names2
95
96	# A negation with nothing to negate is still a mistake.
97	atf_check -s exit:1 -e match:"missing pattern after" \
98	    truss -t '!' /usr/bin/true
99}
100
101atf_test_case none
102none_head()
103{
104	atf_set descr "@none selects no system call"
105}
106none_body()
107{
108	require_truss
109	printf 'hello\n' > input
110
111	# @none's member list is the single negated member "!*", so this
112	# also covers a group whose members exclude rather than include.
113	atf_check -s exit:0 -o inline:"hello\n" truss -o out -t @none cat input
114	syscall_names out names
115	atf_check -o empty cat names
116
117	atf_check -s exit:0 -o inline:"hello\n" \
118	    truss -o out2 -t '!@none' cat input
119	syscall_names out2 names2
120	some_name '^read$' names2
121	some_name '^openat$' names2
122
123	# @none is the empty set rather than a switch: it selects nothing
124	# and leaves the terms before it alone.
125	atf_check -s exit:0 -o inline:"hello\n" \
126	    truss -o out3 -t '@file,@none' cat input
127	syscall_names out3 names3
128	some_name '^openat$' names3
129}
130
131atf_test_case by_number
132by_number_head()
133{
134	atf_set descr "a term may name a system call by number"
135}
136by_number_body()
137{
138	require_truss
139	printf 'hello\n' > input
140
141	# 3 and 4 have been read(2) and write(2) since 4.2BSD.
142	atf_check -s exit:0 -o inline:"hello\n" -e empty \
143	    truss -o out -t 3 cat input
144	syscall_names out names
145	only_names '^read$' names
146	some_name '^read$' names
147
148	atf_check -s exit:0 -o inline:"hello\n" -e empty \
149	    truss -o out2 -t 3,4 cat input
150	syscall_names out2 names2
151	only_names '^(read|write)$' names2
152
153	# Numeric terms negate like any other.
154	atf_check -s exit:0 -o inline:"hello\n" -e empty \
155	    truss -o out3 -t '!3' cat input
156	syscall_names out3 names3
157	no_name '^read$' names3
158	some_name '.' names3
159
160	# Leading zeroes are still just a number.
161	atf_check -s exit:0 -o inline:"hello\n" -e empty \
162	    truss -o out4 -t 003 cat input
163	syscall_names out4 names4
164	only_names '^read$' names4
165}
166
167atf_test_case bad_number
168bad_number_head()
169{
170	atf_set descr "only an unrepresentable system call number is rejected"
171}
172bad_number_body()
173{
174	require_truss
175	printf 'hello\n' > input
176
177	# A process may issue any number the kernel can hold, so a number
178	# beyond the tables truss knows is not second-guessed.  It simply
179	# does not match anything this program happens to call.
180	atf_check -s exit:0 -o inline:"hello\n" -e empty \
181	    truss -o out -t 99999 cat input
182	syscall_names out names
183	atf_check -o empty cat names
184
185	# Too large to be a system call number at all: an error.
186	atf_check -s exit:1 -e match:'system call number is too large' \
187	    truss -t 4294967296 /usr/bin/true
188}
189
190atf_test_case unknown_syscall
191unknown_syscall_head()
192{
193	atf_set descr "a term matching no system call warns but still runs"
194}
195unknown_syscall_body()
196{
197	require_truss
198	printf 'hello\n' > input
199
200	# A typo is a warning, not an error: the command still runs.
201	atf_check -s exit:0 -o inline:"hello\n" \
202	    -e match:'opne: matches no known system call' \
203	    truss -o out -t opne cat input
204	syscall_names out names
205	atf_check -o empty cat names
206
207	# So is a pattern that can never match.
208	atf_check -s exit:0 -o inline:"hello\n" \
209	    -e match:'matches no known system call' \
210	    truss -o out2 -t 'raed*' cat input
211
212	# Names of every ABI truss knows are accepted without complaint,
213	# whether or not that ABI is the one being traced here.
214	for name in read openat linux_write linux_newstat compat11.stat; do
215		atf_check -s exit:0 -o inline:"hello\n" -e empty \
216		    truss -o out3 -t "$name" cat input
217	done
218
219	# A number is the way to name a system call by number; '#' is not
220	# a prefix truss accepts, so it is diagnosed like any other typo.
221	atf_check -s exit:0 -o inline:"hello\n" \
222	    -e match:'matches no known system call' \
223	    truss -o out4 -t '#237' cat input
224}
225
226atf_test_case by_name
227by_name_head()
228{
229	atf_set descr "a term naming one system call selects only that one"
230}
231by_name_body()
232{
233	require_truss
234	printf 'hello\n' > input
235
236	atf_check -s exit:0 -o inline:"hello\n" truss -o out -t read cat input
237	syscall_names out names
238	only_names '^read$' names
239	some_name '^read$' names
240}
241
242atf_test_case by_pattern
243by_pattern_head()
244{
245	atf_set descr "a term may be an fnmatch(3) pattern"
246}
247by_pattern_body()
248{
249	require_truss
250	atf_check ln -s target link
251
252	atf_check -s exit:0 -o ignore truss -o out -t 'readlink*' readlink link
253	syscall_names out names
254	only_names '^readlink' names
255	some_name '^readlink' names
256}
257
258atf_test_case group
259group_head()
260{
261	atf_set descr "an @group selects the system calls it names"
262}
263group_body()
264{
265	require_truss
266	printf 'hello\n' > input
267
268	atf_check -s exit:0 -o inline:"hello\n" truss -o out -t @read cat input
269	syscall_names out names
270	some_name '^read$' names
271	no_name '^(openat|close|mmap|munmap|mprotect)$' names
272}
273
274atf_test_case group_read_excludes_readlink
275group_read_excludes_readlink_head()
276{
277	atf_set descr "@read selects I/O reads but not readlink(2)"
278}
279group_read_excludes_readlink_body()
280{
281	require_truss
282	atf_check ln -s target link
283
284	# readlink(1) calls readlink(2), which @read must not select even
285	# though "read*" does.
286	atf_check -s exit:0 -o ignore truss -o out -t @read readlink link
287	syscall_names out names
288	no_name '^readlink' names
289	some_name '^read$' names
290
291	atf_check -s exit:0 -o ignore truss -o out2 -t 'read*' readlink link
292	syscall_names out2 names2
293	some_name '^readlink' names2
294}
295
296atf_test_case group_reference
297group_reference_head()
298{
299	atf_set descr "@desc includes the members of @read and @write"
300}
301group_reference_body()
302{
303	require_truss
304	printf 'hello\n' > input
305
306	atf_check -s exit:0 -o inline:"hello\n" truss -o out -t @desc cat input
307	syscall_names out names
308	some_name '^read$' names
309	some_name '^close$' names
310}
311
312atf_test_case negation
313negation_head()
314{
315	atf_set descr "a term prefixed with ! excludes what it matches"
316}
317negation_body()
318{
319	require_truss
320
321	atf_check -s exit:0 -o ignore truss -o out -t '!@all' /usr/bin/true
322	syscall_names out names
323	atf_check -o empty cat names
324
325	atf_check -s exit:0 -o ignore truss -o out2 -t '!@memory' /usr/bin/true
326	syscall_names out2 names2
327	no_name '^(mmap|munmap|mprotect)$' names2
328	some_name '.' names2
329}
330
331atf_test_case order
332order_head()
333{
334	atf_set descr "the last term to match a system call wins"
335}
336order_body()
337{
338	require_truss
339	printf 'hello\n' > input
340
341	atf_check -s exit:0 -o ignore truss -o out -t 'read,!read' cat input
342	syscall_names out names
343	atf_check -o empty cat names
344
345	atf_check -s exit:0 -o ignore truss -o out2 -t '!read,read' cat input
346	syscall_names out2 names2
347	only_names '^read$' names2
348	some_name '^read$' names2
349}
350
351atf_test_case accumulate
352accumulate_head()
353{
354	atf_set descr "repeating -t appends to the expression"
355}
356accumulate_body()
357{
358	require_truss
359	atf_check ln -s target link
360
361	atf_check -s exit:0 -o ignore \
362	    truss -o out -t read -t readlink readlink link
363	syscall_names out names
364	only_names '^(read|readlink)$' names
365	some_name '^read$' names
366	some_name '^readlink$' names
367}
368
369atf_test_case count
370count_head()
371{
372	atf_set descr "-c counts only the selected system calls"
373}
374count_body()
375{
376	require_truss
377	printf 'hello\n' > input
378
379	atf_check -s exit:0 -o inline:"hello\n" \
380	    truss -c -o out -t read cat input
381	atf_check -o match:'^read ' cat out
382	atf_check -s exit:1 -o empty grep -q '^openat' out
383}
384
385atf_init_test_cases()
386{
387	atf_add_test_case list
388	atf_add_test_case unknown_group
389	atf_add_test_case empty_term
390	atf_add_test_case none
391	atf_add_test_case by_number
392	atf_add_test_case bad_number
393	atf_add_test_case unknown_syscall
394	atf_add_test_case by_name
395	atf_add_test_case by_pattern
396	atf_add_test_case group
397	atf_add_test_case group_read_excludes_readlink
398	atf_add_test_case group_reference
399	atf_add_test_case negation
400	atf_add_test_case order
401	atf_add_test_case accumulate
402	atf_add_test_case count
403}
404