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.
10 changes: 9 additions & 1 deletion rust/ql/lib/codeql/files/FileSystem.qll
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,22 @@ class Folder = Impl::Folder;

module Folder = Impl::Folder;

/**
* Holds if the file identified by `relativePath` should be treated as though it is external
* to the target project, even though it is within the source code directory. This is used for
* testing.
*/
extensible predicate additionalExternalFile(string relativePath);

/** A file. */
class File extends Container, Impl::File {
/**
* Holds if this file was extracted from the source code of the target project
* (rather than another location such as inside a dependency).
*/
predicate fromSource() {
exists(ExtractorStep s | s.getAction() = "Extract" and s.getFile() = this)
exists(ExtractorStep s | s.getAction() = "Extract" and s.getFile() = this) and
not additionalExternalFile(this.getRelativePath())
}

/**
Expand Down
11 changes: 11 additions & 0 deletions rust/ql/lib/codeql/rust/dataflow/internal/FlowSummaryImpl.qll
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ private import codeql.rust.dataflow.internal.DataFlowImpl
private import codeql.rust.internal.PathResolution
private import codeql.rust.dataflow.FlowSummary
private import codeql.rust.dataflow.Ssa
private import codeql.rust.dataflow.internal.ModelsAsData
private import Content

predicate encodeContentTupleField(TupleFieldContent c, string arg) {
Expand Down Expand Up @@ -46,6 +47,16 @@ module Input implements InputSig<Location, RustDataFlow> {

abstract class SinkBase extends SourceSinkBase { }

predicate neutralElement(
Input::SummarizedCallableBase c, string kind, string provenance, boolean isExact
) {
exists(string path |
neutralModel(path, kind, provenance, _) and
c.getCanonicalPath() = path and
isExact = true
)
}

private class CallExprFunction extends SourceBase, SinkBase {
private CallExpr call;

Expand Down
30 changes: 29 additions & 1 deletion 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 class SummarizedCallableFromModel extends SummarizedCallable::Range {
Expand All @@ -124,7 +138,9 @@ private class SummarizedCallableFromModel extends SummarizedCallable::Range {
summaryModel(path, input_, output_, kind, p, madId) and
f.getCanonicalPath() = path
|
this = f and isExact_ = true and p_ = p
this = f and
isExact_ = true and
p_ = p
or
this.implements(f) and
isExact_ = false and
Expand Down Expand Up @@ -158,6 +174,12 @@ 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
// (the shared code only applies neutral models to summaries at present)
not (
provenance.isGenerated() and
neutralModel(path, "source", _, _)
)
}
}
Expand All @@ -174,6 +196,12 @@ 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
// (the shared code only applies neutral models to summaries at present)
not (
provenance.isGenerated() and
neutralModel(path, "sink", _, _)
)
}
}
Expand Down
27 changes: 27 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/empty.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
extensions:
# Make sure that the extensible model predicates have at least one definition
# to avoid errors about undefined extensionals.
- addsTo:
pack: codeql/rust-all
extensible: sourceModel
data: []
- addsTo:
pack: codeql/rust-all
extensible: sinkModel
data: []
- addsTo:
pack: codeql/rust-all
extensible: summaryModel
data: []
- addsTo:
pack: codeql/rust-all
extensible: excludeFieldTaintStep
data: []
- addsTo:
pack: codeql/rust-all
extensible: neutralModel
data: []
- addsTo:
pack: codeql/rust-all
extensible: additionalExternalFile
data: []
30 changes: 30 additions & 0 deletions rust/ql/test/library-tests/dataflow/models/external_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

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) {}

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

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

pub fn neutral_manual_summary(i: i64) -> i64 {
0
}
21 changes: 21 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,26 @@ fn test_trait_model<T: Ord>(x: T) {
sink(x7);
}

mod external_file;
use external_file::*;

fn test_neutrals() {
// neutral models should cause corresponding generated models to be ignored.
// Thus the `neutral_generated_source`, `neutral_generated_sink` and
// `neutral_generated_summary`, 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
sink(generated_summary(source(7))); // $ hasValueFlow=7
sink(neutral_generated_summary(source(8)));
sink(neutral_manual_summary(source(9))); // $ hasValueFlow=9
}

#[tokio::main]
async fn main() {
test_identify();
Expand All @@ -431,5 +451,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