Use the right scope
VulHunt provides three scopes with different levels of analysis. Choosing the lightest scope that still achieves accurate detection reduces execution time:| Scope | Analysis level | When to use |
|---|---|---|
scope:project | Binary-level searches, function enumeration | Stripped binaries, byte pattern matching, project-level queries |
scope:functions | Function-level call graph, pattern matching | Rules that target specific functions and inspect their calls or structure |
scope:calls | Call site analysis with dataflow/taint tracking | Rules that need to trace how data flows through function arguments |
scope:functions. Use scope:calls when you need to verify that a specific value
reaches a specific argument, not just that a call exists.
For example, if you only need to check whether read_argument calls strcpy,
scope:functions with has_call is sufficient:
scope:calls when you need to verify that a particular argument of the caller
flows into a particular parameter of the callee:
Set narrow conditions
Theconditions field
filters which binaries a rule runs against before any analysis begins. Always set
it to the narrowest match possible. This avoids loading and analyzing binaries that
the rule cannot produce results for.
validate predicates to check for specific byte
patterns or strings before the rule executes:
Use local variables
In Lua, accessing local variables is faster than accessing global variables.
Always declare variables and functions with local.
local prevents scoping issues. Any variable not declared
local is implicitly global and lives in a shared global table. If two files
define the same global name, whichever loads second silently overwrites the first.
This is especially important in modules.
Because modules are required by multiple rules, a non-local variable inside
a .vhm file pollutes the global table for every rule that imports it.
Prefer has_call over calls when possible
The method context:has_call(name) is a
boolean check that returns as soon as a single call is found.
In contrast, context:calls(name) enumerates
all call sites and builds a table with their addresses.
When you only need to know whether a function is called (e.g., for patch detection),
use has_call:
calls always returns a table, and in Lua
an empty table evaluates to true.
This means if context:calls("foo") then is always true, even when there are no
matches. Use next() to check whether the table has any elements:
calls when you need the actual addresses, for instance to annotate them
in the result or to check ordering with
context:precedes.
Use decompilation wisely
Callingproject:decompile() triggers full decompilation of a function and is one of the
most expensive operations available. Prefer lighter alternatives when they are
sufficient:
- Use
has_call/callsto check for the presence or location of function calls. - Use
context:precedesto verify call ordering. - Use
context:findorcontext:matchesfor byte-level pattern matching.
if block:
check function so that cheaper checks
run first and project:decompile() is only reached when necessary.
Return early
Structurecheck functions so that negative cases bail out as soon as possible.
Every VulHunt API call has a cost, especially project:decompile(). Avoid
performing expensive work whose results will be discarded.
Extract reusable logic into modules
When multiple rules share the same helper functions, move them into a module (.vhm file) instead of
duplicating code across rules. This reduces maintenance burden and keeps
individual rules focused on detection logic.