Function reachability determines whether the specific vulnerable function inside a dependency is actually called by your code, directly or through the call graph. A package can be installed, imported and loaded while the vulnerable function it contains is never invoked — in which case the finding describes a risk you do not carry.
Software composition analysis produces the largest finding counts in application security, and the great majority of those findings are not exploitable. Developers learn this quickly, and the rational response to a queue that is mostly noise is to stop reading it — which is how genuinely exploitable dependency vulnerabilities end up sitting alongside hundreds that were never callable.
Filtering by reachability is what makes the remainder tractable. How much is removed varies widely with the language ecosystem, the depth of the dependency tree and the scanner in use, so it is a proportion to measure against a specific backlog rather than to quote from someone else's. The direction is consistent even where the magnitude is not: the queue that reaches a developer stops being mostly noise, which is the precondition for it being read at all.
CVE-2021-44228 in Apache Log4j 2 is the case the technique is usually explained with, because the gap between present and callable was unusually wide and unusually well documented.
The vulnerable artifact is log4j-core, and in a large Java application it arrives by several routes at once: declared directly in one module, pulled in transitively by a framework in another, and shadowed inside a fat jar built by a different team. A manifest scan reports it every time. What determines exposure is whether the application's logging facade is bound to Log4j 2 at all. A service that binds its facade to a different backend ships the vulnerable class on the classpath and never calls it.
Function reachability asks the narrower question: is there a path through the call graph from application code to the message-lookup routine that performs the JNDI resolution. Where the facade binds elsewhere, no such path exists, the jar is loaded and the exposure is zero. Where it does bind to Log4j 2, the path exists and the finding is real — at which point the next question is data reachability, whether attacker-controlled text can reach a logged parameter, which for a request header it almost always can.
Runtime approaches instrument the running application and observe what is actually executed. That is a genuinely strong signal for code paths that get exercised, and it has two structural costs: the agent has to live in every environment and language runtime that matters, and a path never exercised by test or production traffic produces no finding at all. Absence of observation is not absence of reachability.
Static function reachability has the opposite profile. It runs against the codebase itself with no execution dependency and no agent deployed into a running process, so it covers paths that traffic has not exercised — but it must reason about dynamic behaviour rather than watch it, which is where framework magic and reflection make the analysis hard.
A reachable function is not automatically an exploitable one. The call may pass only trusted, internally generated values; the vulnerable branch may require a configuration you do not use; an upstream control may sanitise the input. This is why reachability is best understood as an elimination mechanism — extremely good at ruling findings out, weaker as positive proof.
It is also confined to the code lane. Reachability on assets is a different mechanism entirely, based on file paths, listening ports and whether a component is loaded and running, and the two should not be conflated in a single coverage number.
Does function reachability require running the application? No. It is a static analysis over the codebase and its dependency tree. Runtime approaches answer a related question by observing execution, but they need an agent in every environment and see only the paths traffic exercises.
What about reflection and dependency injection? They are the hard cases and the honest limit of the technique. A call graph that ignores them under-reports reachability; one that assumes the worst over-reports it. How a tool handles dynamic dispatch is the question worth asking in an evaluation.
If a function is unreachable, should the dependency still be upgraded? Usually yes, eventually — on the routine maintenance cadence rather than as a security escalation. Unreachable today is a statement about the current code, and code changes.