| // Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #![recursion_limit = "512"] | |
| use starnix_core::device::DeviceMode; | |
| use starnix_core::device::kobject::DeviceMetadata; | |
| pub use starnix_core::device::terminal::DEVPTS_COUNT; | |
| use starnix_core::device::terminal::{Terminal, TerminalSide, TtyState, get_device_type_for_pts}; | |
| use starnix_core::fs::sysfs::build_device_directory; | |
| use starnix_core::mm::MemoryAccessorExt; | |
| use starnix_core::task::{CurrentTask, EventHandler, Kernel, WaitCanceler, Waiter}; | |
| use starnix_core::vfs::buffers::{InputBuffer, OutputBuffer}; | |
| use starnix_core::vfs::pseudo::vec_directory::{VecDirectory, VecDirectoryEntry}; | |
| use starnix_core::vfs::{ | |
| CacheMode, DirEntry, DirectoryEntryType, FdFlags, FileHandle, FileObject, FileObjectState, | |
| FileOps, FileSystem, FileSystemHandle, FileSystemOps, FileSystemOptions, FsNode, FsNodeHandle, | |
| FsNodeInfo, FsNodeOps, FsStr, FsString, LookupContext, MountInfo, NamespaceNode, SpecialNode, | |
| SymlinkMode, | |
| }; | |
| use starnix_core::{fileops_impl_nonseekable, fileops_impl_noop_sync, fs_node_impl_dir_readonly}; | |
| use starnix_logging::track_stub; | |
| use starnix_syscalls::{SUCCESS, SyscallArg, SyscallResult}; | |
| use starnix_types::vfs::default_statfs; | |
| use starnix_uapi::auth::FsCred; | |
| use starnix_uapi::device_id::{DeviceId, TTY_ALT_MAJOR}; | |
| use starnix_uapi::errors::Errno; | |
| use starnix_uapi::file_mode::mode; | |
| use starnix_uapi::mount_flags::MountFlags; | |
| use starnix_uapi::open_flags::OpenFlags; | |
| use starnix_uapi::signals::SIGWINCH; | |
| use starnix_uapi::termios::{ | |
| into_termio, into_termios2, termios_from_termios2, termios2_from_termios, | |
| }; | |
| use starnix_uapi::user_address::{UserAddress, UserRef}; | |
| use starnix_uapi::vfs::FdEvents; | |
| use starnix_uapi::{ | |
| DEVPTS_SUPER_MAGIC, FIOASYNC, FIONREAD, FIOQSIZE, TCFLSH, TCGETA, TCGETS, TCGETS2, TCGETX, | |
| TCSBRK, TCSBRKP, TCSETA, TCSETAF, TCSETAW, TCSETS, TCSETS2, TCSETSF, TCSETSF2, TCSETSW, | |
| TCSETSW2, TCSETX, TCSETXF, TCSETXW, TCXONC, TIOCCBRK, TIOCCONS, TIOCEXCL, TIOCGETD, | |
| TIOCGICOUNT, TIOCGLCKTRMIOS, TIOCGPGRP, TIOCGPTLCK, TIOCGPTN, TIOCGPTPEER, TIOCGRS485, | |
| TIOCGSERIAL, TIOCGSID, TIOCGSOFTCAR, TIOCGWINSZ, TIOCLINUX, TIOCMBIC, TIOCMBIS, TIOCMGET, | |
| TIOCMIWAIT, TIOCMSET, TIOCNOTTY, TIOCNXCL, TIOCOUTQ, TIOCPKT, TIOCSBRK, TIOCSCTTY, | |
| TIOCSERCONFIG, TIOCSERGETLSR, TIOCSERGETMULTI, TIOCSERGSTRUCT, TIOCSERGWILD, TIOCSERSETMULTI, | |
| TIOCSERSWILD, TIOCSETD, TIOCSLCKTRMIOS, TIOCSPGRP, TIOCSPTLCK, TIOCSRS485, TIOCSSERIAL, | |
| TIOCSSOFTCAR, TIOCSTI, TIOCSWINSZ, TIOCVHANGUP, errno, error, gid_t, ino_t, pid_t, statfs, | |
| uapi, uid_t, | |
| }; | |
| use std::sync::{Arc, Weak}; | |
| // See https://www.kernel.org/doc/Documentation/admin-guide/devices.txt | |
| const DEVPTS_FIRST_MAJOR: u32 = 136; | |
| const DEVPTS_MAJOR_COUNT: u32 = 4; | |
| // The block size of the node in the devpts file system. Value has been taken from | |
| // https://github.com/google/gvisor/blob/master/test/syscalls/linux/pty.cc | |
| const BLOCK_SIZE: usize = 1024; | |
| // The node identifier of the different node in the devpts filesystem. | |
| const ROOT_NODE_ID: ino_t = 1; | |
| const PTMX_NODE_ID: ino_t = 2; | |
| const FIRST_PTS_NODE_ID: ino_t = 3; | |
| pub fn dev_pts_fs( | |
| current_task: &CurrentTask, | |
| options: FileSystemOptions, | |
| ) -> Result<FileSystemHandle, Errno> { | |
| new_pts_fs(¤t_task.kernel(), options) | |
| } | |
| pub fn new_pts_fs(kernel: &Kernel, options: FileSystemOptions) -> Result<FileSystemHandle, Errno> { | |
| let state = if options.params.get(b"newinstance").is_some() { | |
| Arc::new(TtyState::default()) | |
| } else { | |
| kernel.expando.get::<TtyState>() | |
| }; | |
| new_pts_fs_with_state(kernel, options, state) | |
| } | |
| pub fn new_pts_fs_with_state( | |
| kernel: &Kernel, | |
| options: FileSystemOptions, | |
| state: Arc<TtyState>, | |
| ) -> Result<FileSystemHandle, Errno> { | |
| let parse_octal = |m: &str| u32::from_str_radix(m, 8); | |
| let uid = options.params.get_as::<uid_t>(b"uid")?; | |
| let gid = options.params.get_as::<gid_t>(b"gid")?; | |
| let mode = options.params.get_with(b"mode", parse_octal)?.unwrap_or(0o600); | |
| let ptmxmode = options.params.get_with(b"ptmxmode", parse_octal)?.unwrap_or(0); | |
| let dev_pts_fs = DevPtsFs { state: state.clone(), uid, gid, mode, ptmxmode }; | |
| let fs = FileSystem::new(kernel, CacheMode::Uncached, dev_pts_fs, options) | |
| .expect("devpts filesystem constructed with valid options"); | |
| fs.create_root(ROOT_NODE_ID, DevPtsRootDir { state }); | |
| Ok(fs) | |
| } | |
| /// Creates a terminal and returns the main pty and an associated replica pts. | |
| /// | |
| /// This function assumes that `/dev/ptmx` is the `DevPtmxFile` and that devpts | |
| /// is mounted at `/dev/pts`. These assumptions are necessary so that the | |
| /// `FileHandle` objects returned have appropriate `NamespaceNode` objects. | |
| pub fn create_main_and_replica( | |
| current_task: &CurrentTask, | |
| window_size: uapi::winsize, | |
| ) -> Result<(FileHandle, FileHandle), Errno> { | |
| let pty_file = current_task.open_file("/dev/ptmx".into(), OpenFlags::RDWR)?; | |
| let pty = pty_file.downcast_file::<DevPtmxFile>().ok_or_else(|| errno!(ENOTTY))?; | |
| { | |
| let mut terminal = pty.terminal.write(); | |
| terminal.line_discipline.locked = false; | |
| terminal.line_discipline.window_size = window_size; | |
| } | |
| let pts_path = FsString::from(format!("/dev/pts/{}", pty.terminal.id)); | |
| let pts_file = current_task.open_file(pts_path.as_ref(), OpenFlags::RDWR)?; | |
| Ok((pty_file, pts_file)) | |
| } | |
| pub fn tty_device_init(kernel: &Kernel) -> Result<(), Errno> { | |
| let registry = &kernel.device_registry; | |
| // Register /dev/pts/X device type. | |
| for n in 0..DEVPTS_MAJOR_COUNT { | |
| registry | |
| .register_major( | |
| "pts".into(), | |
| DeviceMode::Char, | |
| DEVPTS_FIRST_MAJOR + n, | |
| open_dev_pts_device, | |
| ) | |
| .expect("can register pts{n} device"); | |
| } | |
| // Register tty and ptmx device types. | |
| kernel | |
| .device_registry | |
| .register_major("/dev/tty".into(), DeviceMode::Char, TTY_ALT_MAJOR, open_dev_pts_device) | |
| .expect("can register tty device"); | |
| let tty_class = registry.objects.tty_class(); | |
| registry.add_major_device( | |
| kernel, | |
| "tty".into(), | |
| DeviceMetadata::new("tty".into(), DeviceId::TTY, DeviceMode::Char), | |
| /* parent = */ None, | |
| tty_class.clone(), | |
| build_device_directory, | |
| ); | |
| registry.add_major_device( | |
| kernel, | |
| "ptmx".into(), | |
| DeviceMetadata::new("ptmx".into(), DeviceId::PTMX, DeviceMode::Char), | |
| /* parent = */ None, | |
| tty_class, | |
| build_device_directory, | |
| ); | |
| Ok(()) | |
| } | |
| struct DevPtsFs { | |
| state: Arc<TtyState>, | |
| uid: Option<uid_t>, | |
| gid: Option<gid_t>, | |
| mode: u32, | |
| ptmxmode: u32, | |
| } | |
| impl FileSystemOps for DevPtsFs { | |
| fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> { | |
| Ok(default_statfs(DEVPTS_SUPER_MAGIC)) | |
| } | |
| fn name(&self) -> &'static FsStr { | |
| "devpts".into() | |
| } | |
| fn uses_external_node_ids(&self) -> bool { | |
| false | |
| } | |
| } | |
| impl DevPtsFs { | |
| fn pty_creds_for(&self, current_task: &CurrentTask) -> FsCred { | |
| let creds = current_task.current_creds(); | |
| let uid = self.uid.unwrap_or_else(|| creds.uid); | |
| let gid = self.gid.unwrap_or_else(|| creds.gid); | |
| FsCred { uid, gid } | |
| } | |
| } | |
| struct DevPtsRootDir { | |
| state: Arc<TtyState>, | |
| } | |
| impl FsNodeOps for DevPtsRootDir { | |
| fs_node_impl_dir_readonly!(); | |
| fn create_file_ops( | |
| &self, | |
| _node: &FsNode, | |
| _current_task: &CurrentTask, | |
| _flags: OpenFlags, | |
| ) -> Result<Box<dyn FileOps>, Errno> { | |
| let mut result = vec![]; | |
| result.push(VecDirectoryEntry { | |
| entry_type: DirectoryEntryType::CHR, | |
| name: "ptmx".into(), | |
| inode: Some(PTMX_NODE_ID), | |
| }); | |
| for (id, terminal) in self.state.terminals.read().iter() { | |
| if let Some(terminal) = terminal.upgrade() { | |
| if !terminal.read().is_main_closed() { | |
| result.push(VecDirectoryEntry { | |
| entry_type: DirectoryEntryType::CHR, | |
| name: format!("{id}").into(), | |
| inode: Some((*id as ino_t) + FIRST_PTS_NODE_ID), | |
| }); | |
| } | |
| } | |
| } | |
| Ok(VecDirectory::new_file(result)) | |
| } | |
| fn lookup( | |
| &self, | |
| entry: &DirEntry, | |
| _current_task: &CurrentTask, | |
| name: &FsStr, | |
| ) -> Result<FsNodeHandle, Errno> { | |
| let fs = entry.node.fs(); | |
| let devptsfs = | |
| fs.downcast_ops::<DevPtsFs>().expect("DevPts should only handle `DevPtsFs`s"); | |
| let name = std::str::from_utf8(name).map_err(|_| errno!(ENOENT))?; | |
| if name == "ptmx" { | |
| let mut info = FsNodeInfo::new(mode!(IFCHR, devptsfs.ptmxmode), FsCred::root()); | |
| info.rdev = DeviceId::PTMX; | |
| info.blksize = BLOCK_SIZE; | |
| let node = fs.create_node(PTMX_NODE_ID, SpecialNode, info); | |
| return Ok(node); | |
| } | |
| if let Ok(id) = name.parse::<u32>() { | |
| let terminal = self.state.terminals.read().get(&id).and_then(Weak::upgrade); | |
| if let Some(terminal) = terminal { | |
| if !terminal.read().is_main_closed() { | |
| let ino = (id as ino_t) + FIRST_PTS_NODE_ID; | |
| let mut info = | |
| FsNodeInfo::new(mode!(IFCHR, devptsfs.mode), terminal.fscred.clone()); | |
| info.rdev = get_device_type_for_pts(id); | |
| info.blksize = BLOCK_SIZE; | |
| let node = fs.create_node(ino, SpecialNode, info); | |
| return Ok(node); | |
| } | |
| } | |
| } | |
| error!(ENOENT) | |
| } | |
| } | |
| fn open_dev_pts_device( | |
| current_task: &CurrentTask, | |
| id: DeviceId, | |
| node: &NamespaceNode, | |
| flags: OpenFlags, | |
| ) -> Result<Box<dyn FileOps>, Errno> { | |
| match id { | |
| // /dev/ptmx and /dev/pts/ptmx | |
| DeviceId::PTMX => { | |
| let fs = node.entry.node.fs(); | |
| let Some(devpts_fs) = fs.downcast_ops::<DevPtsFs>() else { | |
| // The device is not in ptmx, let try to find something at pts/ptmx relative to | |
| // the parent of the node | |
| let parent = node.parent().ok_or_else(|| errno!(EINVAL))?; | |
| let mut lookup_context = LookupContext::new(SymlinkMode::Follow); | |
| let ptmx_node = | |
| current_task.lookup_path(&mut lookup_context, parent, "pts/ptmx".into())?; | |
| return open_dev_pts_device(current_task, id, &ptmx_node, flags); | |
| }; | |
| let creds = devpts_fs.pty_creds_for(current_task); | |
| let terminal = devpts_fs.state.get_next_terminal(fs.root().clone(), creds)?; | |
| let name = FsString::from(terminal.id.to_string()); | |
| let replica_dir_entry = | |
| fs.root().component_lookup(current_task, &MountInfo::detached(), name.as_ref())?; | |
| let replica_node = | |
| NamespaceNode { mount: node.mount.clone(), entry: replica_dir_entry }; | |
| Ok(Box::new(DevPtmxFile::new(terminal, Some(replica_node)))) | |
| } | |
| // /dev/tty | |
| DeviceId::TTY => { | |
| let controlling_terminal = current_task.thread_group().controlling_terminal(); | |
| if let Some(controlling_terminal) = controlling_terminal { | |
| match controlling_terminal.side { | |
| TerminalSide::Main => { | |
| Ok(Box::new(DevPtmxFile::new(controlling_terminal.terminal, None))) | |
| } | |
| TerminalSide::Replica => { | |
| Ok(Box::new(TtyFile::new(controlling_terminal.terminal))) | |
| } | |
| } | |
| } else { | |
| error!(ENXIO) | |
| } | |
| } | |
| _ if id.major() < DEVPTS_FIRST_MAJOR | |
| || id.major() >= DEVPTS_FIRST_MAJOR + DEVPTS_MAJOR_COUNT => | |
| { | |
| error!(ENODEV) | |
| } | |
| // /dev/pts/?? | |
| _ => { | |
| let fs = node.entry.node.fs(); | |
| let Some(devpts_fs) = fs.downcast_ops::<DevPtsFs>() else { | |
| return error!(ENOTSUP); | |
| }; | |
| let pts_id = (id.major() - DEVPTS_FIRST_MAJOR) * 256 + id.minor(); | |
| let terminal = devpts_fs | |
| .state | |
| .terminals | |
| .read() | |
| .get(&pts_id) | |
| .and_then(Weak::upgrade) | |
| .ok_or_else(|| errno!(EIO))?; | |
| if terminal.read().line_discipline.locked { | |
| return error!(EIO); | |
| } | |
| if !flags.contains(OpenFlags::NOCTTY) { | |
| // Opening a replica sets the process' controlling TTY when possible. An error indicates it cannot | |
| // be set, and is ignored silently. | |
| let _ = current_task.thread_group().set_controlling_terminal( | |
| current_task, | |
| &terminal, | |
| TerminalSide::Replica, | |
| false, /* steal */ | |
| flags.can_read(), | |
| ); | |
| } | |
| Ok(Box::new(TtyFile::new(terminal))) | |
| } | |
| } | |
| } | |
| struct DevPtmxFile { | |
| terminal: Arc<Terminal>, | |
| /// The replica's NamespaceNode, used to implement `TIOCGPTPEER` when opened via `/dev/ptmx`. | |
| /// It is `None` if opened via `/dev/tty` redirecting to the main terminal. On Linux, `/dev/tty` | |
| /// always redirects to the replica PTY (even if `TIOCSCTTY` is called on the main PTY), where | |
| /// `TIOCGPTPEER` is unsupported. | |
| replica_node: Option<NamespaceNode>, | |
| } | |
| impl DevPtmxFile { | |
| pub fn new(terminal: Arc<Terminal>, replica_node: Option<NamespaceNode>) -> Self { | |
| terminal.main_open(); | |
| Self { terminal, replica_node } | |
| } | |
| } | |
| impl FileOps for DevPtmxFile { | |
| fileops_impl_nonseekable!(); | |
| fileops_impl_noop_sync!(); | |
| fn close(self: Box<Self>, _file: &FileObjectState, _current_task: &CurrentTask) { | |
| self.terminal.disassociate_controlling_session(); | |
| self.terminal.main_close(); | |
| } | |
| fn read( | |
| &self, | |
| file: &FileObject, | |
| current_task: &CurrentTask, | |
| offset: usize, | |
| data: &mut dyn OutputBuffer, | |
| ) -> Result<usize, Errno> { | |
| debug_assert!(offset == 0); | |
| file.blocking_op(current_task, FdEvents::POLLIN | FdEvents::POLLHUP, None, || { | |
| self.terminal.main_read(data) | |
| }) | |
| } | |
| fn write( | |
| &self, | |
| file: &FileObject, | |
| current_task: &CurrentTask, | |
| offset: usize, | |
| data: &mut dyn InputBuffer, | |
| ) -> Result<usize, Errno> { | |
| debug_assert!(offset == 0); | |
| file.blocking_op(current_task, FdEvents::POLLOUT | FdEvents::POLLHUP, None, || { | |
| self.terminal.main_write(data) | |
| }) | |
| } | |
| fn wait_async( | |
| &self, | |
| _file: &FileObject, | |
| _current_task: &CurrentTask, | |
| waiter: &Waiter, | |
| events: FdEvents, | |
| handler: EventHandler, | |
| ) -> Option<WaitCanceler> { | |
| Some(self.terminal.main_wait_async(waiter, events, handler)) | |
| } | |
| fn query_events( | |
| &self, | |
| _file: &FileObject, | |
| _current_task: &CurrentTask, | |
| ) -> Result<FdEvents, Errno> { | |
| Ok(self.terminal.main_query_events()) | |
| } | |
| fn ioctl( | |
| &self, | |
| file: &FileObject, | |
| current_task: &CurrentTask, | |
| request: u32, | |
| arg: SyscallArg, | |
| ) -> Result<SyscallResult, Errno> { | |
| let user_addr = UserAddress::from(arg); | |
| match request { | |
| TIOCGPTN => { | |
| // Get the terminal id. | |
| let value: u32 = self.terminal.id; | |
| current_task.write_object(UserRef::<u32>::new(user_addr), &value)?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCGPTLCK => { | |
| // Get the lock status. | |
| let value = i32::from(self.terminal.read().line_discipline.locked); | |
| current_task.write_object(UserRef::<i32>::new(user_addr), &value)?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCSPTLCK => { | |
| // Lock/Unlock the terminal. | |
| let value = current_task.read_object(UserRef::<i32>::new(user_addr))?; | |
| self.terminal.write().line_discipline.locked = value != 0; | |
| Ok(SUCCESS) | |
| } | |
| TIOCGPTPEER => { | |
| let Some(replica_node) = &self.replica_node else { | |
| return error!(ENOTTY); | |
| }; | |
| if replica_node.mount.flags().contains(MountFlags::NODEV) { | |
| return error!(EACCES); | |
| } | |
| let flags = OpenFlags::from_bits_truncate(u32::from(arg)); | |
| let replica_file = replica_node.open(current_task, flags)?; | |
| let fd_flags = if flags.contains(OpenFlags::CLOEXEC) { | |
| FdFlags::CLOEXEC | |
| } else { | |
| FdFlags::empty() | |
| }; | |
| let fd = current_task.add_file(replica_file, fd_flags)?; | |
| Ok(fd.into()) | |
| } | |
| _ => shared_ioctl(&self.terminal, true, file, current_task, request, arg), | |
| } | |
| } | |
| } | |
| pub struct TtyFile { | |
| terminal: Arc<Terminal>, | |
| } | |
| impl TtyFile { | |
| pub fn new(terminal: Arc<Terminal>) -> Self { | |
| terminal.replica_open(); | |
| Self { terminal } | |
| } | |
| } | |
| impl FileOps for TtyFile { | |
| fileops_impl_nonseekable!(); | |
| fileops_impl_noop_sync!(); | |
| fn close(self: Box<Self>, _file: &FileObjectState, _current_task: &CurrentTask) { | |
| self.terminal.replica_close(); | |
| } | |
| fn read( | |
| &self, | |
| file: &FileObject, | |
| current_task: &CurrentTask, | |
| offset: usize, | |
| data: &mut dyn OutputBuffer, | |
| ) -> Result<usize, Errno> { | |
| debug_assert!(offset == 0); | |
| file.blocking_op(current_task, FdEvents::POLLIN | FdEvents::POLLHUP, None, || { | |
| self.terminal.replica_read(data) | |
| }) | |
| } | |
| fn write( | |
| &self, | |
| file: &FileObject, | |
| current_task: &CurrentTask, | |
| offset: usize, | |
| data: &mut dyn InputBuffer, | |
| ) -> Result<usize, Errno> { | |
| debug_assert!(offset == 0); | |
| file.blocking_op(current_task, FdEvents::POLLOUT | FdEvents::POLLHUP, None, || { | |
| self.terminal.replica_write(data) | |
| }) | |
| } | |
| fn wait_async( | |
| &self, | |
| _file: &FileObject, | |
| _current_task: &CurrentTask, | |
| waiter: &Waiter, | |
| events: FdEvents, | |
| handler: EventHandler, | |
| ) -> Option<WaitCanceler> { | |
| Some(self.terminal.replica_wait_async(waiter, events, handler)) | |
| } | |
| fn query_events( | |
| &self, | |
| _file: &FileObject, | |
| _current_task: &CurrentTask, | |
| ) -> Result<FdEvents, Errno> { | |
| Ok(self.terminal.replica_query_events()) | |
| } | |
| fn ioctl( | |
| &self, | |
| file: &FileObject, | |
| current_task: &CurrentTask, | |
| request: u32, | |
| arg: SyscallArg, | |
| ) -> Result<SyscallResult, Errno> { | |
| shared_ioctl(&self.terminal, false, file, current_task, request, arg) | |
| } | |
| } | |
| /// The ioctl behaviour common to main and replica terminal file descriptors. | |
| fn shared_ioctl( | |
| terminal: &Terminal, | |
| is_main: bool, | |
| file: &FileObject, | |
| current_task: &CurrentTask, | |
| request: u32, | |
| arg: SyscallArg, | |
| ) -> Result<SyscallResult, Errno> { | |
| let user_addr = UserAddress::from(arg); | |
| match request { | |
| FIONREAD => { | |
| // Get the main terminal available bytes for reading. | |
| let value = terminal.read().get_available_read_size(is_main) as u32; | |
| current_task.write_object(UserRef::<u32>::new(user_addr), &value)?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCSCTTY => { | |
| // Make the given terminal the controlling terminal of the calling process. | |
| let steal = bool::from(arg); | |
| current_task.thread_group().set_controlling_terminal( | |
| current_task, | |
| terminal, | |
| TerminalSide::from(is_main), | |
| steal, | |
| file.can_read(), | |
| )?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCNOTTY => { | |
| // Release the controlling terminal. | |
| current_task.thread_group().release_controlling_terminal( | |
| current_task, | |
| terminal, | |
| TerminalSide::from(is_main), | |
| )?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCGPGRP => { | |
| // Get the foreground process group. | |
| let pgid = current_task.thread_group().get_foreground_process_group(terminal)?; | |
| current_task.write_object(UserRef::<pid_t>::new(user_addr), &pgid)?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCSPGRP => { | |
| // Set the foreground process group. | |
| let pgid = current_task.read_object(UserRef::<pid_t>::new(user_addr))?; | |
| if pgid < 0 { | |
| return error!(EINVAL); | |
| } | |
| let pgid = current_task.kernel().pids.get(pgid)?; | |
| current_task.thread_group().set_foreground_process_group( | |
| current_task, | |
| terminal, | |
| &pgid, | |
| )?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCGWINSZ => { | |
| // Get the window size | |
| current_task.write_object( | |
| UserRef::<uapi::winsize>::new(user_addr), | |
| &terminal.read().line_discipline.window_size, | |
| )?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCSWINSZ => { | |
| // If the window is already the required size, do nothing. | |
| let new_winsize = current_task.read_object(UserRef::<uapi::winsize>::new(user_addr))?; | |
| if terminal.read().line_discipline.window_size == new_winsize { | |
| return Ok(SUCCESS); | |
| } | |
| // Set the window size | |
| terminal.write().line_discipline.window_size = new_winsize; | |
| // Send a SIGWINCH signal to the foreground process group. | |
| terminal.send_signals(&[SIGWINCH]); | |
| Ok(SUCCESS) | |
| } | |
| TCGETA => { | |
| let termio = into_termio(terminal.read().termios()); | |
| current_task.write_object(UserRef::<uapi::termio>::new(user_addr), &termio)?; | |
| Ok(SUCCESS) | |
| } | |
| TCGETS => { | |
| // N.B. TCGETS on the main terminal actually returns the configuration of the replica | |
| // end. | |
| let termios = termios_from_termios2(terminal.read().termios()); | |
| current_task.write_object(UserRef::<uapi::termios>::new(user_addr), &termios)?; | |
| Ok(SUCCESS) | |
| } | |
| TCGETS2 => { | |
| current_task.write_object( | |
| UserRef::<uapi::termios2>::new(user_addr), | |
| terminal.read().termios(), | |
| )?; | |
| Ok(SUCCESS) | |
| } | |
| TCSETA => { | |
| let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?; | |
| terminal.set_termios(into_termios2(termio)); | |
| Ok(SUCCESS) | |
| } | |
| TCSETS => { | |
| // N.B. TCSETS on the main terminal actually affects the configuration of the replica | |
| // end. | |
| let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?; | |
| terminal.set_termios(termios2_from_termios(&termios)); | |
| Ok(SUCCESS) | |
| } | |
| TCSETS2 => { | |
| let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?; | |
| terminal.set_termios(termios2); | |
| Ok(SUCCESS) | |
| } | |
| TCSETAF => { | |
| // This should drain the output queue and discard the pending input first. | |
| let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?; | |
| terminal.set_termios(into_termios2(termio)); | |
| Ok(SUCCESS) | |
| } | |
| TCSETSF => { | |
| // This should drain the output queue and discard the pending input first. | |
| let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?; | |
| terminal.set_termios(termios2_from_termios(&termios)); | |
| Ok(SUCCESS) | |
| } | |
| TCSETSF2 => { | |
| // This should drain the output queue and discard the pending input first. | |
| let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?; | |
| terminal.set_termios(termios2); | |
| Ok(SUCCESS) | |
| } | |
| TCSETAW => { | |
| track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETAW drain output queue first"); | |
| let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?; | |
| terminal.set_termios(into_termios2(termio)); | |
| Ok(SUCCESS) | |
| } | |
| TCSETSW => { | |
| track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETSW drain output queue first"); | |
| let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?; | |
| terminal.set_termios(termios2_from_termios(&termios)); | |
| Ok(SUCCESS) | |
| } | |
| TCSETSW2 => { | |
| track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETSW2 drain output queue first"); | |
| let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?; | |
| terminal.set_termios(termios2); | |
| Ok(SUCCESS) | |
| } | |
| TIOCSETD => { | |
| track_stub!( | |
| TODO("https://fxbug.dev/322874060"), | |
| "devpts setting line discipline", | |
| is_main | |
| ); | |
| error!(EINVAL) | |
| } | |
| TCSBRK => Ok(SUCCESS), | |
| TCXONC => { | |
| track_stub!(TODO("https://fxbug.dev/322892912"), "devpts ioctl TCXONC", is_main); | |
| error!(ENOSYS) | |
| } | |
| TCFLSH => { | |
| terminal.flush(is_main, u32::from(arg))?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCEXCL => { | |
| track_stub!(TODO("https://fxbug.dev/322893449"), "devpts ioctl TIOCEXCL", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCNXCL => { | |
| track_stub!(TODO("https://fxbug.dev/322893393"), "devpts ioctl TIOCNXCL", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCOUTQ => { | |
| track_stub!(TODO("https://fxbug.dev/322893723"), "devpts ioctl TIOCOUTQ", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCSTI => { | |
| track_stub!(TODO("https://fxbug.dev/322893780"), "devpts ioctl TIOCSTI", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCMGET => { | |
| track_stub!(TODO("https://fxbug.dev/322893681"), "devpts ioctl TIOCMGET", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCMBIS => { | |
| track_stub!(TODO("https://fxbug.dev/322893709"), "devpts ioctl TIOCMBIS", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCMBIC => { | |
| track_stub!(TODO("https://fxbug.dev/322893610"), "devpts ioctl TIOCMBIC", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCMSET => { | |
| track_stub!(TODO("https://fxbug.dev/322893211"), "devpts ioctl TIOCMSET", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCGSOFTCAR => { | |
| track_stub!(TODO("https://fxbug.dev/322893365"), "devpts ioctl TIOCGSOFTCAR", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCSSOFTCAR => { | |
| track_stub!(TODO("https://fxbug.dev/322894074"), "devpts ioctl TIOCSSOFTCAR", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCLINUX => { | |
| track_stub!(TODO("https://fxbug.dev/322893147"), "devpts ioctl TIOCLINUX", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCCONS => { | |
| track_stub!(TODO("https://fxbug.dev/322893267"), "devpts ioctl TIOCCONS", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCGSERIAL => { | |
| track_stub!(TODO("https://fxbug.dev/322893503"), "devpts ioctl TIOCGSERIAL", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCSSERIAL => { | |
| track_stub!(TODO("https://fxbug.dev/322893663"), "devpts ioctl TIOCSSERIAL", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCPKT => { | |
| if !is_main { | |
| return error!(ENOTTY); | |
| } | |
| let value = current_task.read_object(UserRef::<i32>::new(user_addr))?; | |
| terminal.write().set_packet_mode(value != 0); | |
| Ok(SUCCESS) | |
| } | |
| TIOCGETD => { | |
| track_stub!(TODO("https://fxbug.dev/322893974"), "devpts ioctl TIOCGETD", is_main); | |
| error!(ENOSYS) | |
| } | |
| TCSBRKP => Ok(SUCCESS), | |
| TIOCSBRK => { | |
| track_stub!(TODO("https://fxbug.dev/322893936"), "devpts ioctl TIOCSBRK", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCCBRK => { | |
| track_stub!(TODO("https://fxbug.dev/322893213"), "devpts ioctl TIOCCBRK", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCGSID => { | |
| if !is_main | |
| && !current_task | |
| .thread_group() | |
| .controlling_terminal() | |
| .as_ref() | |
| .is_some_and(|ct| std::ptr::eq(terminal, Arc::as_ptr(&ct.terminal))) | |
| { | |
| return error!(ENOTTY); | |
| } | |
| let sid = terminal | |
| .read() | |
| .controlling_session() | |
| .map(|sid| sid.id) | |
| .ok_or_else(|| errno!(ENOTTY))?; | |
| current_task.write_object(UserRef::<pid_t>::new(user_addr), &sid)?; | |
| Ok(SUCCESS) | |
| } | |
| TIOCGRS485 => { | |
| track_stub!(TODO("https://fxbug.dev/322893728"), "devpts ioctl TIOCGRS485", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCSRS485 => { | |
| track_stub!(TODO("https://fxbug.dev/322893783"), "devpts ioctl TIOCSRS485", is_main); | |
| error!(ENOSYS) | |
| } | |
| TCGETX => { | |
| track_stub!(TODO("https://fxbug.dev/322893327"), "devpts ioctl TCGETX", is_main); | |
| error!(ENOSYS) | |
| } | |
| TCSETX => { | |
| track_stub!(TODO("https://fxbug.dev/322893741"), "devpts ioctl TCSETX", is_main); | |
| error!(ENOSYS) | |
| } | |
| TCSETXF => { | |
| track_stub!(TODO("https://fxbug.dev/322893937"), "devpts ioctl TCSETXF", is_main); | |
| error!(ENOSYS) | |
| } | |
| TCSETXW => { | |
| track_stub!(TODO("https://fxbug.dev/322893899"), "devpts ioctl TCSETXW", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCVHANGUP => { | |
| track_stub!(TODO("https://fxbug.dev/322893742"), "devpts ioctl TIOCVHANGUP", is_main); | |
| error!(ENOSYS) | |
| } | |
| FIOASYNC => { | |
| track_stub!(TODO("https://fxbug.dev/322893269"), "devpts ioctl FIOASYNC", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCSERCONFIG => { | |
| track_stub!(TODO("https://fxbug.dev/322893881"), "devpts ioctl TIOCSERCONFIG", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCSERGWILD => { | |
| track_stub!(TODO("https://fxbug.dev/322893686"), "devpts ioctl TIOCSERGWILD", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCSERSWILD => { | |
| track_stub!(TODO("https://fxbug.dev/322893837"), "devpts ioctl TIOCSERSWILD", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCGLCKTRMIOS => { | |
| track_stub!( | |
| TODO("https://fxbug.dev/322894114"), | |
| "devpts ioctl TIOCGLCKTRMIOS", | |
| is_main | |
| ); | |
| error!(ENOSYS) | |
| } | |
| TIOCSLCKTRMIOS => { | |
| track_stub!( | |
| TODO("https://fxbug.dev/322893711"), | |
| "devpts ioctl TIOCSLCKTRMIOS", | |
| is_main | |
| ); | |
| error!(ENOSYS) | |
| } | |
| TIOCSERGSTRUCT => { | |
| track_stub!( | |
| TODO("https://fxbug.dev/322893828"), | |
| "devpts ioctl TIOCSERGSTRUCT", | |
| is_main | |
| ); | |
| error!(ENOSYS) | |
| } | |
| TIOCSERGETLSR => { | |
| track_stub!(TODO("https://fxbug.dev/322894083"), "devpts ioctl TIOCSERGETLSR", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCSERGETMULTI => { | |
| track_stub!( | |
| TODO("https://fxbug.dev/322893962"), | |
| "devpts ioctl TIOCSERGETMULTI", | |
| is_main | |
| ); | |
| error!(ENOSYS) | |
| } | |
| TIOCSERSETMULTI => { | |
| track_stub!( | |
| TODO("https://fxbug.dev/322893273"), | |
| "devpts ioctl TIOCSERSETMULTI", | |
| is_main | |
| ); | |
| error!(ENOSYS) | |
| } | |
| TIOCMIWAIT => { | |
| track_stub!(TODO("https://fxbug.dev/322894005"), "devpts ioctl TIOCMIWAIT", is_main); | |
| error!(ENOSYS) | |
| } | |
| TIOCGICOUNT => { | |
| track_stub!(TODO("https://fxbug.dev/322893862"), "devpts ioctl TIOCGICOUNT", is_main); | |
| error!(ENOSYS) | |
| } | |
| FIOQSIZE => { | |
| track_stub!(TODO("https://fxbug.dev/322893770"), "devpts ioctl FIOQSIZE", is_main); | |
| error!(ENOSYS) | |
| } | |
| other => { | |
| track_stub!(TODO("https://fxbug.dev/322893712"), "devpts unknown ioctl", other); | |
| error!(ENOTTY) | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use crate::tty_device_init; | |
| use starnix_core::fs::tmpfs::TmpFs; | |
| use starnix_core::task::{ExitStatus, KernelFeatures}; | |
| use starnix_core::testing::*; | |
| use starnix_core::vfs::buffers::{VecInputBuffer, VecOutputBuffer}; | |
| use starnix_core::vfs::fs_args::MountParams; | |
| use starnix_core::vfs::{MountInfo, NamespaceNode, OpenAccessCheck}; | |
| use starnix_types::ownership::Releasable; | |
| use starnix_uapi::auth::Credentials; | |
| use starnix_uapi::file_mode::FileMode; | |
| use starnix_uapi::signals::{ | |
| SIGCHLD, SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTSTP, SIGTTOU, SigSet, | |
| }; | |
| use std::sync::atomic::{AtomicBool, Ordering}; | |
| fn new_pts_fs(kernel: &Kernel) -> FileSystemHandle { | |
| let mut options = FileSystemOptions::default(); | |
| options.params = MountParams::parse("ptmxmode=666".into()).expect("parse option"); | |
| super::new_pts_fs(&kernel, options).expect("create new_pts_fs") | |
| } | |
| fn ioctl<T: zerocopy::IntoBytes + zerocopy::FromBytes + zerocopy::Immutable + Copy>( | |
| current_task: &CurrentTask, | |
| file: &FileHandle, | |
| command: u32, | |
| value: &T, | |
| ) -> Result<T, Errno> { | |
| let address = | |
| map_memory(current_task, UserAddress::default(), std::mem::size_of::<T>() as u64); | |
| let address_ref = UserRef::<T>::new(address); | |
| current_task.write_object(address_ref, value)?; | |
| file.ioctl(current_task, command, address.into())?; | |
| current_task.read_object(address_ref) | |
| } | |
| fn set_controlling_terminal( | |
| current_task: &CurrentTask, | |
| file: &FileHandle, | |
| steal: bool, | |
| ) -> Result<SyscallResult, Errno> { | |
| #[allow(clippy::bool_to_int_with_if)] | |
| file.ioctl(current_task, TIOCSCTTY, steal.into()) | |
| } | |
| fn lookup_node( | |
| task: &CurrentTask, | |
| fs: &FileSystemHandle, | |
| name: &FsStr, | |
| ) -> Result<NamespaceNode, Errno> { | |
| let root = NamespaceNode::new_anonymous(fs.root().clone()); | |
| root.lookup_child(task, &mut Default::default(), name) | |
| } | |
| fn open_file_with_flags( | |
| current_task: &CurrentTask, | |
| fs: &FileSystemHandle, | |
| name: &FsStr, | |
| flags: OpenFlags, | |
| ) -> Result<FileHandle, Errno> { | |
| let node = lookup_node(current_task, fs, name)?; | |
| node.open(current_task, flags) | |
| } | |
| fn open_file( | |
| current_task: &CurrentTask, | |
| fs: &FileSystemHandle, | |
| name: &FsStr, | |
| ) -> Result<FileHandle, Errno> { | |
| open_file_with_flags(current_task, fs, name, OpenFlags::RDWR | OpenFlags::NOCTTY) | |
| } | |
| fn open_ptmx_and_unlock( | |
| current_task: &CurrentTask, | |
| fs: &FileSystemHandle, | |
| ) -> Result<FileHandle, Errno> { | |
| let file = open_file_with_flags(current_task, fs, "ptmx".into(), OpenFlags::RDWR)?; | |
| // Unlock terminal | |
| ioctl::<i32>(current_task, &file, TIOCSPTLCK, &0)?; | |
| Ok(file) | |
| } | |
| #[fuchsia::test] | |
| async fn opening_ptmx_creates_pts() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| lookup_node(task, &fs, "0".into()).unwrap_err(); | |
| let _ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| lookup_node(task, &fs, "0".into()).expect("pty"); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn closing_ptmx_closes_pts() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| lookup_node(task, &fs, "0".into()).unwrap_err(); | |
| let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| let _pts = open_file(task, &fs, "0".into()).expect("open file"); | |
| std::mem::drop(ptmx); | |
| task.trigger_delayed_releaser(); | |
| lookup_node(task, &fs, "0".into()).unwrap_err(); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn pts_are_reused() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| let _ptmx0 = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| let mut _ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| let _ptmx2 = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| lookup_node(task, &fs, "0".into()).expect("component_lookup"); | |
| lookup_node(task, &fs, "1".into()).expect("component_lookup"); | |
| lookup_node(task, &fs, "2".into()).expect("component_lookup"); | |
| std::mem::drop(_ptmx1); | |
| task.trigger_delayed_releaser(); | |
| lookup_node(task, &fs, "1".into()).unwrap_err(); | |
| _ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| lookup_node(task, &fs, "1".into()).expect("component_lookup"); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn opening_inexistant_replica_fails() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| // Initialize pts devices | |
| new_pts_fs(kernel); | |
| let fs = TmpFs::new_fs(kernel); | |
| let mount = MountInfo::detached(); | |
| let pts = fs | |
| .root() | |
| .create_entry(task, &mount, "custom_pts".into(), |dir, mount, name| { | |
| dir.create_node( | |
| task, | |
| mount, | |
| name, | |
| mode!(IFCHR, 0o666), | |
| DeviceId::new(DEVPTS_FIRST_MAJOR, 0), | |
| FsCred::root(), | |
| ) | |
| }) | |
| .expect("custom_pts"); | |
| let node = NamespaceNode::new_anonymous(pts.clone()); | |
| assert!(node.open(task, OpenAccessCheck::skip(OpenFlags::RDONLY)).is_err()); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_open_tty() { | |
| spawn_kernel_with_features_and_run( | |
| async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| let devfs = starnix_core::fs::devtmpfs::DevTmpFs::from_kernel(kernel); | |
| let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| set_controlling_terminal(task, &ptmx, false).expect("set_controlling_terminal"); | |
| let tty = | |
| open_file_with_flags(task, &devfs, "tty".into(), OpenFlags::RDWR).expect("tty"); | |
| // Check that tty is the main terminal by calling the ioctl TIOCGPTN and checking it is | |
| // has the same result as on ptmx. | |
| assert_eq!( | |
| ioctl::<i32>(task, &tty, TIOCGPTN, &0), | |
| ioctl::<i32>(task, &ptmx, TIOCGPTN, &0) | |
| ); | |
| // Detach the controlling terminal. | |
| ioctl::<i32>(task, &ptmx, TIOCNOTTY, &0).expect("detach terminal"); | |
| let pts = open_file(task, &fs, "0".into()).expect("open file"); | |
| set_controlling_terminal(task, &pts, false).expect("set_controlling_terminal"); | |
| let tty = | |
| open_file_with_flags(task, &devfs, "tty".into(), OpenFlags::RDWR).expect("tty"); | |
| // TIOCGPTN is not implemented on replica terminals | |
| assert!(ioctl::<i32>(task, &tty, TIOCGPTN, &0).is_err()); | |
| }, | |
| KernelFeatures { devtmpfs: true, ..Default::default() }, | |
| ) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_unknown_ioctl() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| assert_eq!(ptmx.ioctl(task, 42, Default::default()), error!(ENOTTY)); | |
| let pts_file = open_file(task, &fs, "0".into()).expect("open file"); | |
| assert_eq!(pts_file.ioctl(task, 42, Default::default()), error!(ENOTTY)); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_tiocgptn_ioctl() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| let ptmx0 = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| let ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| let pts0 = ioctl::<u32>(task, &ptmx0, TIOCGPTN, &0).expect("ioctl"); | |
| assert_eq!(pts0, 0); | |
| let pts1 = ioctl::<u32>(task, &ptmx1, TIOCGPTN, &0).expect("ioctl"); | |
| assert_eq!(pts1, 1); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_new_terminal_is_locked() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| let _ptmx_file = open_file(task, &fs, "ptmx".into()).expect("open file"); | |
| let pts = lookup_node(task, &fs, "0".into()).expect("component_lookup"); | |
| assert_eq!(pts.open(task, OpenFlags::RDONLY).map(|_| ()), error!(EIO)); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_lock_ioctls() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| let pts = lookup_node(task, &fs, "0".into()).expect("component_lookup"); | |
| // Check that the lock is not set. | |
| assert_eq!(ioctl::<i32>(task, &ptmx, TIOCGPTLCK, &0), Ok(0)); | |
| // /dev/pts/0 can be opened | |
| pts.open(task, OpenFlags::RDONLY).expect("open"); | |
| // Lock the terminal | |
| ioctl::<i32>(task, &ptmx, TIOCSPTLCK, &42).expect("ioctl"); | |
| // Check that the lock is set. | |
| assert_eq!(ioctl::<i32>(task, &ptmx, TIOCGPTLCK, &0), Ok(1)); | |
| // /dev/pts/0 cannot be opened | |
| assert_eq!(pts.open(task, OpenFlags::RDONLY).map(|_| ()), error!(EIO)); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_ptmx_stats() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| task.set_creds(Credentials::with_ids(22, 22)); | |
| let fs = new_pts_fs(kernel); | |
| let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| let ptmx_stat = ptmx.node().stat(task).expect("stat"); | |
| assert_eq!(ptmx_stat.st_blksize as usize, BLOCK_SIZE); | |
| let pts = open_file(task, &fs, "0".into()).expect("open file"); | |
| let pts_stats = pts.node().stat(task).expect("stat"); | |
| assert_eq!(pts_stats.st_mode & FileMode::PERMISSIONS.bits(), 0o600); | |
| assert_eq!(pts_stats.st_uid, 22); | |
| // TODO(qsr): Check that gid is tty. | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_attach_terminal_when_open() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| let _opened_main = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| // Opening the main terminal should not set the terminal of the session. | |
| assert!(task.thread_group().controlling_terminal().is_none()); | |
| // Opening the terminal should not set the terminal of the session with the NOCTTY flag. | |
| let _opened_replica2 = | |
| open_file_with_flags(task, &fs, "0".into(), OpenFlags::RDWR | OpenFlags::NOCTTY) | |
| .expect("open file"); | |
| assert!(task.thread_group().controlling_terminal().is_none()); | |
| // Opening the replica terminal should set the terminal of the session. | |
| let _opened_replica2 = | |
| open_file_with_flags(task, &fs, "0".into(), OpenFlags::RDWR).expect("open file"); | |
| assert!(task.thread_group().controlling_terminal().is_some()); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_attach_terminal() { | |
| spawn_kernel_and_run(async |task1| { | |
| let kernel = task1.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let task2 = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| task2.thread_group().setsid().expect("setsid"); | |
| let fs = new_pts_fs(kernel); | |
| let opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx"); | |
| let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file"); | |
| assert_eq!(ioctl::<i32>(task1, &opened_main, TIOCGPGRP, &0), error!(ENOTTY)); | |
| assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY)); | |
| set_controlling_terminal(task1, &opened_main, false).unwrap(); | |
| assert_eq!( | |
| ioctl::<i32>(task1, &opened_main, TIOCGPGRP, &0), | |
| Ok(task1.thread_group().pgid().id) | |
| ); | |
| assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY)); | |
| // Cannot steal terminal using the replica. | |
| assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM)); | |
| assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY)); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_steal_terminal() { | |
| spawn_kernel_and_run(async |task1| { | |
| let kernel = task1.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| task1.set_creds(Credentials::with_ids(1, 1)); | |
| let task2 = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| let fs = new_pts_fs(kernel); | |
| let _opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx"); | |
| let wo_opened_replica = | |
| open_file_with_flags(task1, &fs, "0".into(), OpenFlags::WRONLY | OpenFlags::NOCTTY) | |
| .expect("open file"); | |
| assert!(!wo_opened_replica.can_read()); | |
| // FD must be readable for setting the terminal. | |
| assert_eq!(set_controlling_terminal(task1, &wo_opened_replica, false), error!(EPERM)); | |
| let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file"); | |
| // Task must be session leader for setting the terminal. | |
| assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM)); | |
| // Associate terminal to task1. | |
| set_controlling_terminal(task1, &opened_replica, false) | |
| .expect("Associate terminal to task1"); | |
| // Redundant association of the same terminal should succeed (no-op) | |
| set_controlling_terminal(task1, &opened_replica, false) | |
| .expect("Redundant association should succeed"); | |
| task2.thread_group().setsid().expect("setsid"); | |
| // One cannot associate a terminal that is already associated with another process. | |
| assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM)); | |
| // One cannot steal a terminal without the CAP_SYS_ADMIN capacility | |
| assert_eq!(set_controlling_terminal(&task2, &opened_replica, true), error!(EPERM)); | |
| // One can steal a terminal with the CAP_SYS_ADMIN capacility | |
| task2.set_creds(Credentials::with_ids(0, 0)); | |
| // But not without specifying that one wants to steal it. | |
| assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM)); | |
| set_controlling_terminal(&task2, &opened_replica, true) | |
| .expect("Associate terminal to task2"); | |
| assert!(task1.thread_group().controlling_terminal().is_none()); | |
| // If task2 already owns a controlling terminal, attempting to steal a second | |
| // terminal from task1 must fail with EPERM without disassociating task1's terminal. | |
| let _opened_main2 = open_ptmx_and_unlock(task1, &fs).expect("ptmx 1"); | |
| let opened_replica2 = open_file(task1, &fs, "1".into()).expect("open file 1"); | |
| set_controlling_terminal(task1, &opened_replica2, false) | |
| .expect("Associate second terminal to task1"); | |
| assert!(task1.thread_group().controlling_terminal().is_some()); | |
| assert_eq!(set_controlling_terminal(&task2, &opened_replica2, true), error!(EPERM)); | |
| assert!(task1.thread_group().controlling_terminal().is_some()); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_tiocgsid() { | |
| spawn_kernel_and_run(async |task1| { | |
| let kernel = task1.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let mut task2 = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| task2.set_mm_for_test(task1.mm().unwrap().clone()); | |
| task2.thread_group().setsid().expect("setsid"); | |
| let fs = new_pts_fs(kernel); | |
| let opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx"); | |
| let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file"); | |
| // Unattached terminal returns ENOTTY on both main and replica. | |
| assert_eq!(ioctl::<pid_t>(task1, &opened_main, TIOCGSID, &0), error!(ENOTTY)); | |
| assert_eq!(ioctl::<pid_t>(&task2, &opened_replica, TIOCGSID, &0), error!(ENOTTY)); | |
| // Attach controlling session to task2. | |
| set_controlling_terminal(&task2, &opened_replica, false) | |
| .expect("set controlling terminal"); | |
| let expected_sid = task2.thread_group().read().session.id; | |
| // Calling TIOCGSID on both main and replica from the controlling session returns SID. | |
| assert_eq!(ioctl::<pid_t>(&task2, &opened_main, TIOCGSID, &0), Ok(expected_sid)); | |
| assert_eq!(ioctl::<pid_t>(&task2, &opened_replica, TIOCGSID, &0), Ok(expected_sid)); | |
| // From a task in a different session, TIOCGSID on the replica returns ENOTTY, | |
| // whereas TIOCGSID on the main terminal succeeds and returns the controlling SID. | |
| assert_eq!(ioctl::<pid_t>(task1, &opened_replica, TIOCGSID, &0), error!(ENOTTY)); | |
| assert_eq!(ioctl::<pid_t>(task1, &opened_main, TIOCGSID, &0), Ok(expected_sid)); | |
| // After detaching (TIOCNOTTY), TIOCGSID returns ENOTTY. | |
| ioctl::<i32>(&task2, &opened_replica, TIOCNOTTY, &0).expect("detach terminal"); | |
| assert_eq!(ioctl::<pid_t>(&task2, &opened_main, TIOCGSID, &0), error!(ENOTTY)); | |
| assert_eq!(ioctl::<pid_t>(&task2, &opened_replica, TIOCGSID, &0), error!(ENOTTY)); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_set_foreground_process() { | |
| spawn_kernel_and_run(async |init| { | |
| let kernel = init.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let mut task1 = init.clone_task_for_test(0, Some(SIGCHLD)); | |
| // Share the init task's address space so ioctl usercopy operations can | |
| // access memory while running on this test thread. | |
| task1.set_mm_for_test(init.mm().unwrap().clone()); | |
| task1.thread_group().setsid().expect("setsid"); | |
| let pre_existing_task = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| let fs = new_pts_fs(kernel); | |
| let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx"); | |
| let opened_replica = open_file(&pre_existing_task, &fs, "0".into()).expect("open file"); | |
| // Cannot change the foreground process group if the terminal is not the controlling | |
| // terminal. | |
| assert_eq!( | |
| ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task1.pid.id), | |
| error!(ENOTTY) | |
| ); | |
| // Attach terminal to task1; pre-existing session members do not retroactively gain | |
| // the controlling terminal, whereas newly cloned children inherit it. | |
| set_controlling_terminal(&task1, &opened_replica, false).unwrap(); | |
| assert!(pre_existing_task.thread_group().controlling_terminal().is_none()); | |
| let mut task2 = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| task2.set_mm_for_test(init.mm().unwrap().clone()); | |
| task2.thread_group().setpgid(&task2, &task2, &task2.pid).expect("setpgid"); | |
| let task2_pgid = task2.thread_group().pgid(); | |
| assert_ne!(task2_pgid, task1.thread_group().pgid()); | |
| // The foreground process group should be the one of task1 | |
| assert_eq!( | |
| ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), | |
| Ok(task1.thread_group().pgid().id) | |
| ); | |
| // Cannot change the foreground process group to a negative pid. | |
| assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &-1), error!(EINVAL)); | |
| // Cannot change the foreground process group to a invalid process group. | |
| assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &255), error!(ESRCH)); | |
| // Cannot change the foreground process group to a process group in another session. | |
| let init_pgid = init.thread_group().pgid(); | |
| assert_eq!( | |
| ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &init_pgid.id), | |
| error!(EPERM) | |
| ); | |
| // Changing the foreground process while being in background generates SIGTTOU and fails. | |
| assert_eq!( | |
| ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &task2_pgid.id), | |
| error!(EINTR) | |
| ); | |
| assert!(task2.read().has_signal_pending(SIGTTOU)); | |
| // Set the foreground process to task2 process group | |
| ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task2_pgid.id).unwrap(); | |
| // Check that the foreground process has been changed. | |
| let terminal = | |
| Arc::clone(&task1.thread_group().controlling_terminal().as_ref().unwrap().terminal); | |
| assert_eq!( | |
| terminal.read().controlling_session.as_ref().unwrap().foreground_process_group(), | |
| &task2_pgid | |
| ); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_detach_session() { | |
| spawn_kernel_and_run(async |task1| { | |
| let kernel = task1.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let mut task2 = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| task2.thread_group().setsid().expect("setsid"); | |
| let fs = new_pts_fs(kernel); | |
| let _opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx"); | |
| let opened_replica = open_file(task1, &fs, "0".into()).expect("open file"); | |
| // Cannot detach the controlling terminal when none is attached terminal | |
| assert_eq!(ioctl::<i32>(task1, &opened_replica, TIOCNOTTY, &0), error!(ENOTTY)); | |
| set_controlling_terminal(&task2, &opened_replica, false) | |
| .expect("set controlling terminal"); | |
| // Cannot detach the controlling terminal when not in the controlling session. | |
| assert_eq!(ioctl::<i32>(task1, &opened_replica, TIOCNOTTY, &0), error!(ENOTTY)); | |
| // A non-session-leader child in the same session can detach its own controlling | |
| // terminal via TIOCNOTTY without detaching the session leader. | |
| let mut task3 = task2.clone_task_for_test(0, Some(SIGCHLD)); | |
| task3.set_mm_for_test(task1.mm().unwrap().clone()); | |
| task2.set_mm_for_test(task1.mm().unwrap().clone()); | |
| assert!(task3.thread_group().controlling_terminal().is_some()); | |
| assert_eq!(ioctl::<i32>(&task3, &opened_replica, TIOCGPGRP, &0), Ok(task2.pid.id)); | |
| ioctl::<i32>(&task3, &opened_replica, TIOCNOTTY, &0) | |
| .expect("non-leader detach terminal"); | |
| assert!(task3.thread_group().controlling_terminal().is_none()); | |
| assert!(task2.thread_group().controlling_terminal().is_some()); | |
| assert_eq!(ioctl::<i32>(&task3, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY)); | |
| assert_eq!( | |
| ioctl::<i32>(&task3, &opened_replica, TIOCSPGRP, &task2.pid.id), | |
| error!(ENOTTY) | |
| ); | |
| assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), Ok(task2.pid.id)); | |
| // Detach the terminal from the session leader. | |
| ioctl::<i32>(&task2, &opened_replica, TIOCNOTTY, &0).expect("detach terminal"); | |
| assert!(task2.thread_group().controlling_terminal().is_none()); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_send_data_back_and_forth() { | |
| spawn_kernel_and_run(async |task| { | |
| let kernel = task.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let fs = new_pts_fs(kernel); | |
| let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx"); | |
| let pts = open_file(task, &fs, "0".into()).expect("open file"); | |
| let has_data_ready_to_read = |fd: &FileHandle| { | |
| fd.query_events(task).expect("query_events").contains(FdEvents::POLLIN) | |
| }; | |
| let write_and_assert = |fd: &FileHandle, data: &[u8]| { | |
| assert_eq!( | |
| fd.write(task, &mut VecInputBuffer::new(data)).expect("write"), | |
| data.len() | |
| ); | |
| }; | |
| let read_and_check = |fd: &FileHandle, data: &[u8]| { | |
| assert!(has_data_ready_to_read(fd)); | |
| let mut buffer = VecOutputBuffer::new(data.len() + 1); | |
| assert_eq!(fd.read(task, &mut buffer).expect("read"), data.len()); | |
| assert_eq!(data, buffer.data()); | |
| }; | |
| let hello_buffer = b"hello\n"; | |
| let hello_transformed_buffer = b"hello\r\n"; | |
| // Main to replica | |
| write_and_assert(&ptmx, hello_buffer); | |
| read_and_check(&pts, hello_buffer); | |
| // Data has been echoed | |
| read_and_check(&ptmx, hello_transformed_buffer); | |
| // Replica to main | |
| write_and_assert(&pts, hello_buffer); | |
| read_and_check(&ptmx, hello_transformed_buffer); | |
| // Data has not been echoed | |
| assert!(!has_data_ready_to_read(&pts)); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_foreground_pgrp_persistence_and_signal_noop_on_exit() { | |
| spawn_kernel_and_run(async |init| { | |
| let kernel = init.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let mut task1 = init.clone_task_for_test(0, Some(SIGCHLD)); | |
| task1.set_mm_for_test(init.mm().unwrap().clone()); | |
| task1.thread_group().setsid().expect("setsid"); | |
| let task1_pgid = task1.thread_group().pgid(); | |
| let fs = new_pts_fs(kernel); | |
| let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx"); | |
| let opened_replica = open_file(&task1, &fs, "0".into()).expect("open replica"); | |
| set_controlling_terminal(&task1, &opened_replica, false) | |
| .expect("set controlling terminal"); | |
| let terminal = | |
| Arc::clone(&task1.thread_group().controlling_terminal().as_ref().unwrap().terminal); | |
| // Spawn two tasks in a new foreground process group. | |
| let mut task2 = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| task2.set_mm_for_test(init.mm().unwrap().clone()); | |
| task2.thread_group().setpgid(&task2, &task2, &task2.pid).expect("setpgid task2"); | |
| let task2_pgid = task2.thread_group().pgid(); | |
| let task2_pgid_id = task2_pgid.id; | |
| let mut task3 = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| task3.set_mm_for_test(init.mm().unwrap().clone()); | |
| task1.thread_group().setpgid(&task1, &task3, &task2_pgid).expect("setpgid task3"); | |
| // Set task2's process group as the foreground process group. | |
| ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task2_pgid_id) | |
| .expect("set foreground process group"); | |
| assert_eq!(ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), Ok(task2_pgid_id)); | |
| // Mask SIGWINCH on all three tasks so default-ignore disposition does not discard it. | |
| task1.write().set_signal_mask(SigSet::from(SIGWINCH)); | |
| task2.write().set_signal_mask(SigSet::from(SIGWINCH)); | |
| task3.write().set_signal_mask(SigSet::from(SIGWINCH)); | |
| // Deliver SIGWINCH to the foreground group while both tasks are alive. | |
| terminal.send_signals(&[SIGWINCH]); | |
| assert!(task2.thread_group().has_signal_queued(SIGWINCH)); | |
| assert!(task3.thread_group().has_signal_queued(SIGWINCH)); | |
| assert!(!task1.thread_group().has_signal_queued(SIGWINCH)); | |
| // Terminate the process group leader (task2) while task3 remains alive in the group. | |
| task2.thread_group().kill(ExitStatus::Exit(0), None); | |
| std::mem::drop(task2); | |
| // Foreground PGID remains task2_pgid_id, and signals reach the surviving member task3. | |
| assert_eq!(ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), Ok(task2_pgid_id)); | |
| terminal.send_signals(&[SIGINT]); | |
| assert!(task3.thread_group().has_signal_queued(SIGINT)); | |
| assert!(!task1.thread_group().has_signal_queued(SIGINT)); | |
| // Terminate the final member (task3) and reap both zombies so the foreground process | |
| // group is completely reclaimed from PidTable. | |
| task3.thread_group().kill(ExitStatus::Exit(0), None); | |
| std::mem::drop(task3); | |
| { | |
| let mut pids = kernel.pids.lock(); | |
| for zombie in task1.thread_group().write().zombie_children.drain(..) { | |
| zombie.release(&mut pids); | |
| } | |
| } | |
| assert_eq!(kernel.pids.get(task2_pgid_id), error!(ESRCH)); | |
| // Verify tcgetpgrp (TIOCGPGRP) still returns the exited group's PGID. | |
| assert_eq!(ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), Ok(task2_pgid_id)); | |
| // Verify signal dispatch to the exited foreground process group is a safe no-op. | |
| terminal.send_signals(&[SIGINT, SIGQUIT, SIGTSTP, SIGWINCH, SIGHUP]); | |
| assert!(!task1.thread_group().has_signal_queued(SIGINT)); | |
| assert!(!task1.thread_group().has_signal_queued(SIGQUIT)); | |
| assert!(!task1.thread_group().has_signal_queued(SIGTSTP)); | |
| assert!(!task1.thread_group().has_signal_queued(SIGWINCH)); | |
| assert!(!task1.thread_group().has_signal_queued(SIGHUP)); | |
| // Mask SIGTTOU on task1 and reclaim the foreground process group. | |
| task1.write().set_signal_mask(SigSet::from(SIGTTOU) | SigSet::from(SIGWINCH)); | |
| ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task1_pgid.id) | |
| .expect("reclaim foreground process group"); | |
| assert_eq!(ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), Ok(task1_pgid.id)); | |
| // Move foreground to another short-lived process group, exit it, and disassociate. | |
| let mut task4 = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| task4.set_mm_for_test(init.mm().unwrap().clone()); | |
| task4.thread_group().setpgid(&task4, &task4, &task4.pid).expect("setpgid task4"); | |
| let task4_pgid_id = task4.pid.id; | |
| ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task4_pgid_id) | |
| .expect("set foreground to task4"); | |
| task4.thread_group().kill(ExitStatus::Exit(0), None); | |
| std::mem::drop(task4); | |
| terminal.disassociate_controlling_session(); | |
| assert!(task1.thread_group().controlling_terminal().is_none()); | |
| assert!(!task1.thread_group().has_signal_queued(SIGHUP)); | |
| assert!(!task1.thread_group().has_signal_queued(SIGCONT)); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_concurrent_terminal_job_control_stress() { | |
| spawn_kernel_and_run(async |init| { | |
| let kernel = init.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let mut task1 = init.clone_task_for_test(0, Some(SIGCHLD)); | |
| task1.set_mm_for_test(init.mm().unwrap().clone()); | |
| task1.thread_group().setsid().expect("setsid task1"); | |
| task1.write().set_signal_mask(SigSet::from(SIGTTOU)); | |
| let task1_pgid = task1.thread_group().pgid(); | |
| let mut child_a = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| child_a.set_mm_for_test(init.mm().unwrap().clone()); | |
| child_a | |
| .thread_group() | |
| .setpgid(&child_a, &child_a, &child_a.pid) | |
| .expect("setpgid child_a"); | |
| let child_a_pgid = child_a.pid.clone(); | |
| let mut child_b = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| child_b.set_mm_for_test(init.mm().unwrap().clone()); | |
| child_b | |
| .thread_group() | |
| .setpgid(&child_b, &child_b, &child_b.pid) | |
| .expect("setpgid child_b"); | |
| let child_b_pgid = child_b.pid.clone(); | |
| let mut child_m = task1.clone_task_for_test(0, Some(SIGCHLD)); | |
| child_m.set_mm_for_test(init.mm().unwrap().clone()); | |
| let fs = new_pts_fs(kernel); | |
| let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx 0"); | |
| let opened_replica = open_file(&task1, &fs, "0".into()).expect("open replica 0"); | |
| set_controlling_terminal(&task1, &opened_replica, false) | |
| .expect("set controlling terminal 0"); | |
| let terminal = | |
| Arc::clone(&task1.thread_group().controlling_terminal().as_ref().unwrap().terminal); | |
| // Create a second session leader and terminal to stress concurrent disassociation. | |
| let mut task_s2 = init.clone_task_for_test(0, Some(SIGCHLD)); | |
| task_s2.set_mm_for_test(init.mm().unwrap().clone()); | |
| task_s2.thread_group().setsid().expect("setsid task_s2"); | |
| task_s2.write().set_signal_mask(SigSet::from(SIGTTOU)); | |
| let task_s2_pgid = task_s2.pid.clone(); | |
| let _opened_main2 = open_ptmx_and_unlock(init, &fs).expect("ptmx 1"); | |
| let opened_replica2 = open_file(&task_s2, &fs, "1".into()).expect("open replica 1"); | |
| set_controlling_terminal(&task_s2, &opened_replica2, false) | |
| .expect("set controlling terminal 1"); | |
| let terminal2 = Arc::clone( | |
| &task_s2.thread_group().controlling_terminal().as_ref().unwrap().terminal, | |
| ); | |
| let running = Arc::new(AtomicBool::new(true)); | |
| // Thread 1: Concurrently dispatch signals to the foreground process groups. | |
| let sig_term1 = Arc::clone(&terminal); | |
| let sig_term2 = Arc::clone(&terminal2); | |
| let sig_running = Arc::clone(&running); | |
| let signal_thread = std::thread::spawn(move || { | |
| while sig_running.load(Ordering::Relaxed) { | |
| sig_term1.send_signals(&[SIGWINCH, SIGCONT]); | |
| sig_term2.send_signals(&[SIGWINCH, SIGCONT]); | |
| } | |
| }); | |
| // Thread 2: Concurrently read the foreground process group and verify PGID validity. | |
| let get_tg1 = Arc::clone(task1.thread_group()); | |
| let get_term1 = Arc::clone(&terminal); | |
| let get_running = Arc::clone(&running); | |
| let valid_pgids = [task1_pgid.id, child_a_pgid.id, child_b_pgid.id]; | |
| let getter_thread = std::thread::spawn(move || { | |
| while get_running.load(Ordering::Relaxed) { | |
| if let Ok(pgid) = get_tg1.get_foreground_process_group(&get_term1) { | |
| assert!(valid_pgids.contains(&pgid)); | |
| } | |
| } | |
| }); | |
| // Thread 3: Concurrently invoke setsid on group leaders (acquiring PidTableLock). | |
| let setsid_tg1 = Arc::clone(task1.thread_group()); | |
| let setsid_tga = Arc::clone(child_a.thread_group()); | |
| let setsid_running = Arc::clone(&running); | |
| let setsid_thread = std::thread::spawn(move || { | |
| while setsid_running.load(Ordering::Relaxed) { | |
| assert_eq!(setsid_tg1.setsid(), error!(EPERM)); | |
| assert_eq!(setsid_tga.setsid(), error!(EPERM)); | |
| } | |
| }); | |
| // Thread 4: Concurrently disassociate the controlling session on terminal2. | |
| let disassoc_term2 = Arc::clone(&terminal2); | |
| let disassoc_running = Arc::clone(&running); | |
| let disassoc_thread = std::thread::spawn(move || { | |
| while disassoc_running.load(Ordering::Relaxed) { | |
| disassoc_term2.disassociate_controlling_session(); | |
| } | |
| }); | |
| // Main thread: Interleave TIOCSPGRP, setpgid task migration, and terminal attach/detach. | |
| for _ in 0..75 { | |
| let _ = ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &child_a_pgid.id); | |
| task1 | |
| .thread_group() | |
| .setpgid(&task1, &child_m, &child_a_pgid) | |
| .expect("migrate child_m to child_a"); | |
| let _ = ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &child_b_pgid.id); | |
| task1 | |
| .thread_group() | |
| .setpgid(&task1, &child_m, &child_b_pgid) | |
| .expect("migrate child_m to child_b"); | |
| let _ = ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task1_pgid.id); | |
| let _ = set_controlling_terminal(&task_s2, &opened_replica2, false); | |
| let _ = task_s2.thread_group().set_foreground_process_group( | |
| &task_s2, | |
| &terminal2, | |
| &task_s2_pgid, | |
| ); | |
| let _ = ioctl::<i32>(&task_s2, &opened_replica2, TIOCNOTTY, &0); | |
| } | |
| // Disassociate primary terminal while background threads are still active. | |
| terminal.disassociate_controlling_session(); | |
| running.store(false, Ordering::Relaxed); | |
| signal_thread.join().expect("signal_thread join"); | |
| getter_thread.join().expect("getter_thread join"); | |
| setsid_thread.join().expect("setsid_thread join"); | |
| disassoc_thread.join().expect("disassoc_thread join"); | |
| assert!(task1.thread_group().controlling_terminal().is_none()); | |
| assert!(task_s2.thread_group().controlling_terminal().is_none()); | |
| }) | |
| .await; | |
| } | |
| #[fuchsia::test] | |
| async fn test_tiocnotty_non_leader_only_detaches_caller() { | |
| spawn_kernel_and_run(async |init| { | |
| let kernel = init.kernel(); | |
| tty_device_init(kernel).expect("tty_device_init"); | |
| let mut leader = init.clone_task_for_test(0, Some(SIGCHLD)); | |
| leader.set_mm_for_test(init.mm().unwrap().clone()); | |
| leader.thread_group().setsid().expect("setsid"); | |
| let fs = new_pts_fs(kernel); | |
| let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx"); | |
| let opened_replica = open_file(&leader, &fs, "0".into()).expect("open replica"); | |
| set_controlling_terminal(&leader, &opened_replica, false) | |
| .expect("set controlling terminal"); | |
| // Spawn a non-leader process in the same session. | |
| let mut child = leader.clone_task_for_test(0, Some(SIGCHLD)); | |
| child.set_mm_for_test(init.mm().unwrap().clone()); | |
| assert!(leader.thread_group().controlling_terminal().is_some()); | |
| assert!(child.thread_group().controlling_terminal().is_some()); | |
| let terminal = Arc::clone( | |
| &leader.thread_group().controlling_terminal().as_ref().unwrap().terminal, | |
| ); | |
| // Non-leader calling TIOCNOTTY only detaches its own controlling terminal without | |
| // disassociating the session or signaling the foreground process group. | |
| ioctl::<i32>(&child, &opened_replica, TIOCNOTTY, &0) | |
| .expect("non-leader TIOCNOTTY should succeed"); | |
| assert!(child.thread_group().controlling_terminal().is_none()); | |
| assert!(leader.thread_group().controlling_terminal().is_some()); | |
| assert_eq!(terminal.read().controlling_session(), Some(&leader.pid)); | |
| assert!(!leader.thread_group().has_signal_queued(SIGHUP)); | |
| assert!(!child.thread_group().has_signal_queued(SIGHUP)); | |
| // A second TIOCNOTTY from the already-detached non-leader fails with ENOTTY. | |
| assert_eq!(ioctl::<i32>(&child, &opened_replica, TIOCNOTTY, &0), error!(ENOTTY)); | |
| // Session leader calling TIOCNOTTY disassociates the entire session. | |
| ioctl::<i32>(&leader, &opened_replica, TIOCNOTTY, &0) | |
| .expect("leader TIOCNOTTY should succeed"); | |
| assert!(leader.thread_group().controlling_terminal().is_none()); | |
| assert!(terminal.read().controlling_session().is_none()); | |
| }) | |
| .await; | |
| } | |
| } |