PDG Query Language (Cypher)
The Program Dependence Graph (PDG) Query Language uses Cypher, a declarative graph query language, to query dependencies, perform slicing, and verify security policies on program dependence graphs.
Overview
Cypher provides a declarative way to:
Query program dependencies (data and control)
Perform forward and backward slicing
Check information flow properties
Verify security policies
Explore shortest paths with the built-in
shortest-pathanalysis modeAnalyze parameter dependencies
The language supports:
Pattern matching (nodes and relationships)
WHERE clauses for filtering
Variable-length pattern matching
Set operations (UNION, intersection via shared variables)
Aggregations (
COUNT) and ordering (ORDER BY)
Getting Started
Building the PDG
First, compile your program and build the PDG:
# Compile to LLVM IR
clang -emit-llvm -g -c program.c -o program.bc
# Run PDG query tool
./build/bin/lotus-ir-pdg-query program.bc
Interactive Mode
Use -i flag for interactive queries:
./build/bin/lotus-ir-pdg-query -i program.bc
PDG Query (type 'help' or 'quit')
> MATCH (n:FUNC_ENTRY) WHERE n.name = 'main' RETURN n
[Results displayed]
> MATCH (input:FUNC_ENTRY)-[:PARAMETER_OUT]->(ret:INST_RET)-[*]->(n) WHERE input.name = 'getInput' RETURN n
[Slice displayed]
> exit
Each interactive line is parsed as a complete query. Use -q for a single
multi-line query or -f for a batch file.
Single Query Mode
Use -q flag for single query:
./build/bin/lotus-ir-pdg-query -q "MATCH (n:FUNC_ENTRY) WHERE n.name = 'main' RETURN n" program.bc
Batch Query Mode
Use -f flag to run queries from file:
./build/bin/lotus-ir-pdg-query -f queries.txt program.bc
Language Reference
Basic Node Queries
Get All Nodes
Returns all nodes in the PDG.
Syntax: MATCH (n) RETURN n
Example:
MATCH (n) RETURN n
Returns: All nodes in the PDG.
Query Nodes by Type
Select nodes by their label (type).
Syntax: MATCH (n:LABEL) RETURN n
Node Types (Labels):
INST_FUNCALL- Function call instructionsINST_RET- Return instructionsINST_BR- Branch instructionsINST_OTHER- All other instructions (loads, stores, allocas, GEPs, …)FUNC_ENTRY- Function entry pointsPARAM_FORMALIN- Formal input parametersPARAM_FORMALOUT- Formal output parameters (return values)PARAM_ACTUALIN- Actual input parametersPARAM_ACTUALOUT- Actual output parametersVAR_STATICALLOCGLOBALSCOPE- Statically allocated globalsVAR_STATICALLOCMODULESCOPE- Module-scoped statically allocated variablesVAR_STATICALLOCFUNCTIONSCOPE- Function-scoped statically allocated variablesVAR_OTHER- Other variablesFUNC- Function nodesCLASS- Class nodesANNO_VAR- Variable annotationsANNO_GLOBAL- Global annotationsANNO_OTHER- Other annotations
The query engine also accepts the node groups INST, VAR, PARAM,
and ANNO to match all labels in that group.
Example:
MATCH (n:INST_FUNCALL) RETURN n
MATCH (n:INST_BR) RETURN n
Function-Specific Queries
Return Values of a Function
Returns the return values of a function.
Syntax: MATCH (n:FUNC_ENTRY)-[:PARAMETER_OUT]->(ret:INST_RET) WHERE n.name = 'functionName' RETURN ret
Example:
MATCH (n:FUNC_ENTRY)-[:PARAMETER_OUT]->(ret:INST_RET)
WHERE n.name = 'main'
RETURN ret
MATCH (n:FUNC_ENTRY)-[:PARAMETER_OUT]->(ret:INST_RET)
WHERE n.name = 'getInput'
RETURN ret
Returns: Nodes representing return values of the specified function.
Formal Parameters of a Function
Returns the formal parameters of a function.
Syntax: MATCH (n:FUNC_ENTRY)-[:PARAMETER_IN]->(param:PARAM_FORMALIN) WHERE n.name = 'functionName' RETURN param
Example:
MATCH (n:FUNC_ENTRY)-[:PARAMETER_IN]->(param:PARAM_FORMALIN)
WHERE n.name = 'process_data'
RETURN param
Returns: Nodes representing formal input parameters.
Function Entry Points
Returns the entry points of a function.
Syntax: MATCH (n:FUNC_ENTRY) WHERE n.name = 'functionName' RETURN n
Example:
MATCH (n:FUNC_ENTRY)
WHERE n.name = 'main'
RETURN n
Returns: Function entry nodes.
Edge Queries
Query Edges by Type
Select edges by their relationship type.
Syntax: MATCH ()-[r:TYPE]->() RETURN r
Edge Types (Relationship Types):
IND_CALL- Indirect call edgesCONTROLDEP_CALLINV- Control dependency from a call site to the calleeCONTROLDEP_CALLRET- Control dependency from the callee return to the call siteCONTROLDEP_ENTRY- Control dependency from the function entryCONTROLDEP_BR- Control dependency from branchesCONTROLDEP_IND_BR- Control dependency from indirect branchesDATA_DEF_USE- Data definition-use edgesDATA_RAW- Read-after-write edgesDATA_READ- Read edgesDATA_ALIAS- Alias edgesDATA_RET- Return-value data edgesPARAMETER_IN- Parameter input edgesPARAMETER_OUT- Parameter output edgesPARAMETER_FIELD- Field parameter edgesGLOBAL_DEP- Global dependency edgesVAL_DEP- Value dependency edgesCLS_MTH- Class-to-method edgesANNO_VAR- Variable annotation edgesANNO_GLOBAL- Global annotation edgesANNO_OTHER- Other annotation edgesTYPE_OTHEREDGE- Type-related edges
The aliases CALL_INV, CALL_RET, PARAM_IN, and PARAM_OUT are
also accepted in relationship patterns.
Example:
MATCH ()-[r:DATA_DEF_USE]->() RETURN r
MATCH ()-[r:CONTROLDEP_BR]->() RETURN r
Connected Nodes
Get nodes connected by edges.
Syntax: MATCH (a)-[r]->(b) RETURN a, b
Example:
MATCH (a)-[r:DATA_DEF_USE]->(b) RETURN a, b
MATCH (a)-[r:CONTROLDEP_BR]->(b) RETURN a, b
Slicing Operations
Forward Slice
Compute forward slice from given nodes (all nodes reachable from source).
Syntax: MATCH (source)-[*]->(n) RETURN DISTINCT n
Example:
MATCH (input:FUNC_ENTRY)-[:PARAMETER_OUT]->(inputRet:INST_RET)
WHERE input.name = 'getInput'
MATCH (inputRet)-[*]->(n)
RETURN DISTINCT n
Returns: All nodes reachable from the input nodes.
Use Case: Find what program elements are affected by a source.
Backward Slice
Compute backward slice from given nodes (all nodes that can reach the sink).
Syntax: MATCH (n)-[*]->(sink) RETURN DISTINCT n
Example:
MATCH (output:FUNC_ENTRY)-[:PARAMETER_IN]->(outputParam:PARAM_FORMALIN)
WHERE output.name = 'printOutput'
MATCH (n)-[*]->(outputParam)
RETURN DISTINCT n
Returns: All nodes that can reach the input nodes.
Use Case: Find what program elements influence a sink.
Path Between Nodes
Find nodes on paths between two sets of nodes.
Syntax: MATCH (start)-[*]->(end) RETURN DISTINCT end
Example:
MATCH (input:FUNC_ENTRY)-[:PARAMETER_OUT]->(inputRet:INST_RET)
WHERE input.name = 'getInput'
MATCH (output:FUNC_ENTRY)-[:PARAMETER_IN]->(outputParam:PARAM_FORMALIN)
WHERE output.name = 'system'
MATCH (inputRet)-[*]->(outputParam)
RETURN DISTINCT outputParam
Returns: Sink nodes reachable from the source nodes. A non-empty result means at least one source-to-sink path exists.
Use Case: Information flow analysis. Return a node from each end of the
path rather than binding path; use the CLI --analysis=shortest-path or
--analysis=chop modes when you need a materialized witness path.
Shortest Path
Shortest-path queries are a built-in analysis mode rather than a Cypher
function; shortestPath(...) is not part of the query grammar.
Example:
./build/bin/lotus-ir-pdg-query --analysis=shortest-path \
--criteria-query "MATCH (m:INST_FUNCALL) WHERE m.callee = 'malloc' RETURN m" \
--target-query "MATCH (f:INST_FUNCALL) WHERE f.callee = 'free' RETURN f" \
--edge-preset value-flow program.bc
Returns: The shortest dependence path between the criteria and target
nodes selected by the two Cypher queries. Add --format=json to inspect the
witness nodes or --format=dot to render the path.
Set Operations
Union
Union of two sets using UNION keyword.
Syntax: MATCH ... RETURN ... UNION MATCH ... RETURN ...
Example:
MATCH (func1:FUNC_ENTRY)-[:PARAMETER_OUT]->(ret1:INST_RET)
WHERE func1.name = 'func1'
RETURN ret1
UNION
MATCH (func2:FUNC_ENTRY)-[:PARAMETER_OUT]->(ret2:INST_RET)
WHERE func2.name = 'func2'
RETURN ret2
Returns: All nodes in either set.
Intersection
Intersection by binding the same variable in two MATCH clauses.
Syntax: MATCH (a)-[*]->(n) MATCH (b)-[*]->(n) RETURN n
Example:
MATCH (source:FUNC_ENTRY)-[*]->(n)
WHERE source.name = 'read_input'
MATCH (sink:FUNC_ENTRY)-[*]->(n)
WHERE sink.name = 'process'
RETURN n
Returns: Nodes reachable from both functions.
Use Case: Find program points shared by two flows. Bind the same variable in
both MATCH clauses to intersect their result sets.
Difference
The grammar has no direct set-difference operator. Run both queries and subtract the result sets, or exclude rows with a negated condition on a supported property.
Syntax: MATCH (n) WHERE NOT n.property = value RETURN n
Example:
MATCH (calls:INST_FUNCALL)
WHERE NOT calls.callee = 'printf'
RETURN calls
Returns: Nodes in the first set that do not satisfy the negated condition.
Filtering with WHERE
Property Filtering
Filter nodes by properties.
Syntax: MATCH (n) WHERE n.property = value RETURN n
Example:
MATCH (n:FUNC_ENTRY)
WHERE n.name = 'main'
RETURN n
MATCH (n)
WHERE n.src_line > 100 AND n.src_line < 200
RETURN n
Multiple Conditions
Combine conditions with AND, OR, NOT.
Syntax: MATCH (n) WHERE condition1 AND condition2 RETURN n
Example:
MATCH (n:INST_FUNCALL)
WHERE n.callee = 'malloc' OR n.callee = 'calloc'
RETURN n
Security Analysis Examples
Information Flow Analysis
Check if secret data flows to network:
MATCH (secret:FUNC_ENTRY)-[:PARAMETER_OUT]->(secretRet:INST_RET)
WHERE secret.name = 'getPassword'
MATCH (network:FUNC_ENTRY)-[:PARAMETER_IN]->(networkParam:PARAM_FORMALIN)
WHERE network.name = 'sendToNetwork'
MATCH (secretRet)-[*]->(networkParam)
RETURN DISTINCT networkParam
If result is non-empty, there’s a potential information leak. The returned
node is the sink reached by the secret value; use --analysis=shortest-path
if you need the actual witness path.
Sanitization Verification
Check which user inputs reach SQL sinks directly, then check which of those flows pass through a sanitizer:
MATCH (sources:FUNC_ENTRY)-[:PARAMETER_OUT]->(sourceRet:INST_RET)
WHERE sources.name = 'getUserInput'
MATCH (sinks:FUNC_ENTRY)-[:PARAMETER_IN]->(sinkParam:PARAM_FORMALIN)
WHERE sinks.name = 'executeSQL'
MATCH (sourceRet)-[*]->(sinkParam)
RETURN DISTINCT sinkParam
MATCH (sources:FUNC_ENTRY)-[:PARAMETER_OUT]->(sourceRet:INST_RET)
WHERE sources.name = 'getUserInput'
MATCH (sanitizers:FUNC_ENTRY)-[:PARAMETER_OUT]->(sanitizerRet:INST_RET)
WHERE sanitizers.name = 'sanitizeSQL'
MATCH (sinks:FUNC_ENTRY)-[:PARAMETER_IN]->(sinkParam:PARAM_FORMALIN)
WHERE sinks.name = 'executeSQL'
MATCH (sourceRet)-[*]->(sanitizerRet)-[*]->(sinkParam)
RETURN DISTINCT sinkParam
The two queries form the two halves of a sanitization check: sinks returned by
Query 1 but not Query 2 are candidates for review. The query grammar has no
negated path pattern, so express the complement by comparing the two result
sets rather than with NOT EXISTS.
Access Control Policy
Check sensitive file operations guarded by an authorization check:
MATCH (auth:FUNC_ENTRY)-[:PARAMETER_OUT]->(authRet:INST_RET)
WHERE auth.name = 'checkPermission'
MATCH (authRet)-[:CONTROLDEP_BR]->(check)-[:CONTROLDEP_BR]->(fileParam)
MATCH (fileOps:FUNC_ENTRY)-[:PARAMETER_IN]->(fileParam:PARAM_FORMALIN)
WHERE fileOps.name = 'openFile' OR fileOps.name = 'writeFile'
RETURN DISTINCT fileParam
Run the same query without the authorization chain to list every file operation; operations missing from the guarded result need review. As with the sanitization check, the complement is computed outside the query because negated path patterns are not supported.
Null Pointer Analysis
Find potential null dereferences:
MATCH (alloc:INST_FUNCALL)
WHERE alloc.callee = 'malloc' OR alloc.callee = 'fopen'
MATCH (alloc)-[*]->(deref:INST_OTHER)
WHERE deref.opcode IN ['load', 'store']
RETURN deref
Control Flow Analysis
Find which conditionals affect a computation:
MATCH (branches:INST_BR)
MATCH (computation:FUNC_ENTRY)-[:PARAMETER_IN]->(compParam:PARAM_FORMALIN)
WHERE computation.name = 'compute'
MATCH (branches)-[*]->(compParam)
RETURN branches
Parameter Dependency
Find dependencies between function parameters:
MATCH (inputs:FUNC_ENTRY)-[:PARAMETER_IN]->(inputParam:PARAM_FORMALIN)
WHERE inputs.name = 'processData'
MATCH (outputs:FUNC_ENTRY)-[:PARAMETER_OUT]->(outputRet:INST_RET)
WHERE outputs.name = 'processData'
MATCH (inputParam)-[*]->(outputRet)
RETURN DISTINCT outputRet
Taint Analysis
Complete taint analysis from sources to sinks:
MATCH (sources:FUNC_ENTRY)-[:PARAMETER_OUT]->(sourceRet:INST_RET)
WHERE sources.name = 'read' OR sources.name = 'recv' OR sources.name = 'scanf'
MATCH (sinks:FUNC_ENTRY)-[:PARAMETER_IN]->(sinkParam:PARAM_FORMALIN)
WHERE sinks.name = 'system' OR sinks.name = 'exec' OR sinks.name = 'popen'
MATCH (sourceRet)-[*]->(sinkParam)
RETURN DISTINCT sinkParam
Query File Format
Batch query files may contain single-line or multi-line queries:
A line whose first character is
#is a comment and also separates queries.Blank lines separate queries.
A trailing
;terminates the current query.A new
MATCHafter a completed query (one containingRETURNorLIMIT) starts a new query, so files with one query per line also work.
Example query file (security_policy.txt):
# Direct flows from sources to sinks
MATCH (sources:FUNC_ENTRY)-[:PARAMETER_OUT]->(sourceRet:INST_RET)
WHERE sources.name = 'read' OR sources.name = 'recv'
MATCH (sinks:FUNC_ENTRY)-[:PARAMETER_IN]->(sinkParam:PARAM_FORMALIN)
WHERE sinks.name = 'system' OR sinks.name = 'exec'
MATCH (sourceRet)-[*]->(sinkParam)
RETURN DISTINCT sinkParam
# Sensitive operations guarded by checkAuth
MATCH (auth:FUNC_ENTRY)-[:PARAMETER_OUT]->(authRet:INST_RET)
WHERE auth.name = 'checkAuth'
MATCH (authRet)-[:CONTROLDEP_BR]->(check)-[:CONTROLDEP_BR]->(opParam)
MATCH (sensitiveOps:FUNC_ENTRY)-[:PARAMETER_IN]->(opParam:PARAM_FORMALIN)
WHERE sensitiveOps.name = 'deleteFile' OR sensitiveOps.name = 'changePassword'
RETURN DISTINCT opParam
Run with:
./build/bin/lotus-ir-pdg-query -f security_policy.txt program.bc
The multi-line examples elsewhere in this page can be copied directly into a
batch file; separate each example with a blank line or # comment.
Best Practices
Use Descriptive Variable Names:
Good:
MATCH (userInput:FUNC_ENTRY)-[:PARAMETER_OUT]->(inputRet:INST_RET) WHERE userInput.name = 'scanf' RETURN inputRet
Avoid:
MATCH (x:FUNC_ENTRY)-[:PARAMETER_OUT]->(y:INST_RET) WHERE x.name = 'scanf' RETURN y
Break Complex Queries:
Use multiple MATCH clauses for clarity. This query returns input-to-sink flows that pass through a sanitizer:
MATCH (sources:FUNC_ENTRY)-[:PARAMETER_OUT]->(sourceRet:INST_RET) WHERE sources.name = 'getInput' MATCH (sanitizers:FUNC_ENTRY)-[:PARAMETER_OUT]->(sanitizerRet:INST_RET) WHERE sanitizers.name = 'sanitize' MATCH (sinks:FUNC_ENTRY)-[:PARAMETER_IN]->(sinkParam:PARAM_FORMALIN) WHERE sinks.name = 'output' MATCH (sourceRet)-[*]->(sanitizerRet)-[*]->(sinkParam) RETURN DISTINCT sinkParam
To find unsanitized flows, run the same query without the sanitizer pattern and compare the two result sets.
Comment Your Policies:
Comments belong on their own
#lines in batch files:# Policy: user input must reach the database through sanitizeSQL MATCH (input:FUNC_ENTRY)-[:PARAMETER_OUT]->(inputRet:INST_RET) WHERE input.name = 'getUserInput' MATCH (sanitize:FUNC_ENTRY)-[:PARAMETER_OUT]->(sanitizerRet:INST_RET) WHERE sanitize.name = 'sanitizeSQL' MATCH (dbQuery:FUNC_ENTRY)-[:PARAMETER_IN]->(queryParam:PARAM_FORMALIN) WHERE dbQuery.name = 'executeQuery' MATCH (inputRet)-[*]->(sanitizerRet)-[*]->(queryParam) RETURN DISTINCT queryParam
Test Incrementally:
Start with simple queries and build up.
Verify the source exists:
MATCH (n:FUNC_ENTRY) WHERE n.name = 'getInput' RETURN n
Verify the sink exists:
MATCH (n:FUNC_ENTRY) WHERE n.name = 'system' RETURN n
Then check for flows:
MATCH (input:FUNC_ENTRY)-[:PARAMETER_OUT]->(inputRet:INST_RET) WHERE input.name = 'getInput' MATCH (output:FUNC_ENTRY)-[:PARAMETER_IN]->(outputParam:PARAM_FORMALIN) WHERE output.name = 'system' MATCH (inputRet)-[*]->(outputParam) RETURN DISTINCT outputParam
Use DISTINCT for Slices:
MATCH (start)-[*]->(n) RETURN DISTINCT n
Limitations
Imprecision: PDG construction uses alias analysis which may be imprecise, leading to spurious dependencies.
Scalability: Large programs with complex dependencies may result in very large PDGs.
Context Sensitivity: PDG is context-insensitive by default. Multiple calls to the same function are merged.
Array Handling: Array dependencies may be approximated.
Pointer Analysis: Depends on underlying pointer analysis precision.
Performance Tips
Limit Path Length: Use bounded variable-length patterns when possible:
MATCH (start)-[*1..5]->(end) RETURN end
Use Specific Labels: Always specify node labels when possible:
MATCH (n:INST_FUNCALL) RETURN n
Filter Early: Apply WHERE clauses as early as possible:
MATCH (n:FUNC_ENTRY) WHERE n.name = 'main' MATCH (n)-[:PARAMETER_OUT]->(ret:INST_RET) RETURN ret
Troubleshooting
Query Returns Empty Set
Check function names are correct (case-sensitive)
Verify PDG was built successfully
Try simpler queries first (
MATCH (n:FUNC_ENTRY) WHERE n.name = 'main' RETURN n)Use
-vverbose flag
Query Too Slow
Reduce scope of query
Use more specific node/edge selection
Limit path length with
[*1..10]Analyze smaller program subset
Syntax Errors
Check quotes around string values:
WHERE n.name = 'main'Ensure proper Cypher syntax
Balance parentheses and brackets
Check relationship types are correct
See Also
PDG — Program Dependence Graph - PDG construction details
Tutorials and Examples - PDG usage examples
API Reference - Programmatic PDG access
tools/ir/examples/- In-repo example query cookbookPDG Query Example Cookbook - Rendered documentation for the example queries