Vision
The manual page for airportd, the daemon that runs Wi-Fi on every Mac, is 13 lines long and was last revised in 2006:
$ man airportd
airportd(8) System Manager's Manual airportd(8)
NAME
airportd – airport daemon
SYNOPSIS
airportd
DESCRIPTION
airportd manages wireless interfaces. airportd should not be invoked
directly.
macOS 26.5 7/5/2006 macOS 26.5
The binary those 13 lines describe is a universal Mach-O carrying an arm64e and an x86_64 slice; the macOS 26.5 build of it is 3,220,640 bytes. Reading it — never running it — takes 10.4 seconds on an Apple Silicon laptop and returns, among other things, these:
$ openbinary airportd # excerpts from one 24,848-line report
Classes: 37
Methods: 1593
Frameworks: 37
Private frameworks: 15
XPC services: 14
Telemetry strings: 118
Function starts: 2793 (from LC_FUNCTION_STARTS)
Imports: 811 (185 weak, 0 lazy)
── Capabilities (40 from entitlements, 33 from frameworks) ──
[LOCATION]
com.apple.wifi.bypass-location-services — Access WiFi-based location
without user consent
[KEYCHAIN]
com.apple.keystore.sik.access — Access Secure Enclave sealed keys (SIK)
[STORAGE]
com.apple.private.security.nvram.wifi-psks — Read and write WiFi
pre-shared keys in NVRAM
── Unmapped Entitlements (9 — not in knowledge base) ──
com.apple.AppleNVMeEAN.allow
com.apple.keystore.fdr-access
com.apple.private.ZhuGeSupport.CopyValue
com.apple.private.wifid.host.network
com.apple.private.wifid.interface.management
com.apple.private.wifivelocity
com.apple.rootless.volume.iSCPreboot
com.apple.security.iokit-user-client-class
com.apple.security.temporary-exception.iokit-user-client-class
── Credentials (5 CWKeychain + 0 keychain sites) ──
Wi-Fi PSK / EAP credentials: CWKeychainDeleteWiFiPassword,
CWKeychainFindWiFiPassword, CWKeychainSetWiFiEAPIdentity,
CWKeychainSetWiFiEAPUsernameAndPassword, CWKeychainSetWiFiPassword
── telemetry (118 entries) ──
Destinations: {CoreAnalytics: 2, CoreWLAN-analytics: 14, Symptoms: 2,
WiFiAnalytics: 12, WirelessDiagnostics: 8,
wifianalyticsd: 2, other: 56}
-[CWAnalyticsManager submitPowerState:]
-[CWAnalyticsManager submitWiFiAnalyticsMessage:]
-[dpsManager submitDpsSymptom:isCriticalApp:]_block_invoke
That is one slice, not the file. The architecture is auto-detected from the header and only the host slice — arm64e here — is analyzed, so the x86_64 slice contributes nothing to those counts.
None of the above is in the documentation. None of it is a vendor claim either, but the lines differ in how far they are facts:
| Reported fact | Read from | How far it is a fact |
|---|---|---|
| 811 imports, 37 frameworks, 15 private | the LC_DYLD_CHAINED_FIXUPS table and the LC_LOAD_DYLIB load commands | verbatim from structures the loader itself resolves |
| 49 entitlements | the LC_CODE_SIGNATURE SuperBlob — the 0xfade7171 XML blob, or the 0xfade7172 DER blob modern binaries use instead | the key list is the signed blob verbatim; the plain-English description is not. A hand-maintained table of about 34 exact keys and 7 prefix rules describes 40 of them; the other 9 are reported undescribed rather than dropped |
| 118 telemetry strings across 7 destinations | __cstring and __oslogstring text matched against an 18-entry substring table, first match wins | destination labels, not proven sinks. 56 of the 118 match nothing, and the matched list carries printf templates ("%s: Failed to submit task with error: %@") beside real submitters |
What the daemon is reported to do
Seven behavior rules fire on airportd, all at informational severity, and each is a conjunction of an import and an entitlement rather than a traced dataflow:
| Rule | What it reports | What produced it |
|---|---|---|
behavior.wifi-scan-privacy | Wi-Fi scans with a location-bypass entitlement — BSSID lookups uncover physical location | Apple80211Scan import + com.apple.wifi.bypass-location-services |
behavior.nvram-write-credentials | writes Wi-Fi credentials to NVRAM | com.apple.private.security.nvram.wifi-psks + a CWKeychain* import. No path from a keychain read to an NVRAM write is proven |
behavior.background-scan | background Wi-Fi scanning | Apple80211Scan + a dispatch timer source |
behavior.regulatory-domain-modify | modifies 802.11 radio state | Apple80211Set / SetProperty / SetPower |
behavior.darkwake-network | subscribes to dark-wake network events | com.apple.private.dark-wake-network-reachability |
behavior.kernel-tracing | writes to the kernel trace buffer | kdebug_* imports |
behavior.self-sandbox | self-sandboxes via sandbox_init() | the sandbox_init import |
So: a process on your machine can locate you without a consent prompt, holds your Wi-Fi passwords, has the entitlement to write them into NVRAM, sends strings to named analytics destinations — and voluntarily confines itself at startup. The output is an indicator set, not a verdict.
That is the situation for every binary on every machine. Processes nobody outside the vendor has read decide what touches your files, your network, your keychain, your camera. Parts are one command away — codesign -d --entitlements - prints all 49 entitlement keys, otool -L the 37 frameworks — but not the categorization, the descriptions, the cross-reference of entitlement against import, the XPC surface, the telemetry submitters or the call graph behind them. Reaching those meant manual disassembly: one binary, one specialist, days of work. So almost nothing gets looked at.
A compiled binary is not a sealed box. It is an unread document, and reading it can be automatic, fast, and open to anyone who can ask a question.
Reading without execution
Behavior is recovered from the bytes, and no instruction from the subject ever reaches the CPU: no sandbox, no VM, no instrumented run, and no network call anywhere in the analysis path. Where a value exists only at runtime — a string decrypted at startup, an API name resolved from a hash, a packed config — a bounded interpreter walks the lifted intermediate language instead of the processor (Emulator). The bounds are fixed per driver:
| Driver | Step budget | Other bounds |
|---|---|---|
| Jump-table and default probes | 4,096 IL operations — one step is one lifted IL op, not one machine instruction | no wall-clock deadline, so a run is reproducible host to host |
| String / config extraction | 262,144 IL operations — sized so a full 64 KiB overlay decrypt at a few ops per byte completes | any single recovered region is capped at 4,096 bytes |
| Entry-point unpacking | 64,000,000 IL operations — compression-class stubs exhaust the extraction budget | 8-second wall-clock deadline, the only one in the subsystem: a backstop against a per-op cost outlier, bought with determinism |
Exhausting a budget discards the run's result rather than truncating it: a partial decrypt is dropped, not surfaced.
Decoder coverage
Coverage is broad against enumerated specifications and thin against files that have actually shipped (Architectures):
| Count | What it counts | Qualification |
|---|---|---|
| 104 | targets in the coverage registry | the denominator for every row below. Not all are third-party specifications: alongside instruction sets and executable formats it counts internal manifests — CWE detectors, hardening checks, calling conventions, IL opcodes, Apple APIs — and toy languages (Brainfuck, Malbolge) |
| 13,766 | items those targets' specifications enumerate | — |
| 12,975 | of those items with a dispatch arm | 94.3% |
| 15 | targets exercised against a real binary | arm, arm64, arm64e, ebpf, elf, macho, mips32, mips64, powerpc, riscv, s390x, sparc, wasm, x86, x86_64 |
| 89 | targets never exercised against any shipped file | 75 sit at 100% dispatch coverage — exhaustive on paper, meeting their first real input in production. The other 14 are incomplete on paper too: apple_apis 91.4%, coff 56.2% |
Analysis cost and scale
Reading a binary costs seconds, so the unit of work stops being a binary and becomes a machine, a firmware image, a fleet — every file unpacked from a vendor image, every host in an organization, every version of the same product side by side. Not one analyst on one binary over days.
| Operation | Subject | Wall time | Output |
|---|---|---|---|
| Full JSON report | /bin/ls | 0.55 s | — |
| Full report | airportd, 3,220,640 bytes | 10.4 s | 24,848 lines |
openbinary reconstruct | airportd | 66 s | 59 source files, 85,519 lines |
Measured on an idle Apple Silicon laptop; the figures move by more than half again under load.
The MCP tool surface
48 MCP tools sit over the corpus — corpus search, per-function decompilation, call-graph reachability that returns the witness path rather than a yes — so an agent can ask its own follow-up questions instead of receiving a report someone else scoped. It stops being a craft and becomes a query. Not all 48 are open doors:
| Gate | Tools | Effect |
|---|---|---|
| Per-user grant that no plan tier unlocks | get_findings, download_binary, binary_global_stats, annotate_corpus_function | most callers never see them; they return not-found rather than an error |
| Pro plan floor | the 8 annotate_* workspace tools | not-found below Pro |
| None, on the tool — Pro plan floor on the richer result | get_strings full mode, vulnerabilities per-CVE detail | free callers get a reduced result, not an error |
| None | the other 36, those two among them | a caller with no token sees the public corpus only |
Reachability answers carry their own limit. A reachable: false is trustworthy only when the search also reports complete: true — frontier fully resolved, not truncated, roots found. On an Objective-C or XPC daemon it usually is not, because objc_msgSend, dispatch_*, xpc_* and block edges cannot be followed: on airportd itself, a no is not an answer. The tool returns the unresolved call sites and the dispatch boundaries that caused it.
Source reconstruction
openbinary reconstruct turns airportd into a Rust project, because understanding is not the end state: 59 source files, 85,519 lines, all 1,593 recovered methods emitted, none stubbed, zero unrecovered IL operations.
| Part of the generated project | Files | Lines |
|---|---|---|
src/classes/ — one module per recovered Objective-C class | 37 | 70,027 |
src/types.rs — callee stubs and type aliases | 1 | 11,745 |
src/stubs/ — framework stubs | 3 | 3,412 |
src/protocols/ — one module per recovered protocol | 14 | 98 |
src/functions/mod.rs — non-method functions | 1 | 2, and empty |
lib.rs and three mod.rs | 4 | 235 |
Roughly 18% of the line count is generated scaffolding rather than recovered code. Here is one recovered method, the analytics submitter named in the telemetry list above, elisions marked:
// selector: -[CWAnalyticsManager submitPowerState:]
fn CWAnalyticsManager_submitPowerState(mut arg0: AnalyticsManager, mut arg1: bool) {
let mut x0: u64 = 0;
#[allow(non_upper_case_globals)] const sp: u64 = 0;
let mut x0: u64 = (class!(WAMessage)) as u64;
x0 = (func_0x1000e0c9c()) as u64;
x0 = (func_0x1000e7900()) as u64;
if x0 == 0 {
let mut arg10_cstr = 0x1001390b8u64 /* "-[CWAnalyticsManager submitPowerState:]" */;
unsafe { *((((((sp as u64).wrapping_add(16)) as u64).wrapping_add(8)) as usize) as *mut u64) = (&arg10_cstr as *const _ as usize) as u64; }
let mut x0: u64 = (4) as u64;
x0 = (func_0x1000dfdfc(x0, 0x100139078u64 /* "<%s[%d]> %s: %s: Failed to alloc WAMessage for ..." */)) as u64;
return;
} else {
x0 = (class!(NSNumber)) as u64;
x0 = (func_0x1000e99c0()) as u64;
// ...
}
}
The class is named, the selector is named, the branch structure is the original branch structure, and the log strings are resolved back to their literals.
Reconstruction limits
What is missing is everything above the machine level — and, on Mach-O, a third of the functions.
-
Unnamed functions are dropped, not stubbed — 1,040 of the 2,793 functions in
LC_FUNCTION_STARTScarry no symbol name, and the Mach-O scaffolder skips every one on the assumption that a nameless function is runtime or standard-library code. That is whysrc/functions/mod.rsis empty and a stripped Apple daemon reconstructs to Objective-C methods and nothing else. PE and ELF are not filtered. -
Callee names are known but not applied — 1,675 distinct call targets emit as
func_0x…; for 97.7% the name was already recovered, and the text report names the callees of the method above. By the section each target address falls in:Callee section Targets Name already recovered as __objc_stubs1,403 (83.8%) the selector, printed by the same run as objc_msgSend$initWithMetricName:options:__auth_stubs233 (13.9%) an import resolved through the chained-fixups table __text39 (2.3%) nothing — genuinely unnamed -
Every callee is a no-op — all 1,675 are defined in
types.rsas bodies returning zero, called from 16,075 sites in the class modules; theclass!macro expands to0u64at a further 1,607 sites. Fixing the compile errors would still leave the project inert at every cross-function boundary. -
Types — values are still registers typed
u64. -
Stack frames — there is no modelled frame behind the stack pointer, so
spis bound to the constant zero and the store above writes the format-string argument to address 24: syntactically Rust, semantically a null-page write.
Compilation status
openbinary verify runs cargo check over the generated project, and every coverage metric reads perfect while the project does not build. It reports class coverage 100%, method coverage 100%, lift ratio 100%, total unknowns 0 — and then:
Compiles: NO
Errors (4658):
error: `<<` is interpreted as a start of generic arguments for `u64`, not a shift
error[E0425]: cannot find value `lr` in this scope
error[E0061]: this function takes 6 arguments but 2 arguments were supplied
Total unknowns: 0 counts unrecovered IL operations only: the same run's project manifest records 74 unresolved class references against 1,607 resolved, and the emitted source carries 74 /* UNKNOWN classref */ sites.
Every one of the 37 class modules carries errors, concentrated in the XPC surface:
| Class module | Errors |
|---|---|
| Interface subsystem | 1,256 |
| Interface context | 1,125 |
| Connection class | 892 |
4,605 of the 4,658 attach to a source file, and the largest single class — 2,923 — is E0061, a callee stub declared with the arity of one call site and called with another. The no-op stubs are the compile failure, not a separate problem beside it.
The bet is that this distance closes — that a binary gets reconstructed into source you can read, diff, and rebuild, so a closed implementation stops being something you are required to trust. Today the output is a faithful skeleton whose joints all return zero.
Open binaries and hardware sovereignty
Every binary open: readable, searchable, auditable, comparable across every version, whoever shipped it.
And then the part that started it: sovereignty over hardware you already own. Take the best silicon made and refuse the software that shipped with it — Apple-silicon Macs on Linux, keeping the parts that are actually used, AirDrop and AirPlay and the AirPods and iPhone handoff, recovered from their binaries and reimplemented in the open. Porting to each new chipset becomes a pipeline step rather than a multi-year effort. None of that exists yet; it is the reason the reconstruction target is Rust — the same language a reimplementation would be written in, so analysis and replacement are one pipeline run to different depths.
Preconditions
Three conditions had to hold at once: a fast self-contained stack, a never-execute discipline, and consumers that read structured facts.
- A fast, self-contained stack — a decoder and lifter fast enough that reading a binary costs seconds rather than days, with no network dependency anywhere in the analysis path. It ships as three binaries: an operator CLI that needs no server; a coordinator that serves the API and MCP and owns the corpus, accounts and job stores but spawns zero compute of its own; and a worker that runs the engine pool and takes every analysis, reconstruction and unpack job. A coordinator with no worker analyzes nothing.
- A never-execute discipline — the subject is never run, so the same pipeline is safe to point at malware and at a vendor's signed system daemon.
- Consumers that read structured facts — language models and the agents built on them, which is what makes a 24,848-line report on one Wi-Fi daemon usable by someone who has never read disassembly.