Skip to content

Commit 4c557bf

Browse files
committed
stdbuf: add test verifying that execvp is used
Signed-off-by: Etienne Cordonnier <ecordonnier@snap.com>
1 parent 0110357 commit 4c557bf

File tree

1 file changed

+70
-1
lines changed

1 file changed

+70
-1
lines changed

tests/by-util/test_stdbuf.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
5-
// spell-checker:ignore dyld dylib setvbuf
5+
// spell-checker:ignore cmdline dyld dylib PDEATHSIG setvbuf
66
#[cfg(target_os = "linux")]
77
use uutests::at_and_ucmd;
88
use uutests::new_ucmd;
@@ -247,3 +247,72 @@ fn test_stdbuf_non_utf8_paths() {
247247
.succeeds()
248248
.stdout_is("test content for stdbuf\n");
249249
}
250+
251+
#[test]
252+
#[cfg(target_os = "linux")]
253+
fn test_stdbuf_no_fork_regression() {
254+
// Regression test for issue #9066: https://github.com/uutils/coreutils/issues/9066
255+
// The original stdbuf implementation used fork+spawn which broke signal handling
256+
// and PR_SET_PDEATHSIG. This test verifies that stdbuf uses exec() instead.
257+
// With fork: stdbuf process would remain visible in process list
258+
// With exec: stdbuf process is replaced by target command (GNU compatible)
259+
260+
use std::process::{Command, Stdio};
261+
use std::thread;
262+
use std::time::Duration;
263+
264+
let scene = TestScenario::new(util_name!());
265+
266+
// Start stdbuf with a long-running command
267+
let mut child = Command::new(&scene.bin_path)
268+
.args(["stdbuf", "-o0", "sleep", "3"])
269+
.stdout(Stdio::null())
270+
.stderr(Stdio::null())
271+
.spawn()
272+
.expect("Failed to start stdbuf");
273+
274+
let child_pid = child.id();
275+
276+
// Poll until exec happens or timeout
277+
let cmdline_path = format!("/proc/{child_pid}/cmdline");
278+
let timeout = Duration::from_secs(2);
279+
let poll_interval = Duration::from_millis(10);
280+
let start_time = std::time::Instant::now();
281+
282+
let command_name = loop {
283+
if start_time.elapsed() > timeout {
284+
child.kill().ok();
285+
panic!("TIMEOUT: Process {child_pid} did not respond within {timeout:?}");
286+
}
287+
288+
if let Ok(cmdline) = std::fs::read_to_string(&cmdline_path) {
289+
let cmd_parts: Vec<&str> = cmdline.split('\0').collect();
290+
let name = cmd_parts.first().map_or("", |v| v);
291+
292+
// Wait for exec to complete (process name changes from original binary to target)
293+
// Handle both multicall binary (coreutils) and individual utilities (stdbuf)
294+
if !name.contains("coreutils") && !name.contains("stdbuf") && !name.is_empty() {
295+
break name.to_string();
296+
}
297+
}
298+
299+
thread::sleep(poll_interval);
300+
};
301+
302+
// The loop already waited for exec (no longer original binary), so this should always pass
303+
// But keep the assertion as a safety check and clear documentation
304+
assert!(
305+
!command_name.contains("coreutils") && !command_name.contains("stdbuf"),
306+
"REGRESSION: Process {child_pid} is still original binary (coreutils or stdbuf) - fork() used instead of exec()"
307+
);
308+
309+
// Ensure we're running the expected target command
310+
assert!(
311+
command_name.contains("sleep"),
312+
"Expected 'sleep' command at PID {child_pid}, got: {command_name}"
313+
);
314+
315+
// Cleanup
316+
child.kill().ok();
317+
child.wait().ok();
318+
}

0 commit comments

Comments
 (0)