Skip to content

Commit c1515db

Browse files
committed
Ruby: modeling of some file-related concepts for the Pathname class
1 parent 03d0f66 commit c1515db

File tree

4 files changed

+312
-10
lines changed

4 files changed

+312
-10
lines changed

ruby/ql/lib/codeql/ruby/frameworks/core/Pathname.qll

Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,118 @@
22

33
private import codeql.ruby.AST
44
private import codeql.ruby.ApiGraphs
5+
private import codeql.ruby.Concepts
56
private import codeql.ruby.DataFlow
67
private import codeql.ruby.dataflow.FlowSummary
78
private import codeql.ruby.dataflow.internal.DataFlowDispatch
8-
private import codeql.ruby.controlflow.CfgNodes
99

1010
/**
1111
* Modeling of the `Pathname` class from the Ruby standard library.
1212
*
1313
* https://docs.ruby-lang.org/en/3.1/Pathname.html
1414
*/
1515
module Pathname {
16-
/// Flow summary for `Pathname.new`.
16+
/**
17+
* An instance of the `Pathname` class. For example, in
18+
*
19+
* ```rb
20+
* pn = Pathname.new "foo.txt'"
21+
* puts pn.read
22+
* ```
23+
*
24+
* there are three `PathnameInstance`s – the call to `Pathname.new`, the
25+
* assignment `pn = ...`, and the read access to `pn` on the second line.
26+
*
27+
* Every `PathnameInstance` is considered to be a `FileNameSource`.
28+
*/
29+
class PathnameInstance extends FileNameSource, DataFlow::Node {
30+
PathnameInstance() { this = pathnameInstance() }
31+
}
32+
33+
private DataFlow::Node pathnameInstance() {
34+
// A call to `Pathname.new`.
35+
result = API::getTopLevelMember("Pathname").getAnInstantiation()
36+
or
37+
// Class methods on `Pathname` that return a new `Pathname`.
38+
result = API::getTopLevelMember("Pathname").getAMethodCall(["getwd", "pwd",])
39+
or
40+
// Instance methods on `Pathname` that return a new `Pathname`.
41+
exists(DataFlow::CallNode c | result = c |
42+
c.getReceiver() = pathnameInstance() and
43+
c.getMethodName() =
44+
[
45+
"+", "/", "basename", "cleanpath", "expand_path", "join", "realpath",
46+
"relative_path_from", "sub", "sub_ext", "to_path"
47+
]
48+
)
49+
or
50+
exists(DataFlow::Node inst |
51+
inst = pathnameInstance() and
52+
inst.(DataFlow::LocalSourceNode).flowsTo(result)
53+
)
54+
}
55+
56+
/** A call where the receiver is a `Pathname`. */
57+
class PathnameCall extends DataFlow::CallNode {
58+
PathnameCall() { this.getReceiver() instanceof PathnameInstance }
59+
}
60+
61+
/**
62+
* A call to `Pathname#open` or `Pathname#opendir`, considered as a
63+
* `FileSystemAccess`.
64+
*/
65+
class PathnameOpen extends FileSystemAccess::Range, PathnameCall {
66+
PathnameOpen() { this.getMethodName() = ["open", "opendir"] }
67+
68+
override DataFlow::Node getAPathArgument() { result = this.getReceiver() }
69+
}
70+
71+
/** A call to `Pathname#read`, considered as a `FileSystemReadAccess`. */
72+
class PathnameRead extends FileSystemReadAccess::Range, PathnameCall {
73+
PathnameRead() { this.getMethodName() = "read" }
74+
75+
// The path is the receiver (the `Pathname` object).
76+
override DataFlow::Node getAPathArgument() { result = this.getReceiver() }
77+
78+
// The read data is the return value of the call.
79+
override DataFlow::Node getADataNode() { result = this }
80+
}
81+
82+
/** A call to `Pathname#write`, considered as a `FileSystemWriteAccess`. */
83+
class PathnameWrite extends FileSystemWriteAccess::Range, PathnameCall {
84+
PathnameWrite() { this.getMethodName() = "write" }
85+
86+
// The path is the receiver (the `Pathname` object).
87+
override DataFlow::Node getAPathArgument() { result = this.getReceiver() }
88+
89+
// The data to write is the 0th argument.
90+
override DataFlow::Node getADataNode() { result = this.getArgument(0) }
91+
}
92+
93+
/** A call to `Pathname#to_s`, considered as a `FileNameSource`. */
94+
class PathnameToSFilenameSource extends FileNameSource, PathnameCall {
95+
PathnameToSFilenameSource() { this.getMethodName() = "to_s" }
96+
}
97+
98+
private class PathnamePermissionModification extends FileSystemPermissionModification::Range,
99+
PathnameCall {
100+
private DataFlow::Node permissionArg;
101+
102+
PathnamePermissionModification() {
103+
exists(string methodName | this.getMethodName() = methodName |
104+
methodName = ["chmod", "mkdir"] and permissionArg = this.getArgument(0)
105+
or
106+
methodName = "mkpath" and permissionArg = this.getKeywordArgument("mode")
107+
or
108+
methodName = "open" and permissionArg = this.getArgument(1)
109+
// TODO: defaults for optional args? This may depend on the umask
110+
)
111+
}
112+
113+
override DataFlow::Node getAPermissionNode() { result = permissionArg }
114+
}
115+
116+
/** Flow summary for `Pathname.new`. */
17117
private class NewSummary extends SummarizedCallable {
18118
NewSummary() { this = "Pathname.new" }
19119

@@ -28,7 +128,7 @@ module Pathname {
28128
}
29129
}
30130

31-
/// Flow summary for `Pathname#dirname`.
131+
/** Flow summary for `Pathname#dirname`. */
32132
private class DirnameSummary extends SimpleSummarizedCallable {
33133
DirnameSummary() { this = "dirname" }
34134

@@ -39,7 +139,7 @@ module Pathname {
39139
}
40140
}
41141

42-
/// Flow summary for `Pathname#each_filename`.
142+
/** Flow summary for `Pathname#each_filename`. */
43143
private class EachFilenameSummary extends SimpleSummarizedCallable {
44144
EachFilenameSummary() { this = "each_filename" }
45145

@@ -50,7 +150,7 @@ module Pathname {
50150
}
51151
}
52152

53-
/// Flow summary for `Pathname#expand_path`.
153+
/** Flow summary for `Pathname#expand_path`. */
54154
private class ExpandPathSummary extends SimpleSummarizedCallable {
55155
ExpandPathSummary() { this = "expand_path" }
56156

@@ -61,7 +161,7 @@ module Pathname {
61161
}
62162
}
63163

64-
/// Flow summary for `Pathname#join`.
164+
/** Flow summary for `Pathname#join`. */
65165
private class JoinSummary extends SimpleSummarizedCallable {
66166
JoinSummary() { this = "join" }
67167

@@ -72,7 +172,7 @@ module Pathname {
72172
}
73173
}
74174

75-
/// Flow summary for `Pathname#parent`.
175+
/** Flow summary for `Pathname#parent`. */
76176
private class ParentSummary extends SimpleSummarizedCallable {
77177
ParentSummary() { this = "parent" }
78178

@@ -83,7 +183,7 @@ module Pathname {
83183
}
84184
}
85185

86-
/// Flow summary for `Pathname#realpath`.
186+
/** Flow summary for `Pathname#realpath`. */
87187
private class RealpathSummary extends SimpleSummarizedCallable {
88188
RealpathSummary() { this = "realpath" }
89189

@@ -94,7 +194,7 @@ module Pathname {
94194
}
95195
}
96196

97-
/// Flow summary for `Pathname#relative_path_from`.
197+
/** Flow summary for `Pathname#relative_path_from`. */
98198
private class RelativePathFromSummary extends SimpleSummarizedCallable {
99199
RelativePathFromSummary() { this = "relative_path_from" }
100200

@@ -105,7 +205,7 @@ module Pathname {
105205
}
106206
}
107207

108-
/// Flow summary for `Pathname#to_path`.
208+
/** Flow summary for `Pathname#to_path`. */
109209
private class ToPathSummary extends SimpleSummarizedCallable {
110210
ToPathSummary() { this = "to_path" }
111211

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
pathnameInstances
2+
| Pathname.rb:2:1:2:33 | ... = ... |
3+
| Pathname.rb:2:1:2:33 | ... = ... |
4+
| Pathname.rb:2:12:2:33 | call to new |
5+
| Pathname.rb:3:1:3:20 | ... = ... |
6+
| Pathname.rb:3:13:3:20 | foo_path |
7+
| Pathname.rb:4:1:4:8 | foo_path |
8+
| Pathname.rb:6:1:6:29 | ... = ... |
9+
| Pathname.rb:6:1:6:29 | ... = ... |
10+
| Pathname.rb:6:12:6:29 | call to new |
11+
| Pathname.rb:9:1:9:21 | ... = ... |
12+
| Pathname.rb:9:1:9:21 | ... = ... |
13+
| Pathname.rb:9:8:9:21 | call to getwd |
14+
| Pathname.rb:10:1:10:21 | ... = ... |
15+
| Pathname.rb:10:7:10:10 | pwd1 |
16+
| Pathname.rb:10:7:10:21 | ... + ... |
17+
| Pathname.rb:10:14:10:21 | foo_path |
18+
| Pathname.rb:11:1:11:21 | ... = ... |
19+
| Pathname.rb:11:1:11:21 | ... = ... |
20+
| Pathname.rb:11:7:11:10 | pwd1 |
21+
| Pathname.rb:11:7:11:21 | ... / ... |
22+
| Pathname.rb:11:14:11:21 | bar_path |
23+
| Pathname.rb:12:1:12:19 | ... = ... |
24+
| Pathname.rb:12:7:12:10 | pwd1 |
25+
| Pathname.rb:12:7:12:19 | call to basename |
26+
| Pathname.rb:13:1:13:46 | ... = ... |
27+
| Pathname.rb:13:7:13:36 | call to new |
28+
| Pathname.rb:13:7:13:46 | call to cleanpath |
29+
| Pathname.rb:14:1:14:26 | ... = ... |
30+
| Pathname.rb:14:7:14:14 | foo_path |
31+
| Pathname.rb:14:7:14:26 | call to expand_path |
32+
| Pathname.rb:15:1:15:39 | ... = ... |
33+
| Pathname.rb:15:7:15:10 | pwd1 |
34+
| Pathname.rb:15:7:15:39 | call to join |
35+
| Pathname.rb:16:1:16:23 | ... = ... |
36+
| Pathname.rb:16:7:16:14 | foo_path |
37+
| Pathname.rb:16:7:16:23 | call to realpath |
38+
| Pathname.rb:17:1:17:59 | ... = ... |
39+
| Pathname.rb:17:7:17:33 | call to new |
40+
| Pathname.rb:17:7:17:59 | call to relative_path_from |
41+
| Pathname.rb:18:1:18:33 | ... = ... |
42+
| Pathname.rb:18:1:18:33 | ... = ... |
43+
| Pathname.rb:18:7:18:10 | pwd1 |
44+
| Pathname.rb:18:7:18:33 | call to sub |
45+
| Pathname.rb:19:1:19:29 | ... = ... |
46+
| Pathname.rb:19:7:19:14 | foo_path |
47+
| Pathname.rb:19:7:19:29 | call to sub_ext |
48+
| Pathname.rb:20:1:20:22 | ... = ... |
49+
| Pathname.rb:20:7:20:14 | foo_path |
50+
| Pathname.rb:20:7:20:22 | call to to_path |
51+
| Pathname.rb:23:14:23:21 | foo_path |
52+
| Pathname.rb:26:12:26:19 | foo_path |
53+
| Pathname.rb:28:11:28:14 | pwd1 |
54+
| Pathname.rb:32:12:32:19 | foo_path |
55+
| Pathname.rb:35:1:35:8 | foo_path |
56+
| Pathname.rb:38:1:38:8 | foo_path |
57+
| Pathname.rb:39:12:39:19 | foo_path |
58+
| Pathname.rb:41:1:41:3 | p08 |
59+
| Pathname.rb:42:1:42:3 | p01 |
60+
fileSystemAccesses
61+
| Pathname.rb:26:12:26:24 | call to open | Pathname.rb:26:12:26:19 | foo_path |
62+
| Pathname.rb:28:11:28:22 | call to opendir | Pathname.rb:28:11:28:14 | pwd1 |
63+
| Pathname.rb:32:12:32:24 | call to read | Pathname.rb:32:12:32:19 | foo_path |
64+
| Pathname.rb:35:1:35:23 | call to write | Pathname.rb:35:1:35:8 | foo_path |
65+
| Pathname.rb:39:12:39:34 | call to open | Pathname.rb:39:12:39:19 | foo_path |
66+
fileNameSources
67+
| Pathname.rb:2:1:2:33 | ... = ... |
68+
| Pathname.rb:2:1:2:33 | ... = ... |
69+
| Pathname.rb:2:12:2:33 | call to new |
70+
| Pathname.rb:3:1:3:20 | ... = ... |
71+
| Pathname.rb:3:13:3:20 | foo_path |
72+
| Pathname.rb:4:1:4:8 | foo_path |
73+
| Pathname.rb:6:1:6:29 | ... = ... |
74+
| Pathname.rb:6:1:6:29 | ... = ... |
75+
| Pathname.rb:6:12:6:29 | call to new |
76+
| Pathname.rb:9:1:9:21 | ... = ... |
77+
| Pathname.rb:9:1:9:21 | ... = ... |
78+
| Pathname.rb:9:8:9:21 | call to getwd |
79+
| Pathname.rb:10:1:10:21 | ... = ... |
80+
| Pathname.rb:10:7:10:10 | pwd1 |
81+
| Pathname.rb:10:7:10:21 | ... + ... |
82+
| Pathname.rb:10:14:10:21 | foo_path |
83+
| Pathname.rb:11:1:11:21 | ... = ... |
84+
| Pathname.rb:11:1:11:21 | ... = ... |
85+
| Pathname.rb:11:7:11:10 | pwd1 |
86+
| Pathname.rb:11:7:11:21 | ... / ... |
87+
| Pathname.rb:11:14:11:21 | bar_path |
88+
| Pathname.rb:12:1:12:19 | ... = ... |
89+
| Pathname.rb:12:7:12:10 | pwd1 |
90+
| Pathname.rb:12:7:12:19 | call to basename |
91+
| Pathname.rb:13:1:13:46 | ... = ... |
92+
| Pathname.rb:13:7:13:36 | call to new |
93+
| Pathname.rb:13:7:13:46 | call to cleanpath |
94+
| Pathname.rb:14:1:14:26 | ... = ... |
95+
| Pathname.rb:14:7:14:14 | foo_path |
96+
| Pathname.rb:14:7:14:26 | call to expand_path |
97+
| Pathname.rb:15:1:15:39 | ... = ... |
98+
| Pathname.rb:15:7:15:10 | pwd1 |
99+
| Pathname.rb:15:7:15:39 | call to join |
100+
| Pathname.rb:16:1:16:23 | ... = ... |
101+
| Pathname.rb:16:7:16:14 | foo_path |
102+
| Pathname.rb:16:7:16:23 | call to realpath |
103+
| Pathname.rb:17:1:17:59 | ... = ... |
104+
| Pathname.rb:17:7:17:33 | call to new |
105+
| Pathname.rb:17:7:17:59 | call to relative_path_from |
106+
| Pathname.rb:18:1:18:33 | ... = ... |
107+
| Pathname.rb:18:1:18:33 | ... = ... |
108+
| Pathname.rb:18:7:18:10 | pwd1 |
109+
| Pathname.rb:18:7:18:33 | call to sub |
110+
| Pathname.rb:19:1:19:29 | ... = ... |
111+
| Pathname.rb:19:7:19:14 | foo_path |
112+
| Pathname.rb:19:7:19:29 | call to sub_ext |
113+
| Pathname.rb:20:1:20:22 | ... = ... |
114+
| Pathname.rb:20:7:20:14 | foo_path |
115+
| Pathname.rb:20:7:20:22 | call to to_path |
116+
| Pathname.rb:23:14:23:21 | foo_path |
117+
| Pathname.rb:23:14:23:26 | call to to_s |
118+
| Pathname.rb:26:12:26:19 | foo_path |
119+
| Pathname.rb:28:11:28:14 | pwd1 |
120+
| Pathname.rb:32:12:32:19 | foo_path |
121+
| Pathname.rb:35:1:35:8 | foo_path |
122+
| Pathname.rb:38:1:38:8 | foo_path |
123+
| Pathname.rb:39:12:39:19 | foo_path |
124+
| Pathname.rb:41:1:41:3 | p08 |
125+
| Pathname.rb:42:1:42:3 | p01 |
126+
fileSystemReadAccesses
127+
| Pathname.rb:32:12:32:24 | call to read | Pathname.rb:32:12:32:24 | call to read |
128+
fileSystemWriteAccesses
129+
| Pathname.rb:35:1:35:23 | call to write | Pathname.rb:35:16:35:23 | "output" |
130+
fileSystemPermissionModifications
131+
| Pathname.rb:38:1:38:19 | call to chmod | Pathname.rb:38:16:38:19 | 0644 |
132+
| Pathname.rb:39:12:39:34 | call to open | Pathname.rb:39:31:39:34 | 0666 |
133+
| Pathname.rb:41:1:41:14 | call to mkdir | Pathname.rb:41:11:41:14 | 0755 |
134+
| Pathname.rb:42:1:42:22 | call to mkpath | Pathname.rb:42:18:42:21 | 0644 |
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
private import ruby
2+
private import codeql.ruby.Concepts
3+
private import codeql.ruby.DataFlow
4+
private import codeql.ruby.frameworks.core.Pathname
5+
6+
query predicate pathnameInstances(Pathname::PathnameInstance i) { any() }
7+
8+
query predicate fileSystemAccesses(FileSystemAccess a, DataFlow::Node p) {
9+
p = a.getAPathArgument()
10+
}
11+
12+
query predicate fileNameSources(FileNameSource s) { any() }
13+
14+
query predicate fileSystemReadAccesses(FileSystemReadAccess a, DataFlow::Node d) {
15+
d = a.getADataNode()
16+
}
17+
18+
query predicate fileSystemWriteAccesses(FileSystemWriteAccess a, DataFlow::Node d) {
19+
d = a.getADataNode()
20+
}
21+
22+
query predicate fileSystemPermissionModifications(
23+
FileSystemPermissionModification m, DataFlow::Node p
24+
) {
25+
p = m.getAPermissionNode()
26+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
foo_path = Pathname.new "foo.txt"
3+
foo_path2 = foo_path
4+
foo_path
5+
6+
bar_path = Pathname.new 'bar'
7+
8+
# All these calls return new `Pathname` instances
9+
pwd1 = Pathname.getwd
10+
p00 = pwd1 + foo_path
11+
p01 = pwd1 / bar_path
12+
p02 = pwd1.basename
13+
p03 = Pathname.new('bar/../baz.txt').cleanpath
14+
p04 = foo_path.expand_path
15+
p05 = pwd1.join 'bar', 'baz', 'qux.txt'
16+
p06 = foo_path.realpath
17+
p07 = Pathname.new('foo/bar.txt').relative_path_from('foo')
18+
p08 = pwd1.sub 'wibble', 'wobble'
19+
p09 = foo_path.sub_ext '.log'
20+
p10 = foo_path.to_path
21+
22+
# `Pathname#to_s` returns a string that we consider to be a filename source.
23+
foo_string = foo_path.to_s
24+
25+
# File-system accesses
26+
foo_file = foo_path.open
27+
foo_file.close
28+
pwd_dir = pwd1.opendir
29+
pwd_dir.close
30+
31+
# Read from a file
32+
foo_data = foo_path.read
33+
34+
# Write to a file
35+
foo_path.write 'output'
36+
37+
# Permission modifications
38+
foo_path.chmod 0644
39+
foo_file = foo_path.open 'w', 0666
40+
foo_file.close
41+
p08.mkdir 0755
42+
p01.mkpath(mode: 0644)

0 commit comments

Comments
 (0)