Skip to content

Commit e8972b8

Browse files
authored
Merge pull request #8635 from hmac/hmac/io-popen
Ruby: Model IO.popen
2 parents e0c74d4 + a6cab02 commit e8972b8

File tree

10 files changed

+517
-263
lines changed

10 files changed

+517
-263
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import core.Module
1212
import core.Array
1313
import core.String
1414
import core.Regexp
15+
import core.IO
1516

1617
/**
1718
* A system command executed via subshell literal syntax.

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

Lines changed: 4 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -6,267 +6,10 @@ private import ruby
66
private import codeql.ruby.Concepts
77
private import codeql.ruby.ApiGraphs
88
private import codeql.ruby.DataFlow
9-
private import codeql.ruby.frameworks.Core
109
private import codeql.ruby.dataflow.FlowSummary
11-
12-
private DataFlow::Node ioInstanceInstantiation() {
13-
result = API::getTopLevelMember("IO").getAnInstantiation() or
14-
result = API::getTopLevelMember("IO").getAMethodCall(["for_fd", "open", "try_convert"])
15-
}
16-
17-
private DataFlow::Node ioInstance() {
18-
result = ioInstanceInstantiation()
19-
or
20-
exists(DataFlow::Node inst |
21-
inst = ioInstance() and
22-
inst.(DataFlow::LocalSourceNode).flowsTo(result)
23-
)
24-
}
25-
26-
// Match some simple cases where a path argument specifies a shell command to
27-
// be executed. For example, the `"|date"` argument in `IO.read("|date")`, which
28-
// will execute a shell command and read its output rather than reading from the
29-
// filesystem.
30-
private predicate pathArgSpawnsSubprocess(Expr arg) {
31-
arg.getConstantValue().getStringlikeValue().charAt(0) = "|"
32-
}
33-
34-
private DataFlow::Node fileInstanceInstantiation() {
35-
result = API::getTopLevelMember("File").getAnInstantiation()
36-
or
37-
result = API::getTopLevelMember("File").getAMethodCall(["open", "try_convert"])
38-
or
39-
// Calls to `Kernel.open` can yield `File` instances
40-
result.(KernelMethodCall).getMethodName() = "open" and
41-
// Assume that calls that don't invoke shell commands will instead open
42-
// a file.
43-
not pathArgSpawnsSubprocess(result.(KernelMethodCall).getArgument(0).asExpr().getExpr())
44-
}
45-
46-
private DataFlow::Node fileInstance() {
47-
result = fileInstanceInstantiation()
48-
or
49-
exists(DataFlow::Node inst |
50-
inst = fileInstance() and
51-
inst.(DataFlow::LocalSourceNode).flowsTo(result)
52-
)
53-
}
54-
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-
/** DEPRECATED: Alias for getApi */
75-
deprecated string getAPI() { result = this.getApi() }
76-
77-
/** Gets a node representing the data read or written by this call */
78-
abstract DataFlow::Node getADataNodeImpl();
79-
80-
/** Gets a string representation of the receiver kind, either "class" or "instance". */
81-
abstract string getReceiverKind();
82-
}
83-
84-
/**
85-
* A method call that performs a read using either the `IO` or `File` classes.
86-
*/
87-
private class IOOrFileReadMethodCall extends IOOrFileMethodCall {
88-
private string api;
89-
private string receiverKind;
90-
91-
IOOrFileReadMethodCall() {
92-
exists(string methodName | methodName = this.getMethodName() |
93-
// e.g. `{IO,File}.readlines("foo.txt")`
94-
receiverKind = "class" and
95-
methodName = ["binread", "foreach", "read", "readlines"] and
96-
api = ["IO", "File"] and
97-
this = API::getTopLevelMember(api).getAMethodCall(methodName)
98-
or
99-
// e.g. `{IO,File}.new("foo.txt", "r").getc`
100-
receiverKind = "interface" and
101-
(
102-
methodName =
103-
[
104-
"getbyte", "getc", "gets", "pread", "read", "read_nonblock", "readbyte", "readchar",
105-
"readline", "readlines", "readpartial", "sysread"
106-
] and
107-
(
108-
this.getReceiver() = ioInstance() and api = "IO"
109-
or
110-
this.getReceiver() = fileInstance() and api = "File"
111-
)
112-
)
113-
)
114-
}
115-
116-
override string getApi() { result = api }
117-
118-
/** DEPRECATED: Alias for getApi */
119-
deprecated override string getAPI() { result = this.getApi() }
120-
121-
override DataFlow::Node getADataNodeImpl() { result = this }
122-
123-
override string getReceiverKind() { result = receiverKind }
124-
}
125-
126-
/**
127-
* A method call that performs a write using either the `IO` or `File` classes.
128-
*/
129-
private class IOOrFileWriteMethodCall extends IOOrFileMethodCall {
130-
private string api;
131-
private string receiverKind;
132-
private DataFlow::Node dataNode;
133-
134-
IOOrFileWriteMethodCall() {
135-
exists(string methodName | methodName = this.getMethodName() |
136-
// e.g. `{IO,File}.write("foo.txt", "hello\n")`
137-
receiverKind = "class" and
138-
api = ["IO", "File"] and
139-
this = API::getTopLevelMember(api).getAMethodCall(methodName) and
140-
methodName = ["binwrite", "write"] and
141-
dataNode = this.getArgument(1)
142-
or
143-
// e.g. `{IO,File}.new("foo.txt", "a+).puts("hello")`
144-
receiverKind = "interface" and
145-
(
146-
this.getReceiver() = ioInstance() and api = "IO"
147-
or
148-
this.getReceiver() = fileInstance() and api = "File"
149-
) and
150-
(
151-
methodName = ["<<", "print", "putc", "puts", "syswrite", "pwrite", "write_nonblock"] and
152-
dataNode = this.getArgument(0)
153-
or
154-
// Any argument to these methods may be written as data
155-
methodName = ["printf", "write"] and dataNode = this.getArgument(_)
156-
)
157-
)
158-
}
159-
160-
override string getApi() { result = api }
161-
162-
/** DEPRECATED: Alias for getApi */
163-
deprecated override string getAPI() { result = this.getApi() }
164-
165-
override DataFlow::Node getADataNodeImpl() { result = dataNode }
166-
167-
override string getReceiverKind() { result = receiverKind }
168-
}
169-
170-
/**
171-
* Classes and predicates for modeling the core `IO` module.
172-
*/
173-
module IO {
174-
/**
175-
* An instance of the `IO` class, for example in
176-
*
177-
* ```rb
178-
* rand = IO.new(IO.sysopen("/dev/random", "r"), "r")
179-
* rand_data = rand.read(32)
180-
* ```
181-
*
182-
* there are 3 `IOInstance`s - the call to `IO.new`, the assignment
183-
* `rand = ...`, and the read access to `rand` on the second line.
184-
*/
185-
class IOInstance extends DataFlow::Node {
186-
IOInstance() {
187-
this = ioInstance() or
188-
this = fileInstance()
189-
}
190-
}
191-
192-
/**
193-
* A `DataFlow::CallNode` that reads data using the `IO` class. For example,
194-
* the `read` and `readline` calls in:
195-
*
196-
* ```rb
197-
* # invokes the `date` shell command as a subprocess, returning its output as a string
198-
* IO.read("|date")
199-
*
200-
* # reads from the file `foo.txt`, returning its first line as a string
201-
* IO.new(IO.sysopen("foo.txt")).readline
202-
* ```
203-
*
204-
* This class includes only reads that use the `IO` class directly, not those
205-
* that use a subclass of `IO` such as `File`.
206-
*/
207-
class IOReader extends IOOrFileReadMethodCall {
208-
IOReader() { this.getApi() = "IO" }
209-
}
210-
211-
/**
212-
* A `DataFlow::CallNode` that writes data using the `IO` class. For example,
213-
* the `write` and `puts` calls in:
214-
*
215-
* ```rb
216-
* # writes the string `hello world` to the file `foo.txt`
217-
* IO.write("foo.txt", "hello world")
218-
*
219-
* # appends the string `hello again\n` to the file `foo.txt`
220-
* IO.new(IO.sysopen("foo.txt", "a")).puts("hello again")
221-
* ```
222-
*
223-
* This class includes only writes that use the `IO` class directly, not those
224-
* that use a subclass of `IO` such as `File`.
225-
*/
226-
class IOWriter extends IOOrFileWriteMethodCall {
227-
IOWriter() { this.getApi() = "IO" }
228-
}
229-
230-
/**
231-
* A `DataFlow::CallNode` that reads data to the filesystem using the `IO`
232-
* or `File` classes. For example, the `IO.read` and `File#readline` calls in:
233-
*
234-
* ```rb
235-
* # reads the file `foo.txt` and returns its contents as a string.
236-
* IO.read("foo.txt")
237-
*
238-
* # reads from the file `foo.txt`, returning its first line as a string
239-
* File.new("foo.txt").readline
240-
* ```
241-
*/
242-
class FileReader extends IOOrFileReadMethodCall, FileSystemReadAccess::Range {
243-
FileReader() { not this.spawnsSubprocess() }
244-
245-
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
246-
247-
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
248-
}
249-
250-
/**
251-
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
252-
* or `File` classes. For example, the `write` and `puts` calls in:
253-
*
254-
* ```rb
255-
* # writes the string `hello world` to the file `foo.txt`
256-
* IO.write("foo.txt", "hello world")
257-
*
258-
* # appends the string `hello again\n` to the file `foo.txt`
259-
* File.new("foo.txt", "a").puts("hello again")
260-
* ```
261-
*/
262-
class FileWriter extends IOOrFileWriteMethodCall, FileSystemWriteAccess::Range {
263-
FileWriter() { not this.spawnsSubprocess() }
264-
265-
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
266-
267-
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
268-
}
269-
}
10+
private import core.IO
11+
private import core.Kernel::Kernel
12+
private import core.internal.IOOrFile
27013

27114
/**
27215
* Classes and predicates for modeling the core `File` module.
@@ -330,7 +73,7 @@ module File {
33073
])
33174
or
33275
// Instance methods
333-
exists(FileInstance fi |
76+
exists(File::FileInstance fi |
33477
this.getReceiver() = fi and
33578
this.getMethodName() = ["path", "to_path"]
33679
)

0 commit comments

Comments
 (0)