Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F170073557
D59275.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
39 KB
Referenced Files
None
Subscribers
None
D59275.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D59275: truss: add -t to select which system calls are reported
Attached
Detach File
Event Timeline
Log In to Comment