Skip to content

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.

Published on 4 min read

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 dtrace as root, and you can use the high-level syscall provider 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 arg0arg7 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:

  1. SIP blocks probes on signed Apple binaries. If you dtrace -p $(pgrep Safari) you'll get nothing — Safari is signed and AMFI denies attach. The csrutil enable --without dtrace workaround above is the only way around it for restricted-entitlement binaries.

  2. syscall::entry doesn't see nosys returns. When a syscall slot has been removed (or a never-implemented number is called), the syscall provider still records the entry but arg0 of the return is ENOSYS and the call is dispatched to a generic no-op. Check the return value if you're confused why a count looks high.

  3. fbt (Function Boundary Tracing) is gone on Apple Silicon. On Intel macOS, fbt:::entry could 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

Related articles

What actually happens when an arm64 Mac program issues an SVC instruction — from the user-space stub down to the BSD syscall dispatcher and back.
Endpoint Security is the modern, supported path for syscall-level detection on macOS — but its event taxonomy doesn't map 1-to-1 with syscalls. Here's the practical mapping for security work.