From 13bf978f645d259a86745d2d4d4e4d6a8d12c7a3 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Tue, 16 Jun 2026 17:18:53 +0100 Subject: [PATCH] Go CFG: run deferred calls at function exit in LIFO order Model `defer`ed calls so the call runs at function exit rather than inline at the `defer` statement, reproducing the previous control-flow semantics: - Add a per-defer "defer-invoke" node for the deferred call. - deferExitStep wires normal-exit predecessors (return nodes and body fall-through) through the active deferred-call invocations in last-in-first-out order, then on to the normal exit target (the result-read epilogue for named results, or the normal exit node). - The chain is reachability-gated using the defer-free successor relation (succIgnoringDeferExit / isInOrderNode), so only deferred calls that were actually registered on a path are run on that path. - overridesCallableBodyExit / overridesCallableEndAbruptCompletion suppress the default body-exit and return routing for functions containing `defer`, so the epilogue is interposed instead. --- .../go/controlflow/ControlFlowGraphShared.qll | 149 +++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/go/ql/lib/semmle/go/controlflow/ControlFlowGraphShared.qll b/go/ql/lib/semmle/go/controlflow/ControlFlowGraphShared.qll index 60f430c56d4..be489e515b4 100644 --- a/go/ql/lib/semmle/go/controlflow/ControlFlowGraphShared.qll +++ b/go/ql/lib/semmle/go/controlflow/ControlFlowGraphShared.qll @@ -543,6 +543,9 @@ module GoCfg { implicitFieldSelection(n, i, implicitField) and tag = "implicit-field:" + i.toString() ) + or + // Deferred-call invocation node, placed at function exit by `deferExitStep` + n = any(Go::DeferStmt s).getCall() and tag = "defer-invoke" ) } @@ -721,6 +724,7 @@ module GoCfg { or exists(Go::FuncDef fd | ast = fd.getBody() and + not funcHasDefer(fd) and c.getSuccessorType() instanceof ReturnSuccessor and ( // If the function has result variables, route the return completion @@ -745,7 +749,11 @@ module GoCfg { // `return` straight to the normal exit node is suppressed so that the // return is instead caught by `endAbruptCompletion` above and routed // through the result-read epilogue. - exists(c.(Go::FuncDef).getResultVar(0)) and + // + // For functions containing `defer` statements, the default routing is + // likewise suppressed so that returns are routed through the deferred-call + // epilogue (see `deferExitStep`) instead. + (exists(c.(Go::FuncDef).getResultVar(0)) or funcHasDefer(c.(Go::FuncDef))) and completion.getSuccessorType() instanceof ReturnSuccessor } @@ -759,6 +767,145 @@ module GoCfg { ) } + /** Holds if `fd` contains at least one `defer` statement. */ + private predicate funcHasDefer(Go::FuncDef fd) { + exists(Go::DeferStmt s | s.getEnclosingFunction() = fd) + } + + /** + * Holds if `n` is the registration node of `defer` statement `s` (the + * post-order node of the statement, reached once its call's arguments have + * been evaluated). + * + * This uses the reachability-free `isInOrderNode` rather than `n.isIn(s)` + * because it is referenced under negation by `notDeferSucc`, and must + * therefore not depend on `reachable`. + */ + private predicate deferRegistration(PreControlFlowNode n, Go::DeferStmt s) { + isInOrderNode(n, s) + } + + /** + * Holds if `n` is the deferred-invocation node for `defer` statement `s`, + * which models the deferred call running at function exit. + */ + private predicate deferInvoke(PreControlFlowNode n, Go::DeferStmt s) { + n.isAdditional(s.getCall(), "defer-invoke") + } + + /** + * Gets a defer-free successor of `n` that is not a `defer` registration + * node. Walking this relation from a node stops at the next registration + * node, which is how the reachability gate for deferred calls is computed. + * + * This is typed over `PreControlFlowNode` and uses `succIgnoringDeferExit` + * so that it does not depend on `reachable` (which would otherwise create a + * non-monotonic cycle through `deferExitStep`). + */ + private PreControlFlowNode notDeferSucc(PreControlFlowNode n) { + succIgnoringDeferExit(n, result, _) and + not deferRegistration(result, _) + } + + /** Gets a node reachable from `start` over `notDeferSucc`, reflexively. */ + private PreControlFlowNode notDeferReach(PreControlFlowNode start) { + result = start + or + result = notDeferSucc(notDeferReach(start)) + } + + /** Gets the entry node of `fd`. */ + private PreControlFlowNode funcEntry(Go::FuncDef fd) { + result.(EntryNodeImpl).getEnclosingCallable() = fd + } + + /** + * Holds if `s` can be the first `defer` statement registered in `fd`, and + * hence the last to run: its registration node is reachable from the entry + * node without passing through another registration node. + */ + private predicate firstDefer(Go::DeferStmt s, Go::FuncDef fd) { + s.getEnclosingFunction() = fd and + exists(PreControlFlowNode reg, PreControlFlowNode m | + deferRegistration(reg, s) and + m = notDeferReach(funcEntry(fd)) and + succIgnoringDeferExit(m, reg, _) + ) + } + + /** + * Holds if the registration node of `predD` is the next registration node + * reachable from the registration node of `succD`. Then `predD` is + * registered immediately after `succD` and therefore runs immediately + * before it (deferred calls run in last-in-first-out order). + */ + private predicate nextDefer(Go::DeferStmt predD, Go::DeferStmt succD) { + exists(PreControlFlowNode regPred, PreControlFlowNode regSucc, PreControlFlowNode m | + deferRegistration(regPred, predD) and + deferRegistration(regSucc, succD) and + m = notDeferReach(regSucc) and + succIgnoringDeferExit(m, regPred, _) + ) + } + + /** + * Holds if `n` is a normal-exit predecessor of `fd`: a `return` statement + * node, or the fall-through node after the body. + */ + private predicate normalExitPred(PreControlFlowNode n, Go::FuncDef fd) { + exists(Go::ReturnStmt ret | ret.getEnclosingFunction() = fd and n.isIn(ret)) + or + n.isAfter(fd.getBody()) + } + + /** + * Holds if, after running its deferred calls, `fd` should continue at + * `target` on a normal exit. For functions with result variables this is + * the start of the result-read epilogue; otherwise it is the normal exit + * node directly. + */ + private predicate deferChainExitTarget(Go::FuncDef fd, PreControlFlowNode target) { + exists(fd.getResultVar(0)) and target.isAdditional(fd.getBody(), "result-read:0") + or + not exists(fd.getResultVar(_)) and + target.(NormalExitNodeImpl).getEnclosingCallable() = fd + } + + predicate deferExitStep(PreControlFlowNode n1, PreControlFlowNode n2) { + exists(Go::FuncDef fd | funcHasDefer(fd) | + // (a) an exit predecessor with no active defer flows straight to the exit target + normalExitPred(n1, fd) and + n1 = notDeferReach(funcEntry(fd)) and + deferChainExitTarget(fd, n2) + or + // (b) an exit predecessor flows to the invocation of the last-registered active defer + exists(Go::DeferStmt d, PreControlFlowNode reg | + deferRegistration(reg, d) and + d.getEnclosingFunction() = fd and + normalExitPred(n1, fd) and + n1 = notDeferReach(reg) and + deferInvoke(n2, d) + ) + or + // (c) deferred invocations chain in last-in-first-out order + exists(Go::DeferStmt predD, Go::DeferStmt succD | + predD.getEnclosingFunction() = fd and + nextDefer(predD, succD) and + deferInvoke(n1, predD) and + deferInvoke(n2, succD) + ) + or + // (d) the invocation of the first-registered (last to run) defer flows to the exit target + exists(Go::DeferStmt firstD | + firstDefer(firstD, fd) and + deferInvoke(n1, firstD) and + deferChainExitTarget(fd, n2) + ) + ) + } + + predicate overridesCallableBodyExit(Ast::Callable c) { funcHasDefer(c.(Go::FuncDef)) } + predicate step(PreControlFlowNode n1, PreControlFlowNode n2) { rangeLoop(n1, n2) or switchStmt(n1, n2) or