Skip to content

Commit 449732a

Browse files
committed
Shared CFG: add hooks for reachability-gated exit epilogue
Add opt-in InputSig2 predicates to support a function-exit epilogue whose placement depends on reachability (such as Go's deferred calls, run at exit in last-in-first-out order): - deferExitStep: language-provided exit-epilogue edges, wired into explicitStep but excluded from the defer-free reachability. - overridesCallableBodyExit: suppresses the default fall-through edge from a body's "after" node to the normal exit, so the epilogue can be interposed on fall-through paths. To let a language compute the reachability gate for those edges without observing them (and without a non-monotonic cycle through reachable): - explicitStep is split into explicitStepCommon (defer-free) plus deferExitStep, and defaultCfg now negates explicitStepCommon so default evaluation does not depend on deferExitStep. - succIgnoringDeferExit exposes the defer-free successor relation, typed over PreControlFlowNode so it does not depend on reachable. - getASuccessorIgnoringDeferredExit exposes the same relation as a ControlFlowNode member for general use. - isInOrderNode exposes a structural, reachability-free node-identity test for use inside the negations a language needs when computing its gate. - EntryNodeImpl is no longer private, so a language can identify the entry node over PreControlFlowNode. All InputSig2 additions default to none(), leaving other languages unchanged.
1 parent f5eef7d commit 449732a

1 file changed

Lines changed: 138 additions & 4 deletions

File tree

‎shared/controlflow/codeql/controlflow/ControlFlowGraph.qll‎

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,19 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
980980
*/
981981
final class PreControlFlowNode = NodeImpl;
982982

983+
/**
984+
* Holds if `n` is the in-order or post-order control flow node for `ast`.
985+
*
986+
* Unlike the `PreControlFlowNode.isIn` member predicate, this is computed
987+
* structurally (directly from the underlying node representation) and so
988+
* does not give rise to a dependency on node reachability. It is intended
989+
* for languages implementing `Input2::deferExitStep`, whose definition must
990+
* not depend on `reachable` (see `succIgnoringDeferExit`); such languages
991+
* can use this to identify nodes inside a negation without introducing a
992+
* non-monotonic cycle.
993+
*/
994+
predicate isInOrderNode(PreControlFlowNode n, AstNode ast) { n = TAstNode(ast) }
995+
983996
private class BeforeNode extends NodeImpl, TBeforeNode {
984997
private AstNode n;
985998

@@ -1051,7 +1064,7 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
10511064
}
10521065

10531066
/** The `PreControlFlowNode` at the entry point of a callable. */
1054-
final private class EntryNodeImpl extends NodeImpl, TEntryNode {
1067+
final class EntryNodeImpl extends NodeImpl, TEntryNode {
10551068
private Callable c;
10561069

10571070
EntryNodeImpl() { this = TEntryNode(c) }
@@ -1206,6 +1219,44 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
12061219
none()
12071220
}
12081221

1222+
/**
1223+
* Holds if the language-specific implementation takes over the routing of
1224+
* the normal fall-through from callable `c`'s body to its normal exit
1225+
* node.
1226+
*
1227+
* When this holds, the library's default edge from the "after" node of
1228+
* `c`'s body to the normal exit node is suppressed, and the language is
1229+
* responsible for routing the fall-through to the normal exit itself (for
1230+
* example, to interpose a function-exit epilogue such as Go's deferred
1231+
* calls). This complements `callableExitStep`, which the language can use
1232+
* to add the replacement edge into the normal exit node.
1233+
*
1234+
* The default implementation does not override any fall-through edges.
1235+
*/
1236+
default predicate overridesCallableBodyExit(Callable c) { none() }
1237+
1238+
/**
1239+
* Holds if there is a local non-abrupt step from `n1` to `n2` that forms
1240+
* part of a function-exit epilogue whose placement depends on
1241+
* reachability (such as Go's deferred calls, which run at function exit in
1242+
* last-in-first-out order, gated by whether their registration is
1243+
* reachable on the path to a given exit).
1244+
*
1245+
* Edges added here are included in the final control flow graph exactly
1246+
* like ordinary `step` edges, but they are *excluded* when the library
1247+
* computes the defer-free reachability exposed through
1248+
* `getASuccessorIgnoringDeferredExit`. This lets a language compute the
1249+
* reachability gate without observing the epilogue edges it is in the
1250+
* process of defining, avoiding a circularity.
1251+
*
1252+
* Each `deferExitStep` edge must be disjoint from every other `step` edge
1253+
* (i.e. a pair `(n1, n2)` that is a `deferExitStep` must not also arise
1254+
* from `step`), so that the defer-free reachability is well defined.
1255+
*
1256+
* The default implementation adds no such steps.
1257+
*/
1258+
default predicate deferExitStep(PreControlFlowNode n1, PreControlFlowNode n2) { none() }
1259+
12091260
/**
12101261
* Holds if there is a local non-abrupt step from `n1` to `n2`.
12111262
*
@@ -1455,6 +1506,22 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
14551506

14561507
/** Holds if there is a local non-abrupt step from `n1` to `n2`. */
14571508
private predicate explicitStep(PreControlFlowNode n1, PreControlFlowNode n2) {
1509+
explicitStepCommon(n1, n2)
1510+
or
1511+
Input2::deferExitStep(n1, n2)
1512+
}
1513+
1514+
/**
1515+
* Holds if there is a local non-abrupt step from `n1` to `n2`, excluding
1516+
* the reachability-dependent function-exit epilogue edges contributed by
1517+
* `Input2::deferExitStep`.
1518+
*
1519+
* This is the basis for the defer-free reachability exposed through
1520+
* `getASuccessorIgnoringDeferredExit`, and it must not depend on
1521+
* `deferExitStep` (so that a language can compute the reachability gate
1522+
* for its `deferExitStep` edges without circularity).
1523+
*/
1524+
private predicate explicitStepCommon(PreControlFlowNode n1, PreControlFlowNode n2) {
14581525
Input2::step(n1, n2)
14591526
or
14601527
exists(Callable c |
@@ -1479,7 +1546,8 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
14791546
)
14801547
or
14811548
n1.isAfter(getBodyExit(c)) and
1482-
n2.(NormalExitNodeImpl).getEnclosingCallable() = c
1549+
n2.(NormalExitNodeImpl).getEnclosingCallable() = c and
1550+
not Input2::overridesCallableBodyExit(c)
14831551
or
14841552
Input2::callableExitStep(n1, c, true) and
14851553
n2.(NormalExitNodeImpl).getEnclosingCallable() = c
@@ -1870,10 +1938,15 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
18701938
/**
18711939
* Holds if `ast` does not have explicitly defined control flow steps
18721940
* and therefore should use default left-to-right evaluation.
1941+
*
1942+
* This uses `explicitStepCommon` rather than `explicitStep` so that it
1943+
* does not depend on `Input2::deferExitStep` (whose edges never originate
1944+
* from a "before" node, so the two agree on `before` nodes anyway). This
1945+
* keeps the defer-free reachability independent of `deferExitStep`.
18731946
*/
18741947
private predicate defaultCfg(AstNode ast) {
18751948
hasCfg(ast) and
1876-
not explicitStep(any(PreControlFlowNode n | n.isBefore(ast)), _)
1949+
not explicitStepCommon(any(PreControlFlowNode n | n.isBefore(ast)), _)
18771950
}
18781951

18791952
private module ChildDenseRankInput implements DenseRankInputSig1 {
@@ -1927,6 +2000,14 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
19272000
explicitStep(n1, n2) or defaultStep(n1, n2)
19282001
}
19292002

2003+
/**
2004+
* Holds if there is a local non-abrupt step from `n1` to `n2`, excluding
2005+
* the function-exit epilogue edges contributed by `Input2::deferExitStep`.
2006+
*/
2007+
private predicate stepIgnoringDeferExit(PreControlFlowNode n1, PreControlFlowNode n2) {
2008+
explicitStepCommon(n1, n2) or defaultStep(n1, n2)
2009+
}
2010+
19302011
/**
19312012
* Holds if the execution of `ast` may result in an abrupt completion
19322013
* `c` originating at `last`.
@@ -1988,6 +2069,45 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
19882069
preSucc(n1, n2, t)
19892070
}
19902071

2072+
/**
2073+
* Holds if `n2` is a normal successor of `n1` of type `t`, computed over
2074+
* the defer-free step relation `stepIgnoringDeferExit` (i.e. ignoring the
2075+
* epilogue edges added through `Input2::deferExitStep`).
2076+
*
2077+
* Abrupt-completion edges are deliberately omitted: this relation only
2078+
* needs to expose the normal control flow used to compute a language's
2079+
* reachability gate for its `deferExitStep` edges.
2080+
*/
2081+
private predicate preSuccIgnoringDeferExit(
2082+
PreControlFlowNode n1, PreControlFlowNode n2, SuccessorType t
2083+
) {
2084+
stepIgnoringDeferExit(n1, n2) and n2 = TAfterValueNode(_, t)
2085+
or
2086+
stepIgnoringDeferExit(n1, n2) and n2.(AdditionalNode).getSuccessorType() = t
2087+
or
2088+
stepIgnoringDeferExit(n1, n2) and
2089+
not n2 instanceof AfterValueNode and
2090+
not n2 instanceof AdditionalNode and
2091+
t instanceof DirectSuccessor
2092+
}
2093+
2094+
/**
2095+
* Holds if `n2` is a successor of `n1` of type `t`, ignoring the
2096+
* epilogue edges added through `Input2::deferExitStep`.
2097+
*
2098+
* This exposes the defer-free reachability that a language needs in order
2099+
* to compute the reachability gate for its `deferExitStep` edges without
2100+
* observing those edges. It is typed over `PreControlFlowNode` (rather
2101+
* than the reachability-restricted `ControlFlowNode`) so that a language
2102+
* can use it to compute `deferExitStep` without a non-monotonic cycle
2103+
* through `reachable`.
2104+
*/
2105+
cached
2106+
predicate succIgnoringDeferExit(PreControlFlowNode n1, PreControlFlowNode n2, SuccessorType t) {
2107+
Input1::cfgCachedStageRef() and
2108+
preSuccIgnoringDeferExit(n1, n2, t)
2109+
}
2110+
19912111
/** The cached stage of the control flow graph. */
19922112
cached
19932113
module CfgCachedStage {
@@ -2002,7 +2122,8 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
20022122
(simpleLeafNode(_) implies any()) and
20032123
(exists(TBeforeNode(_)) implies any()) and
20042124
(reachable(_) implies any()) and
2005-
(succ(_, _, _) implies any())
2125+
(succ(_, _, _) implies any()) and
2126+
(succIgnoringDeferExit(_, _, _) implies any())
20062127
}
20072128
}
20082129

@@ -2026,6 +2147,19 @@ module Make0<LocationSig Location, AstSig<Location> Ast> {
20262147
/** Gets an immediate successor of this node, if this is not an `ExitNode`. */
20272148
ControlFlowNode getASuccessor() { result = this.getASuccessor(_) }
20282149

2150+
/**
2151+
* Gets an immediate successor of this node, ignoring the
2152+
* reachability-dependent function-exit epilogue edges added through
2153+
* `deferExitStep` (such as Go's deferred calls).
2154+
*
2155+
* This is intended for languages that need to compute reachability
2156+
* before those epilogue edges are added; it should not be used as a
2157+
* general successor relation.
2158+
*/
2159+
ControlFlowNode getASuccessorIgnoringDeferredExit() {
2160+
succIgnoringDeferExit(this, result, _)
2161+
}
2162+
20292163
/** Gets an immediate predecessor of this node, if this is not an `EntryNode`. */
20302164
ControlFlowNode getAPredecessor() { result.getASuccessor() = this }
20312165

0 commit comments

Comments
 (0)