Page MenuHomeFreeBSD

D59275.diff
No OneTemporary

D59275.diff

diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -1251,6 +1251,8 @@
..
truncate
..
+ truss
+ ..
tsort
..
unifdef
diff --git a/usr.bin/truss/Makefile b/usr.bin/truss/Makefile
--- a/usr.bin/truss/Makefile
+++ b/usr.bin/truss/Makefile
@@ -1,9 +1,14 @@
+.include <src.opts.mk>
+
PROG= truss
-SRCS= main.c setup.c syscalls.c
+SRCS= main.c setup.c syscall_filter.c syscalls.c
LIBADD= sysdecode
#CFLAGS+= -I${.CURDIR} -I. -I${SRCTOP}/sys
CFLAGS+= -I${SRCTOP}/sys
+HAS_TESTS=
+SUBDIR.${MK_TESTS}+= tests
+
.include <bsd.prog.mk>
diff --git a/usr.bin/truss/extern.h b/usr.bin/truss/extern.h
--- a/usr.bin/truss/extern.h
+++ b/usr.bin/truss/extern.h
@@ -31,6 +31,9 @@
* SUCH DAMAGE.
*/
+extern void add_syscall_filter(const char *);
+extern void list_syscall_groups(void);
+extern bool syscall_filter_match(const char *, u_int);
extern int print_line_prefix(struct trussinfo *);
extern void setup_and_wait(struct trussinfo *, char **);
extern void start_tracing(struct trussinfo *, pid_t);
diff --git a/usr.bin/truss/main.c b/usr.bin/truss/main.c
--- a/usr.bin/truss/main.c
+++ b/usr.bin/truss/main.c
@@ -56,9 +56,11 @@
static __dead2 void
usage(void)
{
- fprintf(stderr, "%s\n%s\n",
- "usage: truss [-cfaedDHS] [-o file] [-s strsize] -p pid",
- " truss [-cfaedDHS] [-o file] [-s strsize] command [args]");
+ fprintf(stderr, "%s\n%s\n%s\n",
+ "usage: truss [-cfaedDHS] [-o file] [-s strsize] [-t expr] -p pid",
+ " truss [-cfaedDHS] [-o file] [-s strsize] [-t expr] "
+ "command [args]",
+ " truss -t");
exit(1);
}
@@ -85,7 +87,13 @@
trussinfo->strsize = 32;
trussinfo->curthread = NULL;
LIST_INIT(&trussinfo->proclist);
- while ((c = getopt(ac, av, "p:o:facedDs:SH")) != -1) {
+ /*
+ * The leading ':' asks getopt() to report a missing option
+ * argument as ':' rather than '?' so that a bare -t, which lists
+ * the system call groups, can be told from a malformed option.
+ * Diagnosing the other two cases then falls to us.
+ */
+ while ((c = getopt(ac, av, ":p:o:facedDs:t:SH")) != -1) {
switch (c) {
case 'p': /* specified pid */
pid = atoi(optarg);
@@ -121,13 +129,25 @@
if (errstr)
errx(1, "maximum string size is %s: %s", errstr, optarg);
break;
+ case 't': /* Select the system calls to trace */
+ add_syscall_filter(optarg);
+ break;
case 'S': /* Don't trace signals */
trussinfo->flags |= NOSIGS;
break;
case 'H':
trussinfo->flags |= DISPLAYTIDS;
break;
+ case ':':
+ if (optopt == 't') {
+ /* A bare -t lists the system call groups. */
+ list_syscall_groups();
+ return (2);
+ }
+ warnx("option requires an argument -- %c", optopt);
+ usage();
default:
+ warnx("illegal option -- %c", optopt);
usage();
}
}
diff --git a/usr.bin/truss/setup.c b/usr.bin/truss/setup.c
--- a/usr.bin/truss/setup.c
+++ b/usr.bin/truss/setup.c
@@ -472,7 +472,7 @@
}
sc = get_syscall(t, t->cs.number, narg);
- if (sc->unknown)
+ if (sc->unknown && sc->trace)
fprintf(info->outfile, "-- UNKNOWN %s SYSCALL %d --\n",
t->proc->abi->type, t->cs.number);
@@ -481,6 +481,15 @@
t->cs.sc = sc;
+ /*
+ * A system call excluded by -t is never printed, so there is no
+ * point in formatting its arguments.
+ */
+ if (!sc->trace) {
+ clock_gettime(CLOCK_REALTIME, &t->before);
+ return;
+ }
+
/*
* At this point, we set up the system call arguments.
* We ignore any OUT ones, however -- those are arguments that
@@ -554,9 +563,10 @@
sc = t->cs.sc;
/*
* Here, we only look for arguments that have OUT masked in --
- * otherwise, they were handled in enter_syscall().
+ * otherwise, they were handled in enter_syscall(). A system call
+ * excluded by -t is never printed, so none of them are needed.
*/
- for (i = 0; i < sc->decode.nargs; i++) {
+ for (i = 0; i < sc->decode.nargs && sc->trace; i++) {
char *temp;
if (sc->decode.args[i].type & OUT) {
diff --git a/usr.bin/truss/syscall.h b/usr.bin/truss/syscall.h
--- a/usr.bin/truss/syscall.h
+++ b/usr.bin/truss/syscall.h
@@ -224,6 +224,7 @@
struct timespec time; /* Time spent for this call */
int ncalls; /* Number of calls */
int nerror; /* Number of calls that returned with error */
+ bool trace; /* Selected for reporting by -t */
bool unknown; /* Unknown system call */
};
diff --git a/usr.bin/truss/syscall_filter.c b/usr.bin/truss/syscall_filter.c
new file mode 100644
--- /dev/null
+++ b/usr.bin/truss/syscall_filter.c
@@ -0,0 +1,588 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+/*
+ * Selection of the system calls to report, driven by -t.
+ */
+
+#include <sys/param.h>
+#include <sys/queue.h>
+
+#include <err.h>
+#include <fnmatch.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysdecode.h>
+
+#include "truss.h"
+#include "extern.h"
+
+/*
+ * Groups of related system calls, named as "@group" in a -t expression.
+ *
+ * A group's member list is an expression in exactly the form -t accepts,
+ * so that anything a user can write on the command line can also be
+ * written as a group: each member is an fnmatch(3) pattern matched
+ * against the name of a system call, a decimal system call number, or
+ * "@group" naming another group, and any of them may be prefixed with
+ * '!' to exclude rather than include what it matches. Keeping the two
+ * languages identical is deliberate: a group defined elsewhere, from a
+ * -t expression a user supplied, needs no translation to become a
+ * member list here.
+ *
+ * Patterns are preferred over literal names wherever a family of system
+ * calls shares a naming convention (e.g. "extattr_*_file"), so that
+ * system calls added later are picked up without further change here.
+ * Names from the ABIs truss supports beyond the native one are included
+ * where they differ, since the Linux ABI in particular renames a number
+ * of otherwise familiar system calls.
+ *
+ * Adding a group is a matter of adding a member list here and a single
+ * entry to syscall_groups[] below.
+ */
+
+static const char *const group_all[] = {
+ "*",
+ NULL
+};
+
+static const char *const group_none[] = {
+ "!*",
+ NULL
+};
+
+static const char *const group_read[] = {
+ "read", "readv", "pread*", "readahead", "readdir",
+ "recv", "recvfrom", "recvmsg", "recvmmsg*",
+ "aio_read*", "sctp_generic_recvmsg",
+ "kmq_timedreceive", "mq_timedreceive*", "msgrcv",
+ "getdents*", "getdirentries", "copy_file_range", "process_vm_readv",
+ NULL
+};
+
+static const char *const group_write[] = {
+ "write", "writev", "pwrite*",
+ "send", "sendto", "sendmsg", "sendmmsg*", "sendfile*",
+ "aio_write*", "sctp_generic_send*",
+ "kmq_timedsend", "mq_timedsend*", "msgsnd",
+ "copy_file_range", "process_vm_writev",
+ NULL
+};
+
+static const char *const group_desc[] = {
+ "@read", "@write",
+ "close", "close_range", "closefrom", "dup", "dup2", "dup3",
+ "fcntl", "fcntl64", "flock", "fsync", "fdatasync", "syncfs",
+ "sync", "sync_file_range", "ftruncate*", "lseek", "llseek", "ioctl",
+ "poll", "ppoll*", "select", "old_select", "pselect*",
+ "epoll_*", "kqueue*", "kevent*", "eventfd*", "timerfd_*",
+ "inotify_*", "signalfd*", "fanotify_*", "pipe", "pipe2",
+ "fstat", "fstat64", "newfstat", "nfstat", "fstatfs*",
+ "fchdir", "fchmod", "fchown", "fchflags", "futimes", "futimens",
+ "fpathconf", "getdtablesize", "fexecve", "fspacectl",
+ "posix_fadvise", "fadvise64*", "posix_fallocate", "fallocate",
+ "splice", "tee", "vmsplice", "memfd_create", "__specialfd",
+ "posix_openpt", "pddupfd", "aio_*", "lio_listio",
+ "pidfd_*", "io_*", "f*xattr",
+ "__acl_*_fd", "extattr_*_fd", "__mac_*_fd",
+ "cap_fcntls_*", "cap_ioctls_*", "cap_rights_limit",
+ "__cap_rights_get",
+ NULL
+};
+
+static const char *const group_file[] = {
+ "open", "openat", "openat2", "open_by_handle_at", "open_tree",
+ "creat", "stat", "stat64", "newstat", "nstat", "statx",
+ "lstat", "lstat64", "newlstat", "nlstat",
+ "fstatat", "fstatat64", "newfstatat", "statfs", "statfs64",
+ "access", "eaccess", "faccessat*",
+ "chdir", "chroot", "fchroot", "pivot_root",
+ "chmod", "lchmod", "fchmodat*",
+ "chown", "chown16", "lchown", "lchown16", "fchownat",
+ "chflags", "lchflags", "chflagsat",
+ "link", "linkat", "symlink", "symlinkat",
+ "unlink", "unlinkat", "funlinkat",
+ "rename", "renameat", "renameat2",
+ "mkdir", "mkdirat", "rmdir", "mknod", "mknodat",
+ "mkfifo", "mkfifoat", "readlink", "readlinkat",
+ "truncate", "truncate64",
+ "utime", "utimes", "lutimes", "utimensat*", "futimesat",
+ "pathconf", "lpathconf", "__getcwd", "getcwd", "__realpathat",
+ "revoke", "undelete", "acct", "quotactl*", "umask",
+ "mount", "nmount", "unmount", "umount", "oldumount", "move_mount",
+ "fh*", "getfh", "getfhat", "lgetfh", "getfsstat",
+ "name_to_handle_at", "inotify_add_watch*",
+ "execve", "execveat", "__mac_execve",
+ "swapon", "swapoff", "kldload",
+ "getxattr", "lgetxattr", "setxattr", "lsetxattr",
+ "removexattr", "lremovexattr", "listxattr", "llistxattr",
+ "__acl_*_file", "__acl_*_link", "extattr_*_file", "extattr_*_link",
+ "extattrctl", "__mac_*_file", "__mac_*_link",
+ NULL
+};
+
+static const char *const group_net[] = {
+ "socket", "socketcall", "socketpair", "bind", "bindat",
+ "connect", "connectat", "listen", "accept", "accept4",
+ "getpeername", "getsockname", "getsockopt", "setsockopt", "shutdown",
+ "send", "sendto", "sendmsg", "sendmmsg*", "sendfile*",
+ "recv", "recvfrom", "recvmsg", "recvmmsg*", "sctp_*",
+ "setfib", "gethostname", "sethostname",
+ "getdomainname", "setdomainname",
+ "nfssvc", "nlm_syscall", "rpctls_syscall",
+ NULL
+};
+
+static const char *const group_proc[] = {
+ "fork", "vfork", "rfork", "pdfork", "pdrfork", "clone", "clone3",
+ "execve", "execveat", "fexecve", "__mac_execve",
+ "_exit", "exit", "exit_group", "abort2",
+ "wait", "wait4", "wait6", "waitid", "waitpid", "pdwait",
+ "getpid", "getppid", "gettid", "getpgrp", "getpgid", "setpgid",
+ "getsid", "setsid", "getpriority", "setpriority", "nice",
+ "rtprio", "rtprio_thread", "sched_*", "cpuset*",
+ "procctl", "prctl", "arch_prctl", "ptrace",
+ "thr_*", "_umtx_*", "futex*", "sys_futex*", "membarrier",
+ "pdgetpid", "pdkill", "pdopenpid", "pidfd_open", "pidfd_getfd",
+ "jail*", "kcmp", "getcontext", "setcontext", "swapcontext", "yield",
+ "getrusage", "getrlimit", "setrlimit", "getrlimitusage",
+ "old_getrlimit", "prlimit64", "personality", "times", "vhangup",
+ "set_tid_address", "setns", "unshare", "restart_syscall", "rseq",
+ "get_robust_list", "set_robust_list",
+ NULL
+};
+
+static const char *const group_signal[] = {
+ "sig*", "rt_sig*", "rt_tgsigqueueinfo",
+ "kill", "killpg", "thr_kill*", "pdkill",
+ "tkill", "tgkill", "pidfd_send_signal",
+ "sgetmask", "ssetmask", "pause",
+ NULL
+};
+
+static const char *const group_memory[] = {
+ "mmap", "mmap2", "munmap", "mprotect", "pkey_mprotect", "mremap",
+ "madvise", "process_madvise", "mincore", "minherit",
+ "mlock", "mlock2", "munlock", "mlockall", "munlockall", "aio_mlock",
+ "msync", "break", "brk", "sbrk", "vadvise", "getpagesize",
+ "shm_open*", "shm_unlink", "shm_rename",
+ "memfd_create", "memfd_secret", "map_shadow_stack", "userfaultfd",
+ "mbind", "get_mempolicy", "set_mempolicy*",
+ "migrate_pages", "move_pages", "remap_file_pages",
+ "pkey_alloc", "pkey_free", "swapon", "swapoff",
+ NULL
+};
+
+static const char *const group_ipc[] = {
+ "msgctl", "msgget", "msgrcv", "msgsnd", "msgsys",
+ "semctl", "__semctl", "semget", "semop", "semsys", "semtimedop*",
+ "shmat", "shmctl", "shmdt", "shmget", "shmsys",
+ "ksem_*", "kmq_*", "mq_*", "ipc",
+ NULL
+};
+
+static const char *const group_creds[] = {
+ "getuid*", "geteuid*", "getgid*", "getegid*",
+ "getgroups*", "setgroups*",
+ "setuid*", "seteuid", "setgid*", "setegid",
+ "setreuid*", "setregid*", "setresuid*", "setresgid*",
+ "getresuid*", "getresgid*", "setfsuid*", "setfsgid*",
+ "issetugid", "__setugid", "setcred",
+ "getlogin", "setlogin", "getloginclass", "setloginclass",
+ "getauid", "setauid", "getaudit*", "setaudit*",
+ "audit", "auditon", "auditctl",
+ "capget", "capset", "cap_enter", "cap_getmode",
+ "seccomp", "landlock_*",
+ NULL
+};
+
+static const char *const group_time[] = {
+ "clock_*", "nanosleep", "gettimeofday", "settimeofday",
+ "adjtime", "adjtimex", "ntp_*",
+ "getitimer", "setitimer", "ktimer_*", "timer_*", "timerfd_*",
+ "ffclock_*", "time", "stime", "alarm",
+ NULL
+};
+
+struct syscall_group {
+ const char *name;
+ const char *desc;
+ const char *const *members;
+};
+
+/* Kept in alphabetical order; "truss -t" prints it as-is. */
+static const struct syscall_group syscall_groups[] = {
+ { "all", "every system call", group_all },
+ { "creds", "get or set process credentials", group_creds },
+ { "desc", "operate on a file descriptor", group_desc },
+ { "file", "operate on a pathname", group_file },
+ { "ipc", "System V and POSIX IPC", group_ipc },
+ { "memory", "memory mapping and locking", group_memory },
+ { "net", "network and socket operations", group_net },
+ { "none", "no system call", group_none },
+ { "proc", "process and thread lifecycle", group_proc },
+ { "read", "read data from a descriptor", group_read },
+ { "signal", "signal delivery and handling", group_signal },
+ { "time", "clocks, timers and sleeping", group_time },
+ { "write", "write data to a descriptor", group_write },
+};
+
+/*
+ * One comma-separated term of a -t expression. Terms are held in the
+ * order they were given: the last one to match a system call decides
+ * whether it is reported.
+ */
+struct filter_term {
+ STAILQ_ENTRY(filter_term) entries;
+ const struct syscall_group *group; /* @group term, else NULL */
+ char *pattern; /* name pattern, else NULL */
+ u_int number; /* number, if by_number */
+ bool by_number;
+ bool negate;
+};
+
+static bool term_matches_any_syscall(const struct filter_term *);
+
+static STAILQ_HEAD(, filter_term) filter_terms =
+ STAILQ_HEAD_INITIALIZER(filter_terms);
+
+/*
+ * Whether a system call matched by no term at all is reported. An
+ * expression made up only of negated terms subtracts from the full set
+ * of system calls; any other expression selects from an empty one.
+ */
+static bool filter_default = true;
+
+/*
+ * A name reported by sysdecode may carry a prefix naming a compatibility
+ * layer ("compat11.stat"), a non-native ABI ("linux_open",
+ * "freebsd32_ioctl"), or both ("compat4.freebsd32_getfsstat"). Terms are
+ * matched against the name as displayed and against each shortened form,
+ * so that "-t @file" selects stat, compat11.stat and freebsd32_stat alike.
+ */
+static const char *const abi_prefixes[] = {
+ "freebsd32_",
+ "linux_",
+ "linux32_",
+};
+
+/*
+ * A group referring to itself, directly or through others, would recurse
+ * forever. The table above has no such cycle; this only keeps a future
+ * mistake in it from hanging truss.
+ */
+#define GROUP_MAX_DEPTH 8
+
+static const struct syscall_group *
+find_group(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < nitems(syscall_groups); i++) {
+ if (strcmp(name, syscall_groups[i].name) == 0)
+ return (&syscall_groups[i]);
+ }
+ return (NULL);
+}
+
+static bool group_selects(const struct syscall_group *, const char *, u_int,
+ u_int);
+
+static const char *
+strip_abi_prefix(const char *name)
+{
+ size_t i, len;
+
+ for (i = 0; i < nitems(abi_prefixes); i++) {
+ len = strlen(abi_prefixes[i]);
+ if (strncmp(name, abi_prefixes[i], len) == 0)
+ return (name + len);
+ }
+ return (name);
+}
+
+/*
+ * Expand a system call name into the forms a term may match it under:
+ * the name itself, the name with any "compatN." prefix removed, and that
+ * with any ABI prefix removed as well. Returns the number of forms.
+ */
+static u_int
+name_forms(const char *name, const char *forms[3])
+{
+ const char *shorter, *stripped;
+ u_int nforms;
+
+ nforms = 0;
+ forms[nforms++] = name;
+ shorter = strrchr(name, '.');
+ if (shorter != NULL)
+ forms[nforms++] = ++shorter;
+ else
+ shorter = name;
+ stripped = strip_abi_prefix(shorter);
+ if (stripped != shorter)
+ forms[nforms++] = stripped;
+ return (nforms);
+}
+
+/*
+ * Whether one member of a group selects the given system call. The
+ * caller has already consumed any leading '!', leaving the same three
+ * forms a -t term may take: a "@group" reference, a decimal system call
+ * number, or an fnmatch(3) pattern matched against the name.
+ */
+static bool
+member_matches(const char *member, const char *name, u_int number, u_int depth)
+{
+ const struct syscall_group *ref;
+ const char *errstr;
+ const char *forms[3];
+ u_int i, nforms, num;
+
+ /*
+ * A member of "!" alone leaves nothing behind once the caller has
+ * consumed the '!'. The -t parser rejects that outright; say so
+ * explicitly here rather than falling into the numeric branch,
+ * where an empty string would otherwise be offered to strtonum().
+ */
+ if (*member == '\0')
+ return (false);
+
+ if (*member == '@') {
+ ref = find_group(member + 1);
+ return (ref != NULL &&
+ group_selects(ref, name, number, depth + 1));
+ }
+ if (member[strspn(member, "0123456789")] == '\0') {
+ num = (u_int)strtonum(member, 0, UINT_MAX, &errstr);
+ return (errstr == NULL && num == number);
+ }
+ nforms = name_forms(name, forms);
+ for (i = 0; i < nforms; i++) {
+ if (fnmatch(member, forms[i], 0) == 0)
+ return (true);
+ }
+ return (false);
+}
+
+/*
+ * Whether a group selects the given system call.
+ *
+ * A group's member list is an expression in exactly the form -t accepts,
+ * so that a group can say anything a user can say on the command line:
+ * members apply in order, the last one to match decides, and a list of
+ * only negated members starts from every system call rather than from
+ * none. "@none" is therefore written as the one member "!*".
+ */
+static bool
+group_selects(const struct syscall_group *group, const char *name, u_int number,
+ u_int depth)
+{
+ const char *const *member;
+ const char *pattern;
+ bool negate, selects;
+
+ /*
+ * A group with no member list at all selects nothing. The table
+ * below has no such entry, but a group built from anywhere less
+ * hand-audited should not be able to fault truss.
+ */
+ if (depth >= GROUP_MAX_DEPTH || group->members == NULL)
+ return (false);
+
+ selects = true;
+ for (member = group->members; *member != NULL; member++) {
+ if (**member != '!') {
+ selects = false;
+ break;
+ }
+ }
+
+ for (member = group->members; *member != NULL; member++) {
+ pattern = *member;
+ negate = *pattern == '!';
+ if (negate)
+ pattern++;
+ if (member_matches(pattern, name, number, depth))
+ selects = !negate;
+ }
+ return (selects);
+}
+
+/* Print the group table ("-t" with no expression). */
+void
+list_syscall_groups(void)
+{
+ size_t i;
+
+ printf("System call groups usable as @group in a -t expression:\n\n");
+ for (i = 0; i < nitems(syscall_groups); i++)
+ printf(" @%-9s %s\n", syscall_groups[i].name,
+ syscall_groups[i].desc);
+ printf("\n"
+ "Any other term is an fnmatch(3) pattern matched against the\n"
+ "system call name, so \"read\" selects read(2) alone and \"read*\"\n"
+ "also selects readv(2) and readlink(2). A term prefixed with '!'\n"
+ "excludes what that one term matches rather than including it.\n");
+}
+
+/*
+ * Add the terms of one -t expression. Repeating -t appends to the
+ * expression rather than replacing it.
+ */
+void
+add_syscall_filter(const char *expr)
+{
+ struct filter_term *term;
+ const char *errstr;
+ char *copy, *next, *word;
+
+ if ((copy = strdup(expr)) == NULL)
+ err(1, "strdup");
+ next = copy;
+ while ((word = strsep(&next, ",")) != NULL) {
+ bool negate = false;
+
+ if (*word == '!') {
+ negate = true;
+ if (*++word == '\0')
+ errx(1, "missing pattern after '!' in -t %s",
+ expr);
+ }
+
+ /*
+ * Ignore an empty term so that an empty expression, or one
+ * with a stray or trailing comma, adds no terms rather than
+ * being an error. "truss -t ''" thus filters nothing.
+ */
+ if (*word == '\0')
+ continue;
+
+ if ((term = calloc(1, sizeof(*term))) == NULL)
+ err(1, "calloc");
+ term->negate = negate;
+ if (*word == '@') {
+ term->group = find_group(word + 1);
+ if (term->group == NULL)
+ errx(1, "unknown system call group @%s; "
+ "\"truss -t\" lists them", word + 1);
+ } else if (word[strspn(word, "0123456789")] == '\0') {
+ /*
+ * A term of nothing but digits names a system call
+ * by number rather than by name.
+ */
+ term->number = (u_int)strtonum(word, 0, UINT_MAX,
+ &errstr);
+ if (errstr != NULL)
+ errx(1, "system call number is %s: %s", errstr,
+ word);
+ term->by_number = true;
+ } else if ((term->pattern = strdup(word)) == NULL)
+ err(1, "strdup");
+
+ /*
+ * A name that can never match is almost always a typo, so
+ * say so rather than quietly tracing nothing. It is only a
+ * warning: a name is still permitted to be one truss has no
+ * knowledge of.
+ *
+ * Numbers are not checked this way. A process may issue any
+ * number the kernel can hold, whether or not a system call
+ * is implemented behind it; one that is not simply returns
+ * ENOSYS, which truss reports like any other result. The
+ * only number that cannot name a system call is one that
+ * does not fit, which the conversion above rejected.
+ */
+ if (term->pattern != NULL && !term_matches_any_syscall(term))
+ warnx("%s: matches no known system call",
+ term->pattern);
+
+ if (!term->negate)
+ filter_default = false;
+ STAILQ_INSERT_TAIL(&filter_terms, term, entries);
+ }
+ free(copy);
+}
+
+
+/*
+ * Whether a term selects the system call with the given name and number.
+ * A numeric term matches on the number alone, which is what the user
+ * asked for: numbers identify a system call within one ABI, and it is
+ * the ABI of the traced process that decides which one.
+ */
+static bool
+term_matches(const struct filter_term *term, const char *name, u_int number)
+{
+ const char *forms[3];
+ u_int i, nforms;
+
+ if (term->by_number)
+ return (term->number == number);
+ if (term->group != NULL)
+ return (group_selects(term->group, name, number, 0));
+
+ nforms = name_forms(name, forms);
+ for (i = 0; i < nforms; i++) {
+ if (fnmatch(term->pattern, forms[i], 0) == 0)
+ return (true);
+ }
+ return (false);
+}
+
+/*
+ * Whether a term matches any system call of any ABI this build of truss
+ * understands. sysdecode(3) names every system call of every such ABI
+ * whether or not the ABI's module happens to be loaded, and names them
+ * exactly as truss reports them, so it answers the question a user asks
+ * of -t. Codes beyond an ABI's table return NULL.
+ */
+static bool
+term_matches_any_syscall(const struct filter_term *term)
+{
+ static const enum sysdecode_abi abis[] = {
+ SYSDECODE_ABI_FREEBSD,
+ SYSDECODE_ABI_FREEBSD32,
+ SYSDECODE_ABI_LINUX,
+ SYSDECODE_ABI_LINUX32,
+ };
+ const char *name;
+ size_t i;
+ u_int code;
+
+ for (i = 0; i < nitems(abis); i++) {
+ for (code = 0; code < SYSCALL_NORMAL_COUNT; code++) {
+ name = sysdecode_syscallname(abis[i], code);
+ if (name != NULL && term_matches(term, name, code))
+ return (true);
+ }
+ }
+ return (false);
+}
+
+/*
+ * Report whether a system call with the given name is to be traced.
+ * With no -t expression every system call is, as before.
+ */
+bool
+syscall_filter_match(const char *name, u_int number)
+{
+ const struct filter_term *term;
+ bool trace;
+
+ if (STAILQ_EMPTY(&filter_terms))
+ return (true);
+
+ trace = filter_default;
+ STAILQ_FOREACH(term, &filter_terms, entries) {
+ if (term_matches(term, name, number))
+ trace = !term->negate;
+ }
+ return (trace);
+}
diff --git a/usr.bin/truss/syscalls.c b/usr.bin/truss/syscalls.c
--- a/usr.bin/truss/syscalls.c
+++ b/usr.bin/truss/syscalls.c
@@ -919,6 +919,8 @@
name, strlen(procabi->compat_prefix)) == 0)
lookup_name += strlen(procabi->compat_prefix);
+ sc->trace = syscall_filter_match(name, number);
+
for (i = 0; i < nitems(decoded_syscalls); i++) {
if (strcmp(lookup_name, decoded_syscalls[i].name) == 0) {
sc->decode = decoded_syscalls[i];
@@ -2844,6 +2846,8 @@
t = trussinfo->curthread;
sc = t->cs.sc;
+ if (!sc->trace)
+ return;
if (trussinfo->flags & COUNTONLY) {
timespecsub(&t->after, &t->before, &timediff);
timespecadd(&sc->time, &timediff, &sc->time);
diff --git a/usr.bin/truss/tests/Makefile b/usr.bin/truss/tests/Makefile
new file mode 100644
--- /dev/null
+++ b/usr.bin/truss/tests/Makefile
@@ -0,0 +1,8 @@
+PACKAGE= tests
+
+ATF_TESTS_SH= truss_test
+
+DIRS+= TESTSDIR
+beforeinstall: installdirs-TESTSDIR
+
+.include <bsd.test.mk>
diff --git a/usr.bin/truss/tests/truss_test.sh b/usr.bin/truss/tests/truss_test.sh
new file mode 100644
--- /dev/null
+++ b/usr.bin/truss/tests/truss_test.sh
@@ -0,0 +1,403 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+#
+
+# The system calls a program makes vary with the machine and with the
+# run-time linker, so these tests assert which system calls -t may and
+# may not report rather than the exact sequence of them.
+
+require_truss()
+{
+ truss -o /dev/null /usr/bin/true >/dev/null 2>&1 ||
+ atf_skip "unable to trace a child process here"
+}
+
+# Write the sorted, unique names of the system calls reported in a truss
+# output file to another file.
+syscall_names()
+{
+ sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_.]*\)(.*$/\1/p' "$1" | sort -u > "$2"
+}
+
+# Fail unless every name in a file matches an extended regular expression.
+only_names()
+{
+ if grep -Ev "$1" "$2" > unexpected; then
+ atf_fail "reported system calls not selected by the filter:" \
+ "$(tr '\n' ' ' < unexpected)"
+ fi
+}
+
+# Fail if any name in a file matches an extended regular expression.
+no_name()
+{
+ if grep -E "$1" "$2" > unexpected; then
+ atf_fail "system calls excluded by the filter were reported:" \
+ "$(tr '\n' ' ' < unexpected)"
+ fi
+}
+
+# Fail unless at least one name in a file matches.
+some_name()
+{
+ grep -Eq "$1" "$2" ||
+ atf_fail "no system call matching $1 was reported"
+}
+
+atf_test_case list
+list_head()
+{
+ atf_set descr "-t with no expression prints the available groups"
+}
+list_body()
+{
+ atf_check -s exit:2 -o match:'@all' -o match:'@none' \
+ -o match:'@read' -o match:'@write' -o match:'@file' \
+ -o match:'@net' \
+ truss -t
+}
+
+atf_test_case unknown_group
+unknown_group_head()
+{
+ atf_set descr "an unknown @group is rejected"
+}
+unknown_group_body()
+{
+ atf_check -s exit:1 -e match:'unknown system call group @nosuch' \
+ truss -t @nosuch /usr/bin/true
+}
+
+atf_test_case empty_term
+empty_term_head()
+{
+ atf_set descr "an empty term adds nothing to the expression"
+}
+empty_term_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ # An empty expression filters nothing, as if -t were absent.
+ atf_check -s exit:0 -o inline:"hello\n" truss -o out -t '' cat input
+ syscall_names out names
+ some_name '^read$' names
+ some_name '^openat$' names
+
+ # A stray comma is ignored rather than being an error.
+ atf_check -s exit:0 -o inline:"hello\n" \
+ truss -o out2 -t ',read,,write,' cat input
+ syscall_names out2 names2
+ only_names '^(read|write)$' names2
+ some_name '^read$' names2
+
+ # A negation with nothing to negate is still a mistake.
+ atf_check -s exit:1 -e match:"missing pattern after" \
+ truss -t '!' /usr/bin/true
+}
+
+atf_test_case none
+none_head()
+{
+ atf_set descr "@none selects no system call"
+}
+none_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ # @none's member list is the single negated member "!*", so this
+ # also covers a group whose members exclude rather than include.
+ atf_check -s exit:0 -o inline:"hello\n" truss -o out -t @none cat input
+ syscall_names out names
+ atf_check -o empty cat names
+
+ atf_check -s exit:0 -o inline:"hello\n" \
+ truss -o out2 -t '!@none' cat input
+ syscall_names out2 names2
+ some_name '^read$' names2
+ some_name '^openat$' names2
+
+ # @none is the empty set rather than a switch: it selects nothing
+ # and leaves the terms before it alone.
+ atf_check -s exit:0 -o inline:"hello\n" \
+ truss -o out3 -t '@file,@none' cat input
+ syscall_names out3 names3
+ some_name '^openat$' names3
+}
+
+atf_test_case by_number
+by_number_head()
+{
+ atf_set descr "a term may name a system call by number"
+}
+by_number_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ # 3 and 4 have been read(2) and write(2) since 4.2BSD.
+ atf_check -s exit:0 -o inline:"hello\n" -e empty \
+ truss -o out -t 3 cat input
+ syscall_names out names
+ only_names '^read$' names
+ some_name '^read$' names
+
+ atf_check -s exit:0 -o inline:"hello\n" -e empty \
+ truss -o out2 -t 3,4 cat input
+ syscall_names out2 names2
+ only_names '^(read|write)$' names2
+
+ # Numeric terms negate like any other.
+ atf_check -s exit:0 -o inline:"hello\n" -e empty \
+ truss -o out3 -t '!3' cat input
+ syscall_names out3 names3
+ no_name '^read$' names3
+ some_name '.' names3
+
+ # Leading zeroes are still just a number.
+ atf_check -s exit:0 -o inline:"hello\n" -e empty \
+ truss -o out4 -t 003 cat input
+ syscall_names out4 names4
+ only_names '^read$' names4
+}
+
+atf_test_case bad_number
+bad_number_head()
+{
+ atf_set descr "only an unrepresentable system call number is rejected"
+}
+bad_number_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ # A process may issue any number the kernel can hold, so a number
+ # beyond the tables truss knows is not second-guessed. It simply
+ # does not match anything this program happens to call.
+ atf_check -s exit:0 -o inline:"hello\n" -e empty \
+ truss -o out -t 99999 cat input
+ syscall_names out names
+ atf_check -o empty cat names
+
+ # Too large to be a system call number at all: an error.
+ atf_check -s exit:1 -e match:'system call number is too large' \
+ truss -t 4294967296 /usr/bin/true
+}
+
+atf_test_case unknown_syscall
+unknown_syscall_head()
+{
+ atf_set descr "a term matching no system call warns but still runs"
+}
+unknown_syscall_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ # A typo is a warning, not an error: the command still runs.
+ atf_check -s exit:0 -o inline:"hello\n" \
+ -e match:'opne: matches no known system call' \
+ truss -o out -t opne cat input
+ syscall_names out names
+ atf_check -o empty cat names
+
+ # So is a pattern that can never match.
+ atf_check -s exit:0 -o inline:"hello\n" \
+ -e match:'matches no known system call' \
+ truss -o out2 -t 'raed*' cat input
+
+ # Names of every ABI truss knows are accepted without complaint,
+ # whether or not that ABI is the one being traced here.
+ for name in read openat linux_write linux_newstat compat11.stat; do
+ atf_check -s exit:0 -o inline:"hello\n" -e empty \
+ truss -o out3 -t "$name" cat input
+ done
+
+ # A number is the way to name a system call by number; '#' is not
+ # a prefix truss accepts, so it is diagnosed like any other typo.
+ atf_check -s exit:0 -o inline:"hello\n" \
+ -e match:'matches no known system call' \
+ truss -o out4 -t '#237' cat input
+}
+
+atf_test_case by_name
+by_name_head()
+{
+ atf_set descr "a term naming one system call selects only that one"
+}
+by_name_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ atf_check -s exit:0 -o inline:"hello\n" truss -o out -t read cat input
+ syscall_names out names
+ only_names '^read$' names
+ some_name '^read$' names
+}
+
+atf_test_case by_pattern
+by_pattern_head()
+{
+ atf_set descr "a term may be an fnmatch(3) pattern"
+}
+by_pattern_body()
+{
+ require_truss
+ atf_check ln -s target link
+
+ atf_check -s exit:0 -o ignore truss -o out -t 'readlink*' readlink link
+ syscall_names out names
+ only_names '^readlink' names
+ some_name '^readlink' names
+}
+
+atf_test_case group
+group_head()
+{
+ atf_set descr "an @group selects the system calls it names"
+}
+group_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ atf_check -s exit:0 -o inline:"hello\n" truss -o out -t @read cat input
+ syscall_names out names
+ some_name '^read$' names
+ no_name '^(openat|close|mmap|munmap|mprotect)$' names
+}
+
+atf_test_case group_read_excludes_readlink
+group_read_excludes_readlink_head()
+{
+ atf_set descr "@read selects I/O reads but not readlink(2)"
+}
+group_read_excludes_readlink_body()
+{
+ require_truss
+ atf_check ln -s target link
+
+ # readlink(1) calls readlink(2), which @read must not select even
+ # though "read*" does.
+ atf_check -s exit:0 -o ignore truss -o out -t @read readlink link
+ syscall_names out names
+ no_name '^readlink' names
+ some_name '^read$' names
+
+ atf_check -s exit:0 -o ignore truss -o out2 -t 'read*' readlink link
+ syscall_names out2 names2
+ some_name '^readlink' names2
+}
+
+atf_test_case group_reference
+group_reference_head()
+{
+ atf_set descr "@desc includes the members of @read and @write"
+}
+group_reference_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ atf_check -s exit:0 -o inline:"hello\n" truss -o out -t @desc cat input
+ syscall_names out names
+ some_name '^read$' names
+ some_name '^close$' names
+}
+
+atf_test_case negation
+negation_head()
+{
+ atf_set descr "a term prefixed with ! excludes what it matches"
+}
+negation_body()
+{
+ require_truss
+
+ atf_check -s exit:0 -o ignore truss -o out -t '!@all' /usr/bin/true
+ syscall_names out names
+ atf_check -o empty cat names
+
+ atf_check -s exit:0 -o ignore truss -o out2 -t '!@memory' /usr/bin/true
+ syscall_names out2 names2
+ no_name '^(mmap|munmap|mprotect)$' names2
+ some_name '.' names2
+}
+
+atf_test_case order
+order_head()
+{
+ atf_set descr "the last term to match a system call wins"
+}
+order_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ atf_check -s exit:0 -o ignore truss -o out -t 'read,!read' cat input
+ syscall_names out names
+ atf_check -o empty cat names
+
+ atf_check -s exit:0 -o ignore truss -o out2 -t '!read,read' cat input
+ syscall_names out2 names2
+ only_names '^read$' names2
+ some_name '^read$' names2
+}
+
+atf_test_case accumulate
+accumulate_head()
+{
+ atf_set descr "repeating -t appends to the expression"
+}
+accumulate_body()
+{
+ require_truss
+ atf_check ln -s target link
+
+ atf_check -s exit:0 -o ignore \
+ truss -o out -t read -t readlink readlink link
+ syscall_names out names
+ only_names '^(read|readlink)$' names
+ some_name '^read$' names
+ some_name '^readlink$' names
+}
+
+atf_test_case count
+count_head()
+{
+ atf_set descr "-c counts only the selected system calls"
+}
+count_body()
+{
+ require_truss
+ printf 'hello\n' > input
+
+ atf_check -s exit:0 -o inline:"hello\n" \
+ truss -c -o out -t read cat input
+ atf_check -o match:'^read ' cat out
+ atf_check -s exit:1 -o empty grep -q '^openat' out
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case list
+ atf_add_test_case unknown_group
+ atf_add_test_case empty_term
+ atf_add_test_case none
+ atf_add_test_case by_number
+ atf_add_test_case bad_number
+ atf_add_test_case unknown_syscall
+ atf_add_test_case by_name
+ atf_add_test_case by_pattern
+ atf_add_test_case group
+ atf_add_test_case group_read_excludes_readlink
+ atf_add_test_case group_reference
+ atf_add_test_case negation
+ atf_add_test_case order
+ atf_add_test_case accumulate
+ atf_add_test_case count
+}
diff --git a/usr.bin/truss/truss.1 b/usr.bin/truss/truss.1
--- a/usr.bin/truss/truss.1
+++ b/usr.bin/truss/truss.1
@@ -1,5 +1,5 @@
.\"
-.Dd June 18, 2025
+.Dd August 31, 2026
.Dt TRUSS 1
.Os
.Sh NAME
@@ -10,12 +10,16 @@
.Op Fl facedDHS
.Op Fl o Ar file
.Op Fl s Ar strsize
+.Op Fl t Ar expr
.Fl p Ar pid
.Nm
.Op Fl facedDHS
.Op Fl o Ar file
.Op Fl s Ar strsize
+.Op Fl t Ar expr
.Ar command Op Ar args
+.Nm
+.Fl t
.Sh DESCRIPTION
The
.Nm
@@ -76,6 +80,163 @@
The default
.Ar strsize
is 32.
+.It Fl t Ar expr
+Report only the system calls selected by
+.Ar expr ,
+a comma-separated list of terms.
+Whatever follows
+.Fl t
+is taken to be the
+.Ar expr ,
+so one must be given;
+the sole exception is the third form shown above,
+.Ql truss -t
+with nothing after it,
+which prints the groups described below and exits.
+That is not the same as giving an empty
+.Ar expr :
+.Ql truss -t \(dq\(dq
+is an expression with no terms in it,
+and so filters nothing.
+.Pp
+Each term takes one of three forms:
+.Bl -tag -width "@group" -offset indent
+.It Ar name
+The name of a system call,
+as
+.Nm
+prints it,
+for example
+.Ql read .
+A name may also contain the wildcards
+.Ql * ,
+.Ql \&?
+and
+.Ql []
+described in
+.Xr fnmatch 3 ,
+in which case it selects every system call whose name it matches:
+.Ql read*
+selects
+.Xr read 2 ,
+.Xr readv 2
+and
+.Xr readlink 2
+alike.
+.It Ar number
+A system call number in decimal,
+for example
+.Ql 3 .
+The number is the one the ABI of the traced process uses,
+so
+.Ql 3
+selects
+.Xr read 2
+from a native process but
+.Fn close
+from a Linux one.
+.It Cm @ Ns Ar group
+One of the groups of related system calls listed below,
+for example
+.Ql @file .
+.El
+.Pp
+A term prefixed with
+.Ql \&!
+excludes the system calls it matches instead of including them.
+The
+.Ql \&!
+applies only to the term it prefixes,
+not to the terms that follow it.
+.Pp
+An expression whose terms are all negated subtracts from the set of
+every system call;
+any other expression selects from an empty set.
+Terms are applied in the order they are given,
+and the last one to match a system call decides whether it is reported.
+Repeating
+.Fl t
+appends to the expression rather than replacing it,
+so that
+.Fl t Ar a Fl t Ar b
+and
+.Fl t Ar a , Ns Ar b
+are equivalent.
+An empty term is ignored,
+which is why an empty
+.Ar expr
+filters nothing,
+and why a stray comma is not an error.
+.Pp
+The groups are:
+.Bl -tag -width ".Cm @signal" -compact -offset indent
+.It Cm @all
+every system call
+.It Cm @creds
+get or set process credentials
+.It Cm @desc
+operate on a file descriptor
+.It Cm @file
+operate on a pathname
+.It Cm @ipc
+System V and POSIX IPC
+.It Cm @memory
+memory mapping and locking
+.It Cm @net
+network and socket operations
+.It Cm @none
+no system call
+.It Cm @proc
+process and thread lifecycle
+.It Cm @read
+read data from a descriptor
+.It Cm @signal
+signal delivery and handling
+.It Cm @time
+clocks, timers and sleeping
+.It Cm @write
+write data to a descriptor
+.El
+.Pp
+A group is itself defined by an expression of the same form as
+.Ar expr ,
+so a group may name other groups and may exclude as well as include:
+.Cm @desc
+is defined to include
+.Cm @read
+and
+.Cm @write ,
+and
+.Cm @none
+is defined as
+.Ql \&!* .
+.Pp
+A system call is matched both under the name
+.Nm
+displays for it and under that name with any prefix naming a
+compatibility layer or a non-native ABI removed,
+so that
+.Fl t Cm @file
+selects
+.Ql compat11.stat
+and
+.Ql linux_newstat
+as well as
+.Ql stat .
+.Pp
+A name or pattern that matches no system call of any ABI that
+.Nm
+understands is reported with a warning,
+since it is almost always a typo,
+but it is not an error:
+the term is kept and simply never matches.
+A number is not checked against any table,
+as a process may issue any system call number the kernel can hold and
+.Nm
+reports the attempt whether or not a system call is implemented behind
+it;
+only a number too large to be one at all is rejected.
+An unknown group is an error.
.It Fl p Ar pid
Follow the process specified by
.Ar pid
@@ -99,12 +260,35 @@
.Pp
Follow an already-running process:
.Dl $ truss -p 34
+.Pp
+List the groups that
+.Fl t
+understands:
+.Dl $ truss -t
+.Pp
+Show only the file and network activity of a command:
+.Dl $ truss -t @file,@net fetch https://www.freebsd.org/
+.Pp
+Show everything a command does except its memory management:
+.Dl $ truss -t '!@memory' make buildworld
+.Pp
+Show the descriptor activity of a running process apart from the
+data transfers themselves:
+.Dl $ truss -t '@desc,!@read,!@write' -p 34
+.Pp
+Count the
+.Xr readlink 2
+and
+.Xr readlinkat 2
+calls made while starting a program:
+.Dl $ truss -c -t 'readlink*' /bin/ls
.Sh SEE ALSO
.Xr dtrace 1 ,
.Xr kdump 1 ,
.Xr ktrace 1 ,
.Xr ptrace 2 ,
.Xr utrace 2 ,
+.Xr fnmatch 3 ,
.Xr sysdecode 3
.Sh HISTORY
The

File Metadata

Mime Type
text/plain
Expires
Fri, Sep 4, 10:47 AM (18 h, 54 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38020411
Default Alt Text
D59275.diff (39 KB)

Event Timeline