1use std::env;
16use std::io::IoSlice;
17use std::os::fd::OwnedFd;
18use std::os::unix::fs::{OpenOptionsExt, symlink};
19use std::os::unix::io::AsRawFd;
20use std::os::unix::prelude::RawFd;
21use std::path::{Path, PathBuf};
22
23use nix::sys::socket::{self, UnixAddr};
24use nix::sys::stat::{SFlag, major, minor};
25use nix::sys::statfs::FsType;
26use nix::unistd::{close, dup2};
27
28use crate::syscall::Syscall;
29use crate::utils::{VerifyInodeError, verify_inode};
30
31#[derive(Debug)]
32pub enum StdIO {
33 Stdin = 0,
34 Stdout = 1,
35 Stderr = 2,
36}
37
38impl From<StdIO> for i32 {
39 fn from(value: StdIO) -> Self {
40 match value {
41 StdIO::Stdin => 0,
42 StdIO::Stdout => 1,
43 StdIO::Stderr => 2,
44 }
45 }
46}
47
48impl std::fmt::Display for StdIO {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 StdIO::Stdin => write!(f, "stdin"),
52 StdIO::Stdout => write!(f, "stdout"),
53 StdIO::Stderr => write!(f, "stderr"),
54 }
55 }
56}
57
58#[derive(Debug, thiserror::Error)]
59pub enum TTYError {
60 #[error("failed to connect/duplicate {stdio}")]
61 ConnectStdIO { source: nix::Error, stdio: StdIO },
62 #[error("failed to create console socket")]
63 CreateConsoleSocket {
64 source: nix::Error,
65 socket_name: String,
66 },
67 #[error("failed to symlink console socket into container_dir")]
68 Symlink {
69 source: std::io::Error,
70 linked: Box<PathBuf>,
71 console_socket_path: Box<PathBuf>,
72 },
73 #[error("invalid socket name: {socket_name:?}")]
74 InvalidSocketName {
75 socket_name: String,
76 source: nix::Error,
77 },
78 #[error("failed to create console socket fd")]
79 CreateConsoleSocketFd { source: nix::Error },
80 #[error("could not create pseudo terminal")]
81 CreatePseudoTerminal { source: nix::Error },
82 #[error("failed to send pty master")]
83 SendPtyMaster { source: nix::Error },
84 #[error("could not close console socket")]
85 CloseConsoleSocket { source: nix::Error },
86 #[error("failed to create /dev/console")]
87 CreateDevConsole { source: std::io::Error },
88 #[error("failed to mount pty on /dev/console")]
89 MountConsole {
90 source: crate::syscall::SyscallError,
91 },
92 #[error("invalid PTY device: {reason}")]
93 InvalidPty { reason: String },
94 #[error("stat operation failed")]
95 Stat(#[from] nix::Error),
96}
97
98type Result<T> = std::result::Result<T, TTYError>;
99
100pub fn setup_console_socket(
102 container_dir: &Path,
103 console_socket_path: &Path,
104 socket_name: &str,
105) -> Result<OwnedFd> {
106 struct CurrentDirGuard {
107 path: PathBuf,
108 }
109 impl Drop for CurrentDirGuard {
110 fn drop(&mut self) {
111 let _ = env::set_current_dir(&self.path);
112 }
113 }
114 let prev_dir = env::current_dir().unwrap();
118 let _ = env::set_current_dir(container_dir);
119 let _guard = CurrentDirGuard { path: prev_dir };
120
121 let linked = PathBuf::from(socket_name);
122
123 symlink(console_socket_path, &linked).map_err(|err| TTYError::Symlink {
124 source: err,
125 linked: linked.to_path_buf().into(),
126 console_socket_path: console_socket_path.to_path_buf().into(),
127 })?;
128 let csocketfd = socket::socket(
129 socket::AddressFamily::Unix,
130 socket::SockType::Stream,
131 socket::SockFlag::empty(),
132 None,
133 )
134 .map_err(|err| TTYError::CreateConsoleSocketFd { source: err })?;
135 socket::connect(
136 csocketfd.as_raw_fd(),
137 &socket::UnixAddr::new(linked.as_path()).map_err(|err| TTYError::InvalidSocketName {
138 source: err,
139 socket_name: socket_name.to_string(),
140 })?,
141 )
142 .map_err(|e| TTYError::CreateConsoleSocket {
143 source: e,
144 socket_name: socket_name.to_string(),
145 })?;
146
147 Ok(csocketfd)
148}
149
150const PTMX_MAJOR: u64 = 5;
153const PTMX_MINOR: u64 = 2;
155const PTMX_INO: u64 = 2;
157const PTY_SLAVE_MAJOR: u64 = 136;
159#[cfg(target_env = "musl")]
164const DEVPTS_SUPER_MAGIC: u64 = 0x1cd1;
165#[cfg(not(target_env = "musl"))]
166const DEVPTS_SUPER_MAGIC: i64 = 0x1cd1;
167const PTMX_PATH: &[u8] = b"/dev/ptmx";
169const CONSOLE_PATH: &str = "/dev/console";
171
172fn verify_ptmx_handle(ptmx: &OwnedFd) -> Result<()> {
181 verify_inode(ptmx, |stat, fs_stat| {
182 if fs_stat.filesystem_type() != FsType(DEVPTS_SUPER_MAGIC) {
184 return Err(VerifyInodeError::Verification(format!(
185 "ptmx handle is not on a real devpts mount: super magic is {:#x}",
186 fs_stat.filesystem_type().0
187 )));
188 }
189
190 if stat.st_ino != PTMX_INO {
192 return Err(VerifyInodeError::Verification(format!(
193 "ptmx handle has wrong inode number: {}",
194 stat.st_ino
195 )));
196 }
197
198 let mode_type = SFlag::from_bits_truncate(stat.st_mode) & SFlag::S_IFMT;
200 let dev_major = major(stat.st_rdev);
201 let dev_minor = minor(stat.st_rdev);
202
203 if mode_type != SFlag::S_IFCHR || dev_major != PTMX_MAJOR || dev_minor != PTMX_MINOR {
204 return Err(VerifyInodeError::Verification(format!(
205 "ptmx handle is not a real char ptmx device: ftype {:#x} {}:{}",
206 mode_type.bits(),
207 dev_major,
208 dev_minor
209 )));
210 }
211
212 tracing::debug!(
213 ptmx_fd = ptmx.as_raw_fd(),
214 ino = stat.st_ino,
215 "verified ptmx handle"
216 );
217 Ok(())
218 })
219 .map_err(|e| TTYError::InvalidPty {
220 reason: e.to_string(),
221 })
222}
223
224fn verify_pty_slave(slave: &OwnedFd) -> Result<()> {
232 verify_inode(slave, |stat, fs_stat| {
233 if fs_stat.filesystem_type() != FsType(DEVPTS_SUPER_MAGIC) {
235 return Err(VerifyInodeError::Verification(format!(
236 "slave handle is not on a real devpts mount: super magic is {:#x}",
237 fs_stat.filesystem_type().0
238 )));
239 }
240
241 let mode_type = SFlag::from_bits_truncate(stat.st_mode) & SFlag::S_IFMT;
243 let dev_major = major(stat.st_rdev);
244
245 if mode_type != SFlag::S_IFCHR || dev_major != PTY_SLAVE_MAJOR {
246 return Err(VerifyInodeError::Verification(format!(
247 "slave handle is not a real PTY slave device: ftype {:#x} major {}",
248 mode_type.bits(),
249 dev_major
250 )));
251 }
252
253 tracing::debug!(
254 slave_fd = slave.as_raw_fd(),
255 major = dev_major,
256 minor = minor(stat.st_rdev),
257 "verified PTY slave"
258 );
259 Ok(())
260 })
261 .map_err(|e| TTYError::InvalidPty {
262 reason: e.to_string(),
263 })
264}
265
266pub fn setup_console(syscall: &dyn Syscall, console_fd: RawFd, mount: bool) -> Result<()> {
287 let openpty_result = nix::pty::openpty(None, None)
290 .map_err(|err| TTYError::CreatePseudoTerminal { source: err })?;
291
292 let master = &openpty_result.master;
293 let slave = &openpty_result.slave;
294
295 verify_ptmx_handle(master)?;
297 verify_pty_slave(slave)?;
298
299 if mount {
301 mount_console(syscall, slave)?;
302 }
303
304 let pty_name: &[u8] = PTMX_PATH;
306 let iov = [IoSlice::new(pty_name)];
307 let fds = [master.as_raw_fd()];
308 let cmsg = socket::ControlMessage::ScmRights(&fds);
309 socket::sendmsg::<UnixAddr>(console_fd, &iov, &[cmsg], socket::MsgFlags::empty(), None)
310 .map_err(|err| TTYError::SendPtyMaster { source: err })?;
311
312 if unsafe { libc::ioctl(slave.as_raw_fd(), libc::TIOCSCTTY) } < 0 {
314 tracing::warn!("could not TIOCSCTTY");
315 };
316
317 connect_stdio(&slave.as_raw_fd(), &slave.as_raw_fd(), &slave.as_raw_fd())?;
319
320 close(console_fd).map_err(|err| TTYError::CloseConsoleSocket { source: err })?;
322
323 Ok(())
324}
325
326fn mount_console(syscall: &dyn Syscall, slave: &OwnedFd) -> Result<()> {
336 use std::fs::OpenOptions;
337
338 let console_path = Path::new(CONSOLE_PATH);
339
340 tracing::debug!(
341 slave_fd = slave.as_raw_fd(),
342 "mounting PTY on {CONSOLE_PATH}"
343 );
344
345 OpenOptions::new()
350 .create(true)
351 .write(true)
352 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
353 .mode(0o666)
354 .open(console_path)
355 .map_err(|err| {
356 tracing::error!(?err, "failed to create {CONSOLE_PATH}");
357 TTYError::CreateDevConsole { source: err }
358 })?;
359
360 syscall.mount_from_fd(slave, console_path).map_err(|err| {
363 tracing::error!(
364 ?err,
365 slave_fd = slave.as_raw_fd(),
366 "failed to bind mount pty on {CONSOLE_PATH}"
367 );
368 TTYError::MountConsole { source: err }
369 })?;
370
371 tracing::debug!(
372 slave_fd = slave.as_raw_fd(),
373 "mounted PTY on {CONSOLE_PATH}"
374 );
375 Ok(())
376}
377
378fn connect_stdio(stdin: &RawFd, stdout: &RawFd, stderr: &RawFd) -> Result<()> {
379 dup2(stdin.as_raw_fd(), StdIO::Stdin.into()).map_err(|err| TTYError::ConnectStdIO {
380 source: err,
381 stdio: StdIO::Stdin,
382 })?;
383 dup2(stdout.as_raw_fd(), StdIO::Stdout.into()).map_err(|err| TTYError::ConnectStdIO {
384 source: err,
385 stdio: StdIO::Stdout,
386 })?;
387 dup2(stderr.as_raw_fd(), StdIO::Stderr.into()).map_err(|err| TTYError::ConnectStdIO {
390 source: err,
391 stdio: StdIO::Stderr,
392 })?;
393
394 Ok(())
395}
396
397#[cfg(test)]
398mod tests {
399 use std::fs::File;
400 use std::os::fd::IntoRawFd;
401 use std::os::unix::net::UnixListener;
402
403 use anyhow::{Ok, Result};
404 use serial_test::serial;
405
406 use super::*;
407
408 const CONSOLE_SOCKET: &str = "console-socket";
409
410 #[test]
411 #[serial]
412 fn test_setup_console_socket() -> Result<()> {
413 let testdir = tempfile::tempdir()?;
414 let socket_path = Path::join(testdir.path(), "test-socket");
415 let lis = UnixListener::bind(&socket_path);
416 assert!(lis.is_ok());
417 let fd = setup_console_socket(testdir.path(), &socket_path, CONSOLE_SOCKET)?;
418 assert_ne!(fd.as_raw_fd(), -1);
419 Ok(())
420 }
421
422 #[test]
423 #[serial]
424 fn test_setup_console_socket_empty() -> Result<()> {
425 let testdir = tempfile::tempdir()?;
426 let socket_path = Path::join(testdir.path(), "test-socket");
427 let fd = setup_console_socket(testdir.path(), &socket_path, CONSOLE_SOCKET);
428 assert!(fd.is_err());
429 Ok(())
430 }
431
432 #[test]
433 #[serial]
434 fn test_setup_console_socket_invalid() -> Result<()> {
435 let testdir = tempfile::tempdir()?;
436 let socket_path = Path::join(testdir.path(), "test-socket");
437 let _socket = File::create(Path::join(testdir.path(), "console-socket"));
438 assert!(_socket.is_ok());
439 let fd = setup_console_socket(testdir.path(), &socket_path, CONSOLE_SOCKET);
440 assert!(fd.is_err());
441
442 Ok(())
443 }
444
445 #[test]
446 #[serial]
447 fn test_setup_console() -> Result<()> {
448 use crate::syscall::syscall::create_syscall;
449
450 let testdir = tempfile::tempdir()?;
451 let socket_path = Path::join(testdir.path(), "test-socket");
452
453 let old_stdin: RawFd = nix::unistd::dup(StdIO::Stdin.into())?;
457 let old_stdout: RawFd = nix::unistd::dup(StdIO::Stdout.into())?;
458 let old_stderr: RawFd = nix::unistd::dup(StdIO::Stderr.into())?;
459
460 let lis = UnixListener::bind(&socket_path);
461 assert!(lis.is_ok());
462 let fd = setup_console_socket(testdir.path(), &socket_path, CONSOLE_SOCKET)?;
463 let syscall = create_syscall();
467 let status = setup_console(syscall.as_ref(), fd.into_raw_fd(), false);
468
469 dup2(old_stdin, StdIO::Stdin.into())?;
471 dup2(old_stdout, StdIO::Stdout.into())?;
472 dup2(old_stderr, StdIO::Stderr.into())?;
473
474 assert!(status.is_ok(), "setup_console failed: {:?}", status);
475
476 Ok(())
477 }
478
479 #[test]
480 fn test_verify_pty_slave_with_real_pty() -> Result<()> {
481 let openpty_result = nix::pty::openpty(None, None)
483 .map_err(|e| TTYError::CreatePseudoTerminal { source: e })?;
484
485 let result = verify_pty_slave(&openpty_result.slave);
487 assert!(result.is_ok(), "verify_pty_slave failed: {:?}", result);
488
489 Ok(())
490 }
491
492 #[test]
493 fn test_verify_ptmx_handle_with_real_pty() -> Result<()> {
494 let openpty_result = nix::pty::openpty(None, None)
496 .map_err(|e| TTYError::CreatePseudoTerminal { source: e })?;
497
498 let result = verify_ptmx_handle(&openpty_result.master);
500 assert!(result.is_ok(), "verify_ptmx_handle failed: {:?}", result);
501
502 Ok(())
503 }
504
505 #[test]
506 fn test_verify_ptmx_handle_with_regular_file() {
507 use std::fs::File;
508 use std::os::fd::AsFd;
509
510 use tempfile::tempfile;
511
512 let file: File = tempfile().expect("failed to create tempfile");
514 let fd = file.as_fd().try_clone_to_owned().unwrap();
515
516 let result = verify_ptmx_handle(&fd);
518 assert!(
519 result.is_err(),
520 "verify_ptmx_handle should fail for regular file"
521 );
522
523 if let Err(TTYError::InvalidPty { reason }) = result {
524 assert!(
525 reason.contains("devpts") || reason.contains("inode") || reason.contains("device"),
526 "unexpected error reason: {}",
527 reason
528 );
529 }
530 }
531
532 #[test]
533 fn test_verify_pty_slave_with_regular_file() {
534 use std::fs::File;
535 use std::os::fd::AsFd;
536
537 use tempfile::tempfile;
538
539 let file: File = tempfile().expect("failed to create tempfile");
541 let fd = file.as_fd().try_clone_to_owned().unwrap();
542
543 let result = verify_pty_slave(&fd);
545 assert!(
546 result.is_err(),
547 "verify_pty_slave should fail for regular file"
548 );
549
550 if let Err(TTYError::InvalidPty { reason }) = result {
551 assert!(
552 reason.contains("devpts") || reason.contains("device"),
553 "unexpected error reason: {}",
554 reason
555 );
556 }
557 }
558}