Tracing macOS syscalls with dtrace on Apple Silicon
A practical guide to using DTrace to trace BSD syscalls and Mach traps on modern macOS — including SIP requirements, working scripts, and what to do when Apple's syscall probes go missing.
DTrace is still the most powerful syscall-tracing tool on macOS in 2026 — but it's also a tool that fights you a little harder every release. SIP gates most of the useful probes; Apple Silicon broke some of the longstanding scripts that assumed Intel register names; and the BSD/Mach split means you need two different provider conventions to cover the whole syscall surface. This guide walks you through what still works and how to use it.
Prerequisites
DTrace ships in /usr/sbin/dtrace on every Mac, but the probes you need are gated by System Integrity Protection (SIP). Specifically:
- Without SIP changes: you can trace your own processes if you run
dtraceas root, and you can use the high-levelsyscallprovider for any process you launch yourself. - With dtrace_unrestrict (recommended for research): boot into recovery, run
csrutil enable --without dtrace, reboot. Now you can trace any process — including signed Apple binaries.
If you're working on a corporate Mac or a lab device where reducing SIP is acceptable, the second mode is what you want. If you're stuck with full SIP, the syscall provider still works for processes you launch as root.
Tracing every BSD syscall a process makes
The simplest useful script: every BSD syscall, ordered by frequency, for a given PID.
#pragma D option quiet
syscall:::entry
/pid == $target/
{
@[probefunc] = count();
}
END {
printa("%-30s %@d\n", @);
}
Save as syscall-count.d and run:
sudo dtrace -s syscall-count.d -c "/usr/bin/curl -s https://example.com -o /dev/null"
You'll see something like:
mmap 23
read 31
stat64 47
write 12
close 19
...
That's the BSD syscall path. The syscall provider is a thin shim over unix_syscall64() in the kernel — every entry corresponds 1:1 to an entry in the BSD reference (filter by family = BSD).
Tracing Mach traps
Mach traps don't go through the syscall provider. To see them you need the mach_trap provider, which Apple has kept around (though it's lightly documented).
#pragma D option quiet
mach_trap:::entry
/pid == $target/
{
@[probefunc] = count();
}
END {
printa("%-30s %@d\n", @);
}
Run the same way. You'll typically see:
mach_msg2_trap 1432
mach_reply_port 7
task_self_trap 3
thread_self_trap 18
mk_timer_arm_leeway_trap 24
That's the Mach side — every entry maps to a Mach trap reference page (filter by family = Mach). mach_msg2_trap dominates the count because almost every form of cross-process communication on macOS — XPC, distributed objects, IOKit, even AppKit event delivery — bottoms out in a Mach message.
Tracing one specific syscall with arguments
This is where DTrace earns its place over strace/fs_usage. The probes have typed arguments you can inspect inline:
#pragma D option quiet
syscall::open*:entry
{
printf("[%d] %s\n", pid, copyinstr(arg0));
}
sudo dtrace -s open-trace.d
Every open() and openat() call system-wide, with the path resolved at probe time. Useful for finding hardcoded paths in opaque binaries, watching configuration loads, or building a content-aware EDR rule.
The arg0–arg7 registers are the syscall arguments in the order the C prototype declares them. For open that's (path, oflag, mode), so arg0 is the path pointer.
Watching for task_for_pid calls
For security work, the single most interesting Mach trap is task_for_pid. DTrace can show you every successful and failed call system-wide:
#pragma D option quiet
mach_trap::task_for_pid:entry
{
self->target_pid = arg1;
self->caller = execname;
}
mach_trap::task_for_pid:return
/self->target_pid/
{
printf("[%-15s pid=%-5d] task_for_pid(%d) -> %d\n",
self->caller, pid, self->target_pid, arg1);
self->target_pid = 0;
self->caller = 0;
}
You'll see legitimate uses (Xcode debugger, Activity Monitor) and any sketchy ones (a userland process trying to grab launchd or WindowServer). On a clean system this script should be very quiet.
Why Apple's syscall probes sometimes go missing
Two practical gotchas have bitten people repeatedly:
-
SIP blocks probes on signed Apple binaries. If you
dtrace -p $(pgrep Safari)you'll get nothing — Safari is signed and AMFI denies attach. Thecsrutil enable --without dtraceworkaround above is the only way around it for restricted-entitlement binaries. -
syscall::entrydoesn't seenosysreturns. When a syscall slot has been removed (or a never-implemented number is called), thesyscallprovider still records the entry butarg0of the return isENOSYSand the call is dispatched to a generic no-op. Check the return value if you're confused why a count looks high. -
fbt (Function Boundary Tracing) is gone on Apple Silicon. On Intel macOS,
fbt:::entrycould probe arbitrary kernel functions. On arm64 macOS it's disabled. You're limited to the well-known providers (syscall,mach_trap,proc,sched,io).
Alternatives when DTrace can't reach
When DTrace won't cooperate — restricted binaries, or you need higher-level events — Endpoint Security (ES) is the modern path. ES isn't a syscall tracer, but it surfaces ~80 curated events: ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_GET_TASK, and so on. Most are equivalent to a specific syscall.
The next post in this series, Detecting syscall abuse with macOS Endpoint Security in 2026, walks through the practical mapping between syscalls and ES events for security work.
Where to go next
- The reference catalogue gives you per-syscall version history and prototypes that pair with your DTrace output.
- How macOS syscalls work in 2026 explains the kernel side of what
syscall:::entryis actually observing. - The
task_for_piddeep dive is the long-form companion to the DTrace example above.