Sign in

Findings (CWE)

A static-analysis CWE catalog for compiled binaries.

64 CWE classes · 200+ stable rules · 7 signal classes · 20 in MITRE's Top-25 (CWE-22, CWE-78, CWE-89, CWE-94, CWE-119, CWE-125, CWE-190, CWE-269, CWE-276, CWE-287, CWE-306, CWE-416, CWE-434, CWE-476, CWE-502, CWE-787, CWE-798, CWE-862, CWE-863, CWE-918).

The engine reads an executable, lifts it to its intermediate language, recovers control flow and value provenance, and walks the result. Each rule emits findings keyed on an MITRE CWE id with a stable rule identifier, a severity tier, a confidence grade, and call-site attribution where the detector can resolve a caller. The output is a list of facts. Consumers — an analyst, a security team, an automated triage pipeline — assemble those facts into a security claim.

Findings answer one of four security-adjacent questions the engine answers: what is wrong with the code? The other three — is this binary malicious?, what does this binary do?, and does it contain a known-vulnerable version of a known component? — are covered by Malware, Indicators, and CVEs & SBOM. See Security for the orientation map. For the engine that produces findings, see Engine.

Coverage at a glance

The 64 CWE classes group into ten families. Click any CWE to jump to its per-CWE detail entry.

FamilyCountCWEs
Memory safety10119, 120, 121, 122, 125, 415, 416, 476, 787, 825
Dataflow-proven (SSA-taint v2)222, 1284
Cryptography and randomness10295, 326, 327, 330, 332, 337, 338, 347, 916, 1240
Injection and code execution578, 89, 94, 134, 502
Web, server, and cloud5346, 434, 601, 611, 918
Authentication and authorization5285, 287, 306, 862, 863
Identity, privilege, and permissions8269, 273, 312, 345, 693, 732, 782, 798
Filesystem, environment, races915, 59, 243, 276, 367, 377, 426, 560, 668
Allocation correctness2190, 789
Coding hygiene, posture, crashes8209, 215, 242, 252, 369, 467, 479, 676
Total64

Fifty-four of the 64 sit in the curated registry; the other ten (CWE-15, CWE-89, CWE-125, CWE-215, CWE-330, CWE-332, CWE-369, CWE-434, CWE-601, CWE-782) are emitted by detectors that key off MITRE ids without a registry entry. One registry entry — CWE-285, the kernel kauth authorization-bypass shape — has no detector yet; it's a pinned gap held with expected_detections = [] so a future detector can't fire the wrong rule. The SSA-taint dataflow signal class spans sixteen CWEs (15 / 22 / 78 / 89 / 94 / 99 / 119 / 134 / 252 / 415 / 416 / 476 / 787 / 789 / 918 / 1284) whose tight catches require flow-proven sources reaching argument-typed sinks rather than co-occurrence in the call graph.

The newest detector wave targets the web / server / cloud and authentication surfaces that dominate the 2026 CVE record — SQL injection, XXE, SSRF, open redirect, unrestricted upload, JWT alg:none, missing / incorrect authorization, missing pre-auth gates, MITM-able update mechanisms, and weak password-hash work factors. Most are import-set or string gated, so they fire across Mach-O, ELF, and PE rather than only on the proof-grade Mach-O arm64 path.

Triage and prioritisation

A scanner that produces 60-200 findings on a real binary doesn't help an auditor unless it tells them which to read first. The triage signal ships on every finding as a reachable_from_main Boolean on the location enrichment, derived from a forward walk of the recovered call graph. Findings on functions the engine can reach from the binary's entry point — the mainline execution path — carry Some(true). Findings in initialiser-only paths, signal handlers registered but never fired from mainline flow, callbacks that never get hooked up at runtime, and dead-but-linked library code carry Some(false). Findings without a calling function (binary-wide posture, file-level rules) carry None.

The forward walk needs a starting point. Five tiers of anchor resolution run in order:

  1. Named entry point. main or _main in the symbol table.
  2. Daemon entry-loop caller. The function that calls xpc_main, dispatch_main, NSApplicationMain, or __CFRunLoopRun — anchors stripped Apple daemons that have no exported main symbol.
  3. Format-level entry. Mach-O LC_MAIN, PE AddressOfEntryPoint, ELF e_entry. Works on any stripped binary regardless of symbol-table contents.
  4. PIE ELF __libc_start_main shim. When the ELF entry resolves to glibc's _start and main is passed in as a register argument via adrp+add, the data-cross-reference seeds let the walk find it.
  5. Largest-closure fallback. When tiers 1-4 yield a closure covering less than 10 % of the recovered call graph — the signature of a CRT-stub entry that hands off to dyld or glibc through a runtime-resolved indirect call — the function with the largest forward closure becomes an additional walk root. Over-counts mainline-reachable findings on shared-library subgraphs, which is the precision-conservative direction.

The combined effect is broad cross-format reach. Apple system daemons resolve at tier 2; the configd audit splits 147 raw findings into 3 mainline-reachable, 97 unreachable, 47 indirect — a 49× reduction in the primary triage queue. Stripped macOS CLIs (curl, jq, similar) and statically-linked CRT-stub binaries that fall off tier 2 are caught by tier 5; curl/macos resolves 23 of 55 findings as mainline-reachable. Linux ELF binaries resolve through tiers 3-5 depending on libc and language; curl/linux-x86_64 produces 40 of 191 mainline-reachable, ripgrep/linux-aarch64 28 of 62, jq/linux-x86_64 13 of 65. All four numbers were 0 of N before tiers 3-5 landed — every Linux finding looked equally suspicious because no anchor resolved.

What the signal does not prove. Indirect-call edges — function pointers stored in tables, C++ virtual dispatch, ObjC objc_msgSend — are not followed. A finding inside a function reachable only through ObjC dispatch labels Some(false), even when the dispatch is real at runtime. Auditors who suspect dynamic-dispatch reachability spot-check that class of findings manually; the field is a triage hint, not a proof. The forward walk accepts edges from resolved jump tables but stops at unresolved indirect branches, so switch-driven dispatchers transit only when the lifter resolved the table.

The filter is the highest-leverage consumer of the engine's output. Most product surfaces — dashboards, IDE plugins, baseline diffs — sort or hide findings by reachable_from_main before any other dimension.

Detection methodology

Detectors fall into seven signal classes. The class determines what a finding can prove and how it should be read.

Imports — direct symbol-table match on the binary's import set. Proof-grade for "the binary references this symbol" (_strcpy, _objc_msgSend$setExportedInterface:, _kSecAttrAccessibleAlways). Says nothing about whether the call is reachable, attacker-influenced, or guarded — those questions live one layer up in the consumer's triage logic.

Strings — regex or token scan over __cstring, __objc_methname, and CFString surfaces. Proof-grade for presence (an embedded JWT signature, a %n format template, a hardcoded API key); not for use, since the string may be inert metadata.

Call-graph — walks the recovered call graph, looking at the outgoing-call set per function. Splits two ways. Set-membership asks "does this function call A and B?" — the predictable-PRNG-seed and chroot-without-chdir detectors live here. Site-ordered asks "does it call A before B without an intervening sanitiser?" — the TOCTOU, double-free, use-after-free, and null-deref detectors live here. Site-ordered detectors require non-zero per-call-site VA, which the IL bridges populate for Mach-O arm64, Mach-O x86_64, PE x86_64, and ELF arm64 / x86_64.

Byte-aware — walks raw instruction bytes alongside the call graph. The strict register tracker resolves the constant value reaching a target register at a specific call site by walking back through MOVZ and mov xT, xS chains; the literal resolver computes the address loaded into a register via ADRP + ADD or ADR. Two sub-classes live here:

IL+SSA dataflow — full forward-taint analysis over the lifted IL. Each function's IL is reduced to SSA form; per-variable taint state is propagated to a fixed point. Three source classes:

Sources carry their kind so the wrong taint can't reach the wrong sink — an allocator return into a memcpy size argument is not a CWE-1284 candidate. Sinks key off both the call target and the argument position:

Sink kindAt argument ofEmits
Quantitysize arg of memcpy / memmove / strncpy / read / write / …CWE-1284
IndexedWritearray-index writeCWE-787
Readsource arg of a memcpy-class primitiveCWE-119
AllocSizesize arg of malloc / calloc / reallocCWE-789
Derefload of an allocator-return registerCWE-476
ShellArg / ProcArgshell or process spawnCWE-78
DlPathpath arg of dlopen / dlsymCWE-94
FormatStringformat-string arg of the printf familyCWE-134
Pathpath arg of open / fopen / unlink / rename / chmod and the POSIX-2008 *at familyCWE-22
RegistryNamename / sub-key arg of RegOpenKeyEx / RegSetValueEx / RegCreateKeyEx and the Win32 registry familyCWE-99
RegistryDatadata buffer of RegSetValueEx / RegSetKeyValueCWE-15
SsrfURL / host arg of HTTP-client and socket-connect primitivesCWE-918
SqlInjectionquery-string arg of sqlite3_exec / mysql_query / PQexec and the prepared/exec SQL familyCWE-89
ProcessInjectiontarget-handle / memory-write arg of WriteProcessMemory / CreateRemoteThread / NtMapViewOfSectionCWE-94
UseAfterFreeuse of a freed pointerCWE-416 (CWE-415 for the double-free shape)
ClobberedReturn / IsolatedReturnmust-check-return discardedCWE-252

Three sanitiser shapes are recognised by dominator analysis. A candidate is suppressed when every member of its source set is dominated by a matching sanitiser's safe edge:

Confidence: High when no shape touched any source, Medium when shapes matched but none dominated, Low when the source set saturates.

Alias-aware seeding closes the structural recall gap that affected the CWE-476 deref, CWE-252 unchecked-return, and CWE-415 / CWE-416 UAF slices at -O0 and in any compilation mode that spills pointer values into stack slots. The seed-point SSA value dies within a few operations when subsequent reloads produce a different SSA name; the alias pass identifies every SSA value that holds the same logical pointer at the seed (via stack-slot spills, register-copy chains, and stack-slot reads) and seeds them all, bounded to the current function and dominator-reachable predecessors with a 16-member precision cap. The alias pass runs wherever the SSA lattice runs — across the full proof/taint matrix (Mach-O, ELF, and PE; see SSA-taint v2 is cross-format below).

Interprocedural sink-wrapper transparency closes the call-boundary recall gap from the sink side. The intra-procedural lattice only fires when the call target is itself a named sink; a local wrapper that forwards its parameter into a sink — void run_cmd(char *c){ system(c); } — looks like an ordinary Direct(va) call, so the flow is invisible. A single bottom-up fixpoint over the call-graph SCC condensation computes a per-function summary "parameter i provably reaches a sink of class k" (directly or transitively through callees), then flags any caller that passes attacker-tainted data into such a parameter — making the wrapper transparent for the entire taint-v2 sink catalog with one detector. The summary is a positive reachability witness only (an under-approximation: indirect and unsummarised callees add no edge), so it can miss a flow but never manufactures a false positive the intra-procedural baseline would not also produce. Rule family taint.flow.interproc.*, keyed to the same sink classes and CWEs as the intra-procedural lattice (.shell / .exec → CWE-78, .dest-arg → CWE-787, .fmt-arg → CWE-134, .deref → CWE-476, .path-arg → CWE-22, and the rest of the catalog). Live and EMITTING across the proof/taint matrix — measured firing on x86_64 ELF and arm64 (ELF and Mach-O) with no interprocedural false positives on the benign corpus.

Interprocedural callee-access propagation is a distinct, complementary extension that closes the call-boundary recall gap. When a function passes a parameter into a callee that dereferences or frees it, the callee's recovered access pattern is reflected back into the caller's taint state. Three callee-aware rule_ids fire on shapes the intraprocedural lattice cannot see: null-deref.callee-dereferences (CWE-476), use-after-free.callee-uses (CWE-415 / CWE-416), and taint.flow.callee-derefs.<source> (CWE-476 family, with the SSA-taint v2 source kind carried through). The propagation walks bottom-up over the call-graph SCC condensation with a fixpoint only on recursion. Opt-in behind OPENBINARY_INTERPROC_ARG_ACCESS=1 so default reconstruction stays byte-identical; measured impact on the four-fixture corpus: curl 4/8/5, jq 16/3/4, airportd 0/0/1, ripgrep 0/0/0.

Binary-wide — emits one finding per missing protection (PIE, stack canaries, Hardened Runtime, library validation, CS_RESTRICT, dangerous entitlements, debug-info shipped, dSYM-path leak). Keyed off the binary's hardening surface, not per call site. The output is a posture audit, not a per-bug claim.

Composer — runs after every other detector. Cross-correlates findings by their calling-function VA and synthesises multi-detector stacks (compound.2-detector-stack, compound.3-detector-stack, compound.4-detector-stack, compound.many-detector-stack). A binary that fires a tainted-input source detector and a chk-family overrun detector in the same function emits one named compound claim at elevated severity rather than two unrelated findings. The composer also runs cross-detector confirmation: an SSA-taint v2 finding at the same calling-function VA as a static-rule command_injection or dynamic_code finding promotes the latter to High confidence — two independent mechanisms reaching the same call site is a stronger signal than either alone.

The mix matters: a Critical finding from byte-aware proof is a different claim from a Critical finding from a call-graph heuristic. The detector field on every finding names the mechanism; see The finding schema below.

The finding schema

Every detector emits records of the same shape:

FieldMeaning
cwe_idAnchor in the MITRE CWE catalog.
rule_idStable detector-and-shape identifier (dangerous-call.strcpy, xpc.listener.no-code-signing-requirement, oob-write.memcpy-chk-proven). Survives across releases so consumers can group, filter, baseline, and suppress.
detectorWhich mechanism produced the finding: StaticRule (curated symbol or string match), Heuristic (structural pattern over the call graph), SymbolicExec (SSA-taint v2's lattice analysis), Ml (forward-committed slot), Manual (human-annotated, used for training labels).
severityPer-instance hint at the finding's runtime context. See the ladder below.
confidenceHow trustworthy the detection signal is, not how exploitable the finding is.
locationCall-site VA, calling function VA, calling function name. Populated when the detector resolves a caller; None for binary-wide posture findings. Decorated by two post-pass enrichments — forward_calls and reachable_from_main, described below.

Severity ladder. Per-instance, calibrated against runtime mitigations and source patterns:

Severity is per-instance, not per-CWE. A __strcpy_chk call is the same CWE-120 as strcpy, but lower severity. A system() call with a constant-string argument that has no shell metacharacters is the same CWE-78 as system(user_input), but lower severity. Severity carries information about the runtime environment around the source-level bug; the CWE is the source-level bug.

Confidence, by detector mechanism:

Read confidence as "how trustworthy is the detection signal?", not "how exploitable is the finding?".

Location enrichments. Two post-pass enrichments decorate every finding whose caller_va is known. forward_calls is the resolved names of every call site reached from the finding's caller — at-a-glance visibility into which downstream APIs the suspect function calls next, useful when triaging "what does this do after the bad primitive fires?". reachable_from_main is the cross-format triage Boolean; see Triage and prioritisation above for the anchor-resolution mechanism and measured impact across Mach-O, ELF, and PE. Both fields serialise as Option, omitted when None, so consumers parsing earlier-schema JSON continue to work unchanged.

Compound findings. A composer runs after every other detector and synthesises multi-detector stacks. When two distinct findings share a caller_va, the composer emits an additive compound.* finding at elevated severity that names the chain. A binary that calls recv() and then __memcpy_chk() in the same function with a length argument the chk variant proves overruns is the canonical Heartbleed shape; the composer surfaces it as one named claim rather than two unrelated findings.

A compound's strength scales with the type of co-occurrence, not the count. A compound.2-detector-stack that combines an SSA-taint flow with a chk-family proof at the same call site is a strong claim — two independent analyses converged. A compound.many-detector-stack on an interactive-shell main loop (popen + system + getenv + unsafe-open + format-string.fprintf + double-free.same-function + hardcoded-credential.use.structural) is a weaker claim — the function legitimately does many sensitive things and each detector fires correctly on its own pattern, but the co-occurrence is breadth, not depth. Severity reads off the most-severe constituent; consumers ranking compounds for triage should weigh the constituent rule classes, not the count.

Format and architecture coverage

The detector matrix is format- and architecture-aware. Coverage today:

FormatArchitectureSite-VA attributionSymbol resolution
Mach-Oarm64 / arm64eyesfull (nlist + ObjC)
Mach-Ox86_64yesfull (nlist)
PE32+x86_64yesfull (IAT, bare names)
ELFarm64 / x86_64yesfull (PLT / .symtab / .dynsym)

Site-VA attribution lets a finding name the specific call instruction's address inside the calling function — the IDE-jumpable case. Without it, findings still resolve to the calling function but not to the specific call site.

Static-linked ELF binaries are partially blind. With no dynamic imports, the import-keyed detectors (the largest detector class) silently produce zero findings. String-based detectors still work; byte-aware and call-graph detectors that key off recognised symbols do not. Closing the gap means string-based heuristics over the linked-in libc, tracked under follow-up work.

Apple-runtime surfaces are Mach-O-only by construction. The Apple-platform pack — CWE-345 XPC client identity, CWE-312 keychain at-rest protection, CWE-732 TCC entitlement mismatch, CWE-693 DYLD-injection-tolerant compound — is Apple-runtime artefact analysis. Other formats lack the Mach-O entitlement plist, the Objective-C class-ref import, and the codesign blob those detectors read from.

Byte-aware proof-grade detection is arm64-only. The register-trace and literal-resolve passes that underpin proof-grade CWE-787, CWE-125, CWE-121, CWE-190, CWE-269, CWE-560, CWE-825, and CWE-1240 — plus the W^X mmap/mprotect and PT_DENY_ATTACH rules — are arm64-specific. The shape-only CWE-369 detector and CWE-467 detector are dual-architecture; CWE-369 walks both arm64 UDIV / SDIV and x86_64 DIV / IDIV instructions, CWE-467 keys off the size-arg literal at the call regardless of architecture. Equivalent x86_64 byte-aware passes for the proof-grade rules are slotted but not yet implemented; PE x86_64 and Mach-O x86_64 binaries today get the import-, string-, call-graph-, shape-only, and binary-wide-posture coverage but not the proof-grade arithmetic on those CWEs.

SSA-taint v2 is cross-format. The IL+SSA lattice analysis that emits the broader CWE-15 / 22 / 78 / 89 / 94 / 99 / 119 / 134 / 252 / 415 / 416 / 476 / 787 / 789 / 918 / 1284 sink-class catches runs on every format/architecture in the proof/taint matrix (arch_in_supported_matrix): Mach-O (arm64 / arm64e / x86_64, plus ppc and 32-bit arm variants), ELF (arm64 / x86_64 / x86, MIPS, PowerPC, RISC-V, arm, and the embedded register-machine arches), and PE (x86_64 / x86). Managed-bytecode formats (CIL / JVM / pyc / luac / Dalvik) are excluded by design — their calls are token-keyed and their memory is object/ref-based, so the OOB and pointer-taint sink classes are a category mismatch, not a wiring gap. Combinations outside the matrix return an empty taint findings vector — they still get all the import-, string-, call-graph-, byte-aware-, and binary-wide-posture coverage that doesn't depend on the lattice.

Per-CWE detail

CWE-15 — External Control of System or Configuration via Environment

Byte-aware register-trace at getenv / secure_getenv call sites. The literal variable name is resolved at x0 and classified against three buckets: loader-hijack vars (DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, LD_PRELOAD, LD_LIBRARY_PATH, the DYLDFRAMEWORK_* family) at Severity::Medium / Confidence::High; app-custom debug or config vars matching an uppercase identifier with DEBUG / LOG / CONFIG / TRACE / VERBOSE in the name at Severity::Low / Confidence::Medium; standard system vars (PATH, HOME, TMPDIR, …) intentionally not flagged because every Unix tool calls getenv("HOME"). The complementary setenv / putenv / unsetenv writers fire under the same classifier — a binary that writes DYLD_INSERT_LIBRARIES is establishing an injection vector for its child processes. Rule families: env-var-use.loader-hijack, env-var-use.debug-config, env-var-write.loader-hijack.

CWE-22 — Path Traversal

SSA-taint v2's Path sink class. When the lattice proves an external-input source (read, recv, fread, getenv, argv, …) flows into the path argument of open, openat, fopen, creat, unlink, unlinkat, rename, renameat, chmod, fchmodat, chown, lchown, fchownat, mkdir, mkdirat, rmdir, symlink, symlinkat, link, linkat, truncate, access, faccessat, stat, lstat, or fstatat without a dominating sanitiser, the detector emits taint.flow.path-arg.<callee> at CWE-22. The POSIX-2008 *at family is in scope because the path component still flows from input; the dirfd argument doesn't change the traversal exposure. Sanitiser shapes recognised: RealpathNullCheck (a realpath call on the input is null-checked before downstream use — the canonical path-canonicalisation gate). Confidence ladder: High when no sanitiser shape touched any source, Medium when shapes matched but none dominated, Low when the source set saturates. Mach-O arm64 only today. Rule family: taint.flow.path-arg.*.

A second, cross-format detector — archive_extract_unchecked — catches the Zip-Slip shape: a libarchive / libzip / libtar / minizip / miniz importer that reads entry pathnames (archive_entry_pathname and equivalents) without a realpath canonicalisation import. The entry name is part of the archive header — attacker-controlled — and flows straight to the extraction-side open / write, so a crafted ../../etc/... entry escapes the extraction root. Rule families: archive-extract.<lib>.no-path-validation.

Byte-aware flag-mask gate. symlink_follow resolves the flags argument of open / openat / open_nocancel and fires when O_NOFOLLOW (0x0100) is clear and at least one write / create / truncate / append bit is set — a privileged write through a path an attacker can swap for a symlink is the canonical link-following escalation. Read-only opens (O_RDONLY alone) are intentionally not flagged: link following only matters when the code writes, creates, or truncates. CWE-61 (symbolic) and CWE-62 (hard link) are variant children of the same gate. Large Apple CVE cluster across 2025-26. Rules: link-follow.open-without-nofollow.{open,openat,open_nocancel}.

CWE-78 — OS Command Injection

Calls into system, popen, execve, execv, execvp, and posix_spawn flag at import-match time. The byte-aware variant resolves the command-string argument via constant-arg trace at the call site: a literal command with no shell metacharacters demotes to Severity::Info (the source pattern is dangerous but the specific call is not exploitable as injection); a literal with shell metacharacters (;, |, `, $(, &) escalates to Confidence::High; a non-literal argument retains the binary-wide Confidence::Medium finding so the unresolved case stands out. The detector name is command-injection.<callee>.

CWE-89 — SQL Injection

Import-match audit prompt over the SQL-execution surface. sql_injection flags call sites into sqlite3_exec, sqlite3_get_table, mysql_query, mysql_real_query, and the libpq PQexec family — primitives that execute a full SQL string rather than a parameterised statement. The detector can't see whether the query string is concatenated from attacker input (the prepared-statement APIs sqlite3_prepare_v2 / mysql_stmt_prepare / PQexecParams are the safe alternative), so the finding is a surface map: the binary executes dynamic SQL, audit how the string is assembled. MITRE Top-25 #3. Rules: sql-exec.<callee>.

CWE-94 — Code Injection via Dynamic Loading

dlopen, dlsym, and NSClassFromString calls against attacker-controlled paths or names flag CWE-94. The byte-aware variant resolves the path argument: a literal absolute path under /usr/lib/ or /System/Library/ is treated as a benign system load; a literal relative path or a writable directory escalates the finding (and chains into CWE-426 untrusted search path). A non-literal dlopen argument is the canonical "loads code from an attacker-influenced string" shape and emits at Severity::High. Rule families: dynamic-code.dlopen, dynamic-code.nsclass-from-string.

Two remote-eval companions extend the class beyond dynamic loading. lua_eval_remote fires when luaL_loadstring + lua_pcall co-occur with a network-read source — the Redis Lua sandbox-escape shape (CVE-2022-0543). python_eval_remote fires on PyRun_String / PyRun_SimpleString / PyEval_EvalCode / Py_CompileString / PyMarshal_ReadObjectFromString reachable from a network read — the data-science-platform pre-auth RCE shape (LangChain CVE-2024-21513, Marimo, Langflow), excluding the load-from-disk variants (PyRun_File, Py_CompileFile). Both demote to *.local-only when no network source reaches the eval. Rule families: code-injection.{lua,python}-eval.{network-reachable,local-only}.

CWE-119 — Buffer Operations Out of Bounds

The class umbrella for the variant CWEs (CWE-120 / 121 / 122 / 125 / 787). The detector emits the variant rather than the umbrella when the specific shape can be identified, but the umbrella covers the case where a banned function (gets, getwd, wcscpy) is called whose interface admits no safe use under any caller. Detector: dangerous_calls.

CWE-120 — Classic Buffer Overflow

strcpy, strcat, sprintf, vsprintf, gets, scanf/sscanf/fscanf family — primitives with no destination-size argument. Detected at import-match time. The fortified twins (__strcpy_chk, __strcat_chk, __sprintf_chk) carry the same CWE at lower severity (the runtime aborts on overflow, but the source pattern is unchanged). Rule names: dangerous-call.strcpy, dangerous-call.strcat, etc.

CWE-121 — Stack-based Buffer Overflow

Two detectors. The heuristic (loop_heuristic) flags functions with a stack canary and no calls to libc copy primitives — the manual-buffer-copy-loop shape that the static-rule detector misses, at Confidence::Low. The proof-grade detector (stack_overflow_proof) reads three constants from the binary at a memcpy call site — the function's stack frame size from the prologue's sub sp, sp, #F, the destination offset from add x0, sp, #O, and the copy length from MOVZ-resolved x2 — and fires when the arithmetic O + N > F is total. No taint, no flow, no aliasing: given the call site is reached, the overflow is unconditional. Confidence::High, Severity::Critical. Rule: stack-overflow.proof.constant-frame-overrun.

CWE-122 — Heap-based Buffer Overflow

Registered as the heap variant of CWE-119 / 787. Most heap-overrun shapes are subsumed by the chk-family CWE-787 detector, which proves overrun against the dst_size argument regardless of whether the destination lives on the stack or the heap. The cross-function shape — malloc(N) in one function followed by memcpy(_, _, M) in another where M > N — runs through the SSA-taint IndexedWrite sink when the allocation size and the copy length both reach the lattice.

CWE-125 — Out-of-bounds Read

CWE Top-25 entry. Sibling to CWE-787. Fires on __memcpy_chk / __memmove_chk / __bcopy_chk calls where the source argument resolves to an embedded string literal and the byte count n exceeds strlen(literal) + 1. The runtime reads past the literal's NUL terminator into adjacent __cstring bytes — the canonical info-disclosure shape behind Heartbleed-class bugs. The string-family chk variants (__strncpy_chk, __strncat_chk, __strlcpy_chk, __strlcat_chk) are intentionally excluded because they stop at NUL by definition. Confidence::High, Severity::High. Rule: oob-read.memcpy-chk-literal-overrun.

CWE-134 — Use of Externally-Controlled Format String

Two detectors. The string-scan variant flags binaries containing format-string templates with %n (write-where-via-format) or %p (info disclosure via pointer leak) at Confidence::Low. The byte-aware variant resolves the format-string argument at each printf-family call site via ADRP-window trace and emits per-call findings at Confidence::Medium when the string contains dangerous specifiers. Rule families: format-string.write, format-string.pointer-leak.

CWE-190 — Integer Overflow into Allocation

The canonical malloc(count * size) shape. The byte-aware detector reads two constants — the multiplier and the multiplicand — from the call site via strict register trace; when both are constants and their product wraps in 64-bit unsigned arithmetic, fires integer-overflow.mul-into-malloc (and the parallel rules for calloc, realloc, xmalloc, reallocarray). The wrap chains into CWE-787 on subsequent indexed writes into the under-sized buffer. Confidence::High.

CWE-209 — Information Exposure via Error Messages

String-scan over __cstring and __objc_methname for stack-trace template fragments (%@\n%@, at line %d), file-path leakage (/Users/, /private/var/), and format-string pointer leaks. Also flags os_log / NSLog call sites whose format string contains %@ against a non-redacted public argument — the canonical "redacted-by-default convention was bypassed" pattern. Confidence::Medium. Rules: info-disclosure.format-pointer-leak, info-disclosure.os-log.pointer-redacted, info-disclosure.os-log.public-data-leak.

CWE-215 — Insertion of Sensitive Information Into Debugging Code

Binary-wide posture detector across three Mach-O surfaces. posture.debug-info-shipped fires when a release binary still carries a __DWARF segment — symbol names, source-path strings, type info, and inline-function metadata leak directly. posture.symtab-unstripped fires when the function-name density in the symbol table exceeds the stripped-binary norm (a heuristic threshold that lets stripped-with-export-list pass but catches forgotten strip). posture.dsym-path-leak fires when __cstring contains an embedded dSYM bundle path (/Volumes/Build/…/<App>.dSYM/Contents/Resources/DWARF/<App>) — the canonical fingerprint of a release binary that links against a debug archive. Mirrors cwe_checker's DebugInfo check for ELF; ours is Mach-O-only.

CWE-242 — Use of Inherently Dangerous Function

gets specifically — removed from C11 because no caller can use it safely. Detected at import-match. CWE-676 covers the broader SDL banned set; CWE-242 is the variant for the canonical no-safe-use case.

CWE-243 — chroot Jail Without Changing Working Directory

Set-membership call-graph rule. A function that calls chroot without a subsequent chdir("/") in the same function leaves the calling thread's working directory pointing outside the new root — ..-traversal from that CWD escapes the jail. The canonical pre-chroot daemon hardening idiom is chroot(jail); chdir("/") in that order; the reverse order or chroot-only is the bug. Confidence::High, Severity::High. The rule emits on chroot-only; chroot-then-chdir suppresses entirely. Rule: chroot-jail.no-chdir.

CWE-252 — Unchecked Return Value

Two detectors over the same curated must-check set: libc primitives where the return code indicates failure (read, write, recv, send, fork, pthread_create); Security.framework (SecKey*, SecItem*); IOKit (IOServiceOpen, IOConnectCall*). The byte-aware variant classifies the first post-call access to x0: a clobbering write before any read fires the finding; a check-and-branch is a clean read. The SSA-taint v2 variant raises the same shape over the IL+SSA lattice with alias-aware seeding for -O0 and stack-spilled return values — ClobberedReturn when the must-check return value is overwritten before being read on any path, IsolatedReturn when it never reaches a comparison or branch. Rule families: unchecked-return.libc, unchecked-return.security-framework, unchecked-return.iokit, plus the v2 parallels taint.flow.unchecked-return.libc / .security-framework / .iokit. Cross-detector promotion at the same caller_va surfaces the v2 confirmation against the static-rule finding.

CWE-269 — Improper Privilege Management

Byte-aware register-trace at setuid, seteuid, setresuid call sites. When the uid argument resolves to literal zero, fires privilege-escalation.setuid.zero at Confidence::High. Other constant uids surface at Confidence::Medium with the resolved value in the rule_id. The setgid family is excluded pending false-positive measurement on real binaries — the gid-zero shape is rarer than uid-zero, and a noisy roll-out would burn auditor trust.

CWE-273 — Improper Check for Dropped Privileges

Byte-aware register-trace. privilege_drop_check walks setuid / seteuid / setresuid / setreuid / setregid call sites, gates on the target uid resolving to a non-zero constant — a real privilege drop, not the setuid(0) escalation CWE-269 catches — and fires when the same caller never subsequently calls getuid / geteuid / getresuid to confirm the kernel honoured it. The fix shape is setuid(uid); if (getuid() != uid) _exit(1);. Sibling of CWE-252 (checks the return value) and CWE-269 (the class parent). Rules: privilege-drop.no-verify.<callee>.

CWE-276 — Incorrect Default Permissions

Byte-aware mode-and-flags resolution at file-creation sites. insecure_create_mode resolves both the flags argument (for the O_CREAT gate) and the mode argument of open / openat / open_nocancel / creat / open64 / openat64, and fires when O_CREAT is set and the mode is world-writable (& 0o002), 0o777, or carries a SUID / SGID bit. Sibling of CWE-732 (the chmod / mkdir family, handled by insecure_permissions); both can co-fire when code opens with a loose mode then tightens it with chmod — the open-mode finding flags the default-leak window, the chmod finding flags any post-create mistake. MITRE Top-25 #25; broad Apple CVE basis. Rules: insecure-create-mode.{open,creat}.{world-writable,world-everything,setuid-bit,setgid-bit}.

CWE-285 — Improper Authorization

Registered for the kernel-side authorization-bypass shape (CVE-2026-28951, macOS kernel). Userland coverage today is bounded by the CWE-862 CGI detector; kernel kauth-listener and IOKit ACL-bypass patterns are out of scope until the KEXT bridge lands. The labelled case cwe-285-missing-kauth-check pins the gap with expected_detections = [] so a future detector can't quietly fire the wrong rule. No detector emits CWE-285 today — it is the catalog's one registered-but-pending entry.

CWE-287 — Improper Authentication (JWT alg:none)

jwt_no_verify — two-way import gate. Positive: the binary imports a JWT decoder (jwt_decode, cjwt_decode, …). Negative: it imports no JWT signature-verification primitive (jwt_verify, jwt_verify_signature, cjwt_verify, …). Decoder-without-verifier is the canonical alg: none bypass — an attacker forges a token with the algorithm header set to none, the decoder accepts the unsigned payload as authentic identity, authentication is bypassed. CVE-2024-54150 (cjwt), CVE-2022-39227 (python-jwt) class; a 40-CVE Critical cluster in 2026. MITRE Top-25 #13. Rule: improper-auth.jwt.decode-no-verify.

CWE-295 — Improper Certificate Validation

The macOS / iOS TLS-validation-bypass surface. Six rule families across imports, strings, and call-graph + ObjC-class signals. cert-validation.secure-transport-break-on-server-auth fires on SSLSetSessionOption calls that disable server-auth without a matching SecTrustEvaluate on the returned trust object — the canonical Secure Transport break. cert-validation.sec-trust-set-exceptions flags imports of SecTrustSetExceptions and SecTrustSetOptions (the standard "trust this leaf regardless of chain" knobs). cert-validation.deprecated-challenge-handler fires on NSURLConnection's connection:willSendRequestForAuthenticationChallenge: and connection:canAuthenticateAgainstProtectionSpace: — deprecated since iOS 8 / macOS 10.10, almost always implemented as [challenge.sender useCredential:...] without trust evaluation. cert-validation.nsurlsession-delegate-no-evaluate fires on URLSession:didReceiveChallenge:completionHandler: implementations that don't reach SecTrustEvaluate*. cert-validation.credential-for-trust flags the canonical [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] per-caller bypass shape. The string-side rule (cert-validation.ats-exception) flags four App Transport Security plist keys (NSAllowsArbitraryLoads, NSAllowsArbitraryLoadsInWebContent, NSExceptionAllowsInsecureHTTPLoads, NSExceptionRequiresForwardSecrecy=false). Composes additively with unchecked-return.security-framework — both can fire on the same caller. No other static analyzer covers the deprecated-handler and credentialForTrust: shapes today.

A libcurl-flavoured companion, curl_tls_verify, walks curl_easy_setopt call sites and resolves the option enum (x1) and its value (x2): CURLOPT_SSL_VERIFYPEER set to 0 fires Critical (peer-cert trust disabled — MITM with any self-signed cert succeeds); CURLOPT_SSL_VERIFYHOST set to 0 fires High (hostname check disabled — MITM with any valid cert for a different host). This is the dominant binary-detectable cert-validation-disable shape in firmware (D-Link / TP-Link / Zyxel / NETGEAR). Rules: tls-verify.curl.{verifypeer,verifyhost}-disabled.

CWE-306 — Missing Authentication for Critical Function

no_auth_critical — three-way import gate. Positive: HTTP-server primitives (bind + listen + accept / accept4) prove the binary serves network requests. Positive: a child-process-spawn primitive (fork / execve / posix_spawn / system / popen) is the critical capability that, reachable pre-auth, yields RCE. Negative: no authentication primitive across a wide cross-vendor set (crypt, PBKDF2, HMAC / EVP_DigestVerify*, bcrypt / argon2 / scrypt, libgcrypt, Apple Security verify, libjwt, krb5, GSS, ldap_simple_bind_s, pam_authenticate). All three holding proves a pre-auth-RCE capability. MITRE Top-25 #20; a 94-CVE Critical cluster in 2026 (Marimo, Langflow, SmarterMail pre-auth RCEs, plus the IoT and orchestration management-API tail). Rule: missing-auth.network-server.spawn-no-auth.

CWE-312 — Cleartext Storage of Sensitive Information

Apple's deprecated keychain accessibility constants (kSecAttrAccessibleAlways, kSecAttrAccessibleAlwaysThisDeviceOnly) leave keychain items decryptable while the device is locked — and, for the non-ThisDeviceOnly form, even before first unlock after reboot. Detected via direct symbol-import match against the imported global from Security.framework, gated on the binary actually calling SecItemAdd / SecItemUpdate / SecItemCopyMatching. The data_xrefs index attributes the finding to the function that loads the GOT slot for the dangerous constant, so the report names the calling function by VA and symbol. Rules: keychain.accessible-always (Critical) and keychain.accessible-always-this-device-only (High).

CWE-326 — Inadequate Encryption Strength

Byte-aware constant-arg resolution at CCCrypt call sites for AES with key length below 128 bits (16 bytes) and 3DES with key length below 192 bits (24 bytes). Rules: weak-crypto.cccrypt-aes-short-key, weak-crypto.cccrypt-3des-short-key.

CWE-327 — Use of a Broken or Risky Cryptographic Algorithm

DES, MD2, MD4, MD5, SHA-0, SHA-1 detected at import-match. CCCrypt with the algorithm argument resolved as DES (alg = 1) at the call site fires the proof-grade variant weak-crypto.cccrypt-des at Confidence::High; constant-arg resolution is the byte-aware ladder (signature lookup, strict trace, optimistic trace, MOVZ-window scan). Rules: weak-crypto.des, weak-crypto.md5, weak-crypto.sha1.

CWE-330 — Use of Insufficiently Random Values

Co-emitted by the predictable-seed detector when the low-entropy source is one of the canonical "loops don't matter, the user will never notice" candidates — process id (getpid), wall-clock seconds (time(NULL)), or thread id. The rule fires alongside CWE-337 on the seeding pattern, but the CWE-330 finding describes the output — the bytes that downstream code consumes from the PRNG are effectively the seed plus a deterministic transform.

CWE-332 — Insufficient Entropy

Import-table heuristic via the unseeded_prng detector. Fires when the binary imports a libc PRNG output primitive (rand, random, lrand48, mrand48, nrand48, jrand48) but no seeding primitive (srand, srandom, srand48, seed48, lcong48). With no seed call ever made, the PRNG runs from its compile-time default state and emits the same byte stream on every process invocation. Distinct from CWE-337 (seeded with low-entropy source) and CWE-338 (used a weak PRNG at all) — these three detectors are mutually informative but emit independently. Rule: predictable-prng.no-seed-call.

CWE-337 — Predictable Seed in PRNG

Call-graph co-occurrence: a function that calls a PRNG seeding primitive (srand, srandom, srand48, seed48) and a low-entropy source (time, clock, getpid, mach_absolute_time) flags the seeding pattern. Distinct from CWE-338, which flags the PRNG itself; this one flags the seed source.

CWE-338 — Use of Cryptographically Weak PRNG

rand, random, lrand48, srand48 detected at import-match. The fixes (SecRandomCopyBytes, arc4random, getrandom, RAND_bytes) are explicitly excluded so a properly-written binary doesn't fire.

CWE-345 — Insufficient Verification of Authenticity

The macOS XPC client-identity verification surface. NSXPCConnection and NSXPCListener do not validate the peer's code-signing identity by default. A privileged service that exposes an XPC listener without calling setCodeSigningRequirement: (macOS 13+) or hand-rolled validation via auditToken paired with SecCodeCopyGuestWithAttributes will accept method calls from any local process — the canonical macOS local-privilege-escalation primitive (Patrick Wardle's "PEACH" advisory family; the long tail of vendor IPC CVEs).

The detector recognizes the listener (server) role from class-ref imports of OBJC_CLASS_$_NSXPCListener plus selector stubs (setExportedInterface:, setExportedObject:, listener:shouldAcceptNewConnection:), and the connection (client) role from OBJC_CLASS_$_NSXPCConnection plus initWithMachServiceName:, initWithListenerEndpoint:, setRemoteObjectInterface:, remoteObjectProxy. Strong validators (setCodeSigningRequirement:, auditToken, valueForEntitlement:) suppress the finding entirely; the weak validator processIdentifier (pid-spoofable via fork-after-connect) downgrades severity by one tier and annotates the summary rather than suppressing — silently absorbing pid-only validation would over-credit a known-broken pattern.

Per-call-site attribution names the function in the binary that hosts the unvalidated listener or opens the unvalidated connection. Rules: xpc.listener.no-code-signing-requirement (Critical / High under weak-only), xpc.connection.no-code-signing-requirement (High / Medium under weak-only). No other static analyzer covers this surface today.

CWE-346 — Origin Validation Error

origin_validation — shape detector over the origin-comparison surface. Fires when a substring search (strstr / strcasestr / memmem / wcsstr) runs against an embedded trusted-origin literal (trusted.com) — the canonical broken check that trusted.com.attacker.io passes because the trusted host appears as a substring of the attacker-controlled origin. The correct check is a full-string or suffix-anchored comparison. The WebKit Navigation-API same-origin-bypass class (CVE-2026-20643). Rules: auth.origin-substring-bypass.{strstr,strcasestr,memmem,wcsstr,other}.

CWE-347 — Improper Verification of Cryptographic Signature

unverified_dynamic_load — three-way import gate. Fires when the binary imports both dlopen (dynamic code load) and a network-fetch primitive (curl_easy_perform, recv on an accepted socket, an NSURLSession data task) but imports no signature-verification primitive across the major crypto libraries (OpenSSL EVP_DigestVerify* / RSA_verify / ECDSA_verify / ED25519_verify; Apple SecKeyVerifySignature / SecTrustEvaluate*; GPG gpgme_op_verify). The shape is the canonical update-mechanism MITM — download code or data and run it without verifying a signature on the way in. Sparkle CVE-2016-4655 (macOS auto-update MITM) is the historical example; a 46-CVE High+ cluster in 2026 across auto-updaters, plugin systems, package managers, and IoT firmware loaders. Distinct from CWE-345 (broader parent) and CWE-494 (narrower download-without-integrity variant). Rule: signature-verify.missing.dynamic-load.

CWE-367 — Time-of-check Time-of-use (TOCTOU) Race Condition

Site-ordered call-graph detector. Eleven shape variants: a stat / lstat / access / realpath / readlink followed by open / fopen / creat / unlink / rename / rmdir on the same path argument, where the second call is reachable from the first without an intervening mkdir(O_EXCL) or other atomic-create gate. The site-ordering requires non-zero per-call-site VA on every edge. The predictable_tmpfile detector chains in additional TOCTOU-flavoured CWE-367 variants when a hardcoded /tmp/ path appears at a sink whose preceding stat-family check could be raced.

CWE-369 — Divide By Zero

Byte-aware shape detector across arm64 (UDIV / SDIV) and x86_64 (DIV / IDIV). For each division, the preceding 16-instruction window is scanned for a zero-check on the divisor register — CBZ / CBNZ on arm64, TEST r,r or CMP r,0; jne on x86_64. Emits when no recognised check is found. The divisor-register filter is intentionally narrow: arm64 fires only when the divisor is in x0..x7 (the AArch64 AAPCS argument-passing window) so internal computed divisors from libstd hash mixing and BTreeMap block-size constants don't dominate the signal. Rust binaries are skipped entirely — the standard library's checked-arithmetic helpers compile to the same SDIV shape ubiquitously.

Known FP shapes. The window-scan misses any guard that proves "divisor is positive" without a literal compare-to-zero. Two patterns the detector does not recognise: cmp x1, #0x1; b.lt skip (proves x1 ≥ 1 before the divide, common in C overflow-detection helpers like sqlite's sqlite3MulInt64); and the subs / ccmp / b.hi chain emitted when the compiler folds two range checks into a single conditional compare (sqlite's initAvgEq). Both are real guards; both fire as findings. Confidence::Low is calibrated for this — a divide-by-zero finding is a lead, not a claim. Rules: divide-by-zero.udiv.no-check, divide-by-zero.sdiv.no-check, divide-by-zero.div.no-check, divide-by-zero.idiv.no-check.

CWE-377 — Insecure Temporary File

Three independent detectors. The import-match variant flags mktemp, tmpnam, tempnam — primitives that return a path before the file is created, leaving a TOCTOU window; the fixes (mkstemp, mkdtemp, mkostemps) are explicitly excluded. The unsafe_open byte-aware detector resolves the flags argument at open / openat / creat call sites and fires when O_CREAT is set but O_EXCL is not — without O_EXCL the kernel doesn't reject pre-created paths, leaving the race window open. The predictable_tmpfile detector resolves the path argument and fires when the literal sits under /tmp/, /var/tmp/, or /private/tmp/ without an XXXXXX randomness marker — a hardcoded predictable path another process can pre-create or symlink-attack via lsof / fs_usage. Both byte-aware variants fire at Confidence::High when the strict register tracer resolves the constant. Rules: dangerous-call.mktemp / tmpnam / tempnam, unsafe-open.no-o-excl, predictable-tmpfile.hardcoded-path.

CWE-415 — Double Free

Site-ordered call-graph heuristic. A function that calls free on the same register or stack-slot value twice without an intervening pointer assignment fires double-free.same-function. Distinguished from CWE-416 by the second operation being free rather than a use. The SSA-taint v2 UseAfterFree sink class catches the cross-block / aliased shape — a freed pointer is the source, a subsequent free is the sink, and the rule emits taint.flow.uaf.double-free at CWE-415 (the lattice overrides the default UseAfterFree → CWE-416 mapping when the second-side callee is itself a deallocator).

Curated to avoid the cleanup-epilogue long tail. Two suppressions tighten precision against the unconditional-cleanup pattern that produces almost all noise on real binaries. First, adjacent free; free; free (struct-field teardown — free(s->a); free(s->b); free(s)) is excluded; the rule fires only when non-trivial work separates the two frees. Second, the malloc-rich function shape (≥ 3 allocator calls + exactly 1 free) is suppressed — buffer-pool or dynamic-array growth code where the free targets a scratch slot and the subsequent allocation operates on a still-live buffer. The trade-off: an actual double-free that happens to be adjacent or that sits in a malloc-rich function won't fire. Real-binary effect after the curation: airportd 2 / curl 12 / jq 14 / ripgrep 0 (down from 272 hits across the same four binaries before the suppressions).

CWE-416 — Use After Free

Three detectors. The call-graph heuristic flags free(X) followed by a subsequent use of X as a pointer argument to another libc primitive — Confidence::Low, broad recall. The ObjC-specific byte-aware variant (objc_uaf) catches the ARC-elided pattern: objc_release(self.x) followed by an objc_msgSend(self.x, sel) in the same block. The SSA-taint v2 UseAfterFree sink raises the tighter dataflow claim: the freed pointer seeds the lattice, alias-aware seeding propagates the taint across stack-slot spills and register copies, and a subsequent use as a pointer argument to memcpy-class / strcpy-class / objc_msgSend* / Itanium C++ member calls emits taint.flow.uaf.<callee> — cross-block recall the syntactic rule misses. Rules: use-after-free.same-block, objc-uaf.release-then-msgsend, taint.flow.uaf.*.

CWE-426 — Untrusted Search Path

Four rule families across two detectors. The search_path detector emits search-path.suspicious-rpath on LC_RPATH entries pointing to writable or attacker-influenced directories (@executable_path/../, /tmp/, ~/), search-path.dlopen-relative on dlopen with a relative literal path, and search-path.dlopen-writable on dlopen with a literal path under a known-writable directory. The env_var_use detector additionally emits search-path.loader-hijack on getenv("DYLD_INSERT_LIBRARIES") / getenv("LD_PRELOAD") and the wider DYLD_* / LD_* family — a binary that reads these vars is honouring an attacker-controlled search override. The reverse direction (setenv("DYLD_*", ...)) fires under CWE-15 because the writer establishes the override for its child processes; the reader fires under CWE-426 because it honours an override at runtime. Both compose with the CWE-94 dynamic-code rules when the same call site resolves a dlopen against a writable directory.

CWE-434 — Unrestricted Upload of File with Dangerous Type

file_upload — site-ordered shape. Fires when a file written from a network / request source is followed by a chmod that sets an execute bit on the same path — the canonical web-shell-drop primitive. The execute-bit variant (file-upload.write-chmod-exec) is the stronger claim; the no-exec variant flags a writable upload sink for the auditor. Co-fires CWE-862 when the upload handler has no preceding authorization check. MITRE Top-25 #10. Rules: file-upload.write-chmod-{exec,no-exec}.

CWE-467 — Use of sizeof() on a Pointer Type

Byte-aware shape detection across memset, memcpy, memmove, bzero, strncpy, strncat, strncmp. Fires when the size argument resolves to a literal 4 or 8 — pointer-width on 32-bit and 64-bit targets respectively. The canonical bug is memset(p, 0, sizeof(p)) where the programmer meant sizeof(*p).

Known FP shape. The detector cannot tell whether the literal 4 or 8 is sizeof(ptr) or strlen("true"). JSON-keyword recognition and four-character magic-byte comparisons ("true", "this", "\xCA\xFE\xBA\xBE") fire the rule legitimately as sizeof-pointer.strncmp. Rust binaries are skipped because the standard library's Clone / Copy of pointer-sized types compiles to mov w2, #8; bl _memcpy ubiquitously. A Confidence::Low finding here means "look at the call: is the literal a pointer width or a string length?" — auditor decides.

CWE-476 — NULL Pointer Dereference

Two detectors. The site-ordered call-graph rule walks every function for a may-be-NULL-returning call (malloc, calloc, realloc, strdup, fopen, fdopen, popen, opendir, getenv, dlopen) immediately followed by a use of the return value as a pointer argument to another call before any branch-on-null. Rule: null-deref.unchecked-alloc-use, Confidence::Medium. The SSA-taint v2 Deref sink class raises the tighter claim: when allocator-return taint reaches an LDR of the tainted register without a dominating null-check, fires taint.flow.deref.load (or .store) at the lattice's calibrated confidence. The v2 catch covers cross-block flow the syntactic rule misses; the v1 rule still fires when the IL bridge isn't active (non-Mach-O-arm64 binaries).

CWE-479 — Signal Handler Use of a Non-reentrant Function

signal_handler_safety resolves the handler argument (x1) at signal / bsd_signal / sysv_signal call sites to a code VA, walks that handler's forward call list, and fires when any callee is in the POSIX non-async-signal-safe set (malloc, printf, fopen, exit, syslog, strdup, ctime, asctime, …). Calling a non-reentrant function from a signal handler is the classic re-entrancy / deadlock / heap-corruption bug — and an exploitation primitive when the handler interrupts an allocator. Direct sub-pattern of CWE-362; the Apple CWE-362 cluster has 16 assigned CVEs in 2025-26. Rules: signal-handler.non-async-signal-safe.<callee>.

CWE-502 — Deserialization of Untrusted Data

Three deprecated NSKeyedUnarchiver selectors: unarchiveObjectWithData:, unarchiveObjectWithFile:, unarchiveTopLevelObjectWithData:error:. Apple deprecated all three in macOS 10.13 / iOS 11 because they cannot enforce requiringSecureCoding. The detector recognizes both modern Xcode's _objc_msgSend$<selector> stub imports (Tier 1, Confidence::High) and older toolchains' bare-selector dispatch via generic objc_msgSend paired with the OBJC_CLASS_$_NSKeyedUnarchiver class-ref import (Tier 2, Confidence::Medium). Self-implemented same-named methods are filtered. The secure replacements (+unarchivedObjectOfClass[es]:fromData:error:) are explicitly excluded.

CWE-560 — Use of umask() with chmod-style Argument

Byte-aware register-trace at umask call sites. When the argument resolves to a constant in the chmod-permission space (above 0o177, not exactly 0o777), the programmer almost certainly confused the umask convention (mask bits to clear) with chmod (bits to set). Rule: umask.chmod-style-arg.

CWE-601 — Open Redirect

open_redirect — string-shape detector over HTTP redirect construction. Fires when a Location: header is built from a format template that interpolates a runtime value (Location: %s) — the unvalidated-redirect-target shape that phishing and OAuth-token-theft chains rely on. A surface map: the binary builds redirect targets dynamically, audit whether the destination is checked against an allow-list. Rule: open-redirect.location-format-template.

CWE-611 — Improper Restriction of XML External Entity Reference (XXE)

xxe_libxml2 — per-call strict register-trace of the options argument to libxml2's xmlReadMemory / xmlReadFile / xmlReadDoc / xmlReadFd and the xmlCtxtRead* variants. Severity reads off the bit combination of XML_PARSE_NOENT (entity expansion), XML_PARSE_DTDLOAD (external DTD), and XML_PARSE_NONET (network block): NOENT set with NONET clear is Critical (full XXE including SSRF / arbitrary file read); NOENT with NONET set is High (local file read via entity expansion); DTDLOAD with NONET clear is High (external-DTD SSRF / exfil). libxml2 is the dominant cross-platform XML parser (Apple SDK, every Linux distro, embedded firmware); a 13-CVE Critical cluster in 2026 (CVE-2024-25062, plus the SOAP / SAML / XSLT XXE tail). Rules: xxe.libxml2.{noent-with-network,noent-local-only,dtdload-with-network}.

CWE-668 — Exposure of Resource to Wrong Sphere (fd leak on exec)

fd_leak_on_exec — gated on the binary importing an exec-family primitive (posix_spawn / execve / fork / popen / system / …), every open / openat / open_nocancel call site is checked for the O_CLOEXEC (0x0100_0000) bit in its flags argument. A missing O_CLOEXEC means the descriptor is inherited by any spawned child — an authenticated socket, a private file, or a TCC-gated resource opened by a privileged daemon and then leaked to an unprivileged child. The file-descriptor-leak-on-exec sub-shape of the class. Rules: fd-leak-on-exec.no-cloexec.{open,openat,open_nocancel}.

CWE-676 — Use of Potentially Dangerous Function

The SDL banned-function umbrella for alloca, getwd, wcscpy, wcscat, wcsncpy, wcsncat, _alloca, and friends — primitives the SDL bans wholesale because their interface admits no safe use under caller invariants the compiler can't see. Distinct from CWE-242 (gets only); CWE-676 is the broader Microsoft-SDL-derived ban set.

CWE-693 — Protection Mechanism Failure

Four surfaces. The hardening posture detector emits a finding per missing build-time mitigation: PIE absence, executable stack permitted, RWX segments, missing stack canaries, missing FORTIFY_SOURCE, hardened-runtime absent, library-validation absent, CS_RESTRICT absent, plus the dangerous-entitlement set (get-task-allow, disable-library-validation, allow-unsigned-executable-memory, disable-executable-page-protection, allow-dyld-environment-variables, allow-jit).

The DYLD-injection-tolerant compound finding fires when a Developer-ID-signed binary lacks all three of: hardened runtime, library validation, and CS_RESTRICT. Each individual signal is sometimes acceptable (a small CLI tool that doesn't ship to users); the combination on a Developer-ID-signed binary that does ship to users is the canonical macOS local-privilege-escalation surface. Apple-signed and unsigned / ad-hoc binaries are excluded because the loader policy is different and the per-mitigation findings already cover those cases. Rule: dyld.injection-tolerant.

The W^X-violating runtime-allocation detector resolves the protection-flags argument at mmap, mprotect, and vm_protect / mach_vm_protect call sites via the strict register tracer; fires when the resolved literal carries both PROT_WRITE and PROT_EXEC simultaneously ((prot & 6) == 6). Distinct from the load-time posture findings — this catches the runtime establishment of a writable+executable page. Legitimate JIT engines do exist; the auditor decides whether the binary's threat model warrants runtime W^X. Confidence::High, Severity::High. Rules: wx-protection.mmap, wx-protection.mprotect, wx-protection.vm-protect.

The anti-debug primitive detector resolves the request argument at ptrace call sites. When the request equals PT_DENY_ATTACH (31, macOS-specific), fires anti-debug.ptrace.deny-attach at High / High. Canonical fingerprint of jailbreak-detection, anti-tamper, anti-cheat, and anti-RE machinery. Two byte-aware sysctl-mib companions cover the other side of the same family — the (CTL_KERN = 1, KERN_PROC = 14) MOVZ-triplet fingerprint on sysctl argument-vector materialisation, and the P_TRACED = 0x800 post-call mask test (ANDS immediate or the optimizer's UBFX #11, #1 shape) that proves a sysctl probe's post-call code actually reads the trace bit. Defensive use is legitimate; the finding records the call site for the auditor to weigh.

The jailbreak-indicator detector scans __cstring and __objc_methname against a curated set of canonical iOS jailbreak artefacts — Cydia / Sileo / MobileSubstrate, Checkra1n / unc0ver / Taurine, RootHide / Dopamine. A binary that embeds these path fragments is almost always running an in-app jailbreak check (legitimate banking / DRM defence) or, less commonly, a malware fingerprint of the host environment. Fires anti-debug.jailbreak-check at Medium / Medium and surfaces the matched indicator for the auditor's call.

CWE-732 — Incorrect Permission Assignment for Critical Resource

Two surfaces. The byte-aware permission detector resolves the mode argument at chmod, fchmod, chmodat, mkdir, mkdirat, and open-with-O_CREAT call sites and fires when the resolved literal is world-writable (& 0o002), world-executable (& 0o001 on a non-directory), or 0o777.

Known FP shape on mkdir(path, 0777). The standard "create user dotdir if missing" idiom passes 0o777 and relies on the process umask to clamp the on-disk mode (typical 0o022 → 0o755). The detector reads the mode literal but doesn't check whether the calling thread reset its umask via umask(0) before the call, so it fires regardless. A future pass that scans for an umask(0) predecessor in the same function would suppress the idiom; today the auditor confirms by checking whether the caller resets umask.

The TCC entitlement-vs-API mismatch detector fires on sandboxed apps (com.apple.security.app-sandbox = true) that import privacy-sensitive class refs — OBJC_CLASS_$_AVCaptureDevice, OBJC_CLASS_$_CNContactStore, OBJC_CLASS_$_EKEventStore, OBJC_CLASS_$_CLLocationManager, OBJC_CLASS_$_CBCentralManager — without the matching entitlement (com.apple.security.device.camera, personal-information.addressbook, personal-information.calendars, personal-information.location, device.bluetooth). The reverse direction (entitlement claimed without API use) emits a lower-severity over-broad-permission finding. Per-call-site attribution names the function that loads the class-ref GOT slot. Rule families: permission.world-writable, tcc.sandbox.<resource>-no-entitlement, tcc.<resource>-claimed-no-api-use.

A third surface, tcc_db_write, fires when a non-Apple binary writes directly to the TCC privacy store (~/Library/Application Support/com.apple.TCC/TCC.db) — the canonical TCC-bypass primitive that grants itself privacy permissions without the consent prompt. Rule: tcc.db.direct-write.

CWE-782 — Exposed IOCTL with Insufficient Access Control

Import-match audit prompt over the kernel-IPC surface. ioctl opens a device-control surface whose access control lives in the kernel-side driver — the detector cannot verify what gate the call site or the corresponding IOUserClient::externalMethod / character-device cdevsw callback enforces, so the finding is calibrated as a surface map, not a vulnerability claim. IOServiceOpen opens a user-client RPC connection to a kernel IOKit driver; subsequent IOConnectCallMethod traffic is gated by the driver's IOUserClient subclass — audit that the driver enforces entitlement / sandbox / Mach-port checks on the connection request and on each external-method dispatch. Mirrors cwe_checker's CWE-782 module; cwe_checker's own docs acknowledge the same precision limitation. Severity::Info, Confidence::Medium. The IOConnectCallMethod family is intentionally not in this table — those symbols already fire under unchecked-return.iokit (CWE-252) with a tighter "return code was discarded" precision claim, and double-firing on every user-client RPC site would dilute the signal. Rules: dangerous-call.ioctl, dangerous-call.iokit-userclient.open.

CWE-787 — Out-of-bounds Write

CWE Top-25 #1. Two proof-grade rules.

oob-write.memcpy-chk-proven fires on the four-argument fortified family (__memcpy_chk, __memmove_chk, __bcopy_chk, __strncpy_chk, __strncat_chk, __strlcpy_chk, __strlcat_chk) when both the byte count n (x2) and the destination size dst_size (x3) resolve to compile-time constants and n > dst_size. The fortified runtime aborts on this path; given the call site is reached, the source-level copy is unconditionally OOB. The compiler emitted the chk variant precisely because it could not prove safety at compile time; the detector inverts the proof direction.

oob-write.strcpy-chk-literal-overflow fires on the three-argument fortified family (__strcpy_chk, __strcat_chk) when the source resolves to a string literal and strlen(literal) + 1 exceeds the destination size argument. The catch shape is the iconic "compile-time string into too-small buffer" pattern that fills CVE writeups.

Both rules: Confidence::High, Severity::Critical. The SSA-taint v2 detector extends the catch to the dataflow shape: when an external-input source reaches the IndexedWrite sink of a memcpy-class primitive or a sized strncpy / read without a dominating sanitiser, the lattice emits taint.flow.dest-arg.<callee> at CWE-787. The parallel Read sink at the source argument of the same primitives emits CWE-119 (the umbrella class for the read side of the same flow). Rule families: oob-write.* for the proof-grade chk-family catches, taint.flow.dest-arg.* and taint.flow.src-arg.* for the v2 dataflow catches.

CWE-789 — Memory Allocation with Excessive Size Value

Three rule shapes. excessive-alloc.heap fires at malloc, xmalloc, realloc, calloc, reallocarray call sites whose size argument resolves to a compile-time constant above 1 MB. excessive-alloc.stack fires at function prologues whose sub sp, sp, #F exceeds 7500 bytes. Both thresholds match cwe_checker's defaults so cross-tool comparison is direct. The SSA-taint v2 AllocSize sink covers the dynamic case: when external-input taint reaches the size argument of an allocator without a dominating sanitiser, the lattice emits taint.flow.alloc-size.<callee> — the canonical "request a buffer sized from a length prefix the attacker controls" shape that the constant-threshold rules can't see.

CWE-798 — Use of Hard-coded Credentials

Two detectors. The string-scan variant flags shaped tokens — JWT signatures, GitHub PATs, AWS access keys, generic high-entropy base64 — at Confidence::Medium. The byte-aware variant (hardcoded_credential_use) fires on strcmp / memcmp / strncmp call sites where one argument resolves to a literal credential and the other is a runtime value, catching the canonical "compare attacker-supplied token against embedded secret" shape.

CWE-825 — Expired Pointer Dereference / NULL-page Mapping

The W^X-detector's second rule shape. When mmap is called with addr = NULL, MAP_FIXED set, and a non-zero length, the mapping silently overwrites the NULL page — usually the first sign of a process that intends to make NULL dereferences executable. Detected via strict register-trace of the addr (x0) and flags (x3) arguments. Severity::Critical, Confidence::High. Rule: wx-protection.mmap.null-page-fixed. The same call site often co-fires the W^X CWE-693 finding when prot is also W+X — the compound composer then synthesises a single multi-detector stack at elevated severity.

CWE-862 — Missing Authorization

Two modules. missing_auth flags a CGI handler that executes system() with no preceding authentication-verifier call — the 2022-2025 firmware-router cluster. dbus_no_polkit flags a Linux D-Bus interface registered without any polkit (PolicyKit) authorization-check primitive imported — the PwnKit / systemd / NetworkManager / udisks2 / bluez D-Bus-method-handler LPE cluster (CVE-2021-4034 pkexec, CVE-2021-3560). MITRE Top-25 #11; a 197-CVE High+ cluster in 2026 with a KEV pkexec entry. Rules: missing-auth.cgi-without-auth-verifier, missing-auth.dbus.no-polkit.

CWE-863 — Incorrect Authorization

xpc_validation::scan_c_xpc_audit_token — a C-API XPC server (xpc_main / xpc_connection_create_mach_service) with no peer-validation primitive in its imports. The C-XPC sibling of the CWE-345 detector: where 345 covers the Objective-C NSXPCConnection / NSXPCListener surface, this covers the lower-level libxpc C API. CVE-2024-44131 (FileProvider TCC bypass / Migraine) and the macOS bundle-ID-spoof privilege-escalation tail. MITRE Top-25 #24. Rule: xpc.c-api.no-audit-token-validation.

CWE-916 — Use of Password Hash With Insufficient Computational Effort

weak_pbkdf resolves the iteration-count argument at PBKDF2 derivation sites via the strict register tracer, across Apple CCKeyDerivationPBKDF (rounds at x6) and OpenSSL PKCS5_PBKDF2_HMAC / _SHA1 (iter at x4). Three-tier threshold: iter < 1,000 Critical (trivially brute-forceable), iter < 10,000 High (below the NIST SP 800-132 floor), iter < 100,000 Medium (below the OWASP 2023 baseline of 600,000 for SHA-256). Too few iterations means an attacker who steals the hash brute-forces the password in hours, not years. Rules: weak-pbkdf.{cccrypt,openssl}.iter-{critical,high,medium}.

CWE-918 — Server-Side Request Forgery (SSRF)

cloud_metadata_fetch — a binary-wide pre-gate (any URL-fetch primitive imported) plus a string scan for hardcoded cloud-metadata IMDS endpoints: 169.254.169.254 (AWS IMDSv1 / Azure), metadata.google.internal (GCP), 100.100.100.200 (Alibaba), 169.254.170.2 (AWS Fargate task metadata). These literals never appear in normal app code — they signal IMDSv1 deprecation lag (the Capital One / CVE-2019-15376 class) or SSRF-exploitation tooling. Per-string fire; Critical for the IMDSv1 credentials path, High otherwise. MITRE Top-25 #19; a 206-CVE High+ cluster in 2026 with a 17-entry KEV cluster (Confluence / Jira / Zimbra / GitLab). Rules: ssrf.cloud-metadata.<provider> (with a -creds suffix on credential paths).

CWE-1240 — Use of a Cryptographic Primitive with a Risky Implementation

Algorithm-correct, mode-incorrect crypto. weak-crypto.aes-ecb-mode fires when a CCCrypt call resolves the algorithm argument as AES (alg = 0) and the options argument as kCCOptionECBMode (0x2). ECB leaks plaintext block patterns through the ciphertext — the canonical Tux-pattern attack — and the standard fix is CBC or GCM. Confidence::High when both arguments resolve at the call site, Severity::High.

CWE-1284 — Improper Validation of Specified Quantity in Input

SSA-taint v2's tightest claim. When the lattice analysis proves that an external-input source (read, recv, fread, getenv, argv, …) flows specifically to a Quantity sink — the size argument of memcpy / memmove / bcopy / strncpy / strncat / strlcpy / strlcat / read / write — without a dominating sanitiser, the detector emits taint.flow.input-to-quantity at CWE-1284 rather than the looser CWE-120 the v0 co-occurrence rule would tag. Sanitiser shapes recognised: ConstantBound (the source is MIN'd with a literal before reaching the sink), MeasuredBound (the source is compared against the buffer length argument), RealpathNullCheck (a realpath return value is null-checked first). When every member of the source set is dominated by a sanitiser's safe edge, the candidate is suppressed. Confidence: High when no shape touched any source, Medium when shapes matched but none dominated, Low when the source set saturates. The labelled corpus case cwe-1284-length-prefix exercises the canonical shape.

Quality gates and the eval contract

Four hard gates run on every release.

  1. Recall. Every labelled vulnerability case in training/vulnerabilities/cwe-NNN-slug/ carries an expected_detections field listing the rule_ids that case must produce. A miss reds the gate. Cases ship as contrastive twins where possible — a vulnerable variant + a patched variant — so a regression that breaks the negative side is caught the same way as one that breaks the positive side. The current labelled corpus is 174 vulnerable cases at 100% recall.
  2. Precision. Every clean case in tests/sourcecode/ (legitimate code that uses dangerous-pattern primitives correctly) must produce zero findings. A new false positive on the labelled clean set fails the gate. The current clean set is 16 cases at 100% precision.
  3. Pinned-miss honour. Cases tagged with expected_detections = [] document detection gaps the engine intentionally accepts — typically shapes that require interprocedural flow we haven't wired, or patterns where the false-positive cost outweighs the recall. The case must produce zero findings until a detector closes the gap and updates the expectation. This prevents accidental loosening that fires the right CWE for the wrong reason.
  4. Negative pins (forbidden_detections). A vuln or patched-twin case can pin rule_ids that must not fire on its binary. A precision regression that quietly flips the wrong detector on (without breaking the recall side, which would catch a missing detector but not an extra one) reds the gate. The CWE-22 path-traversal hardening cycle was where this primitive was introduced — realpath$DARWIN_EXTSN (the macOS-versioned sanitiser import that the prior detector missed universally) and the block-local-to-function-wide realpath-dst tracking fix together tightened the curl aggregate taint.flow.* cap from 59 to 24 findings, and the negative-pin primitive caught two downstream regressions that the recall-only gate let through.

Real-binary regression caps. Every release runs the detector matrix across fifteen binaries — four Mach-O arm64 (airportd, curl, jq, ripgrep), two Mach-O x86_64 (jq, ripgrep), three PE x86_64 (curl, jq, ripgrep), and six ELF arm64 / x86_64 (the same three binaries, two architectures each). The current baseline is 610 findings across the fifteen binaries. airportd alone produces 79 — the real Apple ObjC daemon with the largest XPC surface in the corpus. The two ripgrep arm64 builds produce 75 between them, almost entirely from the sizeof-pointer and excessive-alloc.stack rules that fire on Rust's standard-library lowering shapes. The static-linked ELF curl/x86_64 fixture produces 31 from the stack-frame-size threshold. Caps are calibrated per binary so a heuristic loosening that doesn't trip the labelled corpus but inflates the per-binary count beyond its cap reds the gate. Per-rule caps live alongside the aggregate cap — taint.flow.path-arg.* carries its own per-binary ceiling (airportd 2, curl 7, jq 2, ripgrep 0) so a flow-loosening that masks under the aggregate still reds the gate.

Per-detector unit tests pin the rule tables against drift. A contributor who adds a new rule but forgets to register its CWE id with the canonical registry, or who renames a rule_id without updating its prefix family, is caught at unit-test time before merge.

False positives and false negatives are both expected. Static analysis over stripped binaries is over-approximation by construction. Calls reachable through indirect dispatch, dynamic class lookup, or computed jump tables the call graph can't resolve are misses. Patterns that match a dangerous shape but are guarded by runtime predicates the analyzer can't see are spurious fires. The gates above keep the rate measurable; the per-CWE detail above documents the specific limitations per detector.

What other tools do, what we do differently

Static binary CWE detection is a small field. The honest comparison:

cwe_checker (Fraunhofer FKIE) is the closest peer — open-source, Ghidra-backed, abstract interpretation over Pcode. Covers around 14 CWEs with measured FP/FN posture per check. Architecture-agnostic via Ghidra's universal lift. macdecompile covers a broader CWE set (64 vs ~14), runs without a Ghidra dependency, ships proof-grade rules for shapes cwe_checker handles heuristically (the chk-family CWE-787 / CWE-125 detectors), and adds an Apple-platform pack cwe_checker does not address. cwe_checker has more mature stack-buffer-overflow analysis on x86_64; we lead on Apple Silicon and Apple-runtime surfaces.

BinAbsInspector (Tencent KeenLab) — Ghidra-backed abstract-interpretation framework, covers around 10 CWEs. Strong on x86_64 firmware; thin on macOS-specific surfaces.

Mayhem / ForAllSecure — fuzzing-oriented, with crash-class classification mapped back to CWEs after the fact. A different posture: dynamic exploration rather than static proof. Complementary, not competitive.

BinSkim (Microsoft) — PE-only, posture-focused (the CWE-693 family). Does codesign / hardening / load-config audits well. No semantic analysis of binary content.

Commercial tools (GrammaTech CodeSentry, Black Duck Binary Analysis, Veracode binary scanning) — mostly SBOM + CVE-by-version-string, with a thin layer of static checks on top. Not direct CWE detection.

What we do differently:

What we don't claim:

Where this fits

The engine produces findings as facts. A finding lists what was detected, where, with what confidence, and at what severity. It does not assert the binary is exploitable, vulnerable in production, or in scope for any specific compliance regime. Those are judgments that depend on the deployment environment, the entry-point reachability from untrusted sources, the secrecy of any embedded credential, and the threat model.

Findings are one of four parallel security-adjacent surfaces the engine produces. The others answer different questions over the same input: Indicators tag what the binary does (capability + MITRE ATT&CK), Malware classifies whether the binary is malicious (four-tier verdict over composed signals), and CVEs & SBOM flag known-vulnerable component versions (version-based, not code-shape). All four walk the same lifted analysis and produce records of compatible shape; consumers read whichever surfaces they need.

openbinary is the surface that takes findings and applies judgment. It groups findings by binary, by CWE, by severity; it baselines them against a known-good build so a CI run reports only the new findings introduced by a change; it ranks findings by reachability from XPC entry points (using the audit matrix from Engine) and from exported symbols; it tracks resolution status across releases; it issues alerts when a known-exploited CVE shape matches a finding in a binary in the user's corpus.

Other surfaces can sit on the same findings stream — internal security-review tools, regulator-facing reports, partner products. The fence between facts and judgment is deliberate. Anything dependent on judgment lives in the surface. Anything dependent on truth lives in the engine.