Skip to content

Commit 6adfea2

Browse files
authored
Merge pull request #7163 from github/ruby/file-reader-extend
Ruby: Extend `FileSystemReadAccess` to include more potential sources of input from the filesystem
2 parents 9f48ae6 + 1ec935d commit 6adfea2

File tree

3 files changed

+71
-60
lines changed

3 files changed

+71
-60
lines changed

ruby/ql/lib/codeql/ruby/frameworks/Files.qll

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private predicate pathArgSpawnsSubprocess(Expr arg) {
3434
private DataFlow::Node fileInstanceInstantiation() {
3535
result = API::getTopLevelMember("File").getAnInstantiation()
3636
or
37-
result = API::getTopLevelMember("File").getAMethodCall("open")
37+
result = API::getTopLevelMember("File").getAMethodCall(["open", "try_convert"])
3838
or
3939
// Calls to `Kernel.open` can yield `File` instances
4040
result.(KernelMethodCall).getMethodName() = "open" and
@@ -52,22 +52,20 @@ private DataFlow::Node fileInstance() {
5252
)
5353
}
5454

55-
private string ioFileReaderClassMethodName() {
56-
result = ["binread", "foreach", "read", "readlines", "try_convert"]
57-
}
55+
private string ioReaderClassMethodName() { result = ["binread", "foreach", "read", "readlines"] }
5856

59-
private string ioFileReaderInstanceMethodName() {
57+
private string ioReaderInstanceMethodName() {
6058
result =
6159
[
6260
"getbyte", "getc", "gets", "pread", "read", "read_nonblock", "readbyte", "readchar",
6361
"readline", "readlines", "readpartial", "sysread"
6462
]
6563
}
6664

67-
private string ioFileReaderMethodName(boolean classMethodCall) {
68-
classMethodCall = true and result = ioFileReaderClassMethodName()
65+
private string ioReaderMethodName(string receiverKind) {
66+
receiverKind = "class" and result = ioReaderClassMethodName()
6967
or
70-
classMethodCall = false and result = ioFileReaderInstanceMethodName()
68+
receiverKind = "instance" and result = ioReaderInstanceMethodName()
7169
}
7270

7371
/**
@@ -100,82 +98,94 @@ module IO {
10098

10199
/**
102100
* A `DataFlow::CallNode` that reads data using the `IO` class. For example,
103-
* the `IO.read call in:
101+
* the `read` and `readline` calls in:
104102
*
105103
* ```rb
104+
* # invokes the `date` shell command as a subprocess, returning its output as a string
106105
* IO.read("|date")
107-
* ```
108106
*
109-
* returns the output of the `date` shell command, invoked as a subprocess.
107+
* # reads from the file `foo.txt`, returning its first line as a string
108+
* IO.new(IO.sysopen("foo.txt")).readline
109+
* ```
110110
*
111-
* This class includes reads both from shell commands and reads from the
112-
* filesystem. For working with filesystem accesses specifically, see
113-
* `IOFileReader` or the `FileSystemReadAccess` concept.
111+
* This class includes only reads that use the `IO` class directly, not those
112+
* that use a subclass of `IO` such as `File`.
114113
*/
115114
class IOReader extends DataFlow::CallNode {
116-
private boolean classMethodCall;
117-
private string api;
115+
private string receiverKind;
118116

119117
IOReader() {
120-
// Class methods
121-
api = ["File", "IO"] and
122-
classMethodCall = true and
123-
this = API::getTopLevelMember(api).getAMethodCall(ioFileReaderMethodName(classMethodCall))
118+
// `IO` class method calls
119+
receiverKind = "class" and
120+
this = API::getTopLevelMember("IO").getAMethodCall(ioReaderMethodName(receiverKind))
124121
or
125-
// IO instance methods
126-
classMethodCall = false and
127-
api = "IO" and
122+
// `IO` instance method calls
123+
receiverKind = "instance" and
128124
exists(IOInstanceStrict ii |
129125
this.getReceiver() = ii and
130-
this.getMethodName() = ioFileReaderMethodName(classMethodCall)
131-
)
132-
or
133-
// File instance methods
134-
classMethodCall = false and
135-
api = "File" and
136-
exists(File::FileInstance fi |
137-
this.getReceiver() = fi and
138-
this.getMethodName() = ioFileReaderMethodName(classMethodCall)
126+
this.getMethodName() = ioReaderMethodName(receiverKind)
139127
)
140128
// TODO: enumeration style methods such as `each`, `foreach`, etc.
141129
}
142130

143131
/**
144-
* Returns the most specific core class used for this read, `IO` or `File`
132+
* Gets a string representation of the receiver kind, either "class" or "instance".
145133
*/
146-
string getAPI() { result = api }
147-
148-
predicate isClassMethodCall() { classMethodCall = true }
134+
string getReceiverKind() { result = receiverKind }
149135
}
150136

151137
/**
152138
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
153-
* class. For example, the `IO.read call in:
139+
* or `File` classes. For example, the `IO.read` and `File#readline` calls in:
154140
*
155141
* ```rb
142+
* # reads the file `foo.txt` and returns its contents as a string.
156143
* IO.read("foo.txt")
157-
* ```
158144
*
159-
* reads the file `foo.txt` and returns its contents as a string.
145+
* # reads from the file `foo.txt`, returning its first line as a string
146+
* File.new("foo.txt").readline
147+
* ```
160148
*/
161-
class IOFileReader extends IOReader, FileSystemReadAccess::Range {
162-
IOFileReader() {
163-
this.getAPI() = "File"
164-
or
165-
this.isClassMethodCall() and
166-
// Assume that calls that don't invoke shell commands will instead
167-
// read from a file.
149+
class FileReader extends DataFlow::CallNode, FileSystemReadAccess::Range {
150+
private string receiverKind;
151+
private string api;
152+
153+
FileReader() {
154+
// A viable `IOReader` that could feasibly read from the filesystem
155+
api = "IO" and
156+
receiverKind = this.(IOReader).getReceiverKind() and
168157
not pathArgSpawnsSubprocess(this.getArgument(0).asExpr().getExpr())
158+
or
159+
api = "File" and
160+
(
161+
// `File` class method calls
162+
receiverKind = "class" and
163+
this = API::getTopLevelMember(api).getAMethodCall(ioReaderMethodName(receiverKind))
164+
or
165+
// `File` instance method calls
166+
receiverKind = "instance" and
167+
exists(File::FileInstance fi |
168+
this.getReceiver() = fi and
169+
this.getMethodName() = ioReaderMethodName(receiverKind)
170+
)
171+
)
172+
// TODO: enumeration style methods such as `each`, `foreach`, etc.
169173
}
170174

171-
// TODO: can we infer a path argument for instance method calls?
175+
// TODO: Currently this only handles class method calls.
176+
// Can we infer a path argument for instance method calls?
172177
// e.g. by tracing back to the instantiation of that instance
173178
override DataFlow::Node getAPathArgument() {
174-
result = this.getArgument(0) and this.isClassMethodCall()
179+
result = this.getArgument(0) and receiverKind = "class"
175180
}
176181

177182
// This class represents calls that return data
178183
override DataFlow::Node getADataNode() { result = this }
184+
185+
/**
186+
* Returns the most specific core class used for this read, `IO` or `File`
187+
*/
188+
string getAPI() { result = api }
179189
}
180190
}
181191

@@ -210,7 +220,7 @@ module File {
210220
* puts f.read()
211221
* ```
212222
*/
213-
class FileModuleReader extends IO::IOFileReader {
223+
class FileModuleReader extends IO::FileReader {
214224
FileModuleReader() { this.getAPI() = "File" }
215225
}
216226

ruby/ql/test/library-tests/frameworks/files/Files.expected

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,24 @@ ioInstances
3636
| Files.rb:24:19:24:40 | call to open |
3737
| Files.rb:35:1:35:56 | ... = ... |
3838
| Files.rb:35:13:35:56 | call to open |
39-
fileReaders
39+
fileModuleReaders
4040
| Files.rb:7:13:7:32 | call to readlines |
4141
ioReaders
42-
| Files.rb:7:13:7:32 | call to readlines | File |
43-
| Files.rb:20:13:20:25 | call to read | IO |
44-
| Files.rb:29:12:29:29 | call to read | IO |
45-
| Files.rb:32:8:32:23 | call to read | IO |
46-
ioFileReaders
47-
| Files.rb:7:13:7:32 | call to readlines | File |
48-
| Files.rb:29:12:29:29 | call to read | IO |
42+
| Files.rb:20:13:20:25 | call to read |
43+
| Files.rb:29:12:29:29 | call to read |
44+
| Files.rb:32:8:32:23 | call to read |
45+
fileReaders
46+
| Files.rb:7:13:7:32 | call to readlines |
47+
| Files.rb:20:13:20:25 | call to read |
48+
| Files.rb:29:12:29:29 | call to read |
4949
fileModuleFilenameSources
5050
| Files.rb:10:6:10:18 | call to path |
5151
| Files.rb:11:6:11:21 | call to to_path |
5252
fileUtilsFilenameSources
5353
| Files.rb:14:8:14:43 | call to makedirs |
5454
fileSystemReadAccesses
5555
| Files.rb:7:13:7:32 | call to readlines |
56+
| Files.rb:20:13:20:25 | call to read |
5657
| Files.rb:29:12:29:29 | call to read |
5758
fileNameSources
5859
| Files.rb:10:6:10:18 | call to path |

ruby/ql/test/library-tests/frameworks/files/Files.ql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ query predicate fileInstances(File::FileInstance i) { any() }
66

77
query predicate ioInstances(IO::IOInstance i) { any() }
88

9-
query predicate fileReaders(File::FileModuleReader r) { any() }
9+
query predicate fileModuleReaders(File::FileModuleReader r) { any() }
1010

11-
query predicate ioReaders(IO::IOReader r, string api) { api = r.getAPI() }
11+
query predicate ioReaders(IO::IOReader r) { any() }
1212

13-
query predicate ioFileReaders(IO::IOFileReader r, string api) { api = r.getAPI() }
13+
query predicate fileReaders(IO::FileReader r) { any() }
1414

1515
query predicate fileModuleFilenameSources(File::FileModuleFilenameSource s) { any() }
1616

0 commit comments

Comments
 (0)