xref: /freebsd/usr.bin/truss/syscall_filter.c (revision af25f24762307bf606690c0aca6a3205a0515914)
1 /*
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
5  */
6 
7 /*
8  * Selection of the system calls to report, driven by -t.
9  */
10 
11 #include <sys/param.h>
12 #include <sys/queue.h>
13 
14 #include <err.h>
15 #include <fnmatch.h>
16 #include <signal.h>
17 #include <stdbool.h>
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21 #include <sysdecode.h>
22 
23 #include "truss.h"
24 #include "extern.h"
25 
26 /*
27  * Groups of related system calls, named as "@group" in a -t expression.
28  *
29  * A group's member list is an expression in exactly the form -t accepts,
30  * so that anything a user can write on the command line can also be
31  * written as a group: each member is an fnmatch(3) pattern matched
32  * against the name of a system call, a decimal system call number, or
33  * "@group" naming another group, and any of them may be prefixed with
34  * '!' to exclude rather than include what it matches.  Keeping the two
35  * languages identical is deliberate: a group defined elsewhere, from a
36  * -t expression a user supplied, needs no translation to become a
37  * member list here.
38  *
39  * Patterns are preferred over literal names wherever a family of system
40  * calls shares a naming convention (e.g. "extattr_*_file"), so that
41  * system calls added later are picked up without further change here.
42  * Names from the ABIs truss supports beyond the native one are included
43  * where they differ, since the Linux ABI in particular renames a number
44  * of otherwise familiar system calls.
45  *
46  * Adding a group is a matter of adding a member list here and a single
47  * entry to syscall_groups[] below.
48  */
49 
50 static const char *const group_all[] = {
51 	"*",
52 	NULL
53 };
54 
55 static const char *const group_none[] = {
56 	"!*",
57 	NULL
58 };
59 
60 static const char *const group_read[] = {
61 	"read", "readv", "pread*", "readahead", "readdir",
62 	"recv", "recvfrom", "recvmsg", "recvmmsg*",
63 	"aio_read*", "sctp_generic_recvmsg",
64 	"kmq_timedreceive", "mq_timedreceive*", "msgrcv",
65 	"getdents*", "getdirentries", "copy_file_range", "process_vm_readv",
66 	NULL
67 };
68 
69 static const char *const group_write[] = {
70 	"write", "writev", "pwrite*",
71 	"send", "sendto", "sendmsg", "sendmmsg*", "sendfile*",
72 	"aio_write*", "sctp_generic_send*",
73 	"kmq_timedsend", "mq_timedsend*", "msgsnd",
74 	"copy_file_range", "process_vm_writev",
75 	NULL
76 };
77 
78 static const char *const group_desc[] = {
79 	"@read", "@write",
80 	"close", "close_range", "closefrom", "dup", "dup2", "dup3",
81 	"fcntl", "fcntl64", "flock", "fsync", "fdatasync", "syncfs",
82 	"sync", "sync_file_range", "ftruncate*", "lseek", "llseek", "ioctl",
83 	"poll", "ppoll*", "select", "old_select", "pselect*",
84 	"epoll_*", "kqueue*", "kevent*", "eventfd*", "timerfd_*",
85 	"inotify_*", "signalfd*", "fanotify_*", "pipe", "pipe2",
86 	"fstat", "fstat64", "newfstat", "nfstat", "fstatfs*",
87 	"fchdir", "fchmod", "fchown", "fchflags", "futimes", "futimens",
88 	"fpathconf", "getdtablesize", "fexecve", "fspacectl",
89 	"posix_fadvise", "fadvise64*", "posix_fallocate", "fallocate",
90 	"splice", "tee", "vmsplice", "memfd_create", "__specialfd",
91 	"posix_openpt", "pddupfd", "aio_*", "lio_listio",
92 	"pidfd_*", "io_*", "f*xattr",
93 	"__acl_*_fd", "extattr_*_fd", "__mac_*_fd",
94 	"cap_fcntls_*", "cap_ioctls_*", "cap_rights_limit",
95 	"__cap_rights_get",
96 	NULL
97 };
98 
99 static const char *const group_file[] = {
100 	"open", "openat", "openat2", "open_by_handle_at", "open_tree",
101 	"creat", "stat", "stat64", "newstat", "nstat", "statx",
102 	"lstat", "lstat64", "newlstat", "nlstat",
103 	"fstatat", "fstatat64", "newfstatat", "statfs", "statfs64",
104 	"access", "eaccess", "faccessat*",
105 	"chdir", "chroot", "fchroot", "pivot_root",
106 	"chmod", "lchmod", "fchmodat*",
107 	"chown", "chown16", "lchown", "lchown16", "fchownat",
108 	"chflags", "lchflags", "chflagsat",
109 	"link", "linkat", "symlink", "symlinkat",
110 	"unlink", "unlinkat", "funlinkat",
111 	"rename", "renameat", "renameat2",
112 	"mkdir", "mkdirat", "rmdir", "mknod", "mknodat",
113 	"mkfifo", "mkfifoat", "readlink", "readlinkat",
114 	"truncate", "truncate64",
115 	"utime", "utimes", "lutimes", "utimensat*", "futimesat",
116 	"pathconf", "lpathconf", "__getcwd", "getcwd", "__realpathat",
117 	"revoke", "undelete", "acct", "quotactl*", "umask",
118 	"mount", "nmount", "unmount", "umount", "oldumount", "move_mount",
119 	"fh*", "getfh", "getfhat", "lgetfh", "getfsstat",
120 	"name_to_handle_at", "inotify_add_watch*",
121 	"execve", "execveat", "__mac_execve",
122 	"swapon", "swapoff", "kldload",
123 	"getxattr", "lgetxattr", "setxattr", "lsetxattr",
124 	"removexattr", "lremovexattr", "listxattr", "llistxattr",
125 	"__acl_*_file", "__acl_*_link", "extattr_*_file", "extattr_*_link",
126 	"extattrctl", "__mac_*_file", "__mac_*_link",
127 	NULL
128 };
129 
130 static const char *const group_net[] = {
131 	"socket", "socketcall", "socketpair", "bind", "bindat",
132 	"connect", "connectat", "listen", "accept", "accept4",
133 	"getpeername", "getsockname", "getsockopt", "setsockopt", "shutdown",
134 	"send", "sendto", "sendmsg", "sendmmsg*", "sendfile*",
135 	"recv", "recvfrom", "recvmsg", "recvmmsg*", "sctp_*",
136 	"setfib", "gethostname", "sethostname",
137 	"getdomainname", "setdomainname",
138 	"nfssvc", "nlm_syscall", "rpctls_syscall",
139 	NULL
140 };
141 
142 static const char *const group_proc[] = {
143 	"fork", "vfork", "rfork", "pdfork", "pdrfork", "clone", "clone3",
144 	"execve", "execveat", "fexecve", "__mac_execve",
145 	"_exit", "exit", "exit_group", "abort2",
146 	"wait", "wait4", "wait6", "waitid", "waitpid", "pdwait",
147 	"getpid", "getppid", "gettid", "getpgrp", "getpgid", "setpgid",
148 	"getsid", "setsid", "getpriority", "setpriority", "nice",
149 	"rtprio", "rtprio_thread", "sched_*", "cpuset*",
150 	"procctl", "prctl", "arch_prctl", "ptrace",
151 	"thr_*", "_umtx_*", "futex*", "sys_futex*", "membarrier",
152 	"pdgetpid", "pdkill", "pdopenpid", "pidfd_open", "pidfd_getfd",
153 	"jail*", "kcmp", "getcontext", "setcontext", "swapcontext", "yield",
154 	"getrusage", "getrlimit", "setrlimit", "getrlimitusage",
155 	"old_getrlimit", "prlimit64", "personality", "times", "vhangup",
156 	"set_tid_address", "setns", "unshare", "restart_syscall", "rseq",
157 	"get_robust_list", "set_robust_list",
158 	NULL
159 };
160 
161 static const char *const group_signal[] = {
162 	"sig*", "rt_sig*", "rt_tgsigqueueinfo",
163 	"kill", "killpg", "thr_kill*", "pdkill",
164 	"tkill", "tgkill", "pidfd_send_signal",
165 	"sgetmask", "ssetmask", "pause",
166 	NULL
167 };
168 
169 static const char *const group_memory[] = {
170 	"mmap", "mmap2", "munmap", "mprotect", "pkey_mprotect", "mremap",
171 	"madvise", "process_madvise", "mincore", "minherit",
172 	"mlock", "mlock2", "munlock", "mlockall", "munlockall", "aio_mlock",
173 	"msync", "break", "brk", "sbrk", "vadvise", "getpagesize",
174 	"shm_open*", "shm_unlink", "shm_rename",
175 	"memfd_create", "memfd_secret", "map_shadow_stack", "userfaultfd",
176 	"mbind", "get_mempolicy", "set_mempolicy*",
177 	"migrate_pages", "move_pages", "remap_file_pages",
178 	"pkey_alloc", "pkey_free", "swapon", "swapoff",
179 	NULL
180 };
181 
182 static const char *const group_ipc[] = {
183 	"msgctl", "msgget", "msgrcv", "msgsnd", "msgsys",
184 	"semctl", "__semctl", "semget", "semop", "semsys", "semtimedop*",
185 	"shmat", "shmctl", "shmdt", "shmget", "shmsys",
186 	"ksem_*", "kmq_*", "mq_*", "ipc",
187 	NULL
188 };
189 
190 static const char *const group_creds[] = {
191 	"getuid*", "geteuid*", "getgid*", "getegid*",
192 	"getgroups*", "setgroups*",
193 	"setuid*", "seteuid", "setgid*", "setegid",
194 	"setreuid*", "setregid*", "setresuid*", "setresgid*",
195 	"getresuid*", "getresgid*", "setfsuid*", "setfsgid*",
196 	"issetugid", "__setugid", "setcred",
197 	"getlogin", "setlogin", "getloginclass", "setloginclass",
198 	"getauid", "setauid", "getaudit*", "setaudit*",
199 	"audit", "auditon", "auditctl",
200 	"capget", "capset", "cap_enter", "cap_getmode",
201 	"seccomp", "landlock_*",
202 	NULL
203 };
204 
205 static const char *const group_time[] = {
206 	"clock_*", "nanosleep", "gettimeofday", "settimeofday",
207 	"adjtime", "adjtimex", "ntp_*",
208 	"getitimer", "setitimer", "ktimer_*", "timer_*", "timerfd_*",
209 	"ffclock_*", "time", "stime", "alarm",
210 	NULL
211 };
212 
213 struct syscall_group {
214 	const char *name;
215 	const char *desc;
216 	const char *const *members;
217 };
218 
219 /* Kept in alphabetical order; "truss -t" prints it as-is. */
220 static const struct syscall_group syscall_groups[] = {
221 	{ "all",    "every system call",			group_all },
222 	{ "creds",  "get or set process credentials",		group_creds },
223 	{ "desc",   "operate on a file descriptor",		group_desc },
224 	{ "file",   "operate on a pathname",			group_file },
225 	{ "ipc",    "System V and POSIX IPC",			group_ipc },
226 	{ "memory", "memory mapping and locking",		group_memory },
227 	{ "net",    "network and socket operations",		group_net },
228 	{ "none",   "no system call",				group_none },
229 	{ "proc",   "process and thread lifecycle",		group_proc },
230 	{ "read",   "read data from a descriptor",		group_read },
231 	{ "signal", "signal delivery and handling",		group_signal },
232 	{ "time",   "clocks, timers and sleeping",		group_time },
233 	{ "write",  "write data to a descriptor",		group_write },
234 };
235 
236 /*
237  * One comma-separated term of a -t expression.  Terms are held in the
238  * order they were given: the last one to match a system call decides
239  * whether it is reported.
240  */
241 struct filter_term {
242 	STAILQ_ENTRY(filter_term) entries;
243 	const struct syscall_group *group; /* @group term, else NULL */
244 	char *pattern;			   /* name pattern, else NULL */
245 	u_int number;			   /* number, if by_number */
246 	bool by_number;
247 	bool negate;
248 };
249 
250 static bool term_matches_any_syscall(const struct filter_term *);
251 
252 static STAILQ_HEAD(, filter_term) filter_terms =
253     STAILQ_HEAD_INITIALIZER(filter_terms);
254 
255 /*
256  * Whether a system call matched by no term at all is reported.  An
257  * expression made up only of negated terms subtracts from the full set
258  * of system calls; any other expression selects from an empty one.
259  */
260 static bool filter_default = true;
261 
262 /*
263  * A name reported by sysdecode may carry a prefix naming a compatibility
264  * layer ("compat11.stat"), a non-native ABI ("linux_open",
265  * "freebsd32_ioctl"), or both ("compat4.freebsd32_getfsstat").  Terms are
266  * matched against the name as displayed and against each shortened form,
267  * so that "-t @file" selects stat, compat11.stat and freebsd32_stat alike.
268  */
269 static const char *const abi_prefixes[] = {
270 	"freebsd32_",
271 	"linux_",
272 	"linux32_",
273 };
274 
275 /*
276  * A group referring to itself, directly or through others, would recurse
277  * forever.  The table above has no such cycle; this only keeps a future
278  * mistake in it from hanging truss.
279  */
280 #define	GROUP_MAX_DEPTH	8
281 
282 static const struct syscall_group *
find_group(const char * name)283 find_group(const char *name)
284 {
285 	size_t i;
286 
287 	for (i = 0; i < nitems(syscall_groups); i++) {
288 		if (strcmp(name, syscall_groups[i].name) == 0)
289 			return (&syscall_groups[i]);
290 	}
291 	return (NULL);
292 }
293 
294 static bool group_selects(const struct syscall_group *, const char *, u_int,
295     u_int);
296 
297 static const char *
strip_abi_prefix(const char * name)298 strip_abi_prefix(const char *name)
299 {
300 	size_t i, len;
301 
302 	for (i = 0; i < nitems(abi_prefixes); i++) {
303 		len = strlen(abi_prefixes[i]);
304 		if (strncmp(name, abi_prefixes[i], len) == 0)
305 			return (name + len);
306 	}
307 	return (name);
308 }
309 
310 /*
311  * Expand a system call name into the forms a term may match it under:
312  * the name itself, the name with any "compatN." prefix removed, and that
313  * with any ABI prefix removed as well.  Returns the number of forms.
314  */
315 static u_int
name_forms(const char * name,const char * forms[3])316 name_forms(const char *name, const char *forms[3])
317 {
318 	const char *shorter, *stripped;
319 	u_int nforms;
320 
321 	nforms = 0;
322 	forms[nforms++] = name;
323 	shorter = strrchr(name, '.');
324 	if (shorter != NULL)
325 		forms[nforms++] = ++shorter;
326 	else
327 		shorter = name;
328 	stripped = strip_abi_prefix(shorter);
329 	if (stripped != shorter)
330 		forms[nforms++] = stripped;
331 	return (nforms);
332 }
333 
334 /*
335  * Whether one member of a group selects the given system call.  The
336  * caller has already consumed any leading '!', leaving the same three
337  * forms a -t term may take: a "@group" reference, a decimal system call
338  * number, or an fnmatch(3) pattern matched against the name.
339  */
340 static bool
member_matches(const char * member,const char * name,u_int number,u_int depth)341 member_matches(const char *member, const char *name, u_int number, u_int depth)
342 {
343 	const struct syscall_group *ref;
344 	const char *errstr;
345 	const char *forms[3];
346 	u_int i, nforms, num;
347 
348 	/*
349 	 * A member of "!" alone leaves nothing behind once the caller has
350 	 * consumed the '!'.  The -t parser rejects that outright; say so
351 	 * explicitly here rather than falling into the numeric branch,
352 	 * where an empty string would otherwise be offered to strtonum().
353 	 */
354 	if (*member == '\0')
355 		return (false);
356 
357 	if (*member == '@') {
358 		ref = find_group(member + 1);
359 		return (ref != NULL &&
360 		    group_selects(ref, name, number, depth + 1));
361 	}
362 	if (member[strspn(member, "0123456789")] == '\0') {
363 		num = (u_int)strtonum(member, 0, UINT_MAX, &errstr);
364 		return (errstr == NULL && num == number);
365 	}
366 	nforms = name_forms(name, forms);
367 	for (i = 0; i < nforms; i++) {
368 		if (fnmatch(member, forms[i], 0) == 0)
369 			return (true);
370 	}
371 	return (false);
372 }
373 
374 /*
375  * Whether a group selects the given system call.
376  *
377  * A group's member list is an expression in exactly the form -t accepts,
378  * so that a group can say anything a user can say on the command line:
379  * members apply in order, the last one to match decides, and a list of
380  * only negated members starts from every system call rather than from
381  * none.  "@none" is therefore written as the one member "!*".
382  */
383 static bool
group_selects(const struct syscall_group * group,const char * name,u_int number,u_int depth)384 group_selects(const struct syscall_group *group, const char *name, u_int number,
385     u_int depth)
386 {
387 	const char *const *member;
388 	const char *pattern;
389 	bool negate, selects;
390 
391 	/*
392 	 * A group with no member list at all selects nothing.  The table
393 	 * below has no such entry, but a group built from anywhere less
394 	 * hand-audited should not be able to fault truss.
395 	 */
396 	if (depth >= GROUP_MAX_DEPTH || group->members == NULL)
397 		return (false);
398 
399 	selects = true;
400 	for (member = group->members; *member != NULL; member++) {
401 		if (**member != '!') {
402 			selects = false;
403 			break;
404 		}
405 	}
406 
407 	for (member = group->members; *member != NULL; member++) {
408 		pattern = *member;
409 		negate = *pattern == '!';
410 		if (negate)
411 			pattern++;
412 		if (member_matches(pattern, name, number, depth))
413 			selects = !negate;
414 	}
415 	return (selects);
416 }
417 
418 /* Print the group table ("-t" with no expression). */
419 void
list_syscall_groups(void)420 list_syscall_groups(void)
421 {
422 	size_t i;
423 
424 	printf("System call groups usable as @group in a -t expression:\n\n");
425 	for (i = 0; i < nitems(syscall_groups); i++)
426 		printf("    @%-9s %s\n", syscall_groups[i].name,
427 		    syscall_groups[i].desc);
428 	printf("\n"
429 	    "Any other term is an fnmatch(3) pattern matched against the\n"
430 	    "system call name, so \"read\" selects read(2) alone and \"read*\"\n"
431 	    "also selects readv(2) and readlink(2).  A term prefixed with '!'\n"
432 	    "excludes what that one term matches rather than including it.\n");
433 }
434 
435 /*
436  * Add the terms of one -t expression.  Repeating -t appends to the
437  * expression rather than replacing it.
438  */
439 void
add_syscall_filter(const char * expr)440 add_syscall_filter(const char *expr)
441 {
442 	struct filter_term *term;
443 	const char *errstr;
444 	char *copy, *next, *word;
445 
446 	if ((copy = strdup(expr)) == NULL)
447 		err(1, "strdup");
448 	next = copy;
449 	while ((word = strsep(&next, ",")) != NULL) {
450 		bool negate = false;
451 
452 		if (*word == '!') {
453 			negate = true;
454 			if (*++word == '\0')
455 				errx(1, "missing pattern after '!' in -t %s",
456 				    expr);
457 		}
458 
459 		/*
460 		 * Ignore an empty term so that an empty expression, or one
461 		 * with a stray or trailing comma, adds no terms rather than
462 		 * being an error.  "truss -t ''" thus filters nothing.
463 		 */
464 		if (*word == '\0')
465 			continue;
466 
467 		if ((term = calloc(1, sizeof(*term))) == NULL)
468 			err(1, "calloc");
469 		term->negate = negate;
470 		if (*word == '@') {
471 			term->group = find_group(word + 1);
472 			if (term->group == NULL)
473 				errx(1, "unknown system call group @%s; "
474 				    "\"truss -t\" lists them", word + 1);
475 		} else if (word[strspn(word, "0123456789")] == '\0') {
476 			/*
477 			 * A term of nothing but digits names a system call
478 			 * by number rather than by name.
479 			 */
480 			term->number = (u_int)strtonum(word, 0, UINT_MAX,
481 			    &errstr);
482 			if (errstr != NULL)
483 				errx(1, "system call number is %s: %s", errstr,
484 				    word);
485 			term->by_number = true;
486 		} else if ((term->pattern = strdup(word)) == NULL)
487 			err(1, "strdup");
488 
489 		/*
490 		 * A name that can never match is almost always a typo, so
491 		 * say so rather than quietly tracing nothing.  It is only a
492 		 * warning: a name is still permitted to be one truss has no
493 		 * knowledge of.
494 		 *
495 		 * Numbers are not checked this way.  A process may issue any
496 		 * number the kernel can hold, whether or not a system call
497 		 * is implemented behind it; one that is not simply returns
498 		 * ENOSYS, which truss reports like any other result.  The
499 		 * only number that cannot name a system call is one that
500 		 * does not fit, which the conversion above rejected.
501 		 */
502 		if (term->pattern != NULL && !term_matches_any_syscall(term))
503 			warnx("%s: matches no known system call",
504 			    term->pattern);
505 
506 		if (!term->negate)
507 			filter_default = false;
508 		STAILQ_INSERT_TAIL(&filter_terms, term, entries);
509 	}
510 	free(copy);
511 }
512 
513 
514 /*
515  * Whether a term selects the system call with the given name and number.
516  * A numeric term matches on the number alone, which is what the user
517  * asked for: numbers identify a system call within one ABI, and it is
518  * the ABI of the traced process that decides which one.
519  */
520 static bool
term_matches(const struct filter_term * term,const char * name,u_int number)521 term_matches(const struct filter_term *term, const char *name, u_int number)
522 {
523 	const char *forms[3];
524 	u_int i, nforms;
525 
526 	if (term->by_number)
527 		return (term->number == number);
528 	if (term->group != NULL)
529 		return (group_selects(term->group, name, number, 0));
530 
531 	nforms = name_forms(name, forms);
532 	for (i = 0; i < nforms; i++) {
533 		if (fnmatch(term->pattern, forms[i], 0) == 0)
534 			return (true);
535 	}
536 	return (false);
537 }
538 
539 /*
540  * Whether a term matches any system call of any ABI this build of truss
541  * understands.  sysdecode(3) names every system call of every such ABI
542  * whether or not the ABI's module happens to be loaded, and names them
543  * exactly as truss reports them, so it answers the question a user asks
544  * of -t.  Codes beyond an ABI's table return NULL.
545  */
546 static bool
term_matches_any_syscall(const struct filter_term * term)547 term_matches_any_syscall(const struct filter_term *term)
548 {
549 	static const enum sysdecode_abi abis[] = {
550 		SYSDECODE_ABI_FREEBSD,
551 		SYSDECODE_ABI_FREEBSD32,
552 		SYSDECODE_ABI_LINUX,
553 		SYSDECODE_ABI_LINUX32,
554 	};
555 	const char *name;
556 	size_t i;
557 	u_int code;
558 
559 	for (i = 0; i < nitems(abis); i++) {
560 		for (code = 0; code < SYSCALL_NORMAL_COUNT; code++) {
561 			name = sysdecode_syscallname(abis[i], code);
562 			if (name != NULL && term_matches(term, name, code))
563 				return (true);
564 		}
565 	}
566 	return (false);
567 }
568 
569 /*
570  * Report whether a system call with the given name is to be traced.
571  * With no -t expression every system call is, as before.
572  */
573 bool
syscall_filter_match(const char * name,u_int number)574 syscall_filter_match(const char *name, u_int number)
575 {
576 	const struct filter_term *term;
577 	bool trace;
578 
579 	if (STAILQ_EMPTY(&filter_terms))
580 		return (true);
581 
582 	trace = filter_default;
583 	STAILQ_FOREACH(term, &filter_terms, entries) {
584 		if (term_matches(term, name, number))
585 			trace = !term->negate;
586 	}
587 	return (trace);
588 }
589