Redirect stdio (pipes and file descriptors)

Hi

I am trying to redirect stdio over pipes. I was able to write data to stdout (the write function at the end of the code snippet) but I cannot seem to get the command "sh" to work as I don't see the prompt.

	let non_block: i32 = 2048;
	let mut sock_flags = nix::sys::socket::SockFlag::from_bits_truncate(non_block);

	let (fd_pipe, fd_pipe2) = nix::sys::socket::socketpair(nix::sys::socket::AddressFamily::Unix, Stream, None, sock_flags).unwrap();

	unsafe
	{
		let message_string = String::from("fd_pipe\0");
		let message = message_string.as_ptr() as *const c_void;

		let mut command_output = std::process::Command::new("/bin/sh")
									.stdin(Stdio::piped())
									.stdout(Stdio::piped())
									.stderr(Stdio::piped())
									.spawn()
									.expect("cannot execute command");

		match command_output.stdin
		{
			None => panic!("cannot get raw fd"),
			Some(standard_io) =>
			{
				println!("standard_io {}",standard_io.as_raw_fd());

				nix::unistd::dup2(tokio::io::stdin().as_raw_fd(),standard_io.as_raw_fd());
				nix::unistd::dup2(tokio::io::stdout().as_raw_fd(),standard_io.as_raw_fd());
				nix::unistd::dup2(tokio::io::stderr().as_raw_fd(),standard_io.as_raw_fd());
				//libc::write(standard_io.as_raw_fd(),message,8);
			},
		}

There won't be any prompt - it will do that only if it's talking to a tty device. Or if you use the "-i" flag.

1 Like

Hi @Donn

I tried adding .arg("-i") but it still does not work.

		let mut command_output = std::process::Command::new("/bin/sh")
									.stdin(Stdio::piped())
									.stdout(Stdio::piped())
									.stderr(Stdio::piped())
									.arg("-i")
									.spawn()
									.expect("cannot execute command");

I guess there must be some problems with the redirection, but strange that the write function was able to write to the pipe.

I don't know these libraries, but ... you know, Bourne shell implementations commonly issue their prompt on stderr, unit 2.

Once you get past this, your problems have only begun, though. The prompt won't be block buffered, luckily, if only because it's on stderr, which is normally line buffered. But most output is normally block buffered, and there's no way to force the process on the other end of the pipe to cough up its buffered text, until it exits. I think that's the main obstacle.

I think you have your arguments in the wrong order. This line will replace the file descriptor representing the process's stdin with the same resource as what is behind your stdin, then it does the same for your stdout, and finally it does the same for your stderr. I also recommend using STDIN_FILENO/STDOUT_FILENO/STDERR_FILENO from nix::libc instead of that.

Hi @Kestrer

My intention was to redirect the execution of command_output into the main stdio of my application, so I wrote it that way.

If I tried to write some data to the file descriptor standard_io.as_raw_fd(), it worked (I could see the message written to the terminal), so that would mean that the redirection is correct? I can see the message displayed, just not the output of the command execution.

Since you replaced standard_io with stderr last, writing to standard_io just writes to stderr, completely bypassing the program.

Hi @Kestrer

My main aim of the program is something similar to this

  • Program executes a command "sh" and creates pipes between the parent process and the child process

  • Output from the child process goes through the pipe (in this case the prompt) to the parent process

  • I type something on my terminal such as "pwd" and this goes into the pipe to the child process

  • Child process executes the command and sends the output back through the pipe to the parent process and displays on my terminal

How should I do this? I am confused by all the redirection and not even sure if I am redirecting correctly.

Eventually, my aim is to have the parent process act as a proxy and forward this I/O through a TLS connection between a remote ncat instance and the child process. For now, I am trying to get the piping part right.

My latest code snippet

		let message_string = String::from("fd_pipe\0");
		let message = message_string.as_ptr() as *const c_void;

		let mut command_output = std::process::Command::new("/bin/sh")
									.stdin(Stdio::piped())
									.stdout(Stdio::piped())
									.stderr(Stdio::piped())
									.spawn()
									.expect("cannot execute command");

		let command_stdin = command_output.stdin.unwrap();
		println!("command_stdin {}", command_stdin.as_raw_fd());
		
		let command_stdout = command_output.stdout.unwrap();
		println!("command_stdout {}", command_stdout.as_raw_fd());
		
		let command_stderr = command_output.stderr.unwrap();
		println!("command_stderr {}", command_stderr.as_raw_fd());

		nix::unistd::dup2(tokio::io::stdin().as_raw_fd(),command_stdin.as_raw_fd());
		nix::unistd::dup2(tokio::io::stdout().as_raw_fd(),command_stdout.as_raw_fd());
		nix::unistd::dup2(tokio::io::stderr().as_raw_fd(),command_stderr.as_raw_fd());

You'll need to spawn multiple threads or use async, as you're copying three things at once. Here is the multithreaded version:

let message_string = String::from("fd_pipe\0");
let message = message_string.as_ptr() as *const c_void;

let mut command_output = std::process::Command::new("/bin/sh")
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .spawn()
    .expect("cannot execute command");

let mut command_stdin = command_output.stdin.unwrap();
println!("command_stdin {}", command_stdin.as_raw_fd());

let copy_stdin_thread = std::thread::spawn(move || {
    io::copy(&mut io::stdin(), &mut command_stdin)
});
		
let mut command_stdout = command_output.stdout.unwrap();
println!("command_stdout {}", command_stdout.as_raw_fd());

let copy_stdout_thread = std::thread::spawn(move || {
   io::copy(&mut command_stdout, &mut io::stdout())
});

let command_stderr = command_output.stderr.unwrap();
println!("command_stderr {}", command_stderr.as_raw_fd());

let copy_stderr_thread = std::thread::spawn(move || {
    io::copy(&mut command_stderr, &mut io::stderr())
});

copy_stdin_thread.join().unwrap()?;
copy_stdout_thread.join().unwrap()?;
copy_stderr_thread.join().unwrap()?;

You may wish to use Stdio::inherit() if you just want to inherit it.

Thanks @Kestrer, it is working now but I am still confused why duplicating the file descriptors did not work?

Could it have worked if I wrote it differently or that was the wrong way to begin with?

dup2(stdin, command_stdin) converts:

Resources | in out err     pipe
          | ^   ^   ^        ^
          | |   |   |        |
FDs       | 0   1   2  command_stdin

Into:

Resources | in out err     pipe
          | ^   ^   ^         
          | |\--|---|--------\
FDs       | 0   1   2  command_stdin

Hi @Kestrer

If I intend to further redirect the stdio over TLS to a remote ncat listener, what crates/functions should I look at?

There are many ways to use TLS in Rust, Rustls is an audited and pure Rust implementation. You should just be able to io::copy the input to the TLS stream.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.