Skip to content

Commit 39dd725

Browse files
NateD-MSFTCopilot
andcommitted
IFSM: add AST-loop OR-branch to irqlChangesBetween for intra-function loop and re-entrant cases
The `irqlChangesBetween` predicate previously decided whether an IRQL-changing call sits between a save and a restore using only source-line position via the `anchorLineForCall` mechanism. This correctly handles cross-function and acyclic in-function cases but trivially fails on loops where the restore is textually above the save (the bracketing line range is empty), even though at runtime the loop back-edge means each iteration's restore is preceded by every IRQL-changing call in the loop body. This change adds a second disjunct to `irqlChangesBetween` that uses AST-loop containment (`Loop.getStmt().getAChild*()`): when the save, restore, and an IRQL-changing call all share a loop body in their common enclosing function, the predicate fires. Combining the two branches with OR is strictly additive and cannot regress any existing true positive. CFG-based reachability was rejected for two reasons documented during investigation: BasicBlock CFG forward-reach is truncated in the extracted DBs we test against, and ControlFlowNode forward reach breaks at `if (call(...))` boundaries (the canonical IFSM pattern). AST-loop containment sidesteps both issues by relying on a densely populated AST relation that reflects the syntactic loop body. Documents the residual limitation: the upstream IRQL analysis library does not consistently bind `getPotentialExitIrqlAtCfn` at the argument expression of `KeSaveFloatingPointState` when the call is inside a loop body, so the `irqlSource != irqlSink` filter still rejects the loop case before `irqlChangesBetween` is consulted. Recovering this true positive requires improvements to the IRQL analysis library, not just to this query. `driver_utility_loop_bad` is retained in `driver_snippet.c` as a documented known false negative so any future improvement to the IRQL library will be detected via SARIF diff. Refreshes `IrqlFloatStateMismatch.qhelp`, regenerates the rendered `IrqlFloatStateMismatch.md`, refreshes the in-tree `IrqlFloatStateMismatch.sarif` (rule-help text), and bumps `@query-version` from v5 to v6. Local test diff: +0/-0 (loop FN does not fire due to upstream binding limitation; no existing TP is regressed). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4cacaf0 commit 39dd725

5 files changed

Lines changed: 134 additions & 38 deletions

File tree

src/drivers/general/queries/IrqlFloatStateMismatch/IrqlFloatStateMismatch.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ Correct example
5656
## Semmle-specific notes
5757
**Wrapper / common-caller pattern.** The query reasons about IRQL-changing calls between save and restore not only when both calls share an enclosing function, but also when they sit inside one-level helper wrappers that are called from a common caller (for example, thin `save_fp_helper` / `restore_fp_helper` functions that simply forward to `KeSaveFloatingPointState` / `KeRestoreFloatingPointState`). In those cases the intermediate IRQL transition is searched in the common caller (or in either helper's enclosing function for the asymmetric case where one side is a helper and the other is direct).
5858

59-
**Remaining limitations.** The position-based filter still does not detect:
59+
**Remaining limitations.** Despite the source-position and AST-loop branches, the predicate still does not detect:
6060

6161
* **IRQL changes performed deep inside helper bodies.** If the helper function itself raises or lowers the IRQL after the save (or before the restore), the change is not visible from the common caller's source-position view, and no intermediate IRQL change is found. Annotating the helper with `_IRQL_raises_` or `_IRQL_saves_global_` makes its IRQL behavior visible without body inspection.
6262
* **Indirect calls.** IRQL changes performed by an indirect call (function pointer or dispatch-table call) between save and restore are not detected, because the predicate only inspects the static call target.
63-
* **Loops where the restore is textually before the save.** The filter compares source line numbers; in a loop body whose first statement is the restore and last statement is the save (with the IRQL change after the save), the line range becomes empty and no intermediate IRQL change is seen.
63+
* **Loops where the restore is textually before the save.** The AST-loop branch of `irqlChangesBetween` correctly recognises that all three calls (save, restore, and an IRQL-changing call) sit inside the same loop body and would fire on this pattern. However, the upstream IRQL analysis library used to compute the IRQL at the save and restore sites does not consistently bind a value at the argument expression of `KeSaveFloatingPointState` when the call is inside a loop body, so the `irqlSource != irqlSink` filter rejects these cases before `irqlChangesBetween` is consulted. Recovering this true positive requires improvements to the IRQL analysis library and not just to this query.
6464
* **Wrapper chains longer than one level.** Only one level of wrapping is currently modelled (the helper is called directly from the common caller). Multi-level wrappers require the same annotation hint described above, or a direct call site.

src/drivers/general/queries/IrqlFloatStateMismatch/IrqlFloatStateMismatch.qhelp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@
7575
where one side is a helper and the other is direct).
7676
</p>
7777
<p>
78-
<b>Remaining limitations.</b> The position-based filter still does
79-
not detect:
78+
<b>Remaining limitations.</b> Despite the source-position and
79+
AST-loop branches, the predicate still does not detect:
8080
</p>
8181
<ul>
8282
<li>
@@ -96,11 +96,18 @@
9696
</li>
9797
<li>
9898
<b>Loops where the restore is textually before the save.</b>
99-
The filter compares source line numbers; in a loop body
100-
whose first statement is the restore and last statement is
101-
the save (with the IRQL change after the save), the line
102-
range becomes empty and no intermediate IRQL change is
103-
seen.
99+
The AST-loop branch of <code>irqlChangesBetween</code>
100+
correctly recognises that all three calls (save, restore,
101+
and an IRQL-changing call) sit inside the same loop body and
102+
would fire on this pattern. However, the upstream IRQL
103+
analysis library used to compute the IRQL at the save and
104+
restore sites does not consistently bind a value at the
105+
argument expression of <code>KeSaveFloatingPointState</code>
106+
when the call is inside a loop body, so the
107+
<code>irqlSource != irqlSink</code> filter rejects these
108+
cases before <code>irqlChangesBetween</code> is consulted.
109+
Recovering this true positive requires improvements to the
110+
IRQL analysis library and not just to this query.
104111
</li>
105112
<li>
106113
<b>Wrapper chains longer than one level.</b> Only one level

src/drivers/general/queries/IrqlFloatStateMismatch/IrqlFloatStateMismatch.ql

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* @tags correctness
1818
* ca_ported
1919
* @scope domainspecific
20-
* @query-version v5
20+
* @query-version v6
2121
*/
2222

2323
import cpp
@@ -101,30 +101,66 @@ private int anchorLineForCall(Function f, FunctionCall fc) {
101101
}
102102

103103
/**
104-
* Holds if there is an IRQL-changing call in some function `f` whose
105-
* source line lies between the save anchor and the restore anchor in
106-
* `f`. The anchor mechanism (see `anchorLineForCall`) lets `f` be:
104+
* Holds if there is an IRQL-changing call between `saveCall` and
105+
* `restoreCall` in some function `f`. This predicate is a sanity filter
106+
* applied on top of the dataflow result: dataflow has already shown the
107+
* floating-point buffer flows from `saveCall` to `restoreCall`, and this
108+
* predicate ensures there is at least one IRQL transition that could
109+
* actually run between them. Without it the query would emit the pure
110+
* may-analysis artifact where two save / restore sites are compared at
111+
* different hypothetical entry IRQLs but no IRQL transition can happen
112+
* between them at runtime.
107113
*
108-
* - the enclosing function of both `saveCall` and `restoreCall`
109-
* (the original same-function case),
110-
* - the common caller that calls thin save / restore helper
111-
* wrappers,
112-
* - the enclosing function of one of the two calls when the other
113-
* is in a one-level helper called from it (asymmetric case).
114+
* Two complementary disjuncts cooperate:
114115
*
115-
* The dataflow library has already established that the floating-
116-
* point buffer flows from `saveCall` to `restoreCall`; this predicate
117-
* is purely a sanity filter to suppress the pure may-analysis
118-
* artifact where two save / restore sites are compared at different
119-
* hypothetical entry IRQLs but no IRQL transition can actually happen
120-
* at runtime between them.
116+
* 1. **Source-position branch.** For some `f`, an IRQL-changing call
117+
* `mid` in `f` lies on a source line bracketed by the anchor lines
118+
* of `saveCall` and `restoreCall` (see `anchorLineForCall`). The
119+
* anchor mechanism lets `f` be the directly enclosing function of
120+
* both calls or a one-level wrapper / common caller, which covers
121+
* the cross-function case that intra-procedural CFG cannot reach.
121122
*
122-
* The position-based check (rather than a CFG-reachability check) is
123-
* required because the cpp control-flow graph in some extracted
124-
* databases does not transitively connect calls across statement
125-
* boundaries, which would silently eliminate true positives.
123+
* 2. **AST-loop branch.** All three calls (`saveCall`, `restoreCall`,
124+
* `mid`) sit inside the body of the same loop in their common
125+
* enclosing function. The loop back-edge means that at runtime
126+
* each iteration's `restoreCall` can be preceded by the previous
127+
* iteration's `saveCall` with `mid` between them, so an
128+
* IRQL-changing `mid` anywhere in the loop body is a real
129+
* transition between save and restore even when `restoreCall` is
130+
* textually above `saveCall`. Source-line position alone cannot
131+
* express this: when the restore is textually earlier than the
132+
* save the bracketing line range is empty and branch (1) trivially
133+
* fails.
134+
*
135+
* The two branches are disjoint in spirit: branch (1) handles
136+
* cross-function and acyclic in-function cases; branch (2) handles
137+
* intra-function loops and re-entrant patterns. Combining them is
138+
* strictly additive (can only enable more findings, never suppress one
139+
* that branch (1) would have flagged), so existing true positives are
140+
* preserved.
141+
*
142+
* Why not full intra-procedural CFG reachability instead of branch (1)?
143+
* The cpp control-flow graph in some extracted databases does not
144+
* transitively connect calls across certain statement boundaries (in
145+
* particular, forward reachability across `if (call(...))` conditions
146+
* is unreliable in our extracted DBs), which would silently eliminate
147+
* true positives that branch (1) catches via source-line bracketing.
148+
* AST-loop containment in branch (2) sidesteps this by relying on the
149+
* AST `Loop.getStmt().getAChild*()` relation, which is densely
150+
* populated and reflects the syntactic loop body directly.
151+
*
152+
* Caveat: branch (2) cooperates with `irqlSource != irqlSink` only when
153+
* the IRQL-analysis library binds `getPotentialExitIrqlAtCfn` at the
154+
* argument expression of `KeSaveFloatingPointState`. In our current
155+
* extracted DBs that binding is not always produced for `save` calls
156+
* inside loop bodies, so some real-world loop true positives may
157+
* still be filtered out by the upstream IRQL filter even when this
158+
* predicate fires; recovering those will require improvements to
159+
* the IRQL analysis library itself.
126160
*/
127161
predicate irqlChangesBetween(FunctionCall saveCall, FunctionCall restoreCall) {
162+
// Branch 1: source-line bracketing in a function `f` that anchors
163+
// both calls (directly enclosing or one-level wrapper / common caller).
128164
exists(Function f, int saveLine, int restoreLine, FunctionCall mid |
129165
saveLine = anchorLineForCall(f, saveCall) and
130166
restoreLine = anchorLineForCall(f, restoreCall) and
@@ -135,6 +171,23 @@ predicate irqlChangesBetween(FunctionCall saveCall, FunctionCall restoreCall) {
135171
mid != saveCall and
136172
mid != restoreCall
137173
)
174+
or
175+
// Branch 2: all three calls live inside the body of the same loop in
176+
// their shared enclosing function. The loop back-edge makes any
177+
// IRQL-changing call in the body a real transition between save and
178+
// restore on a subsequent iteration, even when source-line position
179+
// would put restore before save.
180+
exists(Function f, Loop l, FunctionCall mid |
181+
f = saveCall.getEnclosingFunction() and
182+
f = restoreCall.getEnclosingFunction() and
183+
f = mid.getEnclosingFunction() and
184+
isIrqlChangingCall(mid) and
185+
mid != saveCall and
186+
mid != restoreCall and
187+
l.getStmt().getAChild*() = saveCall.getEnclosingStmt() and
188+
l.getStmt().getAChild*() = restoreCall.getEnclosingStmt() and
189+
l.getStmt().getAChild*() = mid.getEnclosingStmt()
190+
)
138191
}
139192

140193
from

0 commit comments

Comments
 (0)