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