Poppy — dynamic XPC observability and fault injection for macOS

A toolkit combining Frida instrumentation, DTrace probes and custom injectors for unified analysis of XPC daemon behaviour on macOS arm64e — the platform where Pointer Authentication Codes have made static call graphs unreliable.

Stuart Thomas

Independent Security Research — Whitby, North Yorkshire, United Kingdom

24 May 2026  ·  Status: Early-stage / experimental  ·  Platform: macOS arm64e  ·  Licence: MIT  ·  Source: github.com/jetnoir/poppy


1. Summary

Poppy is a macOS research toolkit for observing and probing the behaviour of XPC daemons at runtime. It pairs Frida-driven JavaScript agents with DTrace probes and a handful of custom Objective-C injectors to produce a unified, JSONL-formatted trace that downstream analysers turn into entitlement maps, coverage diffs, and anomaly reports. The use case it was built for is the one that occupied most of my macOS work in 2025-26: a daemon you suspect is doing something interesting on an XPC boundary, but whose static call graph is no longer the whole picture.

The repository is at github.com/jetnoir/poppy, licensed under MIT.

2. Why PAC broke the old playbook

For a long time the standard recipe for understanding a macOS daemon was: pull the binary out of the dyld shared cache, throw it at a disassembler, walk the call graph, identify the XPC handler vtable, follow the dispatch logic by hand. On x86_64 that recipe worked. On arm64e — the silicon Apple has been shipping on consumer hardware since 2020 — it works less and less well, because Pointer Authentication Codes embed a cryptographic signature in the high bits of indirect-branch targets. The signed pointers in memory don’t resemble the unsigned pointers your disassembler is showing you. Dynamic dispatch through PAC-signed vtables becomes invisible to static analysis — you can see that a virtual call is happening, but not where to.

The pragmatic answer is to stop pretending you can recover that information statically. Watch the daemon run, observe what it actually calls, and reconstruct the call graph from runtime evidence. That is what Poppy does.

3. What Poppy does

The shipped feature set covers:

4. Using it

A typical workflow looks like:

# 1. Trace the daemon while it does its normal work
sudo python3 poppy.py run --daemon tipsd --duration 60

# 2. Inject the standard fault corpus
sudo python3 poppy.py inject --daemon tipsd --variants all

# 3. Look at what was unusual
python3 analysers/anomaly.py runs/poppy_tipsd_*.jsonl

# 4. Produce the entitlement map
python3 analysers/entitlement_map.py runs/poppy_*.jsonl --md > entitlements.md

Each step writes JSONL to a timestamped directory under runs/. Steps three and four are independent of step two — you can run them across any earlier trace.

5. Design choices

A few decisions worth flagging:

JSONL as the lingua franca. Every component — Frida agent, DTrace probe, Python harness — writes one JSON object per line. Downstream analysers stream-read. This is unglamorous but it means a one-hour trace produces a file you can grep sensibly, and the analyser pipeline does not have to be Python-aware to participate.

Frida and DTrace, in that order. Frida gives you the high-level XPC dispatch view (it can hook the Objective-C runtime, inspect arguments, modify state). DTrace gives you the system-call view (it sees what the daemon actually asks the kernel for). Both are needed because either alone leaves gaps that the other fills. The two streams are merged on timestamp at analysis time.

Root and SIP. The tool needs root for Frida injection into system daemons. For DTrace probes against Apple-signed binaries it needs SIP disabled — or, equivalently, the appropriate nvram boot-args set. This is a research box, not a workstation, and that posture is appropriate. The README is unambiguous about this.

Optional PySide6 GUI. There is a Qt-based dashboard for live trace viewing, gated behind an optional dependency. The command line is canonical; the GUI is convenience.

6. Status

Poppy is early-stage — an honest description, not a humble-brag. The repository is small, the test coverage is limited, and the public release lags my private working tree by a non-trivial margin. The core workflow (trace → inject → analyse) is solid in my own use; the rough edges are around configuration, error reporting, and the GUI.

I publish it because the underlying technique — Frida + DTrace + JSONL for unified XPC observation — is worth being part of the public conversation about macOS daemon research, not because the tool is finished.

7. Limits and prerequisites

Poppy is released under the MIT licence. The author makes no warranty as to fitness for any particular purpose. Users are responsible for ensuring that their use of the tool complies with applicable law, including the Computer Misuse Act 1990 (England and Wales) and Apple’s own developer agreements. The tool is intended for use against systems the user owns; running it against Apple-supplied software on Apple-supplied hardware is, in the author’s view, plainly within the own-hardware scope of the CMA, but each user’s circumstances are their own to judge.

The dependencies — Frida, DTrace, PySide6, PyObjC — are the work of their respective authors and are used under their own licences.