Skip to content

Commit 67ed41f

Browse files
authored
Merge pull request #471 from rustcoreutils/copilot/audit-xargs-util
Enforce -I mode argument size limits for xargs
2 parents 0dfb3d8 + 99df609 commit 67ed41f

File tree

2 files changed

+155
-1
lines changed

2 files changed

+155
-1
lines changed

process/tests/xargs/mod.rs

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// SPDX-License-Identifier: MIT
88
//
99

10-
use plib::testing::{run_test, TestPlan};
10+
use plib::testing::{run_test, run_test_with_checker, TestPlan};
1111

1212
fn xargs_test(test_data: &str, expected_output: &str, args: Vec<&str>) {
1313
run_test(TestPlan {
@@ -211,3 +211,141 @@ fn xargs_escaped_chars() {
211211
// Test escaped characters
212212
xargs_test("hello\\ world\n", "hello world\n", vec!["echo"]);
213213
}
214+
215+
#[test]
216+
fn xargs_single_quotes() {
217+
// Test single quote (apostrophe) handling
218+
xargs_test("'hello world'\n", "hello world\n", vec!["echo"]);
219+
}
220+
221+
#[test]
222+
fn xargs_mixed_quotes() {
223+
// Test mix of single and double quotes
224+
xargs_test(
225+
"'single' \"double\" plain\n",
226+
"single double plain\n",
227+
vec!["echo"],
228+
);
229+
}
230+
231+
#[test]
232+
fn xargs_empty_input() {
233+
// Empty input should not execute the utility
234+
run_test(TestPlan {
235+
cmd: String::from("xargs"),
236+
args: vec!["echo".to_string()],
237+
stdin_data: String::from(""),
238+
expected_out: String::from(""),
239+
expected_err: String::from(""),
240+
expected_exit_code: 0,
241+
});
242+
}
243+
244+
#[test]
245+
fn xargs_line_continuation() {
246+
// -L with trailing blank should continue to next line
247+
run_test(TestPlan {
248+
cmd: String::from("xargs"),
249+
args: vec!["-L".to_string(), "1".to_string(), "echo".to_string()],
250+
stdin_data: String::from("one \ntwo\nthree\n"),
251+
expected_out: String::from("one two\nthree\n"),
252+
expected_err: String::from(""),
253+
expected_exit_code: 0,
254+
});
255+
}
256+
257+
#[test]
258+
fn xargs_insert_multiple_replstr() {
259+
// -I with multiple occurrences of replstr in same argument
260+
run_test(TestPlan {
261+
cmd: String::from("xargs"),
262+
args: vec![
263+
"-I".to_string(),
264+
"{}".to_string(),
265+
"echo".to_string(),
266+
"{}-{}-{}".to_string(),
267+
],
268+
stdin_data: String::from("test\n"),
269+
expected_out: String::from("test-test-test\n"),
270+
expected_err: String::from(""),
271+
expected_exit_code: 0,
272+
});
273+
}
274+
275+
#[test]
276+
fn xargs_insert_five_args_with_replstr() {
277+
// -I with replstr in 5 arguments (POSIX requires at least 5)
278+
run_test(TestPlan {
279+
cmd: String::from("xargs"),
280+
args: vec![
281+
"-I".to_string(),
282+
"{}".to_string(),
283+
"echo".to_string(),
284+
"{}".to_string(),
285+
"{}".to_string(),
286+
"{}".to_string(),
287+
"{}".to_string(),
288+
"{}".to_string(),
289+
],
290+
stdin_data: String::from("x\n"),
291+
expected_out: String::from("x x x x x\n"),
292+
expected_err: String::from(""),
293+
expected_exit_code: 0,
294+
});
295+
}
296+
297+
#[test]
298+
fn xargs_combine_n_and_s() {
299+
// Combining -n and -s should work together
300+
run_test(TestPlan {
301+
cmd: String::from("xargs"),
302+
args: vec![
303+
"-n".to_string(),
304+
"2".to_string(),
305+
"-s".to_string(),
306+
"20".to_string(),
307+
"echo".to_string(),
308+
],
309+
stdin_data: String::from("a b c d e\n"),
310+
expected_out: String::from("a b\nc d\ne\n"),
311+
expected_err: String::from(""),
312+
expected_exit_code: 0,
313+
});
314+
}
315+
316+
#[test]
317+
fn xargs_insert_arg_size_limit() {
318+
// -I mode should enforce size limit on constructed arguments
319+
// Create a string that when repeated will exceed 4096 bytes
320+
let long_input = "x".repeat(5000);
321+
let test_plan = TestPlan {
322+
cmd: String::from("xargs"),
323+
args: vec![
324+
"-I".to_string(),
325+
"{}".to_string(),
326+
"echo".to_string(),
327+
"{}".to_string(),
328+
],
329+
stdin_data: format!("{}\n", long_input),
330+
expected_out: String::from(""),
331+
expected_err: String::from(""),
332+
expected_exit_code: 1,
333+
};
334+
335+
// Run with custom checker since error format includes Rust error wrapper
336+
run_test_with_checker(test_plan, |plan, output| {
337+
assert_eq!(output.status.code(), Some(plan.expected_exit_code));
338+
assert_eq!(String::from_utf8_lossy(&output.stdout), plan.expected_out);
339+
// Just verify stderr contains the key message parts
340+
let stderr = String::from_utf8_lossy(&output.stderr);
341+
assert!(
342+
stderr.contains("constructed argument"),
343+
"Expected error about constructed argument"
344+
);
345+
assert!(stderr.contains("5000 bytes"), "Expected size in error");
346+
assert!(
347+
stderr.contains("4096 byte limit"),
348+
"Expected limit in error"
349+
);
350+
});
351+
}

process/xargs.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleC
1717
use plib::BUFSZ;
1818

1919
const FALLBACK_ARG_MAX: usize = 131072;
20+
// POSIX requires at least 255 bytes for -I constructed arguments
21+
// We use a higher limit for better usability
22+
const INSERT_ARG_MAX: usize = 4096;
2023

2124
fn get_arg_max() -> usize {
2225
let result = unsafe { libc::sysconf(libc::_SC_ARG_MAX) };
@@ -536,6 +539,19 @@ fn exec_insert_mode(
536539
.map(|arg| arg.replace(replstr, input_arg))
537540
.collect();
538541

542+
// POSIX: Check that constructed arguments don't exceed the limit
543+
// POSIX requires at least 255 bytes, we use INSERT_ARG_MAX (4096)
544+
if let Some(arg) = util_args.iter().find(|arg| arg.len() > INSERT_ARG_MAX) {
545+
return Err(io::Error::new(
546+
io::ErrorKind::InvalidInput,
547+
format!(
548+
"xargs: constructed argument of {} bytes exceeds {} byte limit in insert mode",
549+
arg.len(),
550+
INSERT_ARG_MAX
551+
),
552+
));
553+
}
554+
539555
exec_util(&args.util, util_args, trace, prompt)
540556
}
541557

0 commit comments

Comments
 (0)