Skip to content
Merged
4 changes: 4 additions & 0 deletions rust/ql/lib/change-notes/2026-02-05-neutral-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added support for neutral models (`extensible: neutralModel`) to control where generated source, sink and flow summary models apply.
29 changes: 27 additions & 2 deletions rust/ql/lib/codeql/rust/dataflow/internal/ModelsAsData.qll
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ extensible predicate summaryModel(
QlBuiltins::ExtensionId madId
);

/**
* Holds if a neutral model exists for the function with canonical path `path`. The only
* effect of a neutral model is to prevent generated and inherited models of the corresponding
* `kind` (`source`, `sink` or `summary`) from being applied to that function.
*/
extensible predicate neutralModel(
string path, string kind, string provenance, QlBuiltins::ExtensionId madId
);

/**
* Holds if the given extension tuple `madId` should pretty-print as `model`.
*
Expand All @@ -109,6 +118,11 @@ predicate interpretModelForTest(QlBuiltins::ExtensionId madId, string model) {
summaryModel(path, input, output, kind, _, madId) and
model = "Summary: " + path + "; " + input + "; " + output + "; " + kind
)
or
exists(string path, string kind |
neutralModel(path, kind, _, madId) and
model = "Neutral: " + path + "; " + kind
)
}

private predicate summaryModel(
Expand All @@ -133,15 +147,16 @@ private predicate summaryModelRelevant(
) {
summaryModel(f, input, output, kind, provenance, isInherited, madId) and
// Only apply generated or inherited models to functions in library code and
// when no strictly better model exists
// when no strictly better model (or neutral model) exists
if provenance.isGenerated() or isInherited = true
then
not f.fromSource() and
not exists(Provenance other | summaryModel(f, _, _, _, other, false, _) |
provenance.isGenerated() and other.isManual()
or
provenance = other and isInherited = true
)
) and
not neutralModel(f.getCanonicalPath(), "summary", _, _)
else any()
}

Expand Down Expand Up @@ -180,6 +195,11 @@ private class FlowSourceFromModel extends FlowSource::Range {
exists(QlBuiltins::ExtensionId madId |
sourceModel(path, output, kind, provenance, madId) and
model = "MaD:" + madId.toString()
) and
// Only apply generated models when no neutral model exists
not (
provenance.isGenerated() and
neutralModel(path, "source", _, _)
)
}
}
Expand All @@ -196,6 +216,11 @@ private class FlowSinkFromModel extends FlowSink::Range {
exists(QlBuiltins::ExtensionId madId |
sinkModel(path, input, kind, provenance, madId) and
model = "MaD:" + madId.toString()
) and
// Only apply generated models when no neutral model exists
not (
provenance.isGenerated() and
neutralModel(path, "sink", _, _)
)
}
}
Expand Down
32 changes: 32 additions & 0 deletions rust/ql/test/library-tests/dataflow/models/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,37 @@ fn test_trait_model<T: Ord>(x: T) {
sink(x7);
}

pub fn generated_source(i: i64) -> i64 {
0
}

pub fn neutral_generated_source(i: i64) -> i64 {
0
}

pub fn neutral_manual_source(i: i64) -> i64 {
0
}

pub fn generated_sink(i: i64) {}

pub fn neutral_generated_sink(i: i64) {}

pub fn neutral_manual_sink(i: i64) {}

fn test_neutrals() {
// neutral models should cause corresponding generated models to be ignored.
// Thus, the `neutral_generated_source` and `neutral_generated_sink`, which
// have both a generated and a neutral model, should not have flow.

sink(generated_source(1)); // $ hasValueFlow=1
sink(neutral_generated_source(2));
sink(neutral_manual_source(3)); // $ hasValueFlow=3
generated_sink(source(4)); // $ hasValueFlow=4
neutral_generated_sink(source(5));
neutral_manual_sink(source(6)); // $ hasValueFlow=6
}

#[tokio::main]
async fn main() {
test_identify();
Expand All @@ -431,5 +462,6 @@ async fn main() {
test_simple_sink();
test_get_async_number().await;
test_arg_source();
test_neutrals();
let dummy = Some(0); // ensure that the the `lang:core` crate is extracted
}
Loading
Loading