From c16a2e77d897bbac3d06db30bfb663d76388aee3 Mon Sep 17 00:00:00 2001 From: Arthur Baars Date: Thu, 19 Nov 2020 13:24:44 +0100 Subject: [PATCH] Model local variables --- ql/src/codeql_ruby/Variables.qll | 214 ++++++++++++++++++ .../library-tests/variables/nested_scopes.rb | 42 ++++ ql/test/library-tests/variables/scopes.rb | 13 ++ .../variables/varaccess.expected | 71 ++++++ ql/test/library-tests/variables/varaccess.ql | 13 ++ .../library-tests/variables/variable.expected | 35 +++ ql/test/library-tests/variables/variable.ql | 7 + .../variables/varscopes.expected | 17 ++ ql/test/library-tests/variables/varscopes.ql | 3 + 9 files changed, 415 insertions(+) create mode 100644 ql/src/codeql_ruby/Variables.qll create mode 100644 ql/test/library-tests/variables/nested_scopes.rb create mode 100644 ql/test/library-tests/variables/scopes.rb create mode 100644 ql/test/library-tests/variables/varaccess.expected create mode 100644 ql/test/library-tests/variables/varaccess.ql create mode 100644 ql/test/library-tests/variables/variable.expected create mode 100644 ql/test/library-tests/variables/variable.ql create mode 100644 ql/test/library-tests/variables/varscopes.expected create mode 100644 ql/test/library-tests/variables/varscopes.ql diff --git a/ql/src/codeql_ruby/Variables.qll b/ql/src/codeql_ruby/Variables.qll new file mode 100644 index 00000000000..936d6b818eb --- /dev/null +++ b/ql/src/codeql_ruby/Variables.qll @@ -0,0 +1,214 @@ +/** Provides classes for modeling program variables. */ + +private import ast +private import codeql.Locations + +private newtype TScope = + TTopLevelScope(Program node) or + TModuleScope(Module node) or + TClassScope(AstNode cls) { cls instanceof Class or cls instanceof SingletonClass } or + TMethodScope(AstNode method) { method instanceof Method or method instanceof SingletonMethod } or + TBlockScope(AstNode block) { block instanceof Block or block instanceof DoBlock } + +/** A scope in which variables can be declared. */ +class VariableScope extends TScope { + /** Gets a textual representation of this element. */ + abstract string toString(); + + /** Gets the program element this scope is associated with, if any. */ + abstract AstNode getScopeElement(); + + /** Gets the location of the program element this scope is associated with, if any. */ + final Location getLocation() { result = getScopeElement().getLocation() } + + /** + * Gets a variable that is visible in this scope. + * + * A variable is visible if it is either declared in this scope, or in some outer scope + * (only when this scope is a block scope). + */ + final Variable getAVariable() { result.getDeclaringScope() = this } + + /** + * Gets the variable with the given name that is visible in this scope. + * + * A variable is visible if it is either declared in this scope, or in some outer scope + * (only when this scope is a block scope). + */ + Variable getVariable(string name) { + result = getAVariable() and + result.getValue() = name + } +} + +private AstNode parent(AstNode n) { + result = n.getParent() and + not n = any(VariableScope s).getScopeElement() +} + +/** Gets the enclosing scope for `node`. */ +private VariableScope enclosingScope(AstNode node) { + result.getScopeElement() = parent*(node.getParent()) +} + +/** Holds if `scope` defines `var` as a parameter. */ +private predicate scopeDefinesParameter(VariableScope scope, Identifier var) { + var in [scope + .(BlockScope) + .getScopeElement() + .getAFieldOrChild() + .(BlockParameters) + .getAFieldOrChild+(), + scope + .(MethodScope) + .getScopeElement() + .getAFieldOrChild() + .(MethodParameters) + .getAFieldOrChild+()] +} + +/** Holds if `var` is assigned in `scope`. */ +private predicate scopeAssigns(VariableScope scope, Identifier var) { + var in [any(Assignment assign).getLeft(), any(OperatorAssignment assign).getLeft()] and + scope = enclosingScope(var) +} + +/** Holds if location `one` starts strictly before location `two` */ +pragma[inline] +predicate strictlyBefore(Location one, Location two) { + one.getStartLine() < two.getStartLine() + or + one.getStartLine() = two.getStartLine() and one.getStartColumn() < two.getStartColumn() +} + +cached +private VariableScope blockOuterScopes(BlockScope block) { result = block.getOuterScope+() } + +/** A variable declared in a scope. */ +class Variable extends Identifier { + VariableScope scope; + + Variable() { + scopeDefinesParameter(scope, this) + or + scopeAssigns(scope, this) and + not exists(Identifier other, VariableScope outer | other.getValue() = this.getValue() | + (outer = scope or outer = blockOuterScopes(scope)) and + ( + scopeDefinesParameter(outer, other) + or + scopeAssigns(outer, other) and + strictlyBefore(other.getLocation(), this.getLocation()) + ) + ) + } + + /** Gets the name of this variable. */ + final string getName() { result = this.getValue() } + + /** Gets the scope this variable is declared in. */ + final VariableScope getDeclaringScope() { result = scope } + + /** Gets an access to this variable. */ + VariableAccess getAnAccess() { result.getVariable() = this } +} + +/** A parameter. */ +class Parameter extends Variable { + Parameter() { scopeDefinesParameter(scope, this) } + + final override ParameterAccess getAnAccess() { result = super.getAnAccess() } +} + +/** A local variable. */ +class LocalVariable extends Variable { + LocalVariable() { not scopeDefinesParameter(scope, this) } + + final override LocalVariableAccess getAnAccess() { result = super.getAnAccess() } +} + +/** + * An identifier that refers to a variable. + * + * Examples: + * + * ``` + * function f(o) { + * var w = 0, { x : y, z } = o; // `o` is a variable access + * o = null; // `o` is a variable access + * } + * ``` + */ +class VariableAccess extends Identifier { + Variable variable; + + VariableAccess() { + exists(VariableScope scope | scope = enclosingScope(this) | + variable = scope.getVariable(this.getValue()) and + not strictlyBefore(this.getLocation(), variable.getLocation()) + ) + } + + /** + * Gets the variable this identifier refers to. + */ + Variable getVariable() { result = variable } +} + +/** An identifier that refers to a parameter. */ +class ParameterAccess extends VariableAccess { + override Parameter variable; + + final override Parameter getVariable() { result = variable } +} + +/** An identifier that refers to a local variable. */ +class LocalVariableAccess extends VariableAccess { + override LocalVariable variable; + + final override LocalVariable getVariable() { result = super.getVariable() } +} + +/** A top-level scope. */ +class TopLevelScope extends VariableScope, TTopLevelScope { + final override string toString() { result = "top-level scope" } + + final override AstNode getScopeElement() { TTopLevelScope(result) = this } +} + +/** A module scope. */ +class ModuleScope extends VariableScope, TModuleScope { + final override string toString() { result = "module scope" } + + final override Module getScopeElement() { TModuleScope(result) = this } +} + +/** A class scope. */ +class ClassScope extends VariableScope, TClassScope { + final override string toString() { result = "class scope" } + + final override AstNode getScopeElement() { TClassScope(result) = this } +} + +/** A method scope. */ +class MethodScope extends VariableScope, TMethodScope { + final override string toString() { result = "method scope" } + + final override AstNode getScopeElement() { TMethodScope(result) = this } +} + +/** A block scope. */ +class BlockScope extends VariableScope, TBlockScope { + final override string toString() { result = "block scope" } + + final override AstNode getScopeElement() { TBlockScope(result) = this } + + /** Gets the scope in which this scope is nested, if any. */ + final VariableScope getOuterScope() { result = enclosingScope(this.getScopeElement()) } + + final override Variable getVariable(string name) { + if exists(VariableScope.super.getVariable(name)) + then result = VariableScope.super.getVariable(name) + else result = this.getOuterScope().getVariable(name) + } +} diff --git a/ql/test/library-tests/variables/nested_scopes.rb b/ql/test/library-tests/variables/nested_scopes.rb new file mode 100644 index 00000000000..db0311fe7d2 --- /dev/null +++ b/ql/test/library-tests/variables/nested_scopes.rb @@ -0,0 +1,42 @@ +def a + puts "method a" +end +class C + a = 5 + module M + a = 4 + module N + a = 3 + class D + a = 2 + def show_a + a = 1 + puts a + a.times do |a| + a.times do | x; a| + a = 6 + a.times { |x| puts a } + end + end + end + def show_a2 a + puts a + end + puts a + end + def self.show + puts a # not a variable, but a call to a() + end + class << self + a = 10 + puts a + end + puts a + end + puts a + end + puts a +end +d = C::M::N::D.new +d.show_a + diff --git a/ql/test/library-tests/variables/scopes.rb b/ql/test/library-tests/variables/scopes.rb new file mode 100644 index 00000000000..9ecbad96c5c --- /dev/null +++ b/ql/test/library-tests/variables/scopes.rb @@ -0,0 +1,13 @@ +def a ; "x" end +1.times do | x | + puts a # not a local variable + a = 3 + puts a # local variable +end +a = 6 +puts a +1.times do | x | + puts a # local variable from top-level + a = 3 + puts a # local variable from top-level +end \ No newline at end of file diff --git a/ql/test/library-tests/variables/varaccess.expected b/ql/test/library-tests/variables/varaccess.expected new file mode 100644 index 00000000000..6b0d5882792 --- /dev/null +++ b/ql/test/library-tests/variables/varaccess.expected @@ -0,0 +1,71 @@ +variableAccess +| nested_scopes.rb:5:3:5:3 | a | nested_scopes.rb:4:1:39:3 | class scope | +| nested_scopes.rb:7:5:7:5 | a | nested_scopes.rb:6:3:37:5 | module scope | +| nested_scopes.rb:9:7:9:7 | a | nested_scopes.rb:8:5:35:7 | module scope | +| nested_scopes.rb:11:9:11:9 | a | nested_scopes.rb:10:7:26:9 | class scope | +| nested_scopes.rb:13:11:13:11 | a | nested_scopes.rb:12:9:21:11 | method scope | +| nested_scopes.rb:14:16:14:16 | a | nested_scopes.rb:12:9:21:11 | method scope | +| nested_scopes.rb:15:11:15:11 | a | nested_scopes.rb:12:9:21:11 | method scope | +| nested_scopes.rb:15:23:15:23 | a | nested_scopes.rb:15:19:20:13 | block scope | +| nested_scopes.rb:16:13:16:13 | a | nested_scopes.rb:15:19:20:13 | block scope | +| nested_scopes.rb:16:26:16:26 | x | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:16:29:16:29 | a | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:17:15:17:15 | a | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:18:15:18:15 | a | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:18:26:18:26 | x | nested_scopes.rb:18:23:18:36 | block scope | +| nested_scopes.rb:18:34:18:34 | a | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:22:21:22:21 | a | nested_scopes.rb:22:9:24:11 | method scope | +| nested_scopes.rb:23:16:23:16 | a | nested_scopes.rb:22:9:24:11 | method scope | +| nested_scopes.rb:25:14:25:14 | a | nested_scopes.rb:10:7:26:9 | class scope | +| nested_scopes.rb:31:11:31:11 | a | nested_scopes.rb:30:7:33:9 | class scope | +| nested_scopes.rb:32:16:32:16 | a | nested_scopes.rb:30:7:33:9 | class scope | +| nested_scopes.rb:34:12:34:12 | a | nested_scopes.rb:8:5:35:7 | module scope | +| nested_scopes.rb:36:10:36:10 | a | nested_scopes.rb:6:3:37:5 | module scope | +| nested_scopes.rb:38:8:38:8 | a | nested_scopes.rb:4:1:39:3 | class scope | +| nested_scopes.rb:40:1:40:1 | d | nested_scopes.rb:1:1:42:1 | top-level scope | +| nested_scopes.rb:41:1:41:1 | d | nested_scopes.rb:1:1:42:1 | top-level scope | +| scopes.rb:2:14:2:14 | x | scopes.rb:2:9:6:3 | block scope | +| scopes.rb:4:4:4:4 | a | scopes.rb:2:9:6:3 | block scope | +| scopes.rb:5:9:5:9 | a | scopes.rb:2:9:6:3 | block scope | +| scopes.rb:7:1:7:1 | a | scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:8:6:8:6 | a | scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:9:14:9:14 | x | scopes.rb:9:9:13:3 | block scope | +| scopes.rb:10:9:10:9 | a | scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:11:4:11:4 | a | scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:12:9:12:9 | a | scopes.rb:1:1:13:3 | top-level scope | +parameterAccess +| nested_scopes.rb:15:23:15:23 | a | nested_scopes.rb:15:19:20:13 | block scope | +| nested_scopes.rb:16:13:16:13 | a | nested_scopes.rb:15:19:20:13 | block scope | +| nested_scopes.rb:16:26:16:26 | x | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:16:29:16:29 | a | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:17:15:17:15 | a | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:18:15:18:15 | a | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:18:26:18:26 | x | nested_scopes.rb:18:23:18:36 | block scope | +| nested_scopes.rb:18:34:18:34 | a | nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:22:21:22:21 | a | nested_scopes.rb:22:9:24:11 | method scope | +| nested_scopes.rb:23:16:23:16 | a | nested_scopes.rb:22:9:24:11 | method scope | +| scopes.rb:2:14:2:14 | x | scopes.rb:2:9:6:3 | block scope | +| scopes.rb:9:14:9:14 | x | scopes.rb:9:9:13:3 | block scope | +localVariableAccess +| nested_scopes.rb:5:3:5:3 | a | nested_scopes.rb:4:1:39:3 | class scope | +| nested_scopes.rb:7:5:7:5 | a | nested_scopes.rb:6:3:37:5 | module scope | +| nested_scopes.rb:9:7:9:7 | a | nested_scopes.rb:8:5:35:7 | module scope | +| nested_scopes.rb:11:9:11:9 | a | nested_scopes.rb:10:7:26:9 | class scope | +| nested_scopes.rb:13:11:13:11 | a | nested_scopes.rb:12:9:21:11 | method scope | +| nested_scopes.rb:14:16:14:16 | a | nested_scopes.rb:12:9:21:11 | method scope | +| nested_scopes.rb:15:11:15:11 | a | nested_scopes.rb:12:9:21:11 | method scope | +| nested_scopes.rb:25:14:25:14 | a | nested_scopes.rb:10:7:26:9 | class scope | +| nested_scopes.rb:31:11:31:11 | a | nested_scopes.rb:30:7:33:9 | class scope | +| nested_scopes.rb:32:16:32:16 | a | nested_scopes.rb:30:7:33:9 | class scope | +| nested_scopes.rb:34:12:34:12 | a | nested_scopes.rb:8:5:35:7 | module scope | +| nested_scopes.rb:36:10:36:10 | a | nested_scopes.rb:6:3:37:5 | module scope | +| nested_scopes.rb:38:8:38:8 | a | nested_scopes.rb:4:1:39:3 | class scope | +| nested_scopes.rb:40:1:40:1 | d | nested_scopes.rb:1:1:42:1 | top-level scope | +| nested_scopes.rb:41:1:41:1 | d | nested_scopes.rb:1:1:42:1 | top-level scope | +| scopes.rb:4:4:4:4 | a | scopes.rb:2:9:6:3 | block scope | +| scopes.rb:5:9:5:9 | a | scopes.rb:2:9:6:3 | block scope | +| scopes.rb:7:1:7:1 | a | scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:8:6:8:6 | a | scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:10:9:10:9 | a | scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:11:4:11:4 | a | scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:12:9:12:9 | a | scopes.rb:1:1:13:3 | top-level scope | diff --git a/ql/test/library-tests/variables/varaccess.ql b/ql/test/library-tests/variables/varaccess.ql new file mode 100644 index 00000000000..5ab0e216f47 --- /dev/null +++ b/ql/test/library-tests/variables/varaccess.ql @@ -0,0 +1,13 @@ +import codeql_ruby.Variables + +query predicate variableAccess(VariableAccess var, VariableScope scope) { + scope = var.getVariable().getDeclaringScope() +} + +query predicate parameterAccess(ParameterAccess var, VariableScope scope) { + scope = var.getVariable().getDeclaringScope() +} + +query predicate localVariableAccess(LocalVariableAccess var, VariableScope scope) { + scope = var.getVariable().getDeclaringScope() +} diff --git a/ql/test/library-tests/variables/variable.expected b/ql/test/library-tests/variables/variable.expected new file mode 100644 index 00000000000..6d2277e0b14 --- /dev/null +++ b/ql/test/library-tests/variables/variable.expected @@ -0,0 +1,35 @@ +variable +| nested_scopes.rb:5:3:5:3 | a | +| nested_scopes.rb:7:5:7:5 | a | +| nested_scopes.rb:9:7:9:7 | a | +| nested_scopes.rb:11:9:11:9 | a | +| nested_scopes.rb:13:11:13:11 | a | +| nested_scopes.rb:15:23:15:23 | a | +| nested_scopes.rb:16:26:16:26 | x | +| nested_scopes.rb:16:29:16:29 | a | +| nested_scopes.rb:18:26:18:26 | x | +| nested_scopes.rb:22:21:22:21 | a | +| nested_scopes.rb:31:11:31:11 | a | +| nested_scopes.rb:40:1:40:1 | d | +| scopes.rb:2:14:2:14 | x | +| scopes.rb:4:4:4:4 | a | +| scopes.rb:7:1:7:1 | a | +| scopes.rb:9:14:9:14 | x | +parameter +| nested_scopes.rb:15:23:15:23 | a | +| nested_scopes.rb:16:26:16:26 | x | +| nested_scopes.rb:16:29:16:29 | a | +| nested_scopes.rb:18:26:18:26 | x | +| nested_scopes.rb:22:21:22:21 | a | +| scopes.rb:2:14:2:14 | x | +| scopes.rb:9:14:9:14 | x | +localVariable +| nested_scopes.rb:5:3:5:3 | a | +| nested_scopes.rb:7:5:7:5 | a | +| nested_scopes.rb:9:7:9:7 | a | +| nested_scopes.rb:11:9:11:9 | a | +| nested_scopes.rb:13:11:13:11 | a | +| nested_scopes.rb:31:11:31:11 | a | +| nested_scopes.rb:40:1:40:1 | d | +| scopes.rb:4:4:4:4 | a | +| scopes.rb:7:1:7:1 | a | diff --git a/ql/test/library-tests/variables/variable.ql b/ql/test/library-tests/variables/variable.ql new file mode 100644 index 00000000000..54300c416b4 --- /dev/null +++ b/ql/test/library-tests/variables/variable.ql @@ -0,0 +1,7 @@ +import codeql_ruby.Variables + +query predicate variable(Variable v) { any() } + +query predicate parameter(Parameter p) { any() } + +query predicate localVariable(LocalVariable v) { any() } diff --git a/ql/test/library-tests/variables/varscopes.expected b/ql/test/library-tests/variables/varscopes.expected new file mode 100644 index 00000000000..88a58f78a71 --- /dev/null +++ b/ql/test/library-tests/variables/varscopes.expected @@ -0,0 +1,17 @@ +| nested_scopes.rb:1:1:3:3 | method scope | +| nested_scopes.rb:1:1:42:1 | top-level scope | +| nested_scopes.rb:4:1:39:3 | class scope | +| nested_scopes.rb:6:3:37:5 | module scope | +| nested_scopes.rb:8:5:35:7 | module scope | +| nested_scopes.rb:10:7:26:9 | class scope | +| nested_scopes.rb:12:9:21:11 | method scope | +| nested_scopes.rb:15:19:20:13 | block scope | +| nested_scopes.rb:16:21:19:15 | block scope | +| nested_scopes.rb:18:23:18:36 | block scope | +| nested_scopes.rb:22:9:24:11 | method scope | +| nested_scopes.rb:27:7:29:9 | method scope | +| nested_scopes.rb:30:7:33:9 | class scope | +| scopes.rb:1:1:1:15 | method scope | +| scopes.rb:1:1:13:3 | top-level scope | +| scopes.rb:2:9:6:3 | block scope | +| scopes.rb:9:9:13:3 | block scope | diff --git a/ql/test/library-tests/variables/varscopes.ql b/ql/test/library-tests/variables/varscopes.ql new file mode 100644 index 00000000000..fcc13759b3c --- /dev/null +++ b/ql/test/library-tests/variables/varscopes.ql @@ -0,0 +1,3 @@ +import codeql_ruby.Variables + +select any(VariableScope x)