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.
Independent Security Research — Whitby, North Yorkshire, United Kingdom
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:
- XPC handler tracing. Every
xpc_connection_*handler that fires is logged, with the message type, sender PID, and entitlement context at the moment of dispatch. - Message tracing. The actual XPC dictionary contents are dumped to JSONL — payloads, keys, types — subject to the size limits you set.
- Entitlement check monitoring. Calls to the entitlement-checking APIs are intercepted and recorded, so you can see which entitlements the daemon actually consults at runtime (almost always a smaller set than the
codesign -d --entitlementsoutput suggests). - Fault injection. A controlled corpus of malformed XPC messages is sent to the target, one variant at a time, and the daemon’s response is recorded. This is the bit that turns observation into research.
- Anomaly detection. A small analyser walks the JSONL output looking for deviations from baseline behaviour: handlers that fire only under fault conditions, entitlements consulted only on edge cases, messages that produce unusual response patterns.
- Coverage diffing and entitlement mapping. Compare two runs and see what is different; produce a Markdown map of the daemon’s effective entitlement surface.
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
- macOS only. The XPC machinery is Apple-specific. Pretty much everything about Poppy is too.
- Root and (usually) SIP-disabled. This is a research-box tool, not something you run on your daily driver.
- Apple-signed daemons only. Third-party XPC services work too, but the entitlement-mapping logic assumes a system daemon and the anomaly baseline is calibrated against one.
- Fault corpus is small. The shipped corpus covers the obvious malformed-message classes; serious research will want to grow it.
- Not a bug-finder. Poppy tells you what the daemon does. It will not tell you what the daemon does wrong. That judgement is the researcher’s.
Legal note
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.