Making Hidden Dependencies Visible

A roadblock rarely lives at the exact line where a firmware service finally gives up. In FIRMENSTEIN, the problem is therefore not only to find a suspicious instruction, but to connect that instruction to the library, daemon, shared resource, or earlier API result that gave it meaning. This chapter explains how the API Dependency Graph and Proactive Diagnosis make those relationships visible, so a diagnostic report becomes a bounded explanation that can guide an intervention rather than an opaque model guess.

The challenge is particularly sharp for Linux-based firmware because the available program is often a stripped binary. Names, types, and source-level boundaries that would make an ordinary application easier to follow may be absent. Even when a service calls a familiar API, the behavior that matters may be implemented by a vendor wrapper, a shared library, or another process. A local view of the service can show the symptom while hiding the dependency that caused it.

The graph connects code to the state it depends on

FIRMENSTEIN’s API Dependency Graph, or ADG, begins with a familiar reverse-engineering representation: a Code Property Graph for each binary. Such a graph can represent local structure, including control flow and data dependencies inside one program. The important extension is to join the graphs of the target service and the components it actually depends on, then add relationships that cross binary and process boundaries.

The distinction between local and cross-boundary evidence matters. Within one function, a data-dependency edge may show that a value flows from an assignment into a comparison. Across a function boundary, the analysis must connect a call site to the implementation receiving its argument and returning its result. Across a process boundary, there may be no ordinary call edge at all. The relationship may instead be exposed by a shared resource identifier, a persistent device path, or the name of an IPC endpoint. Each kind of edge answers a different question about where state comes from and how it travels.

FIRMENSTEIN describes three cross-boundary relationships. Inter-program control flow links a call site in one binary to the entry point of a function in a shared library. Inter-program data flow connects arguments at the call site to callee parameters and the callee’s return statement to the caller’s result. Shared-state dependency links functions that touch the same global variable or system resource even when they do not invoke one another directly. A device path or Unix-domain socket name can be as important to the execution relationship as a variable passed through a function call.

These relationships preserve different parts of the runtime contract, so treating them as interchangeable would produce a misleading diagnosis. Control-flow linkage answers who is invoked. Data-flow linkage answers what crosses that invocation and where the result returns. Shared-state linkage answers which functions can influence one another without a direct call at all. Consider a service that asks a wrapper for configuration. The wrapper may call a library implementation, the implementation may communicate with a daemon, and the returned value may be copied into a global buffer that another function later reads. The graph needs to retain the chain without pretending that the daemon call is an ordinary in-process function call or that the global variable is merely a returned argument.

This separation also clarifies ownership. The service owns the decision to branch or continue. The library or wrapper may own the translation between the service’s request and an external operation. A daemon or operating-system resource may own the state that makes the answer meaningful. The ADG does not merge those owners into one abstract component; it records the interfaces at which responsibility changes. That lets a later diagnosis ask whether the repair belongs in environment preparation, in a function model, or at a particular call site. It also makes hidden coupling visible: two apparently unrelated functions may be coupled because they use the same persistent resource, even if neither passes a variable directly to the other.

The address mapping is part of this ownership story. Reasoning happens over readable decompiled code, but an intervention changes execution at a native instruction location. If those representations drift apart, a report can be conceptually right and operationally unusable. Mapping in both directions lets the analysis start from a semantic use and end at a concrete call site, then take a runtime observation and return to the surrounding logic. The map does not decide whether a proposed value is valid, but it prevents a correct semantic question from being lost at the representation boundary.

classDiagram class TargetService class APICallSite class LibraryWrapper class DaemonProcess class SharedResource class DiagnosticReport class InstructionAddress class ArgumentReturnDataflow TargetService --> APICallSite : contains APICallSite --> LibraryWrapper : invokes APICallSite --> ArgumentReturnDataflow : carries ArgumentReturnDataflow --> LibraryWrapper : crosses boundary LibraryWrapper --> SharedResource : accesses TargetService --> DaemonProcess : IPC dependency DaemonProcess --> SharedResource : shares state APICallSite --> DiagnosticReport : diagnosed as DiagnosticReport --> InstructionAddress : maps to

This is not a whole-firmware catalogue. The graph is built with a dependency-aware worklist: begin with the target, follow the libraries it uses, and add other binaries when evidence suggests they participate in a relevant dependency. That restriction is an architectural choice. Including everything would increase analysis overhead and context without necessarily improving the explanation of the target service. Limiting the graph also creates a boundary: a component that cannot be discovered through the available call, data, resource, or IPC evidence may remain outside the modeled relationship.

IPC discovery is a useful example of why the graph needs more than ordinary call analysis. FIRMENSTEIN looks for environment-facing IPC APIs and extracts identifiers such as Unix-domain socket paths or shared-memory names. It then uses those identifiers to locate dependent binaries. A vendor may hide the interaction behind a wrapper library rather than placing the endpoint string directly in the service, so the search also follows binaries that depend on the same IPC-related library. The result is not a proof that every daemon has been found; it is a structured way to turn a hidden process relationship into an analyzable dependency.

The graph also has to connect the language of analysis with the language of execution. FIRMENSTEIN maintains a bidirectional mapping between decompiled code tokens and instruction addresses. An agent can use the decompiled representation to reason about a call and still identify where an execution intervention must attach. In the other direction, a runtime trace can be mapped back to the code context that produced it. Without that alignment, diagnosis might explain the right function but be unable to locate the right native call site, or an emulator might report an address that is difficult to interpret semantically.

This division of labor clarifies what the ADG does and does not do. It supplies structural relationships and a coordinate system. It does not, by itself, decide which API outcome violates the firmware’s internal expectations. Nor does a graph edge automatically reveal whether a buffer contains an address, a configuration string, or a protocol response. Those judgments require a second layer that can select relevant evidence and reason about how it is used.

Proactive Diagnosis follows meaning, not just edges

FIRMENSTEIN’s Proactive Diagnosis has two cooperating parts: a Proactive Code Slicing Agent and a Diagnosis Agent. The first narrows the program context around environment-facing APIs; the second interprets that context and writes a structured roadblock report. The division is important because searching every instruction for every possible dependency would be expensive, while asking a language model to infer the whole firmware’s meaning from an undifferentiated dump would be unreliable.

The slicing agent starts by identifying generic APIs that may interact with the runtime environment. Pure computation and memory-manipulation routines are not useful starting points for this purpose, so the agent filters them out and classifies the remaining calls by the resources they may touch: files, networks, devices, or IPC. This is not yet a diagnosis. It is a way to choose the places where environmental state can enter or leave the program.

From each selected call site, the agent follows two directions. It works backward to discover where arguments came from, because the origin can reveal which configuration or resource the service is actually asking about. It also works forward to see how the return value and side effects influence later variables, branches, and calls. The result is a multi-source, bidirectional slice: not a claim about the whole program, but a focused region that contains the API, its inputs, its outputs, and the uses that make those outputs significant.

Ordinary static taint analysis is good at explicit propagation rules. If a value is copied from one variable into another, a tool can often follow the edge. Firmware creates harder cases. A vendor-specific wrapper may transfer a string or structure in a way that the generic analyzer does not recognize. A file descriptor may be opened in one function and used much later, with the intervening operations carrying the meaning of the underlying device. A local variable in decompiled code may be only an opaque byte buffer, making the relationship between a field and a later check difficult to recover.

FIRMENSTEIN addresses these gaps with semantic guidance and task-specific tools. A variable relationship tracker helps identify candidates for further inter-procedural tracing. An implementation resolver locates a concrete library implementation across binary boundaries. A global-state tracker finds functions that touch the same global variable or system resource. A stack-layout analyzer helps infer when local storage corresponds to fields in a stack-allocated structure. These tools do not replace the graph; they give the agent ways to ask the graph more useful questions when a simple edge is insufficient.

The “proactive” part of the design responds to a practical weakness in reactive agent workflows. A reactive agent may stop after tracing a few variables and declare success before it has reached the state that explains the failure. It may also receive a large context and overlook a relationship that is present but buried. FIRMENSTEIN first enriches the slice around identified variables, then lets the agent reason about what to inspect next, and enriches the context again after that action. The reasoning trace remains available across tool calls, while semantically related fragments can be merged to control context growth.

This loop has a useful interpretation: the agent is not being asked to hallucinate a complete execution story from the first symptom. It is being required to keep the relevant context open until the relationship becomes terminal or the analysis times out. The approach still depends on the agent making good selections and on the underlying decompilation being informative. Proactivity reduces premature closure; it does not turn incomplete code into ground truth.

The Diagnosis Agent then turns the slice into a Roadblock Diagnostic Report. The report identifies the target API and its location, characterizes the roadblock category, describes the meaning of parameters and return values, summarizes the function’s role, states constraints on critical variables, and suggests an intervention direction. In practical terms, this report answers questions that a generic crash log cannot: what dependency was expected, what state did the service need, and what outcome would allow the intended path to continue?

A report is best understood as a structured hypothesis with attached evidence. Its value comes from preserving the relationship between the API and the downstream logic, not from sounding certain. If decompilation has lost a type or function prototype, the report may misread an argument. If context aggregation includes irrelevant fragments, the diagnosis may be pulled toward the wrong explanation. FIRMENSTEIN’s design allows a failed emulation to produce a trace that can be mapped back to the decompiled code, enabling a later diagnosis pass; it also permits a human expert to adjust the report. Those mechanisms acknowledge that diagnosis is iterative rather than infallible.

The same boundary applies to the ADG. Its token-to-address mapping can point an intervention to an exact native location, but an exact location does not guarantee an exact interpretation. A shared resource edge can show that two functions touch the same identifier, but it does not by itself establish which value is valid in every state. A useful diagnosis must combine structural evidence with the service’s observed constraints and preserve uncertainty where that combination remains incomplete.

That is the point at which hidden dependencies become actionable. FIRMENSTEIN has moved from a visible symptom to a set of connected program and environment relationships, then from those relationships to a report that names the state the service appears to require. The next judgment is harder: whether that report truly explains the intended path, and whether the proposed repair preserves the state and side effects that path needs. A graph can reveal the dependency; it cannot excuse a semantic shortcut.

References