Sign in

VulHunt vs openbinary

openbinary findings /usr/bin/tar returns 88 findings on the arm64e slice of the universal binary macOS ships — auto-detect takes the native slice, and the x86_64 one is a separate program as far as any of this is concerned. Eleven carry a taint witness — an ordered chain of virtual addresses from a seeded input to a dangerous primitive — and three of those eleven begin at a named external source rather than at a function's own parameter. One of the three, with the virtual addresses rewritten from decimal to hex and the caller's fifteen-entry forward-call list dropped:

{
  "cwe_id": 119,
  "detector": { "kind": "symbolic-exec" },
  "confidence": "high",
  "severity": "medium",
  "rule_id": "taint.flow.src-arg.__memcpy_chk",
  "summary": "Tainted value reaches the source buffer of a sized primitive — read amplification or oracle of attacker-controlled bytes.",
  "location": {
    "va": "0x10000863c",
    "symbol": "__memcpy_chk",
    "caller_va": "0x10000856c",
    "caller_name": "set_reader_options",
    "reachable_from_main": true,
    "flow_path": [
      { "va": "0x100008594", "role": "source", "symbol": "getenv" },
      { "va": "0x10000863c", "role": "sink",   "symbol": "__memcpy_chk" }
    ]
  }
}

The code between those two addresses:

_set_reader_options:
100008590  add  x0, x0, #0xfdc        ; "TAR_READER_OPTIONS"
100008594  bl   _getenv               ; ← source
100008598  stur x0, [x29, #-0x18]
1000085a8  mov  x8, #0x1d             ; len("__ignore_wrong_module_name__,")
1000085b4  bl   _strlen
1000085b8  add  x8, x0, #1            ; strlen(env) + 1
1000085cc  add  x0, x8, x9            ; 0x1d + strlen(env) + 1
1000085d8  bl   _malloc_type_malloc
1000085dc  str  x0, [sp, #0x18]       ; the buffer
100008628  bl   ___memcpy_chk         ; the 0x1d-byte literal prefix
100008630  ldr  x0, [sp, #0x18]       ; ← destination
100008634  ldur x1, [x29, #-0x18]     ; ← the getenv result
100008638  ldr  x2, [sp, #0x20]       ; strlen(env) + 1
10000863c  bl   ___memcpy_chk         ; ← sink

The flow is real: the bytes of TAR_READER_OPTIONS reach a copy primitive, and the caller is reachable from main. The bug is not. The buffer was allocated at 0x1d + strlen(env) + 1 bytes and the copy length is strlen(env) + 1, so it cannot overrun. The severity is arithmetic on the callee name, not on that reasoning: a tainted source buffer is High by default and drops one tier to Medium because __memcpy_chk carries the fortified _chk suffix. Proving the flow is mechanical. Deciding whether a proven flow is a bug is not, and that is the axis both engines are actually competing on.

Binarly open-sourced the VulHunt Community Edition under GPLv3 in March 2026. It is the closest public analogue to the Findings surface. Exploitation-maturity scoring, SBOM/CBOM validation and fleet management stay in the paid Binarly Transparency Platform and are not in the open source.

Summary of differences

QuestionAnswer
What is VulHunt built on?Lua rules over the BIAS substrate, with Ghidra's decompiler embedded whole and lifting through sleigh
What can each engine read?VulHunt: ELF, PE, UEFI, shellcode, five architecture modules, no Mach-O loader. openbinary: 104 spec-registry targets of which 72 are architectures — 48 lifted to IL, 24 decode-only — Mach-O included
Where does VulHunt lead?Semantic C-grep over decompiled pseudo-C, Lua rule authoring, deep UEFI/SMM analysis, type libraries and inline annotated explanations
Where does openbinary lead?Architecture and format breadth, constant-arithmetic proofs, SSA-taint with interprocedural summaries, SARIF witnesses, 48 MCP tools and the surrounding SAST/SCA/malware surface
What does a finding cost to read?88 findings on /usr/bin/tar, 564 on /usr/bin/ssh, 867 on curl for Windows, 2,629 on a statically linked curl for Linux; 11–20% of every one of those counts is a synthesized correlation of the others
Where does openbinary go silent?Taint, use-after-free, loop-bounded overflow and double-fetch all sit behind one format/architecture matrix: PE is x86 and x86-64 only, and a static stripped ELF yields zero taint findings on any architecture
What does neither ship?Automatic triage — ranking, clustering, confirming or dismissing findings so an auditor reads ten instead of the whole list

VulHunt architecture

VulHunt is a Lua rule engine over BIAS — Binary Analysis and Inspection System, dual MIT/Apache — with Ghidra's decompiler embedded whole. The engine is the product; the community writes the rules, and no rule packs ship in the Community Edition tree — only the builder and CLI command that push a rule directory to a registry as an OCI artifact for the paid platform to pull.

LayerImplementationMeasured detail
Rule languageLua scripts evaluated at one of three scopes: whole project, per function, per call site1,711 lines of in-tree Lua: 1,058 a vendored third-party functional-programming library, 613 the engine's own runtime prelude, 40 scope-condition helpers
DecompilerGhidra, embedded107 C++ translation units under bias-core/cxx/decompiler/ghidra
LiftingGhidra's sleigh by way of the fugue crateFive architecture modules register with the core: x86 (covering x86-64), ARM, AArch64, eBPF, Xtensa
IRSSA form over nine statement kinds, with phi nodes per blockAssign, Store, Branch, CBranch, Call, Return, Skip, Intrinsic, PointerHint — exposed into Lua as userdata so a rule can pattern-match on it directly
Firmware modelbias-core/src/efi228 KB across eleven files: services.rs at 85 KB tracking EFI boot and runtime services, smi.rs at 44 KB for SMI handlers, globals.rs at 33 KB, plus GUID tables, PEI phase modelling, interprocedural typed aliases
Signature compatibilitySeparate cratesRead FwHunt, FLIRT and PatFind signatures
Vulnerability vocabularyFirst-class types, not string tagsCPE, CVSS, CWE, MBC, PURL, an environment-reachability model
Agent surfaceMCP serverSeven tools; the one that matters is query_project, described in its own schema as "Execute a Lua script against the currently open project and return the result as JSON." The agentic story is that the model writes the rule

UEFI and SMM are the depth of the tree.

Input formats

There is no Mach-O loader. The set is ELF, PE, UEFI firmware volumes and TE images, raw shellcode, Binarly's own BA2 package, and a Binary Ninja database importer. Mach-O exists as a name in two places only: a format enum whose string parser can spell it, and two identical UnsupportedFormat("Mach-O") reject arms in the EFI module loader, sitting under the same treatment for ELF, which does have a loader of its own. Nothing routes a Mach-O anywhere.

Rule query API

The rule query API is a dataflow language, and a rule author can reason about value provenance at a call site and semantically grep pseudo-C in the same script, without recompiling anything.

Capability comparison

Directionopenbinary leadsVulHunt leadsparity

Foundation

What each engine is built on.

Axis
Binarly VulHunt
openbinary
Heritage
UEFI / firmware-module security; POSIX bolted on
Cross-format binary + firmware-image + source SCA — broad
Substrate
BIAS (C++/Rust) wrapping the Ghidra decompiler
Own-authored lifters + IL + SSA, no Ghidra
IR
P-code-style SSA, exposed into Lua as userdata
Own IL + SSA (typed taint lattice), not a scripting surface
Lifters / arch
Ghidra sleigh via fugue; five registered arch modules: x86 (+x64), ARM, AArch64, eBPF, Xtensa
Own decode/lift across 104 ISA/format targets — 57/57 rizin-arch parity incl. exotic (sparc, xtensa, avr, h8300, msp430, superh, v850, z80, tricore, m68k, s390x, hexagon, loongarch, nds32) + x86/x64/arm/arm64
Cross-arch rule portability
One rule runs across x86 / ARM, 32 / 64-bit — the unified Ghidra-PCode IR normalises the architecture away
SSA-taint v2 + the TOML catalog run cross-format (Mach-O / ELF / PE / WASM, incl. MIPS / PPC / RISC-V and the embedded lifter set); chk-family proofs are dual-dispatch (arm64 byte tracer, lifter-resolved args elsewhere). Residual arm64-only: the stack-frame proof + a few MOVZ-window rules

Detection technique

How a vulnerability is found in the bytes.

Axis
Binarly VulHunt
openbinary
Semantic code-pattern matching
Weggli / ast-grep / semgrep queries over decompiled C — arch-independent, one pattern across ISAs
Binary side: IL/SSA + byte-aware, no pseudo-C matching. Source side: tree-sitter walkers over real source (20 language walkers)
Taint / dataflow
Intra-procedural taint + callee in/out var annotations + sanitiser support; interprocedural in the UEFI domain
SSA-taint v2: typed source × typed sink (Quantity / IndexedWrite / Path / ShellArg / SSRF / Deref), dominator-based sanitiser suppression, alias-aware seeding, inter-procedural sink-wrapper summaries — cross-format (Mach-O / ELF / PE / WASM)
UEFI / SMM analysis
Deep vuln analysis — SMM callout / double-fetch / CommBuffer-pointer (TOCTOU), PEI/DXE service tracking, interprocedural typed aliases (efi/ 228 KB: services.rs 85 KB, smi.rs 44 KB)
Parse + identify UEFI (TE, FV/FFS, capsules, EFI-GPT) + shipped SMM-callout detection (efi.smm-callout, CWE-829 — handler-root reachability × gBS/gRT call classification over the efi-model platform recovery). CommBuffer nested-pointer taint is specced, not shipped
Proof-grade detection
Not emphasised (pattern + dataflow)
Constant-arithmetic proofs — stack-frame overrun O+N > F, mul-into-malloc wrap, chk-literal OOB → mathematical Critical
Byte / IR pattern matching
Byte-pattern + IR matching; FLIRT / FWHunt / PatFind shims
Byte-aware register trace + literal resolve; FLIRT lib-ID wired
Type libraries / signatures
Type libraries, function signatures, annotated listings
Prototype/typedef mined corpus; no shipped type-library surface

Findings & triage

What a finding carries and how it's ranked.

Axis
Binarly VulHunt
openbinary
Rule authoring
Lua scripting + weggli + value/CFG query API (is_const_pre_call, origin, dominates, is_reachable) — arbitrary new detection logic, no recompile
Declarative TOML topic catalog (40 CWE topics, 1,190 detector-tagged sink / dangerous-call entries) — new rule = one entry, no Rust. One entry may name several consumers (detectors = ["binary", "source-c", …]); 106 of 1,190 do, across the binary engine + 10 source languages. Only novel gate *semantics* need Rust
Rule scopes
project / function / call-site
binary-wide / call-graph / call-site VA — same tiers
CWE coverage
buffer overflow / auth-bypass / UEFI classes; rule-pack extensible
72 CWE classes (71 emitting), 300+ rules, 8 signal classes, 20 of MITRE Top-25
Reachability
“Reachability properties” (scored in paid BTP)
reachable_from_main + 7-tier root anchoring (entry point, main, run-loop daemons, __libc_start_main callees, initialisers/TLS, exports, EH landing pads), plus reachable_before_auth; measured — curl.exe x86-64 PE 166 of 867 mainline-reachable, /usr/bin/ssh 278 of 564
Exploitation maturity
Yes — maturity scoring (paid BTP)
No maturity score; per-instance severity + confidence-by-mechanism
Finding correlation
Per-rule
Composer — compound 2/3/4-detector stacks + cross-detector promotion
Annotated / explainable output
Findings annotate decompiled C at exact addresses with root-cause notes; type libraries + FLIRT make stripped binaries explainable
Call-site VA + calling-fn name + forward_calls; no inline pseudo-C annotation
AI / agentic triage
Markets agentic triage + rule-gen + patch-analysis via MCP + skills (LLM-driven, bring-your-own-agent)
MCP server + reachable_from_main filter + a vuln-triage agent skill; no in-product auto-triage classifier yet — roadmap

Surrounding capability

Everything beyond the per-binary finding.

Axis
Binarly VulHunt
openbinary
Source-level SAST
No — binary only
Yes — 20 language walkers, 10 production-dispatched (py / js+ts / php / go / java / swift / c / ruby / c# / kotlin) + 10 more behind the gap-probe harness; real taint tracking in 9 of them
SBOM / CVE / components
SBOM / CBOM validation (enterprise BTP); formal CPE / PURL / CVSS / MBC taxonomies in-tree
Lockfile SCA across 11 ecosystems (npm / PyPI / Cargo / Maven / Go / Debian / Alpine / RPM / …) + web-lib banner CVEs + in-binary vulnerable-component rules (CWE-1395) + vuln-db; CWE + CVE, no CVSS scoring
Malware / ML / similarity
No malware detection (MBC taxonomy constants only)
Deterministic 4-tier verdict (23 ledger sources, 46 semantic scanners, 4 strength bands, 0 FP on the CI benign gate) + ML layer (ONNX learned behavioral embedding, per-type anomaly models, binary-type classifier) + capa-style indicators + MITRE ATT&CK + DNA similarity (ANN) + variant clustering
Firmware unpacking
BA2 (own archive format)
ext4 / UFS2 / OCI / squashfs / xz-MBR / UEFI (capsule, FV, TE, GPT)…, 86k-file OPNsense verified

Delivery & integration

How each ships and plugs into a workflow.

Axis
Binarly VulHunt
openbinary
Loaders / ingestion
ELF / PE / UEFI FV+TE / shellcode / BA2 / BNDB (Binary Ninja DB); no Mach-O loader — the format appears only as an EFI-loader reject arm (UnsupportedFormat("Mach-O"))
format-routed upload (Mach-O / ELF / PE + firmware images); no RE-tool DB ingest
MCP / agentic
Built-in MCP server (bias-vulhunt-mcp, 7 tools — query_project runs arbitrary Lua)
Built-in MCP server (ob-mcp, 48 tools)
Distribution / model
Open-source CE (GPLv3) + paid BTP SaaS
Own server + web product + CLI + MCP
Output schema
JSON / JSONL / zstd; reachability + annotated listings
JSON findings (cwe_id / rule_id / detector / severity / confidence / location + enrichments) + SARIF 2.1.0 export (ob-sarif — findings, secrets, capabilities, ATT&CK taxonomy)

Cross-architecture rule portability

Both engines now write one rule that fires across architectures, by different routes. A VulHunt rule written against normalised P-code fires on every sleigh-supported ISA — VulHunt's headline claim, and now close to parity. openbinary gets there through SSA-taint passes driven from one catalog:

RuleFires in
taint.flow.path-arg.fopenthe arm64e /usr/bin/tar above
taint.flow.path-arg.CreateFileAcurl's Windows x86-64 build
taint.flow.src-arg.strcpyan x86-64 ELF, on the witness getenvstrcpy in main

Which of them can fire is decided by a format-and-architecture matrix, not by the rule:

FormatArchitecture names admitted
ELF39
Mach-O13
PE2 — x86_64 and x86

The same matrix gates use-after-free, loop-bounded read and write, sprintf overflow and kernel double-fetch, so an unlisted combination loses the whole IL-dataflow tier at once rather than one rule. One curl source built two ways measures the cliff:

curl buildFindingsTaint findings
Windows x86-648676
Windows arm64100

The taint tier is portable across everything the matrix lists and absent everywhere else, so architecture-bound is not only the arithmetic-proof tier.

Semantic matching over decompiled C

Weggli, ast-grep and semgrep over decompiled C have no counterpart. openbinary matches on IL and SSA and on raw bytes, and its pseudo-C is not exposed to rules at all. Semantic C-grep against readable decompiler output is architecture-independent for free.

Rule authoring: Lua versus a declarative catalog

Adding a sink is cheaper in openbinary; adding a new kind of gate is cheaper in VulHunt, and that is the sharpest real difference between the two. openbinary's catalog is 40 topic files holding 1,190 detector-tagged entries — a new shell-exec sink is one entry, no Rust. But novel gate semantics — conjunctive gates, variadic payload positions, struct-literal field values — still require a walker change. VulHunt's is_const_pre_call and pre_call_origin express those directly in a script.

The cheap-to-add path is also the smaller half of the sink surface: the engine's primary sink table is 291 entries compiled in, and the catalog adds 109 binary-tagged rows on top of it, deduplicated by name.

Finding explanations

VulHunt explains a finding in line; openbinary explains it by context. A VulHunt finding pins a root-cause note to an exact instruction address in the decompiled C, and type libraries plus FLIRT keep that readable on stripped binaries. openbinary resolves a finding to a call site, a calling function, and the ordered list of calls that function makes next — for set_reader_options above, fifteen names from getenv to lafe_errc; it does not explain itself in line.

UEFI and SMM coverage

VulHunt leads on firmware depth. openbinary ships one firmware vulnerability rule, efi.smm-callout (CWE-829), which walks SMI-handler roots against gBS/gRT call classification over its EFI platform recovery, plus a structural kernel double-fetch detector for the copy_from_user TOCTOU shape. VulHunt covers CommBuffer nested-pointer taint, PEI/DXE service tracking, and interprocedural typed aliases on top of that.

Architecture and format breadth

Breadth runs the other way: 104 spec-registry targets against VulHunt's five registered architecture modules. That difference is where firmware-blob diversity lives. Mach-O works. But the 104 is not 104 architectures, and only part of it carries analysis rather than disassembly:

TierCountWhat it supports
Formats, platforms and meta specs32ELF, Mach-O, PE, TE, COFF, DEX, calling conventions, hardening — not architectures at all
Architectures with an IL lift39Full chain: taint, CWE detectors, value-range
Bytecode and VM targets with a lift9Dalvik, JVM, WASM, CIL, EBC, Lua, Python, BPF, cBPF
Architectures that decode only24Recognised and disassembled, no IL — so no taint, no CWE findings, no value-range

Rizin parity sits at 57 of 59 architectures and 63 of 65 formats shared with librz.

Proof tiers and taint

openbinary's top finding tier is proof, not inference, and it is close to empty on real binaries. When the frame size from a function prologue, the destination offset and the copy length of a memcpy all reach the call site as compile-time constants, O + N > F settles the question arithmetically and the finding is Critical with no dataflow hedge. It fires zero times on /usr/bin/tar, /usr/bin/ssh and every curl build measured here; its only firings in the corpus are 9 on the arm64 macOS ripgrep, in two unnamed functions on the Rust panic-helper path where the compiler statically builds error buffers — almost certainly not bugs. All 10 Critical findings on tar come instead from the cross-detector correlation composer, which emits one severity tier above the maximum of its inputs, capped at Critical.

Below that tier, SSA-taint carries typed sources against typed sinks — quantity, indexed write, path, shell argument, SSRF, dereference — with dominator-based sanitiser suppression, alias-aware seeding, and inter-procedural sink-wrapper summaries: the case where recv() bytes cross a function boundary and reach system() one frame down, which no intra-procedural walker sees.

Output formats and surrounding tools

Findings serialize to SARIF 2.1.0, so a flow_path becomes a SARIF codeFlow carrying one threadFlowLocation per step — the same shape CodeQL and Semgrep consumers already parse. The flow is short: of 103 witnesses across tar, ssh and curl for Windows, 102 have exactly two steps — source, then sink — and one has three. The MCP server exposes 48 tools against VulHunt's seven. The surround answers questions VulHunt does not ask:

Reachability filtering

Volume, not detection, is the workload, and a forward call-graph walk is what makes it readable — but how much it removes depends entirely on how the binary was linked: a dynamically linked system tool has almost nothing off the path, a statically linked one has almost nothing on it.

BinaryFindingsOn the mainline pathOff itNo answerOff-path shareCompound
/usr/bin/tar, Mach-O arm64e8880080%14
/usr/bin/ssh, Mach-O arm64e5642782374942%104
curl, Windows x86-64867166694780%170
curl, Linux x86-64, static and stripped1,9093231,579783%not measured
curl, Linux arm64, static and stripped2,6293222,300787%287

The filter hides seven findings in eight of the static curl builds and none at all of tar's. Off the path is not proof of dead code, only that no static edge chain from a root reaches the caller — an indirect dispatch still can, and the field says false rather than "unreachable" for that reason.

"No answer" is not only binary-wide rules. Of ssh's 49, six are genuine binary-wide rules — posture flags, a hardcoded-secret marker, two weak-crypto entries — and 43 are interprocedural taint findings that do have a call site: they carry the sink address and the calling function's name but no calling-function address, and the placement walk keys on that address alone. One of tar's eight is the same shape.

The compound column is synthesis, not independent detection: the cross-detector correlation composer groups findings by calling function and, where two or more distinct rules converge, emits an additional compound.* finding one severity tier higher, leaving the originals in the list. Between one finding in nine and one in five in every count above arrives that way.

Call-graph roots

Anchoring that walk takes seven tiers of roots, because a stripped daemon has no main:

  1. The format's own entry point.
  2. main itself.
  3. The callers of xpc_main / dispatch_main / NSApplicationMain / __CFRunLoopRun.
  4. The direct callees of __libc_start_main.
  5. Static initialisers and TLS callbacks.
  6. Every defined export.
  7. C++ exception landing pads.

If the resulting closure covers less than 10% of the call graph's caller nodes it is declared untrustworthy and every unreached finding is left null instead of stamped false.

Pre-authentication reachability

openbinary computes pre-authentication reachability; VulHunt's open tree does not. A second Boolean asks whether a network listener reaches the call site without crossing a recognised authentication primitive on any path — the CVSS-9.8 unauthenticated-RCE shape. VulHunt declares a reachability type — undetermined, entrypoint, runtime-invocation, runtime-dependency — and never constructs anything but the default, undetermined: outside its own module the type appears only in the re-export beside it and one error variant. The scoring is a paid-platform feature.

Limits

Arithmetic proofs

The arithmetic-proof detector is arm64-only and explicitly v0, and when it cannot resolve all three constants it stays silent, so the failure mode is a miss. What defeats it:

Catalog fan-out

The shared rule catalog is a shared schema, not a fan-out multiplier. Thirteen consumers are declared — the binary engine, eleven source languages and a firmware tier — and almost every entry reaches exactly one of them.

Detector-tagged entriesDetectors namedWhich, and why
1,084exactly oneMost sinks are language-specific: os.system exists only in Python, shell_exec only in PHP, so the entry has nowhere else to go
105exactly twoA name two consumers share — libc system, tagged for the C walker and the binary engine
1threePOSIX open — the C walker, the Python walker and the binary engine
1,190The whole sink and dangerous-call catalog, across 40 topic files. The capability catalog beside it, 77 more files, carries no detector tags at all

Per consumer it is just as lopsided, and two of the thirteen are declared but empty:

ConsumerTagged entries
Python321
PHP223
JavaScript201
C123
Binary engine109
Ruby107
Java100
Go74
Swift22
Kotlin9
C#8
Rust walker, firmware tier0

What the catalog buys is that every shell-execution sink for every consumer lives in one file with one shape, not that one line becomes eleven detectors.

Firmware topics without a detector

Two firmware topic files, cwe-20-smm-commbuffer.toml and cwe-367-nvram-double-fetch.toml, have no consuming detector. They are catalogued, not shipped, and they are blocked differently: the CommBuffer topic is metadata awaiting a taint source and a sanitiser gate, while the NVRAM double-fetch shape cannot be written down at all, because the catalog's lifecycle-predicate vocabulary has no way to express it.

Kernel double-fetch detection

The kernel double-fetch detector that did ship carries no proof at all: it is purely structural over the SSA IL, grouping copy-in calls by the structural identity of their user-address argument. What that costs:

Interprocedural taint

Two independent interprocedural passes emit into the same taint.flow.interproc.* rule namespace with different sink vocabularies. The engine-side function summary is deliberately narrow — six neutral argument positions, no CWE vocabulary, so the analysis layer stays free of detector concepts — and everything else collapses there into one undifferentiated taint.flow.interproc.other. The second pass, in the detector layer, keeps its own parameter-to-sink summary over the full eighteen-kind catalog, which is why rows the narrow summary cannot express still fire.

Argument position or sink kindNeutral engine summaryFull detector summaryFired on /usr/bin/ssh
Written-throughyesyes1
Read-throughyesyes16
Lengthyesyes10
Bare dereferenceyesyes0
Exec argumentyesyes0
Format stringyesyes2
Pathno slotyes8
Loader pathno slotyes0
Registry name and datano slotyes0
SSRFno slotyes0
SQL injectionno slotyes0
Process injectionno slotyes0
Unchecked returnno slotyes0
Allocation sizeno slotyes2
Use-after-freeno slotyes0

The tier switches itself off above 6,000 functions in the binary: the summary comes back empty and analysis falls back to intraprocedural — sound but silent — and large firmware images sit above that line. The findings that do fire prove which parameter index reaches the sink, not the ordered operation sequence, so they carry no witness path and no calling-function address: the cross-function flow is the one case that arrives both without an address chain and without a reachability answer.

Taint results mix converged and over-approximated states

The lattice fixpoint has a step cap, and hitting it does not discard the result — it lowers the finding's confidence by one tier. An emitted taint finding is therefore either a converged answer or an over-approximation, and that confidence value is the only thing in the output distinguishing them.

Static linking plus stripping removes the taint tier entirely

Both curl builds for Linux sit inside the supported format-and-architecture matrix, and neither produces a single taint finding:

curl buildFindingsTaint findingsWitnesses
Linux x86-64, static and stripped1,90900
Linux arm64, static and stripped2,62900

The sink tables are keyed on resolved callee names. A statically linked, stripped binary has neither dynamic imports nor a symbol table, so its calls into libc land on addresses with no names attached, no call site classifies as a sink, and nothing seeds. This is not an architecture gap — the x86-64 build is the most supported target there is. The binary shape most common in firmware is the shape that yields no witnesses, and it is also the shape that produces the most output to read: 2,629, not 867, is the high-water mark.

True flows that are not bugs

The /usr/bin/tar finding at the top is a true flow that is not a bug. Three of tar's eleven witnesses are the same shape: an environment variable copied into a buffer sized from its own length. Any engine that emits taint witnesses emits these. The honest position is that a witness answers which input reaches this primitive, and a human or a classifier still has to answer and does that matter.

Those three are also the only tar witnesses that begin at a named external input. Most witnesses start at function.parameter instead — the analysis seeds a function's own parameters as potentially attacker-influenced, without proving any caller supplies attacker data:

BinaryWitnessesFrom a named external sourceFrom function.parameter
/usr/bin/tar113 — getenv8
/usr/bin/ssh9116 — getenv, fgets, read75

"Carries a taint witness" therefore means a chain exists from a seed, not a chain exists from a proven external input.

Automatic triage

Nobody ships automatic triage of CWE findings: take the 88 on tar or the 2,629 on the static Linux curl, rank them, cluster them, confirm or dismiss each, and explain the decision so an auditor reads ten rather than the whole list. VulHunt markets an agentic workflow — bring your own model, drive the engine over MCP, write Lua. openbinary has 48 MCP tools and the reachability filter; the agent skills in its tree triage a binary for malware, not a findings list for exploitability. Neither has an in-product classifier that turns 2,629 findings into a queue.

The output is not even severity-ordered. Findings sort alphabetically by rule name and then by address, so the list opens on whatever name sorts first — archive-extract.* on tar, anti-debug.* on ssh — and the Critical entries sit wherever their rule names land. Sorting by severity is one line of work; deciding what to read first is not, and the engine that produces the most findings only wins if it also says which to read first.


Read against the VulHunt Community Edition source at commit 256a558. vulhunt.re · VulHunt repo · VulHunt in Depth — Binarly · Help Net Security · Findings · Engine · Source SAST.