Skip to content

Commit deafc1b

Browse files
authored
Merge pull request #22390 from geoffw0/formatflow
Rust: Add core::fmt::Write models
2 parents e8d8a67 + 5bcbb8f commit deafc1b

8 files changed

Lines changed: 181 additions & 62 deletions

File tree

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 data-flow models for `core::fmt::Write`. This may improve detection of vulnerabilities where tainted data is written to a formatted output buffer.

rust/ql/lib/codeql/rust/frameworks/stdlib/core.model.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ extensions:
129129
- ["<core::str>::parse", "Argument[self].Reference", "ReturnValue.Field[core::result::Result::Ok(0)]", "taint", "manual"]
130130
- ["<core::str>::trim", "Argument[self].Reference", "ReturnValue.Reference", "taint", "manual"]
131131
- ["<core::str>::to_string", "Argument[self].Reference", "ReturnValue", "taint", "manual"]
132+
# Fmt
133+
- ["core::fmt::Write::write_fmt", "Argument[0]", "Argument[self].Reference", "taint", "manual"]
134+
- ["core::fmt::Write::write_str", "Argument[0].Reference", "Argument[self].Reference", "taint", "manual"]
135+
- ["core::fmt::Write::write_char", "Argument[0]", "Argument[self].Reference", "taint", "manual"]
136+
- ["core::fmt::write", "Argument[1]", "Argument[0].Reference", "taint", "manual"]
132137
# Ord
133138
- ["core::cmp::Ord::min", "Argument[self,0]", "ReturnValue", "value", "manual"]
134139
- ["core::cmp::Ord::max", "Argument[self,0]", "ReturnValue", "value", "manual"]
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
| main.rs:13:10:13:25 | FormatArgsExpr | 1 |
22
| main.rs:16:10:16:23 | FormatArgsExpr | 0 |
33
| main.rs:19:13:19:36 | FormatArgsExpr | 1 |
4-
| main.rs:33:25:33:33 | FormatArgsExpr | 1 |
5-
| main.rs:38:13:38:32 | FormatArgsExpr | 1 |
6-
| main.rs:39:13:39:32 | FormatArgsExpr | 0 |
7-
| main.rs:49:5:49:27 | FormatArgsExpr | 1 |
8-
| main.rs:50:5:50:26 | FormatArgsExpr | 0 |
9-
| main.rs:51:5:51:25 | FormatArgsExpr | 1 |
10-
| main.rs:52:5:52:24 | FormatArgsExpr | 0 |
4+
| main.rs:27:25:27:33 | FormatArgsExpr | 1 |
5+
| main.rs:38:40:38:46 | FormatArgsExpr | 0 |
6+
| main.rs:43:52:43:58 | FormatArgsExpr | 0 |
7+
| main.rs:49:13:49:33 | FormatArgsExpr | 1 |
8+
| main.rs:54:13:54:33 | FormatArgsExpr | 0 |
9+
| main.rs:64:5:64:27 | FormatArgsExpr | 1 |
10+
| main.rs:65:5:65:26 | FormatArgsExpr | 0 |
11+
| main.rs:66:5:66:25 | FormatArgsExpr | 1 |
12+
| main.rs:67:5:67:24 | FormatArgsExpr | 0 |
Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
#select
2-
| main.rs:49:5:49:27 | FormatArgsExpr | main.rs:47:19:47:45 | ...::var(...) | main.rs:49:5:49:27 | FormatArgsExpr | Log entry depends on a $@. | main.rs:47:19:47:45 | ...::var(...) | user-provided value |
3-
| main.rs:50:5:50:26 | FormatArgsExpr | main.rs:47:19:47:45 | ...::var(...) | main.rs:50:5:50:26 | FormatArgsExpr | Log entry depends on a $@. | main.rs:47:19:47:45 | ...::var(...) | user-provided value |
4-
| main.rs:51:5:51:25 | FormatArgsExpr | main.rs:47:19:47:45 | ...::var(...) | main.rs:51:5:51:25 | FormatArgsExpr | Log entry depends on a $@. | main.rs:47:19:47:45 | ...::var(...) | user-provided value |
5-
| main.rs:52:5:52:24 | FormatArgsExpr | main.rs:47:19:47:45 | ...::var(...) | main.rs:52:5:52:24 | FormatArgsExpr | Log entry depends on a $@. | main.rs:47:19:47:45 | ...::var(...) | user-provided value |
2+
| main.rs:64:5:64:27 | FormatArgsExpr | main.rs:62:19:62:45 | ...::var(...) | main.rs:64:5:64:27 | FormatArgsExpr | Log entry depends on a $@. | main.rs:62:19:62:45 | ...::var(...) | user-provided value |
3+
| main.rs:65:5:65:26 | FormatArgsExpr | main.rs:62:19:62:45 | ...::var(...) | main.rs:65:5:65:26 | FormatArgsExpr | Log entry depends on a $@. | main.rs:62:19:62:45 | ...::var(...) | user-provided value |
4+
| main.rs:66:5:66:25 | FormatArgsExpr | main.rs:62:19:62:45 | ...::var(...) | main.rs:66:5:66:25 | FormatArgsExpr | Log entry depends on a $@. | main.rs:62:19:62:45 | ...::var(...) | user-provided value |
5+
| main.rs:67:5:67:24 | FormatArgsExpr | main.rs:62:19:62:45 | ...::var(...) | main.rs:67:5:67:24 | FormatArgsExpr | Log entry depends on a $@. | main.rs:62:19:62:45 | ...::var(...) | user-provided value |
66
edges
7-
| main.rs:47:9:47:15 | tainted | main.rs:49:5:49:27 | FormatArgsExpr | provenance | Sink:MaD:2 |
8-
| main.rs:47:9:47:15 | tainted | main.rs:50:5:50:26 | FormatArgsExpr | provenance | Sink:MaD:1 |
9-
| main.rs:47:9:47:15 | tainted | main.rs:51:5:51:25 | FormatArgsExpr | provenance | Sink:MaD:2 |
10-
| main.rs:47:9:47:15 | tainted | main.rs:52:5:52:24 | FormatArgsExpr | provenance | Sink:MaD:1 |
11-
| main.rs:47:19:47:45 | ...::var(...) | main.rs:47:19:47:45 | ...::var(...) [Ok] | provenance | Src:MaD:3 |
12-
| main.rs:47:19:47:45 | ...::var(...) [Ok] | main.rs:47:19:47:65 | ... .unwrap_or_default() | provenance | MaD:4 |
13-
| main.rs:47:19:47:65 | ... .unwrap_or_default() | main.rs:47:9:47:15 | tainted | provenance | |
7+
| main.rs:62:9:62:15 | tainted | main.rs:64:5:64:27 | FormatArgsExpr | provenance | Sink:MaD:2 |
8+
| main.rs:62:9:62:15 | tainted | main.rs:65:5:65:26 | FormatArgsExpr | provenance | Sink:MaD:1 |
9+
| main.rs:62:9:62:15 | tainted | main.rs:66:5:66:25 | FormatArgsExpr | provenance | Sink:MaD:2 |
10+
| main.rs:62:9:62:15 | tainted | main.rs:67:5:67:24 | FormatArgsExpr | provenance | Sink:MaD:1 |
11+
| main.rs:62:19:62:45 | ...::var(...) | main.rs:62:19:62:45 | ...::var(...) [Ok] | provenance | Src:MaD:3 |
12+
| main.rs:62:19:62:45 | ...::var(...) [Ok] | main.rs:62:19:62:65 | ... .unwrap_or_default() | provenance | MaD:4 |
13+
| main.rs:62:19:62:65 | ... .unwrap_or_default() | main.rs:62:9:62:15 | tainted | provenance | |
1414
models
1515
| 1 | Sink: std::io::stdio::_eprint; Argument[0]; log-injection |
1616
| 2 | Sink: std::io::stdio::_print; Argument[0]; log-injection |
1717
| 3 | Source: std::env::var; ReturnValue.Field[core::result::Result::Ok(0)]; environment |
1818
| 4 | Summary: <core::result::Result>::unwrap_or_default; Argument[self].Field[core::result::Result::Ok(0)]; ReturnValue; value |
1919
nodes
20-
| main.rs:47:9:47:15 | tainted | semmle.label | tainted |
21-
| main.rs:47:19:47:45 | ...::var(...) | semmle.label | ...::var(...) |
22-
| main.rs:47:19:47:45 | ...::var(...) [Ok] | semmle.label | ...::var(...) [Ok] |
23-
| main.rs:47:19:47:65 | ... .unwrap_or_default() | semmle.label | ... .unwrap_or_default() |
24-
| main.rs:49:5:49:27 | FormatArgsExpr | semmle.label | FormatArgsExpr |
25-
| main.rs:50:5:50:26 | FormatArgsExpr | semmle.label | FormatArgsExpr |
26-
| main.rs:51:5:51:25 | FormatArgsExpr | semmle.label | FormatArgsExpr |
27-
| main.rs:52:5:52:24 | FormatArgsExpr | semmle.label | FormatArgsExpr |
20+
| main.rs:62:9:62:15 | tainted | semmle.label | tainted |
21+
| main.rs:62:19:62:45 | ...::var(...) | semmle.label | ...::var(...) |
22+
| main.rs:62:19:62:45 | ...::var(...) [Ok] | semmle.label | ...::var(...) [Ok] |
23+
| main.rs:62:19:62:65 | ... .unwrap_or_default() | semmle.label | ... .unwrap_or_default() |
24+
| main.rs:64:5:64:27 | FormatArgsExpr | semmle.label | FormatArgsExpr |
25+
| main.rs:65:5:65:26 | FormatArgsExpr | semmle.label | FormatArgsExpr |
26+
| main.rs:66:5:66:25 | FormatArgsExpr | semmle.label | FormatArgsExpr |
27+
| main.rs:67:5:67:24 | FormatArgsExpr | semmle.label | FormatArgsExpr |
2828
subpaths

rust/ql/test/library-tests/format-macros-legacy/inline-taint-flow.expected

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,51 @@
11
models
2-
| 1 | Summary: alloc::fmt::format; Argument[0]; ReturnValue; taint |
2+
| 1 | Summary: <alloc::string::String>::as_str; Argument[self].Reference; ReturnValue.Reference; taint |
3+
| 2 | Summary: alloc::fmt::format; Argument[0]; ReturnValue; taint |
4+
| 3 | Summary: core::fmt::Write::write_fmt; Argument[0]; Argument[self].Reference; taint |
5+
| 4 | Summary: core::fmt::Write::write_str; Argument[0].Reference; Argument[self].Reference; taint |
6+
| 5 | Summary: core::fmt::write; Argument[1]; Argument[0].Reference; taint |
37
edges
48
| main.rs:12:9:12:9 | a | main.rs:13:10:13:25 | FormatArgsExpr | provenance | |
59
| main.rs:12:13:12:21 | source(...) | main.rs:12:9:12:9 | a | provenance | |
610
| main.rs:13:10:13:25 | ...::format(...) | main.rs:13:10:13:25 | MacroExpr | provenance | |
7-
| main.rs:13:10:13:25 | FormatArgsExpr | main.rs:13:10:13:25 | ...::format(...) | provenance | MaD:1 |
11+
| main.rs:13:10:13:25 | FormatArgsExpr | main.rs:13:10:13:25 | ...::format(...) | provenance | MaD:2 |
812
| main.rs:15:9:15:9 | b | main.rs:16:10:16:23 | FormatArgsExpr | provenance | |
913
| main.rs:15:13:15:21 | source(...) | main.rs:15:9:15:9 | b | provenance | |
1014
| main.rs:16:10:16:23 | ...::format(...) | main.rs:16:10:16:23 | MacroExpr | provenance | |
11-
| main.rs:16:10:16:23 | FormatArgsExpr | main.rs:16:10:16:23 | ...::format(...) | provenance | MaD:1 |
15+
| main.rs:16:10:16:23 | FormatArgsExpr | main.rs:16:10:16:23 | ...::format(...) | provenance | MaD:2 |
1216
| main.rs:18:9:18:9 | c | main.rs:19:13:19:36 | FormatArgsExpr | provenance | |
1317
| main.rs:18:13:18:21 | source(...) | main.rs:18:9:18:9 | c | provenance | |
1418
| main.rs:19:9:19:9 | s | main.rs:20:10:20:10 | s | provenance | |
1519
| main.rs:19:13:19:36 | ...::format(...) | main.rs:19:9:19:9 | s | provenance | |
16-
| main.rs:19:13:19:36 | FormatArgsExpr | main.rs:19:13:19:36 | ...::format(...) | provenance | MaD:1 |
20+
| main.rs:19:13:19:36 | FormatArgsExpr | main.rs:19:13:19:36 | ...::format(...) | provenance | MaD:2 |
21+
| main.rs:26:9:26:9 | a | main.rs:27:9:27:9 | b | provenance | |
22+
| main.rs:26:13:26:21 | source(...) | main.rs:26:9:26:9 | a | provenance | |
23+
| main.rs:27:9:27:9 | b | main.rs:28:30:28:30 | b | provenance | |
24+
| main.rs:28:9:28:9 | c | main.rs:29:10:29:10 | c | provenance | |
25+
| main.rs:28:13:28:31 | ...::format(...) | main.rs:28:9:28:9 | c | provenance | |
26+
| main.rs:28:30:28:30 | b | main.rs:28:13:28:31 | ...::format(...) | provenance | MaD:2 |
27+
| main.rs:32:9:32:9 | d | main.rs:33:28:33:28 | d | provenance | |
28+
| main.rs:32:13:32:21 | source(...) | main.rs:32:9:32:9 | d | provenance | |
29+
| main.rs:33:13:33:16 | [post] buf1 | main.rs:34:10:34:13 | buf1 | provenance | |
30+
| main.rs:33:28:33:28 | d | main.rs:33:28:33:37 | d.as_str() [&ref] | provenance | MaD:1 |
31+
| main.rs:33:28:33:37 | d.as_str() [&ref] | main.rs:33:13:33:16 | [post] buf1 | provenance | MaD:4 |
32+
| main.rs:37:9:37:9 | e | main.rs:38:28:38:46 | MacroExpr | provenance | |
33+
| main.rs:37:13:37:21 | source(...) | main.rs:37:9:37:9 | e | provenance | |
34+
| main.rs:38:13:38:16 | [post] buf2 | main.rs:39:10:39:13 | buf2 | provenance | |
35+
| main.rs:38:28:38:46 | MacroExpr | main.rs:38:13:38:16 | [post] buf2 | provenance | MaD:3 |
36+
| main.rs:42:9:42:9 | f | main.rs:43:40:43:58 | MacroExpr | provenance | |
37+
| main.rs:42:13:42:21 | source(...) | main.rs:42:9:42:9 | f | provenance | |
38+
| main.rs:43:29:43:37 | [post] &mut buf3 [&ref] | main.rs:43:34:43:37 | [post] buf3 | provenance | |
39+
| main.rs:43:34:43:37 | [post] buf3 | main.rs:44:10:44:13 | buf3 | provenance | |
40+
| main.rs:43:40:43:58 | MacroExpr | main.rs:43:29:43:37 | [post] &mut buf3 [&ref] | provenance | MaD:5 |
41+
| main.rs:48:9:48:9 | g | main.rs:49:13:49:33 | FormatArgsExpr | provenance | |
42+
| main.rs:48:13:48:21 | source(...) | main.rs:48:9:48:9 | g | provenance | |
43+
| main.rs:49:13:49:33 | FormatArgsExpr | main.rs:49:20:49:23 | [post] buf4 | provenance | MaD:3 |
44+
| main.rs:49:20:49:23 | [post] buf4 | main.rs:50:10:50:13 | buf4 | provenance | |
45+
| main.rs:53:9:53:9 | h | main.rs:54:13:54:33 | FormatArgsExpr | provenance | |
46+
| main.rs:53:13:53:21 | source(...) | main.rs:53:9:53:9 | h | provenance | |
47+
| main.rs:54:13:54:33 | FormatArgsExpr | main.rs:54:22:54:25 | [post] buf5 | provenance | MaD:3 |
48+
| main.rs:54:22:54:25 | [post] buf5 | main.rs:55:10:55:13 | buf5 | provenance | |
1749
nodes
1850
| main.rs:12:9:12:9 | a | semmle.label | a |
1951
| main.rs:12:13:12:21 | source(...) | semmle.label | source(...) |
@@ -31,9 +63,49 @@ nodes
3163
| main.rs:19:13:19:36 | ...::format(...) | semmle.label | ...::format(...) |
3264
| main.rs:19:13:19:36 | FormatArgsExpr | semmle.label | FormatArgsExpr |
3365
| main.rs:20:10:20:10 | s | semmle.label | s |
66+
| main.rs:26:9:26:9 | a | semmle.label | a |
67+
| main.rs:26:13:26:21 | source(...) | semmle.label | source(...) |
68+
| main.rs:27:9:27:9 | b | semmle.label | b |
69+
| main.rs:28:9:28:9 | c | semmle.label | c |
70+
| main.rs:28:13:28:31 | ...::format(...) | semmle.label | ...::format(...) |
71+
| main.rs:28:30:28:30 | b | semmle.label | b |
72+
| main.rs:29:10:29:10 | c | semmle.label | c |
73+
| main.rs:32:9:32:9 | d | semmle.label | d |
74+
| main.rs:32:13:32:21 | source(...) | semmle.label | source(...) |
75+
| main.rs:33:13:33:16 | [post] buf1 | semmle.label | [post] buf1 |
76+
| main.rs:33:28:33:28 | d | semmle.label | d |
77+
| main.rs:33:28:33:37 | d.as_str() [&ref] | semmle.label | d.as_str() [&ref] |
78+
| main.rs:34:10:34:13 | buf1 | semmle.label | buf1 |
79+
| main.rs:37:9:37:9 | e | semmle.label | e |
80+
| main.rs:37:13:37:21 | source(...) | semmle.label | source(...) |
81+
| main.rs:38:13:38:16 | [post] buf2 | semmle.label | [post] buf2 |
82+
| main.rs:38:28:38:46 | MacroExpr | semmle.label | MacroExpr |
83+
| main.rs:39:10:39:13 | buf2 | semmle.label | buf2 |
84+
| main.rs:42:9:42:9 | f | semmle.label | f |
85+
| main.rs:42:13:42:21 | source(...) | semmle.label | source(...) |
86+
| main.rs:43:29:43:37 | [post] &mut buf3 [&ref] | semmle.label | [post] &mut buf3 [&ref] |
87+
| main.rs:43:34:43:37 | [post] buf3 | semmle.label | [post] buf3 |
88+
| main.rs:43:40:43:58 | MacroExpr | semmle.label | MacroExpr |
89+
| main.rs:44:10:44:13 | buf3 | semmle.label | buf3 |
90+
| main.rs:48:9:48:9 | g | semmle.label | g |
91+
| main.rs:48:13:48:21 | source(...) | semmle.label | source(...) |
92+
| main.rs:49:13:49:33 | FormatArgsExpr | semmle.label | FormatArgsExpr |
93+
| main.rs:49:20:49:23 | [post] buf4 | semmle.label | [post] buf4 |
94+
| main.rs:50:10:50:13 | buf4 | semmle.label | buf4 |
95+
| main.rs:53:9:53:9 | h | semmle.label | h |
96+
| main.rs:53:13:53:21 | source(...) | semmle.label | source(...) |
97+
| main.rs:54:13:54:33 | FormatArgsExpr | semmle.label | FormatArgsExpr |
98+
| main.rs:54:22:54:25 | [post] buf5 | semmle.label | [post] buf5 |
99+
| main.rs:55:10:55:13 | buf5 | semmle.label | buf5 |
34100
subpaths
35101
testFailures
36102
#select
37103
| main.rs:13:10:13:25 | MacroExpr | main.rs:12:13:12:21 | source(...) | main.rs:13:10:13:25 | MacroExpr | $@ | main.rs:12:13:12:21 | source(...) | source(...) |
38104
| main.rs:16:10:16:23 | MacroExpr | main.rs:15:13:15:21 | source(...) | main.rs:16:10:16:23 | MacroExpr | $@ | main.rs:15:13:15:21 | source(...) | source(...) |
39105
| main.rs:20:10:20:10 | s | main.rs:18:13:18:21 | source(...) | main.rs:20:10:20:10 | s | $@ | main.rs:18:13:18:21 | source(...) | source(...) |
106+
| main.rs:29:10:29:10 | c | main.rs:26:13:26:21 | source(...) | main.rs:29:10:29:10 | c | $@ | main.rs:26:13:26:21 | source(...) | source(...) |
107+
| main.rs:34:10:34:13 | buf1 | main.rs:32:13:32:21 | source(...) | main.rs:34:10:34:13 | buf1 | $@ | main.rs:32:13:32:21 | source(...) | source(...) |
108+
| main.rs:39:10:39:13 | buf2 | main.rs:37:13:37:21 | source(...) | main.rs:39:10:39:13 | buf2 | $@ | main.rs:37:13:37:21 | source(...) | source(...) |
109+
| main.rs:44:10:44:13 | buf3 | main.rs:42:13:42:21 | source(...) | main.rs:44:10:44:13 | buf3 | $@ | main.rs:42:13:42:21 | source(...) | source(...) |
110+
| main.rs:50:10:50:13 | buf4 | main.rs:48:13:48:21 | source(...) | main.rs:50:10:50:13 | buf4 | $@ | main.rs:48:13:48:21 | source(...) | source(...) |
111+
| main.rs:55:10:55:13 | buf5 | main.rs:53:13:53:21 | source(...) | main.rs:55:10:55:13 | buf5 | $@ | main.rs:53:13:53:21 | source(...) | source(...) |

rust/ql/test/library-tests/format-macros-legacy/main.rs

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ fn source(i: i64) -> String {
66
i.to_string()
77
}
88

9-
fn sink(s: String) {}
9+
fn sink(_s: String) {}
1010

1111
pub fn format_flow() {
1212
let a = source(1);
@@ -21,23 +21,38 @@ pub fn format_flow() {
2121
}
2222

2323
pub fn exercises_reconstruction() {
24-
// No flow assertions here: these exercise reconstruction of the rest of the
25-
// family, including the writer-argument handling of `write!`/`writeln!`.
26-
//
27-
// Flow into a writer buffer is not recovered on <1.94 (the
28-
// `buf.write_fmt(..)` desugaring is absent), but this matches native
29-
// behavior on >=1.94, where the `Write::write_fmt` content-to-self taint
30-
// model is also missing. So it is a pre-existing model gap, not a
31-
// regression from the reconstruction.
32-
let d = source(4);
33-
let _ = format_args!("{}", d);
24+
// these exercise reconstruction of the rest of the family, including the
25+
// writer-argument handling of `write!`/`writeln!`.
26+
let a = source(4);
27+
let b = format_args!("{}", a);
28+
let c = std::fmt::format(b);
29+
sink(c); // $ hasTaintFlow=4
30+
31+
let mut buf1 = String::new();
32+
let d = source(5);
33+
let _ = buf1.write_str(d.as_str());
34+
sink(buf1); // $ hasTaintFlow=5
35+
36+
let mut buf2 = String::new();
37+
let e = source(6);
38+
let _ = buf2.write_fmt(format_args!("{e}"));
39+
sink(buf2); // $ hasTaintFlow=6
40+
41+
let mut buf3 = String::new();
42+
let f = source(7);
43+
let _ = std::fmt::write(&mut buf3, format_args!("{f}"));
44+
sink(buf3); // $ hasTaintFlow=7
3445

3546
use std::fmt::Write;
36-
let mut buf = String::new();
37-
let e = source(5);
38-
let _ = write!(buf, "{}", e);
39-
let _ = writeln!(buf, "{e}");
40-
sink(buf);
47+
let mut buf4 = String::new();
48+
let g = source(8);
49+
let _ = write!(buf4, "{}", g);
50+
sink(buf4); // $ hasTaintFlow=8
51+
52+
let mut buf5 = String::new();
53+
let h = source(9);
54+
let _ = writeln!(buf5, "{h}");
55+
sink(buf5); // $ hasTaintFlow=9
4156
}
4257

4358
// The log-injection sinks (`println!`/`eprintln!`/`panic!`) are reconstructed

0 commit comments

Comments
 (0)