Skip to main content
This page covers recommendations for writing efficient and maintainable VulHunt rules.

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: The calls scope involves dataflow and taint analysis, which is more expensive than 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:
Use 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

The conditions 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.
For additional filtering, use validate predicates to check for specific byte patterns or strings before the rule executes:
Validators run before any scope is evaluated, so they are a cheap way to skip binaries that are not relevant to the rule.

Use local variables

In Lua, accessing local variables is faster than accessing global variables. Always declare variables and functions with local.
Beyond performance, 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:
Note that 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:
Use 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

Calling project: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 / calls to check for the presence or location of function calls.
  • Use context:precedes to verify call ordering.
  • Use context:find or context:matches for byte-level pattern matching.
Reserve decompilation and syntax queries for cases where you need to reason about the structure of the decompiled pseudocode, for example verifying that a bounds check precedes a vulnerable call within the same if block:
When using decompilation, structure your check function so that cheaper checks run first and project:decompile() is only reached when necessary.

Return early

Structure check 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.