Skip to content

Commit ec7ced2

Browse files
committed
Fuzz the expr command
1 parent bd0fb81 commit ec7ced2

File tree

4 files changed

+228
-0
lines changed

4 files changed

+228
-0
lines changed

.github/workflows/CICD.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ jobs:
149149
## Run it
150150
cd fuzz
151151
cargo +nightly fuzz run fuzz_test -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
152+
- name: Run fuzz_expr for XX seconds
153+
continue-on-error: true
154+
shell: bash
155+
run: |
156+
## Run it
157+
cd fuzz
158+
cargo +nightly fuzz run fuzz_expr -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
152159
- name: Run fuzz_parse_glob for XX seconds
153160
shell: bash
154161
run: |

fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ rand = { version = "0.8", features = ["small_rng"] }
1515
uucore = { path = "../src/uucore/" }
1616
uu_date = { path = "../src/uu/date/" }
1717
uu_test = { path = "../src/uu/test/" }
18+
uu_expr = { path = "../src/uu/expr/" }
1819

1920

2021
# Prevent this from interfering with workspaces
@@ -27,6 +28,12 @@ path = "fuzz_targets/fuzz_date.rs"
2728
test = false
2829
doc = false
2930

31+
[[bin]]
32+
name = "fuzz_expr"
33+
path = "fuzz_targets/fuzz_expr.rs"
34+
test = false
35+
doc = false
36+
3037
[[bin]]
3138
name = "fuzz_test"
3239
path = "fuzz_targets/fuzz_test.rs"

fuzz/fuzz_targets/fuzz_common.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use std::process::Command;
7+
use std::sync::atomic::Ordering;
8+
use std::sync::{atomic::AtomicBool, Once};
9+
10+
static CHECK_GNU: Once = Once::new();
11+
static IS_GNU: AtomicBool = AtomicBool::new(false);
12+
13+
pub fn is_gnu_cmd(cmd_path: &str) -> Result<(), std::io::Error> {
14+
CHECK_GNU.call_once(|| {
15+
let version_output = Command::new(cmd_path).arg("--version").output().unwrap();
16+
17+
println!("version_output {:#?}", version_output);
18+
19+
let version_str = String::from_utf8_lossy(&version_output.stdout).to_string();
20+
if version_str.contains("GNU coreutils") {
21+
IS_GNU.store(true, Ordering::Relaxed);
22+
}
23+
});
24+
25+
if IS_GNU.load(Ordering::Relaxed) {
26+
Ok(())
27+
} else {
28+
panic!("Not the GNU implementation");
29+
}
30+
}

fuzz/fuzz_targets/fuzz_expr.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
// spell-checker:ignore parens
6+
7+
#![no_main]
8+
use libfuzzer_sys::fuzz_target;
9+
use uu_expr::uumain;
10+
11+
use rand::seq::SliceRandom;
12+
use rand::Rng;
13+
use std::ffi::OsString;
14+
15+
use libc::{dup, dup2, STDOUT_FILENO};
16+
use std::process::Command;
17+
mod fuzz_common;
18+
use crate::fuzz_common::is_gnu_cmd;
19+
20+
static CMD_PATH: &str = "expr";
21+
22+
fn run_gnu_expr(args: &[OsString]) -> Result<(String, i32), std::io::Error> {
23+
is_gnu_cmd(CMD_PATH)?; // Check if it's a GNU implementation
24+
25+
let mut command = Command::new(CMD_PATH);
26+
for arg in args {
27+
command.arg(arg);
28+
}
29+
let output = command.output()?;
30+
let exit_code = output.status.code().unwrap_or(-1);
31+
if output.status.success() {
32+
Ok((
33+
String::from_utf8_lossy(&output.stdout).to_string(),
34+
exit_code,
35+
))
36+
} else {
37+
Err(std::io::Error::new(
38+
std::io::ErrorKind::Other,
39+
format!("GNU expr execution failed with exit code {}", exit_code),
40+
))
41+
}
42+
}
43+
44+
fn generate_random_string(max_length: usize) -> String {
45+
let mut rng = rand::thread_rng();
46+
let valid_utf8: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
47+
.chars()
48+
.collect();
49+
let invalid_utf8 = [0xC3, 0x28]; // Invalid UTF-8 sequence
50+
let mut result = String::new();
51+
52+
for _ in 0..rng.gen_range(1..=max_length) {
53+
if rng.gen_bool(0.9) {
54+
let ch = valid_utf8.choose(&mut rng).unwrap();
55+
result.push(*ch);
56+
} else {
57+
let ch = invalid_utf8.choose(&mut rng).unwrap();
58+
if let Some(c) = char::from_u32(*ch as u32) {
59+
result.push(c);
60+
}
61+
}
62+
}
63+
64+
result
65+
}
66+
67+
fn generate_expr(max_depth: u32) -> String {
68+
let mut rng = rand::thread_rng();
69+
let ops = ["+", "-", "*", "/", "%", "<", ">", "=", "&", "|"];
70+
71+
let mut expr = String::new();
72+
let mut depth = 0;
73+
let mut last_was_operator = false;
74+
75+
while depth <= max_depth {
76+
if last_was_operator || depth == 0 {
77+
// Add a number
78+
expr.push_str(&rng.gen_range(1..=100).to_string());
79+
last_was_operator = false;
80+
} else {
81+
// 90% chance to add an operator followed by a number
82+
if rng.gen_bool(0.9) {
83+
let op = *ops.choose(&mut rng).unwrap();
84+
expr.push_str(&format!(" {} ", op));
85+
last_was_operator = true;
86+
}
87+
// 10% chance to add a random string (potentially invalid syntax)
88+
else {
89+
let random_str = generate_random_string(rng.gen_range(1..=10));
90+
expr.push_str(&random_str);
91+
last_was_operator = false;
92+
}
93+
}
94+
depth += 1;
95+
}
96+
97+
// Ensure the expression ends with a number if it ended with an operator
98+
if last_was_operator {
99+
expr.push_str(&rng.gen_range(1..=100).to_string());
100+
}
101+
102+
expr
103+
}
104+
105+
fuzz_target!(|_data: &[u8]| {
106+
let mut rng = rand::thread_rng();
107+
let expr = generate_expr(rng.gen_range(0..=20));
108+
let mut args = vec![OsString::from("expr")];
109+
args.extend(expr.split_whitespace().map(OsString::from));
110+
111+
// Save the original stdout file descriptor
112+
let original_stdout_fd = unsafe { dup(STDOUT_FILENO) };
113+
114+
// Create a pipe to capture stdout
115+
let mut pipe_fds = [-1; 2];
116+
unsafe { libc::pipe(pipe_fds.as_mut_ptr()) };
117+
let uumain_exit_code;
118+
{
119+
// Redirect stdout to the write end of the pipe
120+
unsafe { dup2(pipe_fds[1], STDOUT_FILENO) };
121+
122+
// Run uumain with the provided arguments
123+
uumain_exit_code = uumain(args.clone().into_iter());
124+
125+
// Restore original stdout
126+
unsafe { dup2(original_stdout_fd, STDOUT_FILENO) };
127+
unsafe { libc::close(original_stdout_fd) };
128+
}
129+
// Close the write end of the pipe
130+
unsafe { libc::close(pipe_fds[1]) };
131+
132+
// Read captured output from the read end of the pipe
133+
let mut captured_output = Vec::new();
134+
let mut read_buffer = [0; 1024];
135+
loop {
136+
let bytes_read = unsafe {
137+
libc::read(
138+
pipe_fds[0],
139+
read_buffer.as_mut_ptr() as *mut libc::c_void,
140+
read_buffer.len(),
141+
)
142+
};
143+
if bytes_read <= 0 {
144+
break;
145+
}
146+
captured_output.extend_from_slice(&read_buffer[..bytes_read as usize]);
147+
}
148+
149+
// Close the read end of the pipe
150+
unsafe { libc::close(pipe_fds[0]) };
151+
152+
// Convert captured output to a string
153+
let rust_output = String::from_utf8_lossy(&captured_output)
154+
.to_string()
155+
.trim()
156+
.to_owned();
157+
158+
// Run GNU expr with the provided arguments and compare the output
159+
match run_gnu_expr(&args[1..]) {
160+
Ok((gnu_output, gnu_exit_code)) => {
161+
let gnu_output = gnu_output.trim().to_owned();
162+
if uumain_exit_code != gnu_exit_code {
163+
println!("Expression: {}", expr);
164+
println!("Rust code: {}", uumain_exit_code);
165+
println!("GNU code: {}", gnu_exit_code);
166+
panic!("Different error codes");
167+
}
168+
if rust_output != gnu_output {
169+
println!("Expression: {}", expr);
170+
println!("Rust output: {}", rust_output);
171+
println!("GNU output: {}", gnu_output);
172+
panic!("Different output between Rust & GNU");
173+
} else {
174+
println!(
175+
"Outputs matched for expression: {} => Result: {}",
176+
expr, rust_output
177+
);
178+
}
179+
}
180+
Err(_) => {
181+
println!("GNU expr execution failed for expression: {}", expr);
182+
}
183+
}
184+
});

0 commit comments

Comments
 (0)