From 99f2b993361da29802814f0240faa5402fd0b9d8 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Sun, 6 Sep 2026 23:59:19 -0400 Subject: [PATCH] fix(ddg): drop dependence edges whose endpoint is not a body node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A WALA instruction whose covering source statement cannot be identified maps to the sentinel ":0" — a well-formed localId that names no body node. Both PDG producers emitted edges on it, so 90 of 5,475 ddg edges on daytrader8 at -a 4 dangled, every one carrying prov:["points-to"]. Such an edge is unfollowable. The Neo4j emitter materialises body nodes from body{}, so it silently could not write them, which made ddg the only place a consumer reading both artefacts saw them disagree. Dropping is sound in the direction that matters: the sentinel marks an over-approximation, so the edge is a may-dependence whose exact endpoint was never established. Emitting a synthetic vertex instead would need a body-node kind the schema has no vocabulary for. DdgEdges.dropDangling is applied by both attach points — L3WalaOverlays for the wala engine's own ddg and L4WalaOverlays.merge for the points-to half — so no producer can reintroduce the shape. The L3 log line now reports what it dropped. On daytrader8 at -a 4: 90 dangling edges before, 0 after, and the JSON and the Neo4j snapshot now report the same 5,385 ddg edges. Closes #228 --- .../java/com/ibm/cldk/L3WalaOverlays.java | 12 +++- .../java/com/ibm/cldk/L4WalaOverlays.java | 5 +- .../syntactic_analysis/dataflow/DdgEdges.java | 44 ++++++++++++ .../dataflow/DdgEdgesTest.java | 71 +++++++++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/ibm/cldk/syntactic_analysis/dataflow/DdgEdges.java create mode 100644 src/test/java/com/ibm/cldk/syntactic_analysis/dataflow/DdgEdgesTest.java diff --git a/src/main/java/com/ibm/cldk/L3WalaOverlays.java b/src/main/java/com/ibm/cldk/L3WalaOverlays.java index 6f6167ff..2d085806 100644 --- a/src/main/java/com/ibm/cldk/L3WalaOverlays.java +++ b/src/main/java/com/ibm/cldk/L3WalaOverlays.java @@ -23,11 +23,13 @@ import com.github.javaparser.ast.stmt.BlockStmt; import com.ibm.cldk.schema.CanId; import com.ibm.cldk.schema.JCallable; +import com.ibm.cldk.schema.JDdgEdge; import com.ibm.cldk.schema.JModule; import com.ibm.cldk.schema.JType; import com.ibm.cldk.syntactic_analysis.L1BuildContext; import com.ibm.cldk.syntactic_analysis.controlflow.BodyNodeBuilder; import com.ibm.cldk.syntactic_analysis.controlflow.ControlFlowGraph; +import com.ibm.cldk.syntactic_analysis.dataflow.DdgEdges; import com.ibm.cldk.utils.Log; import com.ibm.cldk.wala.InstructionToNode; import com.ibm.cldk.wala.WalaAnalysis; @@ -87,6 +89,7 @@ public static void apply( int matched = 0; int skippedNoMatch = 0; int totalOverApprox = 0; + int droppedDangling = 0; for (MethodIr m : wala.applicationMethods()) { Optional joinedOpt = join(m, typeIndex, parseCache, modules); @@ -125,7 +128,11 @@ public static void apply( callable.setBody(cfg.nodes()); callable.setCfg(cfg.toCfgEdges()); callable.setCdg(pdg.cdg); - callable.setDdg(pdg.ddg); + // An edge whose endpoint is an unmapped sentinel names no body node, so no consumer can + // follow it (issue #228). The body is already set above, so the guard sees the final map. + List resolved = DdgEdges.dropDangling(callable, pdg.ddg); + droppedDangling += pdg.ddg.size() - resolved.size(); + callable.setDdg(resolved); matched++; totalOverApprox += mapper.overApproximationCount(); @@ -133,7 +140,8 @@ public static void apply( Log.info("L3 WALA overlays applied: " + matched + " callable(s) covered, " + skippedNoMatch + " skipped (no source match), " - + totalOverApprox + " sentinel over-approximation(s)"); + + totalOverApprox + " sentinel over-approximation(s), " + + droppedDangling + " unresolved ddg edge(s) dropped"); } // ----- the WALA-method → JCallable join ----------------------------------------------------- diff --git a/src/main/java/com/ibm/cldk/L4WalaOverlays.java b/src/main/java/com/ibm/cldk/L4WalaOverlays.java index 773f4b08..5c4367df 100644 --- a/src/main/java/com/ibm/cldk/L4WalaOverlays.java +++ b/src/main/java/com/ibm/cldk/L4WalaOverlays.java @@ -19,6 +19,7 @@ import com.ibm.cldk.schema.JCallable; import com.ibm.cldk.schema.JDdgEdge; import com.ibm.cldk.schema.JModule; +import com.ibm.cldk.syntactic_analysis.dataflow.DdgEdges; import com.ibm.cldk.utils.Log; import com.ibm.cldk.wala.InstructionToNode; import com.ibm.cldk.wala.WalaAnalysis; @@ -261,7 +262,9 @@ private static int merge(JCallable callable, List produced) { } int added = 0; - for (JDdgEdge edge : produced) { + // An unmapped WALA instruction maps to the ":0" sentinel, which names no body node; + // an edge on one is unfollowable in JSON and unwritable in Neo4j, so it never lands (#228). + for (JDdgEdge edge : DdgEdges.dropDangling(callable, produced)) { if (!edge.getProv().contains("points-to")) { continue; } diff --git a/src/main/java/com/ibm/cldk/syntactic_analysis/dataflow/DdgEdges.java b/src/main/java/com/ibm/cldk/syntactic_analysis/dataflow/DdgEdges.java new file mode 100644 index 00000000..a97d99cb --- /dev/null +++ b/src/main/java/com/ibm/cldk/syntactic_analysis/dataflow/DdgEdges.java @@ -0,0 +1,44 @@ +package com.ibm.cldk.syntactic_analysis.dataflow; + +import com.ibm.cldk.schema.JCallable; +import com.ibm.cldk.schema.JDdgEdge; +import java.util.ArrayList; +import java.util.List; + +/** Shared guards over a callable's {@code ddg}, applied by every producer that attaches edges to it. */ +public final class DdgEdges { + + private DdgEdges() {} + + /** + * Drops the edges of {@code edges} whose {@code src} or {@code dst} is not a body node of + * {@code callable}. + * + *

A WALA instruction whose covering source statement cannot be identified maps to the + * sentinel {@code ":0"} (see {@link com.ibm.cldk.wala.InstructionToNode}), which is a + * well-formed {@code localId} but names no body node. An edge on that sentinel is unusable: + * the Neo4j emitter materialises body nodes from {@code body{}}, so it silently cannot write + * the edge, and a JSON consumer resolving endpoints finds nothing on the other end. Rather + * than emit a dependence a reader cannot follow, the edge is dropped here — the same + * information the graph already carried, now stated identically in both artefacts. + * + *

Dropping is sound in the direction that matters: the sentinel marks an + * over-approximation, so the edge is a may-dependence whose exact endpoint was never + * established. The alternative — emitting a synthetic {@code :0} vertex — would need a + * body-node kind no schema vocabulary has. + * + * @return the edges that resolve on both ends, in input order + */ + public static List dropDangling(JCallable callable, List edges) { + if (edges == null || edges.isEmpty() || callable.getBody() == null) { + return edges; + } + List kept = new ArrayList<>(edges.size()); + for (JDdgEdge e : edges) { + if (callable.getBody().containsKey(e.getSrc()) && callable.getBody().containsKey(e.getDst())) { + kept.add(e); + } + } + return kept; + } +} diff --git a/src/test/java/com/ibm/cldk/syntactic_analysis/dataflow/DdgEdgesTest.java b/src/test/java/com/ibm/cldk/syntactic_analysis/dataflow/DdgEdgesTest.java new file mode 100644 index 00000000..63505854 --- /dev/null +++ b/src/test/java/com/ibm/cldk/syntactic_analysis/dataflow/DdgEdgesTest.java @@ -0,0 +1,71 @@ +package com.ibm.cldk.syntactic_analysis.dataflow; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import com.ibm.cldk.schema.JBodyNode; +import com.ibm.cldk.schema.JCallable; +import com.ibm.cldk.schema.JDdgEdge; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class DdgEdgesTest { + + private static JCallable callableWithBody(String... keys) { + JCallable c = new JCallable(); + Map body = new LinkedHashMap<>(); + for (String k : keys) { + JBodyNode n = new JBodyNode(); + n.setKind("statement"); + body.put(k, n); + } + c.setBody(body); + return c; + } + + private static JDdgEdge edge(String src, String dst) { + JDdgEdge e = new JDdgEdge(); + e.setSrc(src); + e.setDst(dst); + e.setVar("v"); + e.getProv().add("points-to"); + return e; + } + + @Test + void dropsEdgesWhoseEndpointIsNotABodyNode() { + JCallable c = callableWithBody("10:9", "12:9"); + List kept = DdgEdges.dropDangling( + c, + Arrays.asList( + edge("10:9", "12:9"), // both resolve + edge("11:0", "12:9"), // src is the unmapped sentinel + edge("10:9", "11:0"))); // dst is the unmapped sentinel + + assertEquals(1, kept.size(), "only the edge that resolves on both ends survives"); + assertEquals("10:9", kept.get(0).getSrc()); + assertEquals("12:9", kept.get(0).getDst()); + } + + @Test + void keepsEveryEdgeWhenAllEndpointsResolve() { + JCallable c = callableWithBody("@entry", "10:9", "10:9/actual_in:0"); + List edges = Arrays.asList(edge("@entry", "10:9"), edge("10:9", "10:9/actual_in:0")); + assertEquals(edges, DdgEdges.dropDangling(c, edges)); + } + + @Test + void passesEmptyAndBodylessInputStraightThrough() { + List empty = new ArrayList<>(); + assertSame(empty, DdgEdges.dropDangling(callableWithBody("10:9"), empty)); + + JCallable noBody = new JCallable(); + noBody.setBody(null); + List edges = Arrays.asList(edge("10:9", "11:0")); + assertSame(edges, DdgEdges.dropDangling(noBody, edges), "no body map ⇒ nothing to check against"); + } +}