Skip to content

Commit 9196b64

Browse files
authored
Merge pull request #8138 from github/ruby/file-write
Ruby: Implement `FileSystemWriteAccess` concept
2 parents 746290d + 6b8537c commit 9196b64

File tree

6 files changed

+230
-78
lines changed

6 files changed

+230
-78
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added `FileSystemWriteAccess` concept to model data written to the filesystem.

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,35 @@ module FileSystemReadAccess {
9292
}
9393
}
9494

95+
/**
96+
* A data flow node that writes data to the file system.
97+
*
98+
* Extend this class to refine existing API models. If you want to model new APIs,
99+
* extend `FileSystemWriteAccess::Range` instead.
100+
*/
101+
class FileSystemWriteAccess extends FileSystemAccess instanceof FileSystemWriteAccess::Range {
102+
/**
103+
* Gets a node that represents data written to the file system by this access.
104+
*/
105+
DataFlow::Node getADataNode() { result = FileSystemWriteAccess::Range.super.getADataNode() }
106+
}
107+
108+
/** Provides a class for modeling new file system writes. */
109+
module FileSystemWriteAccess {
110+
/**
111+
* A data flow node that writes data to the file system.
112+
*
113+
* Extend this class to model new APIs. If you want to refine existing API models,
114+
* extend `FileSystemWriteAccess` instead.
115+
*/
116+
abstract class Range extends FileSystemAccess::Range {
117+
/**
118+
* Gets a node that represents data written to the file system by this access.
119+
*/
120+
abstract DataFlow::Node getADataNode();
121+
}
122+
}
123+
95124
/**
96125
* A data flow node that sets the permissions for one or more files.
97126
*

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

Lines changed: 150 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,110 @@ private DataFlow::Node fileInstance() {
5252
)
5353
}
5454

55-
private string ioReaderClassMethodName() { result = ["binread", "foreach", "read", "readlines"] }
56-
57-
private string ioReaderInstanceMethodName() {
58-
result =
59-
[
60-
"getbyte", "getc", "gets", "pread", "read", "read_nonblock", "readbyte", "readchar",
61-
"readline", "readlines", "readpartial", "sysread"
62-
]
55+
abstract private class IOOrFileMethodCall extends DataFlow::CallNode {
56+
// TODO: Currently this only handles class method calls.
57+
// Can we infer a path argument for instance method calls?
58+
// e.g. by tracing back to the instantiation of that instance
59+
DataFlow::Node getAPathArgumentImpl() {
60+
result = this.getArgument(0) and this.getReceiverKind() = "class"
61+
}
62+
63+
/**
64+
* Holds if this call appears to read/write from/to a spawned subprocess,
65+
* rather than to/from a file.
66+
*/
67+
predicate spawnsSubprocess() {
68+
pathArgSpawnsSubprocess(this.getAPathArgumentImpl().asExpr().getExpr())
69+
}
70+
71+
/** Gets the API used to perform this call, either "IO" or "File" */
72+
abstract string getAPI();
73+
74+
/** Gets a node representing the data read or written by this call */
75+
abstract DataFlow::Node getADataNodeImpl();
76+
77+
/** Gets a string representation of the receiver kind, either "class" or "instance". */
78+
abstract string getReceiverKind();
6379
}
6480

65-
private string ioReaderMethodName(string receiverKind) {
66-
receiverKind = "class" and result = ioReaderClassMethodName()
67-
or
68-
receiverKind = "instance" and result = ioReaderInstanceMethodName()
81+
/**
82+
* A method call that performs a read using either the `IO` or `File` classes.
83+
*/
84+
private class IOOrFileReadMethodCall extends IOOrFileMethodCall {
85+
private string api;
86+
private string receiverKind;
87+
88+
IOOrFileReadMethodCall() {
89+
exists(string methodName | methodName = this.getMethodName() |
90+
// e.g. `{IO,File}.readlines("foo.txt")`
91+
receiverKind = "class" and
92+
methodName = ["binread", "foreach", "read", "readlines"] and
93+
api = ["IO", "File"] and
94+
this = API::getTopLevelMember(api).getAMethodCall(methodName)
95+
or
96+
// e.g. `{IO,File}.new("foo.txt", "r").getc`
97+
receiverKind = "interface" and
98+
(
99+
methodName =
100+
[
101+
"getbyte", "getc", "gets", "pread", "read", "read_nonblock", "readbyte", "readchar",
102+
"readline", "readlines", "readpartial", "sysread"
103+
] and
104+
(
105+
this.getReceiver() = ioInstance() and api = "IO"
106+
or
107+
this.getReceiver() = fileInstance() and api = "File"
108+
)
109+
)
110+
)
111+
}
112+
113+
override string getAPI() { result = api }
114+
115+
override DataFlow::Node getADataNodeImpl() { result = this }
116+
117+
override string getReceiverKind() { result = receiverKind }
118+
}
119+
120+
/**
121+
* A method call that performs a write using either the `IO` or `File` classes.
122+
*/
123+
private class IOOrFileWriteMethodCall extends IOOrFileMethodCall {
124+
private string api;
125+
private string receiverKind;
126+
private DataFlow::Node dataNode;
127+
128+
IOOrFileWriteMethodCall() {
129+
exists(string methodName | methodName = this.getMethodName() |
130+
// e.g. `{IO,File}.write("foo.txt", "hello\n")`
131+
receiverKind = "class" and
132+
api = ["IO", "File"] and
133+
this = API::getTopLevelMember(api).getAMethodCall(methodName) and
134+
methodName = ["binwrite", "write"] and
135+
dataNode = this.getArgument(1)
136+
or
137+
// e.g. `{IO,File}.new("foo.txt", "a+).puts("hello")`
138+
receiverKind = "interface" and
139+
(
140+
this.getReceiver() = ioInstance() and api = "IO"
141+
or
142+
this.getReceiver() = fileInstance() and api = "File"
143+
) and
144+
(
145+
methodName = ["<<", "print", "putc", "puts", "syswrite", "pwrite", "write_nonblock"] and
146+
dataNode = this.getArgument(0)
147+
or
148+
// Any argument to these methods may be written as data
149+
methodName = ["printf", "write"] and dataNode = this.getArgument(_)
150+
)
151+
)
152+
}
153+
154+
override string getAPI() { result = api }
155+
156+
override DataFlow::Node getADataNodeImpl() { result = dataNode }
157+
158+
override string getReceiverKind() { result = receiverKind }
69159
}
70160

71161
/**
@@ -111,31 +201,31 @@ module IO {
111201
* This class includes only reads that use the `IO` class directly, not those
112202
* that use a subclass of `IO` such as `File`.
113203
*/
114-
class IOReader extends DataFlow::CallNode {
115-
private string receiverKind;
116-
117-
IOReader() {
118-
// `IO` class method calls
119-
receiverKind = "class" and
120-
this = API::getTopLevelMember("IO").getAMethodCall(ioReaderMethodName(receiverKind))
121-
or
122-
// `IO` instance method calls
123-
receiverKind = "instance" and
124-
exists(IOInstanceStrict ii |
125-
this.getReceiver() = ii and
126-
this.getMethodName() = ioReaderMethodName(receiverKind)
127-
)
128-
// TODO: enumeration style methods such as `each`, `foreach`, etc.
129-
}
204+
class IOReader extends IOOrFileReadMethodCall {
205+
IOReader() { this.getAPI() = "IO" }
206+
}
130207

131-
/**
132-
* Gets a string representation of the receiver kind, either "class" or "instance".
133-
*/
134-
string getReceiverKind() { result = receiverKind }
208+
/**
209+
* A `DataFlow::CallNode` that writes data using the `IO` class. For example,
210+
* the `write` and `puts` calls in:
211+
*
212+
* ```rb
213+
* # writes the string `hello world` to the file `foo.txt`
214+
* IO.write("foo.txt", "hello world")
215+
*
216+
* # appends the string `hello again\n` to the file `foo.txt`
217+
* IO.new(IO.sysopen("foo.txt", "a")).puts("hello again")
218+
* ```
219+
*
220+
* This class includes only writes that use the `IO` class directly, not those
221+
* that use a subclass of `IO` such as `File`.
222+
*/
223+
class IOWriter extends IOOrFileWriteMethodCall {
224+
IOWriter() { this.getAPI() = "IO" }
135225
}
136226

137227
/**
138-
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
228+
* A `DataFlow::CallNode` that reads data to the filesystem using the `IO`
139229
* or `File` classes. For example, the `IO.read` and `File#readline` calls in:
140230
*
141231
* ```rb
@@ -146,46 +236,32 @@ module IO {
146236
* File.new("foo.txt").readline
147237
* ```
148238
*/
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
157-
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.
173-
}
239+
class FileReader extends IOOrFileReadMethodCall, FileSystemReadAccess::Range {
240+
FileReader() { not this.spawnsSubprocess() }
174241

175-
// TODO: Currently this only handles class method calls.
176-
// Can we infer a path argument for instance method calls?
177-
// e.g. by tracing back to the instantiation of that instance
178-
override DataFlow::Node getAPathArgument() {
179-
result = this.getArgument(0) and receiverKind = "class"
180-
}
242+
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
181243

182-
// This class represents calls that return data
183-
override DataFlow::Node getADataNode() { result = this }
244+
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
245+
}
184246

185-
/**
186-
* Returns the most specific core class used for this read, `IO` or `File`
187-
*/
188-
string getAPI() { result = api }
247+
/**
248+
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
249+
* or `File` classes. For example, the `write` and `puts` calls in:
250+
*
251+
* ```rb
252+
* # writes the string `hello world` to the file `foo.txt`
253+
* IO.write("foo.txt", "hello world")
254+
*
255+
* # appends the string `hello again\n` to the file `foo.txt`
256+
* File.new("foo.txt", "a").puts("hello again")
257+
* ```
258+
*/
259+
class FileWriter extends IOOrFileWriteMethodCall, FileSystemWriteAccess::Range {
260+
FileWriter() { not this.spawnsSubprocess() }
261+
262+
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
263+
264+
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
189265
}
190266
}
191267

@@ -231,6 +307,10 @@ module File {
231307
*/
232308
class FileModuleReader extends IO::FileReader {
233309
FileModuleReader() { this.getAPI() = "File" }
310+
311+
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
312+
313+
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
234314
}
235315

236316
/**

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
fileInstances
2-
| Files.rb:2:1:2:30 | ... = ... |
3-
| Files.rb:2:1:2:30 | ... = ... |
4-
| Files.rb:2:12:2:30 | call to new |
2+
| Files.rb:2:1:2:36 | ... = ... |
3+
| Files.rb:2:1:2:36 | ... = ... |
4+
| Files.rb:2:12:2:36 | call to new |
55
| Files.rb:3:1:3:21 | ... = ... |
66
| Files.rb:3:1:3:21 | ... = ... |
77
| Files.rb:3:14:3:21 | foo_file |
@@ -15,10 +15,12 @@ fileInstances
1515
| Files.rb:24:19:24:40 | call to open |
1616
| Files.rb:37:1:37:33 | ... = ... |
1717
| Files.rb:37:14:37:33 | call to open |
18+
| Files.rb:40:1:40:8 | foo_file |
19+
| Files.rb:41:1:41:26 | call to open |
1820
ioInstances
19-
| Files.rb:2:1:2:30 | ... = ... |
20-
| Files.rb:2:1:2:30 | ... = ... |
21-
| Files.rb:2:12:2:30 | call to new |
21+
| Files.rb:2:1:2:36 | ... = ... |
22+
| Files.rb:2:1:2:36 | ... = ... |
23+
| Files.rb:2:12:2:36 | call to new |
2224
| Files.rb:3:1:3:21 | ... = ... |
2325
| Files.rb:3:1:3:21 | ... = ... |
2426
| Files.rb:3:14:3:21 | foo_file |
@@ -40,6 +42,12 @@ ioInstances
4042
| Files.rb:35:13:35:56 | call to open |
4143
| Files.rb:37:1:37:33 | ... = ... |
4244
| Files.rb:37:14:37:33 | call to open |
45+
| Files.rb:40:1:40:8 | foo_file |
46+
| Files.rb:41:1:41:26 | call to open |
47+
| Files.rb:44:1:44:45 | ... = ... |
48+
| Files.rb:44:1:44:45 | ... = ... |
49+
| Files.rb:44:11:44:45 | call to open |
50+
| Files.rb:48:1:48:7 | io_file |
4351
fileModuleReaders
4452
| Files.rb:7:13:7:32 | call to readlines |
4553
ioReaders
@@ -64,7 +72,21 @@ fileSystemAccesses
6472
| Files.rb:20:13:20:25 | call to read |
6573
| Files.rb:29:12:29:29 | call to read |
6674
| Files.rb:37:14:37:33 | call to open |
75+
| Files.rb:40:1:40:22 | call to puts |
76+
| Files.rb:41:1:41:26 | call to open |
77+
| Files.rb:41:1:41:43 | call to write |
78+
| Files.rb:48:1:48:40 | call to printf |
6779
fileNameSources
6880
| Files.rb:10:6:10:18 | call to path |
6981
| Files.rb:11:6:11:21 | call to to_path |
7082
| Files.rb:14:8:14:43 | call to makedirs |
83+
ioWriters
84+
| Files.rb:48:1:48:40 | call to printf |
85+
fileWriters
86+
| Files.rb:40:1:40:22 | call to puts |
87+
| Files.rb:41:1:41:43 | call to write |
88+
| Files.rb:48:1:48:40 | call to printf |
89+
fileSystemWriteAccesses
90+
| Files.rb:40:1:40:22 | call to puts |
91+
| Files.rb:41:1:41:43 | call to write |
92+
| Files.rb:48:1:48:40 | call to printf |

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@ query predicate fileSystemReadAccesses(FileSystemReadAccess a) { any() }
2121
query predicate fileSystemAccesses(FileSystemAccess a) { any() }
2222

2323
query predicate fileNameSources(FileNameSource s) { any() }
24+
25+
query predicate ioWriters(IO::IOWriter r) { any() }
26+
27+
query predicate fileWriters(IO::FileWriter r) { any() }
28+
29+
query predicate fileSystemWriteAccesses(FileSystemWriteAccess a) { any() }

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# `foo_file` is a `File` instance
2-
foo_file = File.new("foo.txt")
2+
foo_file = File.new("foo.txt", "a+")
33
foo_file_2 = foo_file
44
foo_file
55

@@ -34,4 +34,15 @@
3434
# `rand_open` is an `IO` instance
3535
rand_open = IO.open(IO.sysopen("/dev/random", "r"), "r")
3636

37-
foo_file_3 = File.open("foo.txt")
37+
foo_file_3 = File.open("foo.txt")
38+
39+
# File write accesses
40+
foo_file.puts("hello")
41+
File.open("foo.txt", "a+").write("world\n")
42+
43+
# IO instance
44+
io_file = IO.open(IO.sysopen("foo.txt", "w"))
45+
str_1 = "hello"
46+
int_1 = 123
47+
# File/IO write
48+
io_file.printf("%s: %d\n", str_1, int_1)

0 commit comments

Comments
 (0)