mirror of
https://github.com/github/codeql.git
synced 2026-07-02 18:15:33 +02:00
Preparatory refactor for the shared-CFG dataflow migration. Deprecates the AstNode.getAFlowNode() cached predicate on the public Python QL API and rewrites all ~140 internal callers across lib/, src/, test/, and tools/ from `expr.getAFlowNode() = cfgNode` to `cfgNode.getNode() = expr`, using ControlFlowNode.getNode() which already exists in Flow.qll. The predicate itself is preserved (with a deprecation note pointing at the new pattern) so external users do not experience churn — they can migrate at their own pace and the AST/CFG hierarchies still get the intended untangling once the deprecation eventually elapses. Semantic noop verified by: - All 361 lib/ + src/ queries compile clean. - All 122 ControlFlow + PointsTo library-tests pass. - All 64 dataflow library-tests pass. - All 113 Variables/Exceptions/Expressions/Statements/Functions/Imports/ Security/CWE-798/ModificationOfParameterWithDefault query-tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
150 lines
5.5 KiB
Plaintext
150 lines
5.5 KiB
Plaintext
/** INTERNAL - Helper predicates for queries that inspect the comparison of objects using `is`. */
|
|
|
|
import python
|
|
private import LegacyPointsTo
|
|
|
|
/** Holds if the comparison `comp` uses `is` or `is not` (represented as `op`) to compare its `left` and `right` arguments. */
|
|
predicate comparison_using_is(Compare comp, ControlFlowNode left, Cmpop op, ControlFlowNode right) {
|
|
exists(CompareNode fcomp | fcomp.getNode() = comp |
|
|
fcomp.operands(left, op, right) and
|
|
(op instanceof Is or op instanceof IsNot)
|
|
)
|
|
}
|
|
|
|
/** Holds if the class `c` overrides the default notion of equality or comparison. */
|
|
predicate overrides_eq_or_cmp(ClassValue c) {
|
|
major_version() = 2 and c.hasAttribute("__eq__")
|
|
or
|
|
c.declaresAttribute("__eq__") and not c = Value::named("object")
|
|
or
|
|
exists(ClassValue sup | sup = c.getASuperType() and not sup = Value::named("object") |
|
|
sup.declaresAttribute("__eq__")
|
|
)
|
|
or
|
|
major_version() = 2 and c.hasAttribute("__cmp__")
|
|
}
|
|
|
|
/** Holds if the class `cls` is likely to only have a single instance throughout the program. */
|
|
predicate probablySingleton(ClassValue cls) {
|
|
strictcount(Value inst | inst.getClass() = cls) = 1
|
|
or
|
|
cls = Value::named("None").getClass()
|
|
}
|
|
|
|
/** Holds if using `is` to compare instances of the class `c` is likely to cause unexpected behavior. */
|
|
predicate invalid_to_use_is_portably(ClassValue c) {
|
|
overrides_eq_or_cmp(c) and
|
|
// Exclude type/builtin-function/bool as it is legitimate to compare them using 'is' but they implement __eq__
|
|
not c = Value::named("type") and
|
|
not c = ClassValue::builtinFunction() and
|
|
not c = Value::named("bool") and
|
|
// OK to compare with 'is' if a singleton
|
|
not probablySingleton(c)
|
|
}
|
|
|
|
/** Holds if the control flow node `f` points to either `True`, `False`, or `None`. */
|
|
predicate simple_constant(ControlFlowNodeWithPointsTo f) {
|
|
exists(Value val | f.pointsTo(val) |
|
|
val = Value::named("True") or val = Value::named("False") or val = Value::named("None")
|
|
)
|
|
}
|
|
|
|
private predicate cpython_interned_value(Expr e) {
|
|
exists(string text | text = e.(StringLiteral).getText() |
|
|
text.length() = 0
|
|
or
|
|
text.length() = 1 and text.regexpMatch("[U+0000-U+00ff]")
|
|
)
|
|
or
|
|
exists(int i | i = e.(IntegerLiteral).getN().toInt() | -5 <= i and i <= 256)
|
|
or
|
|
exists(Tuple t | t = e and not exists(t.getAnElt()))
|
|
}
|
|
|
|
/**
|
|
* The set of values that can be expected to be interned across
|
|
* the main implementations of Python. PyPy, Jython, etc tend to
|
|
* follow CPython, but it varies, so this is a best guess.
|
|
*/
|
|
private predicate universally_interned_value(Expr e) {
|
|
e.(IntegerLiteral).getN().toInt() = 0
|
|
or
|
|
exists(Tuple t | t = e and not exists(t.getAnElt()))
|
|
or
|
|
e.(StringLiteral).getText() = ""
|
|
}
|
|
|
|
/** Holds if the expression `e` points to an interned constant in CPython. */
|
|
predicate cpython_interned_constant(ExprWithPointsTo e) {
|
|
exists(Expr const | e.pointsTo(_, const) | cpython_interned_value(const))
|
|
}
|
|
|
|
/** Holds if the expression `e` points to a value that can be reasonably expected to be interned across all implementations of Python. */
|
|
predicate universally_interned_constant(ExprWithPointsTo e) {
|
|
exists(Expr const | e.pointsTo(_, const) | universally_interned_value(const))
|
|
}
|
|
|
|
private predicate comparison_both_types(Compare comp, Cmpop op, ClassValue cls1, ClassValue cls2) {
|
|
exists(ControlFlowNodeWithPointsTo op1, ControlFlowNodeWithPointsTo op2 |
|
|
comparison_using_is(comp, op1, op, op2) or comparison_using_is(comp, op2, op, op1)
|
|
|
|
|
op1.inferredValue().getClass() = cls1 and
|
|
op2.inferredValue().getClass() = cls2
|
|
)
|
|
}
|
|
|
|
private predicate comparison_one_type(Compare comp, Cmpop op, ClassValue cls) {
|
|
not comparison_both_types(comp, _, _, _) and
|
|
exists(ControlFlowNodeWithPointsTo operand |
|
|
comparison_using_is(comp, operand, op, _) or comparison_using_is(comp, _, op, operand)
|
|
|
|
|
operand.inferredValue().getClass() = cls
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Holds if using `is` or `is not` as the operator `op` in the comparison `comp` would be invalid when applied to the class `cls`.
|
|
*/
|
|
predicate invalid_portable_is_comparison(Compare comp, Cmpop op, ClassValue cls) {
|
|
// OK to use 'is' when defining '__eq__'
|
|
not exists(Function eq | eq.getName() = "__eq__" or eq.getName() = "__ne__" |
|
|
eq = comp.getScope().getScope*()
|
|
) and
|
|
(
|
|
comparison_one_type(comp, op, cls) and invalid_to_use_is_portably(cls)
|
|
or
|
|
exists(ClassValue other | comparison_both_types(comp, op, cls, other) |
|
|
invalid_to_use_is_portably(cls) and
|
|
invalid_to_use_is_portably(other)
|
|
)
|
|
) and
|
|
// OK to use 'is' when comparing items from a known set of objects
|
|
not exists(ExprWithPointsTo left, ExprWithPointsTo right, Value val |
|
|
comp.compares(left, op, right) and
|
|
exists(ImmutableLiteral il | il = val.(ConstantObjectInternal).getLiteral())
|
|
|
|
|
left.pointsTo(val) and right.pointsTo(val)
|
|
or
|
|
// Simple constant in module, probably some sort of sentinel
|
|
exists(AstNode origin |
|
|
not left.pointsTo(_) and
|
|
right.pointsTo(val, origin) and
|
|
origin.getScope().getEnclosingModule() = comp.getScope().getEnclosingModule()
|
|
)
|
|
) and
|
|
// OK to use 'is' when comparing with a member of an enum
|
|
not exists(ExprWithPointsTo left, ExprWithPointsTo right, AstNode origin |
|
|
comp.compares(left, op, right) and
|
|
enum_member(origin)
|
|
|
|
|
left.pointsTo(_, origin) or right.pointsTo(_, origin)
|
|
)
|
|
}
|
|
|
|
private predicate enum_member(AstNode obj) {
|
|
exists(ClassValue cls, AssignStmt asgn | cls.getASuperType().getName() = "Enum" |
|
|
cls.getScope() = asgn.getScope() and
|
|
asgn.getValue() = obj
|
|
)
|
|
}
|