Skip to main content

starnix_modules_devpts/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![recursion_limit = "512"]
6
7use starnix_core::device::DeviceMode;
8use starnix_core::device::kobject::DeviceMetadata;
9pub use starnix_core::device::terminal::DEVPTS_COUNT;
10use starnix_core::device::terminal::{Terminal, TerminalSide, TtyState, get_device_type_for_pts};
11use starnix_core::fs::sysfs::build_device_directory;
12use starnix_core::mm::MemoryAccessorExt;
13use starnix_core::task::{CurrentTask, EventHandler, Kernel, WaitCanceler, Waiter};
14use starnix_core::vfs::buffers::{InputBuffer, OutputBuffer};
15use starnix_core::vfs::pseudo::vec_directory::{VecDirectory, VecDirectoryEntry};
16use starnix_core::vfs::{
17    CacheMode, DirEntry, DirectoryEntryType, FdFlags, FileHandle, FileObject, FileObjectState,
18    FileOps, FileSystem, FileSystemHandle, FileSystemOps, FileSystemOptions, FsNode, FsNodeHandle,
19    FsNodeInfo, FsNodeOps, FsStr, FsString, LookupContext, MountInfo, NamespaceNode, SpecialNode,
20    SymlinkMode,
21};
22use starnix_core::{fileops_impl_nonseekable, fileops_impl_noop_sync, fs_node_impl_dir_readonly};
23use starnix_logging::track_stub;
24use starnix_syscalls::{SUCCESS, SyscallArg, SyscallResult};
25use starnix_types::vfs::default_statfs;
26use starnix_uapi::auth::FsCred;
27use starnix_uapi::device_id::{DeviceId, TTY_ALT_MAJOR};
28use starnix_uapi::errors::Errno;
29use starnix_uapi::file_mode::mode;
30use starnix_uapi::mount_flags::MountFlags;
31use starnix_uapi::open_flags::OpenFlags;
32use starnix_uapi::signals::SIGWINCH;
33use starnix_uapi::termios::{
34    into_termio, into_termios2, termios_from_termios2, termios2_from_termios,
35};
36use starnix_uapi::user_address::{UserAddress, UserRef};
37use starnix_uapi::vfs::FdEvents;
38use starnix_uapi::{
39    DEVPTS_SUPER_MAGIC, FIOASYNC, FIONREAD, FIOQSIZE, TCFLSH, TCGETA, TCGETS, TCGETS2, TCGETX,
40    TCSBRK, TCSBRKP, TCSETA, TCSETAF, TCSETAW, TCSETS, TCSETS2, TCSETSF, TCSETSF2, TCSETSW,
41    TCSETSW2, TCSETX, TCSETXF, TCSETXW, TCXONC, TIOCCBRK, TIOCCONS, TIOCEXCL, TIOCGETD,
42    TIOCGICOUNT, TIOCGLCKTRMIOS, TIOCGPGRP, TIOCGPTLCK, TIOCGPTN, TIOCGPTPEER, TIOCGRS485,
43    TIOCGSERIAL, TIOCGSID, TIOCGSOFTCAR, TIOCGWINSZ, TIOCLINUX, TIOCMBIC, TIOCMBIS, TIOCMGET,
44    TIOCMIWAIT, TIOCMSET, TIOCNOTTY, TIOCNXCL, TIOCOUTQ, TIOCPKT, TIOCSBRK, TIOCSCTTY,
45    TIOCSERCONFIG, TIOCSERGETLSR, TIOCSERGETMULTI, TIOCSERGSTRUCT, TIOCSERGWILD, TIOCSERSETMULTI,
46    TIOCSERSWILD, TIOCSETD, TIOCSLCKTRMIOS, TIOCSPGRP, TIOCSPTLCK, TIOCSRS485, TIOCSSERIAL,
47    TIOCSSOFTCAR, TIOCSTI, TIOCSWINSZ, TIOCVHANGUP, errno, error, gid_t, ino_t, pid_t, statfs,
48    uapi, uid_t,
49};
50use std::sync::{Arc, Weak};
51
52// See https://www.kernel.org/doc/Documentation/admin-guide/devices.txt
53const DEVPTS_FIRST_MAJOR: u32 = 136;
54const DEVPTS_MAJOR_COUNT: u32 = 4;
55// The block size of the node in the devpts file system. Value has been taken from
56// https://github.com/google/gvisor/blob/master/test/syscalls/linux/pty.cc
57const BLOCK_SIZE: usize = 1024;
58
59// The node identifier of the different node in the devpts filesystem.
60const ROOT_NODE_ID: ino_t = 1;
61const PTMX_NODE_ID: ino_t = 2;
62const FIRST_PTS_NODE_ID: ino_t = 3;
63
64pub fn dev_pts_fs(
65    current_task: &CurrentTask,
66    options: FileSystemOptions,
67) -> Result<FileSystemHandle, Errno> {
68    new_pts_fs(&current_task.kernel(), options)
69}
70
71pub fn new_pts_fs(kernel: &Kernel, options: FileSystemOptions) -> Result<FileSystemHandle, Errno> {
72    let state = if options.params.get(b"newinstance").is_some() {
73        Arc::new(TtyState::default())
74    } else {
75        kernel.expando.get::<TtyState>()
76    };
77
78    new_pts_fs_with_state(kernel, options, state)
79}
80
81pub fn new_pts_fs_with_state(
82    kernel: &Kernel,
83    options: FileSystemOptions,
84    state: Arc<TtyState>,
85) -> Result<FileSystemHandle, Errno> {
86    let parse_octal = |m: &str| u32::from_str_radix(m, 8);
87    let uid = options.params.get_as::<uid_t>(b"uid")?;
88    let gid = options.params.get_as::<gid_t>(b"gid")?;
89    let mode = options.params.get_with(b"mode", parse_octal)?.unwrap_or(0o600);
90    let ptmxmode = options.params.get_with(b"ptmxmode", parse_octal)?.unwrap_or(0);
91
92    let dev_pts_fs = DevPtsFs { state: state.clone(), uid, gid, mode, ptmxmode };
93
94    let fs = FileSystem::new(kernel, CacheMode::Uncached, dev_pts_fs, options)
95        .expect("devpts filesystem constructed with valid options");
96    fs.create_root(ROOT_NODE_ID, DevPtsRootDir { state });
97    Ok(fs)
98}
99
100/// Creates a terminal and returns the main pty and an associated replica pts.
101///
102/// This function assumes that `/dev/ptmx` is the `DevPtmxFile` and that devpts
103/// is mounted at `/dev/pts`. These assumptions are necessary so that the
104/// `FileHandle` objects returned have appropriate `NamespaceNode` objects.
105pub fn create_main_and_replica(
106    current_task: &CurrentTask,
107    window_size: uapi::winsize,
108) -> Result<(FileHandle, FileHandle), Errno> {
109    let pty_file = current_task.open_file("/dev/ptmx".into(), OpenFlags::RDWR)?;
110    let pty = pty_file.downcast_file::<DevPtmxFile>().ok_or_else(|| errno!(ENOTTY))?;
111    {
112        let mut terminal = pty.terminal.write();
113        terminal.line_discipline.locked = false;
114        terminal.line_discipline.window_size = window_size;
115    }
116    let pts_path = FsString::from(format!("/dev/pts/{}", pty.terminal.id));
117    let pts_file = current_task.open_file(pts_path.as_ref(), OpenFlags::RDWR)?;
118    Ok((pty_file, pts_file))
119}
120
121pub fn tty_device_init(kernel: &Kernel) -> Result<(), Errno> {
122    let registry = &kernel.device_registry;
123
124    // Register /dev/pts/X device type.
125    for n in 0..DEVPTS_MAJOR_COUNT {
126        registry
127            .register_major(
128                "pts".into(),
129                DeviceMode::Char,
130                DEVPTS_FIRST_MAJOR + n,
131                open_dev_pts_device,
132            )
133            .expect("can register pts{n} device");
134    }
135
136    // Register tty and ptmx device types.
137    kernel
138        .device_registry
139        .register_major("/dev/tty".into(), DeviceMode::Char, TTY_ALT_MAJOR, open_dev_pts_device)
140        .expect("can register tty device");
141
142    let tty_class = registry.objects.tty_class();
143    registry.add_major_device(
144        kernel,
145        "tty".into(),
146        DeviceMetadata::new("tty".into(), DeviceId::TTY, DeviceMode::Char),
147        /* parent = */ None,
148        tty_class.clone(),
149        build_device_directory,
150    );
151    registry.add_major_device(
152        kernel,
153        "ptmx".into(),
154        DeviceMetadata::new("ptmx".into(), DeviceId::PTMX, DeviceMode::Char),
155        /* parent = */ None,
156        tty_class,
157        build_device_directory,
158    );
159    Ok(())
160}
161
162struct DevPtsFs {
163    state: Arc<TtyState>,
164    uid: Option<uid_t>,
165    gid: Option<gid_t>,
166    mode: u32,
167    ptmxmode: u32,
168}
169
170impl FileSystemOps for DevPtsFs {
171    fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> {
172        Ok(default_statfs(DEVPTS_SUPER_MAGIC))
173    }
174    fn name(&self) -> &'static FsStr {
175        "devpts".into()
176    }
177
178    fn uses_external_node_ids(&self) -> bool {
179        false
180    }
181}
182
183impl DevPtsFs {
184    fn pty_creds_for(&self, current_task: &CurrentTask) -> FsCred {
185        let creds = current_task.current_creds();
186        let uid = self.uid.unwrap_or_else(|| creds.uid);
187        let gid = self.gid.unwrap_or_else(|| creds.gid);
188        FsCred { uid, gid }
189    }
190}
191
192struct DevPtsRootDir {
193    state: Arc<TtyState>,
194}
195
196impl FsNodeOps for DevPtsRootDir {
197    fs_node_impl_dir_readonly!();
198
199    fn create_file_ops(
200        &self,
201        _node: &FsNode,
202        _current_task: &CurrentTask,
203        _flags: OpenFlags,
204    ) -> Result<Box<dyn FileOps>, Errno> {
205        let mut result = vec![];
206        result.push(VecDirectoryEntry {
207            entry_type: DirectoryEntryType::CHR,
208            name: "ptmx".into(),
209            inode: Some(PTMX_NODE_ID),
210        });
211        for (id, terminal) in self.state.terminals.read().iter() {
212            if let Some(terminal) = terminal.upgrade() {
213                if !terminal.read().is_main_closed() {
214                    result.push(VecDirectoryEntry {
215                        entry_type: DirectoryEntryType::CHR,
216                        name: format!("{id}").into(),
217                        inode: Some((*id as ino_t) + FIRST_PTS_NODE_ID),
218                    });
219                }
220            }
221        }
222        Ok(VecDirectory::new_file(result))
223    }
224
225    fn lookup(
226        &self,
227        entry: &DirEntry,
228        _current_task: &CurrentTask,
229        name: &FsStr,
230    ) -> Result<FsNodeHandle, Errno> {
231        let fs = entry.node.fs();
232        let devptsfs =
233            fs.downcast_ops::<DevPtsFs>().expect("DevPts should only handle `DevPtsFs`s");
234        let name = std::str::from_utf8(name).map_err(|_| errno!(ENOENT))?;
235        if name == "ptmx" {
236            let mut info = FsNodeInfo::new(mode!(IFCHR, devptsfs.ptmxmode), FsCred::root());
237            info.rdev = DeviceId::PTMX;
238            info.blksize = BLOCK_SIZE;
239            let node = fs.create_node(PTMX_NODE_ID, SpecialNode, info);
240            return Ok(node);
241        }
242        if let Ok(id) = name.parse::<u32>() {
243            let terminal = self.state.terminals.read().get(&id).and_then(Weak::upgrade);
244            if let Some(terminal) = terminal {
245                if !terminal.read().is_main_closed() {
246                    let ino = (id as ino_t) + FIRST_PTS_NODE_ID;
247                    let mut info =
248                        FsNodeInfo::new(mode!(IFCHR, devptsfs.mode), terminal.fscred.clone());
249                    info.rdev = get_device_type_for_pts(id);
250                    info.blksize = BLOCK_SIZE;
251                    let node = fs.create_node(ino, SpecialNode, info);
252                    return Ok(node);
253                }
254            }
255        }
256        error!(ENOENT)
257    }
258}
259
260fn open_dev_pts_device(
261    current_task: &CurrentTask,
262    id: DeviceId,
263    node: &NamespaceNode,
264    flags: OpenFlags,
265) -> Result<Box<dyn FileOps>, Errno> {
266    match id {
267        // /dev/ptmx and /dev/pts/ptmx
268        DeviceId::PTMX => {
269            let fs = node.entry.node.fs();
270            let Some(devpts_fs) = fs.downcast_ops::<DevPtsFs>() else {
271                // The device is not in ptmx, let try to find something at pts/ptmx relative to
272                // the parent of the node
273                let parent = node.parent().ok_or_else(|| errno!(EINVAL))?;
274                let mut lookup_context = LookupContext::new(SymlinkMode::Follow);
275                let ptmx_node =
276                    current_task.lookup_path(&mut lookup_context, parent, "pts/ptmx".into())?;
277                return open_dev_pts_device(current_task, id, &ptmx_node, flags);
278            };
279
280            let creds = devpts_fs.pty_creds_for(current_task);
281            let terminal = devpts_fs.state.get_next_terminal(fs.root().clone(), creds)?;
282            let name = FsString::from(terminal.id.to_string());
283            let replica_dir_entry =
284                fs.root().component_lookup(current_task, &MountInfo::detached(), name.as_ref())?;
285            let replica_node =
286                NamespaceNode { mount: node.mount.clone(), entry: replica_dir_entry };
287            Ok(Box::new(DevPtmxFile::new(terminal, Some(replica_node))))
288        }
289        // /dev/tty
290        DeviceId::TTY => {
291            let controlling_terminal = current_task.thread_group().controlling_terminal();
292            if let Some(controlling_terminal) = controlling_terminal {
293                match controlling_terminal.side {
294                    TerminalSide::Main => {
295                        Ok(Box::new(DevPtmxFile::new(controlling_terminal.terminal, None)))
296                    }
297                    TerminalSide::Replica => {
298                        Ok(Box::new(TtyFile::new(controlling_terminal.terminal)))
299                    }
300                }
301            } else {
302                error!(ENXIO)
303            }
304        }
305        _ if id.major() < DEVPTS_FIRST_MAJOR
306            || id.major() >= DEVPTS_FIRST_MAJOR + DEVPTS_MAJOR_COUNT =>
307        {
308            error!(ENODEV)
309        }
310        // /dev/pts/??
311        _ => {
312            let fs = node.entry.node.fs();
313            let Some(devpts_fs) = fs.downcast_ops::<DevPtsFs>() else {
314                return error!(ENOTSUP);
315            };
316            let pts_id = (id.major() - DEVPTS_FIRST_MAJOR) * 256 + id.minor();
317            let terminal = devpts_fs
318                .state
319                .terminals
320                .read()
321                .get(&pts_id)
322                .and_then(Weak::upgrade)
323                .ok_or_else(|| errno!(EIO))?;
324            if terminal.read().line_discipline.locked {
325                return error!(EIO);
326            }
327            if !flags.contains(OpenFlags::NOCTTY) {
328                // Opening a replica sets the process' controlling TTY when possible. An error indicates it cannot
329                // be set, and is ignored silently.
330                let _ = current_task.thread_group().set_controlling_terminal(
331                    current_task,
332                    &terminal,
333                    TerminalSide::Replica,
334                    false, /* steal */
335                    flags.can_read(),
336                );
337            }
338            Ok(Box::new(TtyFile::new(terminal)))
339        }
340    }
341}
342
343struct DevPtmxFile {
344    terminal: Arc<Terminal>,
345
346    /// The replica's NamespaceNode, used to implement `TIOCGPTPEER` when opened via `/dev/ptmx`.
347    /// It is `None` if opened via `/dev/tty` redirecting to the main terminal. On Linux, `/dev/tty`
348    /// always redirects to the replica PTY (even if `TIOCSCTTY` is called on the main PTY), where
349    /// `TIOCGPTPEER` is unsupported.
350    replica_node: Option<NamespaceNode>,
351}
352
353impl DevPtmxFile {
354    pub fn new(terminal: Arc<Terminal>, replica_node: Option<NamespaceNode>) -> Self {
355        terminal.main_open();
356        Self { terminal, replica_node }
357    }
358}
359
360impl FileOps for DevPtmxFile {
361    fileops_impl_nonseekable!();
362    fileops_impl_noop_sync!();
363
364    fn close(self: Box<Self>, _file: &FileObjectState, _current_task: &CurrentTask) {
365        self.terminal.disassociate_controlling_session();
366        self.terminal.main_close();
367    }
368
369    fn read(
370        &self,
371        file: &FileObject,
372        current_task: &CurrentTask,
373        offset: usize,
374        data: &mut dyn OutputBuffer,
375    ) -> Result<usize, Errno> {
376        debug_assert!(offset == 0);
377        file.blocking_op(current_task, FdEvents::POLLIN | FdEvents::POLLHUP, None, || {
378            self.terminal.main_read(data)
379        })
380    }
381
382    fn write(
383        &self,
384        file: &FileObject,
385        current_task: &CurrentTask,
386        offset: usize,
387        data: &mut dyn InputBuffer,
388    ) -> Result<usize, Errno> {
389        debug_assert!(offset == 0);
390        file.blocking_op(current_task, FdEvents::POLLOUT | FdEvents::POLLHUP, None, || {
391            self.terminal.main_write(data)
392        })
393    }
394
395    fn wait_async(
396        &self,
397        _file: &FileObject,
398        _current_task: &CurrentTask,
399        waiter: &Waiter,
400        events: FdEvents,
401        handler: EventHandler,
402    ) -> Option<WaitCanceler> {
403        Some(self.terminal.main_wait_async(waiter, events, handler))
404    }
405
406    fn query_events(
407        &self,
408        _file: &FileObject,
409        _current_task: &CurrentTask,
410    ) -> Result<FdEvents, Errno> {
411        Ok(self.terminal.main_query_events())
412    }
413
414    fn ioctl(
415        &self,
416        file: &FileObject,
417        current_task: &CurrentTask,
418        request: u32,
419        arg: SyscallArg,
420    ) -> Result<SyscallResult, Errno> {
421        let user_addr = UserAddress::from(arg);
422        match request {
423            TIOCGPTN => {
424                // Get the terminal id.
425                let value: u32 = self.terminal.id;
426                current_task.write_object(UserRef::<u32>::new(user_addr), &value)?;
427                Ok(SUCCESS)
428            }
429            TIOCGPTLCK => {
430                // Get the lock status.
431                let value = i32::from(self.terminal.read().line_discipline.locked);
432                current_task.write_object(UserRef::<i32>::new(user_addr), &value)?;
433                Ok(SUCCESS)
434            }
435            TIOCSPTLCK => {
436                // Lock/Unlock the terminal.
437                let value = current_task.read_object(UserRef::<i32>::new(user_addr))?;
438                self.terminal.write().line_discipline.locked = value != 0;
439                Ok(SUCCESS)
440            }
441            TIOCGPTPEER => {
442                let Some(replica_node) = &self.replica_node else {
443                    return error!(ENOTTY);
444                };
445
446                if replica_node.mount.flags().contains(MountFlags::NODEV) {
447                    return error!(EACCES);
448                }
449
450                let flags = OpenFlags::from_bits_truncate(u32::from(arg));
451                let replica_file = replica_node.open(current_task, flags)?;
452
453                let fd_flags = if flags.contains(OpenFlags::CLOEXEC) {
454                    FdFlags::CLOEXEC
455                } else {
456                    FdFlags::empty()
457                };
458                let fd = current_task.add_file(replica_file, fd_flags)?;
459                Ok(fd.into())
460            }
461            _ => shared_ioctl(&self.terminal, true, file, current_task, request, arg),
462        }
463    }
464}
465
466pub struct TtyFile {
467    terminal: Arc<Terminal>,
468}
469
470impl TtyFile {
471    pub fn new(terminal: Arc<Terminal>) -> Self {
472        terminal.replica_open();
473        Self { terminal }
474    }
475}
476
477impl FileOps for TtyFile {
478    fileops_impl_nonseekable!();
479    fileops_impl_noop_sync!();
480
481    fn close(self: Box<Self>, _file: &FileObjectState, _current_task: &CurrentTask) {
482        self.terminal.replica_close();
483    }
484
485    fn read(
486        &self,
487        file: &FileObject,
488        current_task: &CurrentTask,
489        offset: usize,
490        data: &mut dyn OutputBuffer,
491    ) -> Result<usize, Errno> {
492        debug_assert!(offset == 0);
493        file.blocking_op(current_task, FdEvents::POLLIN | FdEvents::POLLHUP, None, || {
494            self.terminal.replica_read(data)
495        })
496    }
497
498    fn write(
499        &self,
500        file: &FileObject,
501        current_task: &CurrentTask,
502        offset: usize,
503        data: &mut dyn InputBuffer,
504    ) -> Result<usize, Errno> {
505        debug_assert!(offset == 0);
506        file.blocking_op(current_task, FdEvents::POLLOUT | FdEvents::POLLHUP, None, || {
507            self.terminal.replica_write(data)
508        })
509    }
510
511    fn wait_async(
512        &self,
513        _file: &FileObject,
514        _current_task: &CurrentTask,
515        waiter: &Waiter,
516        events: FdEvents,
517        handler: EventHandler,
518    ) -> Option<WaitCanceler> {
519        Some(self.terminal.replica_wait_async(waiter, events, handler))
520    }
521
522    fn query_events(
523        &self,
524        _file: &FileObject,
525        _current_task: &CurrentTask,
526    ) -> Result<FdEvents, Errno> {
527        Ok(self.terminal.replica_query_events())
528    }
529
530    fn ioctl(
531        &self,
532        file: &FileObject,
533        current_task: &CurrentTask,
534        request: u32,
535        arg: SyscallArg,
536    ) -> Result<SyscallResult, Errno> {
537        shared_ioctl(&self.terminal, false, file, current_task, request, arg)
538    }
539}
540
541/// The ioctl behaviour common to main and replica terminal file descriptors.
542fn shared_ioctl(
543    terminal: &Terminal,
544    is_main: bool,
545    file: &FileObject,
546    current_task: &CurrentTask,
547    request: u32,
548    arg: SyscallArg,
549) -> Result<SyscallResult, Errno> {
550    let user_addr = UserAddress::from(arg);
551    match request {
552        FIONREAD => {
553            // Get the main terminal available bytes for reading.
554            let value = terminal.read().get_available_read_size(is_main) as u32;
555            current_task.write_object(UserRef::<u32>::new(user_addr), &value)?;
556            Ok(SUCCESS)
557        }
558        TIOCSCTTY => {
559            // Make the given terminal the controlling terminal of the calling process.
560            let steal = bool::from(arg);
561            current_task.thread_group().set_controlling_terminal(
562                current_task,
563                terminal,
564                TerminalSide::from(is_main),
565                steal,
566                file.can_read(),
567            )?;
568            Ok(SUCCESS)
569        }
570        TIOCNOTTY => {
571            // Release the controlling terminal.
572            current_task.thread_group().release_controlling_terminal(
573                current_task,
574                terminal,
575                TerminalSide::from(is_main),
576            )?;
577            Ok(SUCCESS)
578        }
579        TIOCGPGRP => {
580            // Get the foreground process group.
581            let pgid = current_task.thread_group().get_foreground_process_group(terminal)?;
582            current_task.write_object(UserRef::<pid_t>::new(user_addr), &pgid)?;
583            Ok(SUCCESS)
584        }
585        TIOCSPGRP => {
586            // Set the foreground process group.
587            let pgid = current_task.read_object(UserRef::<pid_t>::new(user_addr))?;
588            if pgid < 0 {
589                return error!(EINVAL);
590            }
591            let pgid = current_task.kernel().pids.get(pgid)?;
592            current_task.thread_group().set_foreground_process_group(
593                current_task,
594                terminal,
595                &pgid,
596            )?;
597            Ok(SUCCESS)
598        }
599        TIOCGWINSZ => {
600            // Get the window size
601            current_task.write_object(
602                UserRef::<uapi::winsize>::new(user_addr),
603                &terminal.read().line_discipline.window_size,
604            )?;
605            Ok(SUCCESS)
606        }
607        TIOCSWINSZ => {
608            // If the window is already the required size, do nothing.
609            let new_winsize = current_task.read_object(UserRef::<uapi::winsize>::new(user_addr))?;
610            if terminal.read().line_discipline.window_size == new_winsize {
611                return Ok(SUCCESS);
612            }
613            // Set the window size
614            terminal.write().line_discipline.window_size = new_winsize;
615
616            // Send a SIGWINCH signal to the foreground process group.
617            terminal.send_signals(&[SIGWINCH]);
618            Ok(SUCCESS)
619        }
620        TCGETA => {
621            let termio = into_termio(terminal.read().termios());
622            current_task.write_object(UserRef::<uapi::termio>::new(user_addr), &termio)?;
623            Ok(SUCCESS)
624        }
625        TCGETS => {
626            // N.B. TCGETS on the main terminal actually returns the configuration of the replica
627            // end.
628            let termios = termios_from_termios2(terminal.read().termios());
629            current_task.write_object(UserRef::<uapi::termios>::new(user_addr), &termios)?;
630            Ok(SUCCESS)
631        }
632        TCGETS2 => {
633            current_task.write_object(
634                UserRef::<uapi::termios2>::new(user_addr),
635                terminal.read().termios(),
636            )?;
637            Ok(SUCCESS)
638        }
639        TCSETA => {
640            let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?;
641            terminal.set_termios(into_termios2(termio));
642            Ok(SUCCESS)
643        }
644        TCSETS => {
645            // N.B. TCSETS on the main terminal actually affects the configuration of the replica
646            // end.
647            let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?;
648            terminal.set_termios(termios2_from_termios(&termios));
649            Ok(SUCCESS)
650        }
651        TCSETS2 => {
652            let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?;
653            terminal.set_termios(termios2);
654            Ok(SUCCESS)
655        }
656        TCSETAF => {
657            // This should drain the output queue and discard the pending input first.
658            let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?;
659            terminal.set_termios(into_termios2(termio));
660            Ok(SUCCESS)
661        }
662        TCSETSF => {
663            // This should drain the output queue and discard the pending input first.
664            let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?;
665            terminal.set_termios(termios2_from_termios(&termios));
666            Ok(SUCCESS)
667        }
668        TCSETSF2 => {
669            // This should drain the output queue and discard the pending input first.
670            let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?;
671            terminal.set_termios(termios2);
672            Ok(SUCCESS)
673        }
674        TCSETAW => {
675            track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETAW drain output queue first");
676            let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?;
677            terminal.set_termios(into_termios2(termio));
678            Ok(SUCCESS)
679        }
680        TCSETSW => {
681            track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETSW drain output queue first");
682            let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?;
683            terminal.set_termios(termios2_from_termios(&termios));
684            Ok(SUCCESS)
685        }
686        TCSETSW2 => {
687            track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETSW2 drain output queue first");
688            let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?;
689            terminal.set_termios(termios2);
690            Ok(SUCCESS)
691        }
692        TIOCSETD => {
693            track_stub!(
694                TODO("https://fxbug.dev/322874060"),
695                "devpts setting line discipline",
696                is_main
697            );
698            error!(EINVAL)
699        }
700        TCSBRK => Ok(SUCCESS),
701        TCXONC => {
702            track_stub!(TODO("https://fxbug.dev/322892912"), "devpts ioctl TCXONC", is_main);
703            error!(ENOSYS)
704        }
705        TCFLSH => {
706            terminal.flush(is_main, u32::from(arg))?;
707            Ok(SUCCESS)
708        }
709        TIOCEXCL => {
710            track_stub!(TODO("https://fxbug.dev/322893449"), "devpts ioctl TIOCEXCL", is_main);
711            error!(ENOSYS)
712        }
713        TIOCNXCL => {
714            track_stub!(TODO("https://fxbug.dev/322893393"), "devpts ioctl TIOCNXCL", is_main);
715            error!(ENOSYS)
716        }
717        TIOCOUTQ => {
718            track_stub!(TODO("https://fxbug.dev/322893723"), "devpts ioctl TIOCOUTQ", is_main);
719            error!(ENOSYS)
720        }
721        TIOCSTI => {
722            track_stub!(TODO("https://fxbug.dev/322893780"), "devpts ioctl TIOCSTI", is_main);
723            error!(ENOSYS)
724        }
725        TIOCMGET => {
726            track_stub!(TODO("https://fxbug.dev/322893681"), "devpts ioctl TIOCMGET", is_main);
727            error!(ENOSYS)
728        }
729        TIOCMBIS => {
730            track_stub!(TODO("https://fxbug.dev/322893709"), "devpts ioctl TIOCMBIS", is_main);
731            error!(ENOSYS)
732        }
733        TIOCMBIC => {
734            track_stub!(TODO("https://fxbug.dev/322893610"), "devpts ioctl TIOCMBIC", is_main);
735            error!(ENOSYS)
736        }
737        TIOCMSET => {
738            track_stub!(TODO("https://fxbug.dev/322893211"), "devpts ioctl TIOCMSET", is_main);
739            error!(ENOSYS)
740        }
741        TIOCGSOFTCAR => {
742            track_stub!(TODO("https://fxbug.dev/322893365"), "devpts ioctl TIOCGSOFTCAR", is_main);
743            error!(ENOSYS)
744        }
745        TIOCSSOFTCAR => {
746            track_stub!(TODO("https://fxbug.dev/322894074"), "devpts ioctl TIOCSSOFTCAR", is_main);
747            error!(ENOSYS)
748        }
749        TIOCLINUX => {
750            track_stub!(TODO("https://fxbug.dev/322893147"), "devpts ioctl TIOCLINUX", is_main);
751            error!(ENOSYS)
752        }
753        TIOCCONS => {
754            track_stub!(TODO("https://fxbug.dev/322893267"), "devpts ioctl TIOCCONS", is_main);
755            error!(ENOSYS)
756        }
757        TIOCGSERIAL => {
758            track_stub!(TODO("https://fxbug.dev/322893503"), "devpts ioctl TIOCGSERIAL", is_main);
759            error!(ENOSYS)
760        }
761        TIOCSSERIAL => {
762            track_stub!(TODO("https://fxbug.dev/322893663"), "devpts ioctl TIOCSSERIAL", is_main);
763            error!(ENOSYS)
764        }
765        TIOCPKT => {
766            if !is_main {
767                return error!(ENOTTY);
768            }
769            let value = current_task.read_object(UserRef::<i32>::new(user_addr))?;
770            terminal.write().set_packet_mode(value != 0);
771            Ok(SUCCESS)
772        }
773        TIOCGETD => {
774            track_stub!(TODO("https://fxbug.dev/322893974"), "devpts ioctl TIOCGETD", is_main);
775            error!(ENOSYS)
776        }
777        TCSBRKP => Ok(SUCCESS),
778        TIOCSBRK => {
779            track_stub!(TODO("https://fxbug.dev/322893936"), "devpts ioctl TIOCSBRK", is_main);
780            error!(ENOSYS)
781        }
782        TIOCCBRK => {
783            track_stub!(TODO("https://fxbug.dev/322893213"), "devpts ioctl TIOCCBRK", is_main);
784            error!(ENOSYS)
785        }
786        TIOCGSID => {
787            if !is_main
788                && !current_task
789                    .thread_group()
790                    .controlling_terminal()
791                    .as_ref()
792                    .is_some_and(|ct| std::ptr::eq(terminal, Arc::as_ptr(&ct.terminal)))
793            {
794                return error!(ENOTTY);
795            }
796            let sid = terminal
797                .read()
798                .controlling_session()
799                .map(|sid| sid.id)
800                .ok_or_else(|| errno!(ENOTTY))?;
801            current_task.write_object(UserRef::<pid_t>::new(user_addr), &sid)?;
802            Ok(SUCCESS)
803        }
804        TIOCGRS485 => {
805            track_stub!(TODO("https://fxbug.dev/322893728"), "devpts ioctl TIOCGRS485", is_main);
806            error!(ENOSYS)
807        }
808        TIOCSRS485 => {
809            track_stub!(TODO("https://fxbug.dev/322893783"), "devpts ioctl TIOCSRS485", is_main);
810            error!(ENOSYS)
811        }
812        TCGETX => {
813            track_stub!(TODO("https://fxbug.dev/322893327"), "devpts ioctl TCGETX", is_main);
814            error!(ENOSYS)
815        }
816        TCSETX => {
817            track_stub!(TODO("https://fxbug.dev/322893741"), "devpts ioctl TCSETX", is_main);
818            error!(ENOSYS)
819        }
820        TCSETXF => {
821            track_stub!(TODO("https://fxbug.dev/322893937"), "devpts ioctl TCSETXF", is_main);
822            error!(ENOSYS)
823        }
824        TCSETXW => {
825            track_stub!(TODO("https://fxbug.dev/322893899"), "devpts ioctl TCSETXW", is_main);
826            error!(ENOSYS)
827        }
828        TIOCVHANGUP => {
829            track_stub!(TODO("https://fxbug.dev/322893742"), "devpts ioctl TIOCVHANGUP", is_main);
830            error!(ENOSYS)
831        }
832        FIOASYNC => {
833            track_stub!(TODO("https://fxbug.dev/322893269"), "devpts ioctl FIOASYNC", is_main);
834            error!(ENOSYS)
835        }
836        TIOCSERCONFIG => {
837            track_stub!(TODO("https://fxbug.dev/322893881"), "devpts ioctl TIOCSERCONFIG", is_main);
838            error!(ENOSYS)
839        }
840        TIOCSERGWILD => {
841            track_stub!(TODO("https://fxbug.dev/322893686"), "devpts ioctl TIOCSERGWILD", is_main);
842            error!(ENOSYS)
843        }
844        TIOCSERSWILD => {
845            track_stub!(TODO("https://fxbug.dev/322893837"), "devpts ioctl TIOCSERSWILD", is_main);
846            error!(ENOSYS)
847        }
848        TIOCGLCKTRMIOS => {
849            track_stub!(
850                TODO("https://fxbug.dev/322894114"),
851                "devpts ioctl TIOCGLCKTRMIOS",
852                is_main
853            );
854            error!(ENOSYS)
855        }
856        TIOCSLCKTRMIOS => {
857            track_stub!(
858                TODO("https://fxbug.dev/322893711"),
859                "devpts ioctl TIOCSLCKTRMIOS",
860                is_main
861            );
862            error!(ENOSYS)
863        }
864        TIOCSERGSTRUCT => {
865            track_stub!(
866                TODO("https://fxbug.dev/322893828"),
867                "devpts ioctl TIOCSERGSTRUCT",
868                is_main
869            );
870            error!(ENOSYS)
871        }
872        TIOCSERGETLSR => {
873            track_stub!(TODO("https://fxbug.dev/322894083"), "devpts ioctl TIOCSERGETLSR", is_main);
874            error!(ENOSYS)
875        }
876        TIOCSERGETMULTI => {
877            track_stub!(
878                TODO("https://fxbug.dev/322893962"),
879                "devpts ioctl TIOCSERGETMULTI",
880                is_main
881            );
882            error!(ENOSYS)
883        }
884        TIOCSERSETMULTI => {
885            track_stub!(
886                TODO("https://fxbug.dev/322893273"),
887                "devpts ioctl TIOCSERSETMULTI",
888                is_main
889            );
890            error!(ENOSYS)
891        }
892        TIOCMIWAIT => {
893            track_stub!(TODO("https://fxbug.dev/322894005"), "devpts ioctl TIOCMIWAIT", is_main);
894            error!(ENOSYS)
895        }
896        TIOCGICOUNT => {
897            track_stub!(TODO("https://fxbug.dev/322893862"), "devpts ioctl TIOCGICOUNT", is_main);
898            error!(ENOSYS)
899        }
900        FIOQSIZE => {
901            track_stub!(TODO("https://fxbug.dev/322893770"), "devpts ioctl FIOQSIZE", is_main);
902            error!(ENOSYS)
903        }
904        other => {
905            track_stub!(TODO("https://fxbug.dev/322893712"), "devpts unknown ioctl", other);
906            error!(ENOTTY)
907        }
908    }
909}
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914    use crate::tty_device_init;
915    use starnix_core::fs::tmpfs::TmpFs;
916    use starnix_core::task::{ExitStatus, KernelFeatures};
917    use starnix_core::testing::*;
918    use starnix_core::vfs::buffers::{VecInputBuffer, VecOutputBuffer};
919    use starnix_core::vfs::fs_args::MountParams;
920    use starnix_core::vfs::{MountInfo, NamespaceNode, OpenAccessCheck};
921    use starnix_types::ownership::Releasable;
922    use starnix_uapi::auth::Credentials;
923    use starnix_uapi::file_mode::FileMode;
924    use starnix_uapi::signals::{
925        SIGCHLD, SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTSTP, SIGTTOU, SigSet,
926    };
927    use std::sync::atomic::{AtomicBool, Ordering};
928
929    fn new_pts_fs(kernel: &Kernel) -> FileSystemHandle {
930        let mut options = FileSystemOptions::default();
931        options.params = MountParams::parse("ptmxmode=666".into()).expect("parse option");
932        super::new_pts_fs(&kernel, options).expect("create new_pts_fs")
933    }
934
935    fn ioctl<T: zerocopy::IntoBytes + zerocopy::FromBytes + zerocopy::Immutable + Copy>(
936        current_task: &CurrentTask,
937        file: &FileHandle,
938        command: u32,
939        value: &T,
940    ) -> Result<T, Errno> {
941        let address =
942            map_memory(current_task, UserAddress::default(), std::mem::size_of::<T>() as u64);
943        let address_ref = UserRef::<T>::new(address);
944        current_task.write_object(address_ref, value)?;
945        file.ioctl(current_task, command, address.into())?;
946        current_task.read_object(address_ref)
947    }
948
949    fn set_controlling_terminal(
950        current_task: &CurrentTask,
951        file: &FileHandle,
952        steal: bool,
953    ) -> Result<SyscallResult, Errno> {
954        #[allow(clippy::bool_to_int_with_if)]
955        file.ioctl(current_task, TIOCSCTTY, steal.into())
956    }
957
958    fn lookup_node(
959        task: &CurrentTask,
960        fs: &FileSystemHandle,
961        name: &FsStr,
962    ) -> Result<NamespaceNode, Errno> {
963        let root = NamespaceNode::new_anonymous(fs.root().clone());
964        root.lookup_child(task, &mut Default::default(), name)
965    }
966
967    fn open_file_with_flags(
968        current_task: &CurrentTask,
969        fs: &FileSystemHandle,
970        name: &FsStr,
971        flags: OpenFlags,
972    ) -> Result<FileHandle, Errno> {
973        let node = lookup_node(current_task, fs, name)?;
974        node.open(current_task, flags)
975    }
976
977    fn open_file(
978        current_task: &CurrentTask,
979        fs: &FileSystemHandle,
980        name: &FsStr,
981    ) -> Result<FileHandle, Errno> {
982        open_file_with_flags(current_task, fs, name, OpenFlags::RDWR | OpenFlags::NOCTTY)
983    }
984
985    fn open_ptmx_and_unlock(
986        current_task: &CurrentTask,
987        fs: &FileSystemHandle,
988    ) -> Result<FileHandle, Errno> {
989        let file = open_file_with_flags(current_task, fs, "ptmx".into(), OpenFlags::RDWR)?;
990
991        // Unlock terminal
992        ioctl::<i32>(current_task, &file, TIOCSPTLCK, &0)?;
993
994        Ok(file)
995    }
996
997    #[fuchsia::test]
998    async fn opening_ptmx_creates_pts() {
999        spawn_kernel_and_run(async |task| {
1000            let kernel = task.kernel();
1001            tty_device_init(kernel).expect("tty_device_init");
1002            let fs = new_pts_fs(kernel);
1003            lookup_node(task, &fs, "0".into()).unwrap_err();
1004            let _ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1005            lookup_node(task, &fs, "0".into()).expect("pty");
1006        })
1007        .await;
1008    }
1009
1010    #[fuchsia::test]
1011    async fn closing_ptmx_closes_pts() {
1012        spawn_kernel_and_run(async |task| {
1013            let kernel = task.kernel();
1014            tty_device_init(kernel).expect("tty_device_init");
1015            let fs = new_pts_fs(kernel);
1016            lookup_node(task, &fs, "0".into()).unwrap_err();
1017            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1018            let _pts = open_file(task, &fs, "0".into()).expect("open file");
1019            std::mem::drop(ptmx);
1020            task.trigger_delayed_releaser();
1021            lookup_node(task, &fs, "0".into()).unwrap_err();
1022        })
1023        .await;
1024    }
1025
1026    #[fuchsia::test]
1027    async fn pts_are_reused() {
1028        spawn_kernel_and_run(async |task| {
1029            let kernel = task.kernel();
1030            tty_device_init(kernel).expect("tty_device_init");
1031            let fs = new_pts_fs(kernel);
1032
1033            let _ptmx0 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1034            let mut _ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1035            let _ptmx2 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1036
1037            lookup_node(task, &fs, "0".into()).expect("component_lookup");
1038            lookup_node(task, &fs, "1".into()).expect("component_lookup");
1039            lookup_node(task, &fs, "2".into()).expect("component_lookup");
1040
1041            std::mem::drop(_ptmx1);
1042            task.trigger_delayed_releaser();
1043
1044            lookup_node(task, &fs, "1".into()).unwrap_err();
1045
1046            _ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1047            lookup_node(task, &fs, "1".into()).expect("component_lookup");
1048        })
1049        .await;
1050    }
1051
1052    #[fuchsia::test]
1053    async fn opening_inexistant_replica_fails() {
1054        spawn_kernel_and_run(async |task| {
1055            let kernel = task.kernel();
1056            tty_device_init(kernel).expect("tty_device_init");
1057            // Initialize pts devices
1058            new_pts_fs(kernel);
1059            let fs = TmpFs::new_fs(kernel);
1060            let mount = MountInfo::detached();
1061            let pts = fs
1062                .root()
1063                .create_entry(task, &mount, "custom_pts".into(), |dir, mount, name| {
1064                    dir.create_node(
1065                        task,
1066                        mount,
1067                        name,
1068                        mode!(IFCHR, 0o666),
1069                        DeviceId::new(DEVPTS_FIRST_MAJOR, 0),
1070                        FsCred::root(),
1071                    )
1072                })
1073                .expect("custom_pts");
1074            let node = NamespaceNode::new_anonymous(pts.clone());
1075            assert!(node.open(task, OpenAccessCheck::skip(OpenFlags::RDONLY)).is_err());
1076        })
1077        .await;
1078    }
1079
1080    #[fuchsia::test]
1081    async fn test_open_tty() {
1082        spawn_kernel_with_features_and_run(
1083            async |task| {
1084                let kernel = task.kernel();
1085                tty_device_init(kernel).expect("tty_device_init");
1086                let fs = new_pts_fs(kernel);
1087                let devfs = starnix_core::fs::devtmpfs::DevTmpFs::from_kernel(kernel);
1088
1089                let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1090                set_controlling_terminal(task, &ptmx, false).expect("set_controlling_terminal");
1091                let tty =
1092                    open_file_with_flags(task, &devfs, "tty".into(), OpenFlags::RDWR).expect("tty");
1093                // Check that tty is the main terminal by calling the ioctl TIOCGPTN and checking it is
1094                // has the same result as on ptmx.
1095                assert_eq!(
1096                    ioctl::<i32>(task, &tty, TIOCGPTN, &0),
1097                    ioctl::<i32>(task, &ptmx, TIOCGPTN, &0)
1098                );
1099
1100                // Detach the controlling terminal.
1101                ioctl::<i32>(task, &ptmx, TIOCNOTTY, &0).expect("detach terminal");
1102                let pts = open_file(task, &fs, "0".into()).expect("open file");
1103                set_controlling_terminal(task, &pts, false).expect("set_controlling_terminal");
1104                let tty =
1105                    open_file_with_flags(task, &devfs, "tty".into(), OpenFlags::RDWR).expect("tty");
1106                // TIOCGPTN is not implemented on replica terminals
1107                assert!(ioctl::<i32>(task, &tty, TIOCGPTN, &0).is_err());
1108            },
1109            KernelFeatures { devtmpfs: true, ..Default::default() },
1110        )
1111        .await;
1112    }
1113
1114    #[fuchsia::test]
1115    async fn test_unknown_ioctl() {
1116        spawn_kernel_and_run(async |task| {
1117            let kernel = task.kernel();
1118            tty_device_init(kernel).expect("tty_device_init");
1119            let fs = new_pts_fs(kernel);
1120
1121            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1122            assert_eq!(ptmx.ioctl(task, 42, Default::default()), error!(ENOTTY));
1123
1124            let pts_file = open_file(task, &fs, "0".into()).expect("open file");
1125            assert_eq!(pts_file.ioctl(task, 42, Default::default()), error!(ENOTTY));
1126        })
1127        .await;
1128    }
1129
1130    #[fuchsia::test]
1131    async fn test_tiocgptn_ioctl() {
1132        spawn_kernel_and_run(async |task| {
1133            let kernel = task.kernel();
1134            tty_device_init(kernel).expect("tty_device_init");
1135            let fs = new_pts_fs(kernel);
1136            let ptmx0 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1137            let ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1138
1139            let pts0 = ioctl::<u32>(task, &ptmx0, TIOCGPTN, &0).expect("ioctl");
1140            assert_eq!(pts0, 0);
1141
1142            let pts1 = ioctl::<u32>(task, &ptmx1, TIOCGPTN, &0).expect("ioctl");
1143            assert_eq!(pts1, 1);
1144        })
1145        .await;
1146    }
1147
1148    #[fuchsia::test]
1149    async fn test_new_terminal_is_locked() {
1150        spawn_kernel_and_run(async |task| {
1151            let kernel = task.kernel();
1152            tty_device_init(kernel).expect("tty_device_init");
1153            let fs = new_pts_fs(kernel);
1154            let _ptmx_file = open_file(task, &fs, "ptmx".into()).expect("open file");
1155
1156            let pts = lookup_node(task, &fs, "0".into()).expect("component_lookup");
1157            assert_eq!(pts.open(task, OpenFlags::RDONLY).map(|_| ()), error!(EIO));
1158        })
1159        .await;
1160    }
1161
1162    #[fuchsia::test]
1163    async fn test_lock_ioctls() {
1164        spawn_kernel_and_run(async |task| {
1165            let kernel = task.kernel();
1166            tty_device_init(kernel).expect("tty_device_init");
1167            let fs = new_pts_fs(kernel);
1168            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1169            let pts = lookup_node(task, &fs, "0".into()).expect("component_lookup");
1170
1171            // Check that the lock is not set.
1172            assert_eq!(ioctl::<i32>(task, &ptmx, TIOCGPTLCK, &0), Ok(0));
1173            // /dev/pts/0 can be opened
1174            pts.open(task, OpenFlags::RDONLY).expect("open");
1175
1176            // Lock the terminal
1177            ioctl::<i32>(task, &ptmx, TIOCSPTLCK, &42).expect("ioctl");
1178            // Check that the lock is set.
1179            assert_eq!(ioctl::<i32>(task, &ptmx, TIOCGPTLCK, &0), Ok(1));
1180            // /dev/pts/0 cannot be opened
1181            assert_eq!(pts.open(task, OpenFlags::RDONLY).map(|_| ()), error!(EIO));
1182        })
1183        .await;
1184    }
1185
1186    #[fuchsia::test]
1187    async fn test_ptmx_stats() {
1188        spawn_kernel_and_run(async |task| {
1189            let kernel = task.kernel();
1190            tty_device_init(kernel).expect("tty_device_init");
1191            task.set_creds(Credentials::with_ids(22, 22));
1192            let fs = new_pts_fs(kernel);
1193            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1194            let ptmx_stat = ptmx.node().stat(task).expect("stat");
1195            assert_eq!(ptmx_stat.st_blksize as usize, BLOCK_SIZE);
1196            let pts = open_file(task, &fs, "0".into()).expect("open file");
1197            let pts_stats = pts.node().stat(task).expect("stat");
1198            assert_eq!(pts_stats.st_mode & FileMode::PERMISSIONS.bits(), 0o600);
1199            assert_eq!(pts_stats.st_uid, 22);
1200            // TODO(qsr): Check that gid is tty.
1201        })
1202        .await;
1203    }
1204
1205    #[fuchsia::test]
1206    async fn test_attach_terminal_when_open() {
1207        spawn_kernel_and_run(async |task| {
1208            let kernel = task.kernel();
1209            tty_device_init(kernel).expect("tty_device_init");
1210            let fs = new_pts_fs(kernel);
1211            let _opened_main = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1212            // Opening the main terminal should not set the terminal of the session.
1213            assert!(task.thread_group().controlling_terminal().is_none());
1214            // Opening the terminal should not set the terminal of the session with the NOCTTY flag.
1215            let _opened_replica2 =
1216                open_file_with_flags(task, &fs, "0".into(), OpenFlags::RDWR | OpenFlags::NOCTTY)
1217                    .expect("open file");
1218            assert!(task.thread_group().controlling_terminal().is_none());
1219
1220            // Opening the replica terminal should set the terminal of the session.
1221            let _opened_replica2 =
1222                open_file_with_flags(task, &fs, "0".into(), OpenFlags::RDWR).expect("open file");
1223            assert!(task.thread_group().controlling_terminal().is_some());
1224        })
1225        .await;
1226    }
1227
1228    #[fuchsia::test]
1229    async fn test_attach_terminal() {
1230        spawn_kernel_and_run(async |task1| {
1231            let kernel = task1.kernel();
1232            tty_device_init(kernel).expect("tty_device_init");
1233            let task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1234            task2.thread_group().setsid().expect("setsid");
1235
1236            let fs = new_pts_fs(kernel);
1237            let opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx");
1238            let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file");
1239
1240            assert_eq!(ioctl::<i32>(task1, &opened_main, TIOCGPGRP, &0), error!(ENOTTY));
1241            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY));
1242
1243            set_controlling_terminal(task1, &opened_main, false).unwrap();
1244            assert_eq!(
1245                ioctl::<i32>(task1, &opened_main, TIOCGPGRP, &0),
1246                Ok(task1.thread_group().pgid().id)
1247            );
1248            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY));
1249
1250            // Cannot steal terminal using the replica.
1251            assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM));
1252            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY));
1253        })
1254        .await;
1255    }
1256
1257    #[fuchsia::test]
1258    async fn test_steal_terminal() {
1259        spawn_kernel_and_run(async |task1| {
1260            let kernel = task1.kernel();
1261            tty_device_init(kernel).expect("tty_device_init");
1262            task1.set_creds(Credentials::with_ids(1, 1));
1263
1264            let task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1265
1266            let fs = new_pts_fs(kernel);
1267            let _opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx");
1268            let wo_opened_replica =
1269                open_file_with_flags(task1, &fs, "0".into(), OpenFlags::WRONLY | OpenFlags::NOCTTY)
1270                    .expect("open file");
1271            assert!(!wo_opened_replica.can_read());
1272
1273            // FD must be readable for setting the terminal.
1274            assert_eq!(set_controlling_terminal(task1, &wo_opened_replica, false), error!(EPERM));
1275
1276            let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file");
1277            // Task must be session leader for setting the terminal.
1278            assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM));
1279
1280            // Associate terminal to task1.
1281            set_controlling_terminal(task1, &opened_replica, false)
1282                .expect("Associate terminal to task1");
1283
1284            // Redundant association of the same terminal should succeed (no-op)
1285            set_controlling_terminal(task1, &opened_replica, false)
1286                .expect("Redundant association should succeed");
1287
1288            task2.thread_group().setsid().expect("setsid");
1289
1290            // One cannot associate a terminal that is already associated with another process.
1291            assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM));
1292
1293            // One cannot steal a terminal without the CAP_SYS_ADMIN capacility
1294            assert_eq!(set_controlling_terminal(&task2, &opened_replica, true), error!(EPERM));
1295
1296            // One can steal a terminal with the CAP_SYS_ADMIN capacility
1297            task2.set_creds(Credentials::with_ids(0, 0));
1298            // But not without specifying that one wants to steal it.
1299            assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM));
1300            set_controlling_terminal(&task2, &opened_replica, true)
1301                .expect("Associate terminal to task2");
1302
1303            assert!(task1.thread_group().controlling_terminal().is_none());
1304
1305            // If task2 already owns a controlling terminal, attempting to steal a second
1306            // terminal from task1 must fail with EPERM without disassociating task1's terminal.
1307            let _opened_main2 = open_ptmx_and_unlock(task1, &fs).expect("ptmx 1");
1308            let opened_replica2 = open_file(task1, &fs, "1".into()).expect("open file 1");
1309            set_controlling_terminal(task1, &opened_replica2, false)
1310                .expect("Associate second terminal to task1");
1311            assert!(task1.thread_group().controlling_terminal().is_some());
1312            assert_eq!(set_controlling_terminal(&task2, &opened_replica2, true), error!(EPERM));
1313            assert!(task1.thread_group().controlling_terminal().is_some());
1314        })
1315        .await;
1316    }
1317
1318    #[fuchsia::test]
1319    async fn test_tiocgsid() {
1320        spawn_kernel_and_run(async |task1| {
1321            let kernel = task1.kernel();
1322            tty_device_init(kernel).expect("tty_device_init");
1323            let mut task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1324            task2.set_mm_for_test(task1.mm().unwrap().clone());
1325            task2.thread_group().setsid().expect("setsid");
1326
1327            let fs = new_pts_fs(kernel);
1328            let opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx");
1329            let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file");
1330
1331            // Unattached terminal returns ENOTTY on both main and replica.
1332            assert_eq!(ioctl::<pid_t>(task1, &opened_main, TIOCGSID, &0), error!(ENOTTY));
1333            assert_eq!(ioctl::<pid_t>(&task2, &opened_replica, TIOCGSID, &0), error!(ENOTTY));
1334
1335            // Attach controlling session to task2.
1336            set_controlling_terminal(&task2, &opened_replica, false)
1337                .expect("set controlling terminal");
1338            let expected_sid = task2.thread_group().read().session.id;
1339
1340            // Calling TIOCGSID on both main and replica from the controlling session returns SID.
1341            assert_eq!(ioctl::<pid_t>(&task2, &opened_main, TIOCGSID, &0), Ok(expected_sid));
1342            assert_eq!(ioctl::<pid_t>(&task2, &opened_replica, TIOCGSID, &0), Ok(expected_sid));
1343
1344            // From a task in a different session, TIOCGSID on the replica returns ENOTTY,
1345            // whereas TIOCGSID on the main terminal succeeds and returns the controlling SID.
1346            assert_eq!(ioctl::<pid_t>(task1, &opened_replica, TIOCGSID, &0), error!(ENOTTY));
1347            assert_eq!(ioctl::<pid_t>(task1, &opened_main, TIOCGSID, &0), Ok(expected_sid));
1348
1349            // After detaching (TIOCNOTTY), TIOCGSID returns ENOTTY.
1350            ioctl::<i32>(&task2, &opened_replica, TIOCNOTTY, &0).expect("detach terminal");
1351            assert_eq!(ioctl::<pid_t>(&task2, &opened_main, TIOCGSID, &0), error!(ENOTTY));
1352            assert_eq!(ioctl::<pid_t>(&task2, &opened_replica, TIOCGSID, &0), error!(ENOTTY));
1353        })
1354        .await;
1355    }
1356
1357    #[fuchsia::test]
1358    async fn test_set_foreground_process() {
1359        spawn_kernel_and_run(async |init| {
1360            let kernel = init.kernel();
1361            tty_device_init(kernel).expect("tty_device_init");
1362            let mut task1 = init.clone_task_for_test(0, Some(SIGCHLD));
1363            // Share the init task's address space so ioctl usercopy operations can
1364            // access memory while running on this test thread.
1365            task1.set_mm_for_test(init.mm().unwrap().clone());
1366            task1.thread_group().setsid().expect("setsid");
1367            let pre_existing_task = task1.clone_task_for_test(0, Some(SIGCHLD));
1368
1369            let fs = new_pts_fs(kernel);
1370            let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx");
1371            let opened_replica = open_file(&pre_existing_task, &fs, "0".into()).expect("open file");
1372
1373            // Cannot change the foreground process group if the terminal is not the controlling
1374            // terminal.
1375            assert_eq!(
1376                ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task1.pid.id),
1377                error!(ENOTTY)
1378            );
1379
1380            // Attach terminal to task1; pre-existing session members do not retroactively gain
1381            // the controlling terminal, whereas newly cloned children inherit it.
1382            set_controlling_terminal(&task1, &opened_replica, false).unwrap();
1383            assert!(pre_existing_task.thread_group().controlling_terminal().is_none());
1384
1385            let mut task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1386            task2.set_mm_for_test(init.mm().unwrap().clone());
1387            task2.thread_group().setpgid(&task2, &task2, &task2.pid).expect("setpgid");
1388            let task2_pgid = task2.thread_group().pgid();
1389            assert_ne!(task2_pgid, task1.thread_group().pgid());
1390
1391            // The foreground process group should be the one of task1
1392            assert_eq!(
1393                ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0),
1394                Ok(task1.thread_group().pgid().id)
1395            );
1396
1397            // Cannot change the foreground process group to a negative pid.
1398            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &-1), error!(EINVAL));
1399
1400            // Cannot change the foreground process group to a invalid process group.
1401            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &255), error!(ESRCH));
1402
1403            // Cannot change the foreground process group to a process group in another session.
1404            let init_pgid = init.thread_group().pgid();
1405            assert_eq!(
1406                ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &init_pgid.id),
1407                error!(EPERM)
1408            );
1409
1410            // Changing the foreground process while being in background generates SIGTTOU and fails.
1411            assert_eq!(
1412                ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &task2_pgid.id),
1413                error!(EINTR)
1414            );
1415            assert!(task2.read().has_signal_pending(SIGTTOU));
1416
1417            // Set the foreground process to task2 process group
1418            ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task2_pgid.id).unwrap();
1419
1420            // Check that the foreground process has been changed.
1421            let terminal =
1422                Arc::clone(&task1.thread_group().controlling_terminal().as_ref().unwrap().terminal);
1423            assert_eq!(
1424                terminal.read().controlling_session.as_ref().unwrap().foreground_process_group(),
1425                &task2_pgid
1426            );
1427        })
1428        .await;
1429    }
1430
1431    #[fuchsia::test]
1432    async fn test_detach_session() {
1433        spawn_kernel_and_run(async |task1| {
1434            let kernel = task1.kernel();
1435            tty_device_init(kernel).expect("tty_device_init");
1436            let mut task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1437            task2.thread_group().setsid().expect("setsid");
1438
1439            let fs = new_pts_fs(kernel);
1440            let _opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx");
1441            let opened_replica = open_file(task1, &fs, "0".into()).expect("open file");
1442
1443            // Cannot detach the controlling terminal when none is attached terminal
1444            assert_eq!(ioctl::<i32>(task1, &opened_replica, TIOCNOTTY, &0), error!(ENOTTY));
1445
1446            set_controlling_terminal(&task2, &opened_replica, false)
1447                .expect("set controlling terminal");
1448
1449            // Cannot detach the controlling terminal when not in the controlling session.
1450            assert_eq!(ioctl::<i32>(task1, &opened_replica, TIOCNOTTY, &0), error!(ENOTTY));
1451
1452            // A non-session-leader child in the same session can detach its own controlling
1453            // terminal via TIOCNOTTY without detaching the session leader.
1454            let mut task3 = task2.clone_task_for_test(0, Some(SIGCHLD));
1455            task3.set_mm_for_test(task1.mm().unwrap().clone());
1456            task2.set_mm_for_test(task1.mm().unwrap().clone());
1457            assert!(task3.thread_group().controlling_terminal().is_some());
1458            assert_eq!(ioctl::<i32>(&task3, &opened_replica, TIOCGPGRP, &0), Ok(task2.pid.id));
1459
1460            ioctl::<i32>(&task3, &opened_replica, TIOCNOTTY, &0)
1461                .expect("non-leader detach terminal");
1462            assert!(task3.thread_group().controlling_terminal().is_none());
1463            assert!(task2.thread_group().controlling_terminal().is_some());
1464            assert_eq!(ioctl::<i32>(&task3, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY));
1465            assert_eq!(
1466                ioctl::<i32>(&task3, &opened_replica, TIOCSPGRP, &task2.pid.id),
1467                error!(ENOTTY)
1468            );
1469            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), Ok(task2.pid.id));
1470
1471            // Detach the terminal from the session leader.
1472            ioctl::<i32>(&task2, &opened_replica, TIOCNOTTY, &0).expect("detach terminal");
1473            assert!(task2.thread_group().controlling_terminal().is_none());
1474        })
1475        .await;
1476    }
1477
1478    #[fuchsia::test]
1479    async fn test_send_data_back_and_forth() {
1480        spawn_kernel_and_run(async |task| {
1481            let kernel = task.kernel();
1482            tty_device_init(kernel).expect("tty_device_init");
1483            let fs = new_pts_fs(kernel);
1484            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1485            let pts = open_file(task, &fs, "0".into()).expect("open file");
1486
1487            let has_data_ready_to_read = |fd: &FileHandle| {
1488                fd.query_events(task).expect("query_events").contains(FdEvents::POLLIN)
1489            };
1490
1491            let write_and_assert = |fd: &FileHandle, data: &[u8]| {
1492                assert_eq!(
1493                    fd.write(task, &mut VecInputBuffer::new(data)).expect("write"),
1494                    data.len()
1495                );
1496            };
1497
1498            let read_and_check = |fd: &FileHandle, data: &[u8]| {
1499                assert!(has_data_ready_to_read(fd));
1500                let mut buffer = VecOutputBuffer::new(data.len() + 1);
1501                assert_eq!(fd.read(task, &mut buffer).expect("read"), data.len());
1502                assert_eq!(data, buffer.data());
1503            };
1504
1505            let hello_buffer = b"hello\n";
1506            let hello_transformed_buffer = b"hello\r\n";
1507
1508            // Main to replica
1509            write_and_assert(&ptmx, hello_buffer);
1510            read_and_check(&pts, hello_buffer);
1511
1512            // Data has been echoed
1513            read_and_check(&ptmx, hello_transformed_buffer);
1514
1515            // Replica to main
1516            write_and_assert(&pts, hello_buffer);
1517            read_and_check(&ptmx, hello_transformed_buffer);
1518
1519            // Data has not been echoed
1520            assert!(!has_data_ready_to_read(&pts));
1521        })
1522        .await;
1523    }
1524
1525    #[fuchsia::test]
1526    async fn test_foreground_pgrp_persistence_and_signal_noop_on_exit() {
1527        spawn_kernel_and_run(async |init| {
1528            let kernel = init.kernel();
1529            tty_device_init(kernel).expect("tty_device_init");
1530
1531            let mut task1 = init.clone_task_for_test(0, Some(SIGCHLD));
1532            task1.set_mm_for_test(init.mm().unwrap().clone());
1533            task1.thread_group().setsid().expect("setsid");
1534            let task1_pgid = task1.thread_group().pgid();
1535
1536            let fs = new_pts_fs(kernel);
1537            let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx");
1538            let opened_replica = open_file(&task1, &fs, "0".into()).expect("open replica");
1539            set_controlling_terminal(&task1, &opened_replica, false)
1540                .expect("set controlling terminal");
1541
1542            let terminal =
1543                Arc::clone(&task1.thread_group().controlling_terminal().as_ref().unwrap().terminal);
1544
1545            // Spawn two tasks in a new foreground process group.
1546            let mut task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1547            task2.set_mm_for_test(init.mm().unwrap().clone());
1548            task2.thread_group().setpgid(&task2, &task2, &task2.pid).expect("setpgid task2");
1549            let task2_pgid = task2.thread_group().pgid();
1550            let task2_pgid_id = task2_pgid.id;
1551
1552            let mut task3 = task1.clone_task_for_test(0, Some(SIGCHLD));
1553            task3.set_mm_for_test(init.mm().unwrap().clone());
1554            task1.thread_group().setpgid(&task1, &task3, &task2_pgid).expect("setpgid task3");
1555
1556            // Set task2's process group as the foreground process group.
1557            ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task2_pgid_id)
1558                .expect("set foreground process group");
1559            assert_eq!(ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), Ok(task2_pgid_id));
1560
1561            // Mask SIGWINCH on all three tasks so default-ignore disposition does not discard it.
1562            task1.write().set_signal_mask(SigSet::from(SIGWINCH));
1563            task2.write().set_signal_mask(SigSet::from(SIGWINCH));
1564            task3.write().set_signal_mask(SigSet::from(SIGWINCH));
1565
1566            // Deliver SIGWINCH to the foreground group while both tasks are alive.
1567            terminal.send_signals(&[SIGWINCH]);
1568            assert!(task2.thread_group().has_signal_queued(SIGWINCH));
1569            assert!(task3.thread_group().has_signal_queued(SIGWINCH));
1570            assert!(!task1.thread_group().has_signal_queued(SIGWINCH));
1571
1572            // Terminate the process group leader (task2) while task3 remains alive in the group.
1573            task2.thread_group().kill(ExitStatus::Exit(0), None);
1574            std::mem::drop(task2);
1575
1576            // Foreground PGID remains task2_pgid_id, and signals reach the surviving member task3.
1577            assert_eq!(ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), Ok(task2_pgid_id));
1578            terminal.send_signals(&[SIGINT]);
1579            assert!(task3.thread_group().has_signal_queued(SIGINT));
1580            assert!(!task1.thread_group().has_signal_queued(SIGINT));
1581
1582            // Terminate the final member (task3) and reap both zombies so the foreground process
1583            // group is completely reclaimed from PidTable.
1584            task3.thread_group().kill(ExitStatus::Exit(0), None);
1585            std::mem::drop(task3);
1586            {
1587                let mut pids = kernel.pids.lock();
1588                for zombie in task1.thread_group().write().zombie_children.drain(..) {
1589                    zombie.release(&mut pids);
1590                }
1591            }
1592            assert_eq!(kernel.pids.get(task2_pgid_id), error!(ESRCH));
1593
1594            // Verify tcgetpgrp (TIOCGPGRP) still returns the exited group's PGID.
1595            assert_eq!(ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), Ok(task2_pgid_id));
1596
1597            // Verify signal dispatch to the exited foreground process group is a safe no-op.
1598            terminal.send_signals(&[SIGINT, SIGQUIT, SIGTSTP, SIGWINCH, SIGHUP]);
1599            assert!(!task1.thread_group().has_signal_queued(SIGINT));
1600            assert!(!task1.thread_group().has_signal_queued(SIGQUIT));
1601            assert!(!task1.thread_group().has_signal_queued(SIGTSTP));
1602            assert!(!task1.thread_group().has_signal_queued(SIGWINCH));
1603            assert!(!task1.thread_group().has_signal_queued(SIGHUP));
1604
1605            // Mask SIGTTOU on task1 and reclaim the foreground process group.
1606            task1.write().set_signal_mask(SigSet::from(SIGTTOU) | SigSet::from(SIGWINCH));
1607            ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task1_pgid.id)
1608                .expect("reclaim foreground process group");
1609            assert_eq!(ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0), Ok(task1_pgid.id));
1610
1611            // Move foreground to another short-lived process group, exit it, and disassociate.
1612            let mut task4 = task1.clone_task_for_test(0, Some(SIGCHLD));
1613            task4.set_mm_for_test(init.mm().unwrap().clone());
1614            task4.thread_group().setpgid(&task4, &task4, &task4.pid).expect("setpgid task4");
1615            let task4_pgid_id = task4.pid.id;
1616            ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task4_pgid_id)
1617                .expect("set foreground to task4");
1618            task4.thread_group().kill(ExitStatus::Exit(0), None);
1619            std::mem::drop(task4);
1620
1621            terminal.disassociate_controlling_session();
1622            assert!(task1.thread_group().controlling_terminal().is_none());
1623            assert!(!task1.thread_group().has_signal_queued(SIGHUP));
1624            assert!(!task1.thread_group().has_signal_queued(SIGCONT));
1625        })
1626        .await;
1627    }
1628
1629    #[fuchsia::test]
1630    async fn test_concurrent_terminal_job_control_stress() {
1631        spawn_kernel_and_run(async |init| {
1632            let kernel = init.kernel();
1633            tty_device_init(kernel).expect("tty_device_init");
1634
1635            let mut task1 = init.clone_task_for_test(0, Some(SIGCHLD));
1636            task1.set_mm_for_test(init.mm().unwrap().clone());
1637            task1.thread_group().setsid().expect("setsid task1");
1638            task1.write().set_signal_mask(SigSet::from(SIGTTOU));
1639            let task1_pgid = task1.thread_group().pgid();
1640
1641            let mut child_a = task1.clone_task_for_test(0, Some(SIGCHLD));
1642            child_a.set_mm_for_test(init.mm().unwrap().clone());
1643            child_a
1644                .thread_group()
1645                .setpgid(&child_a, &child_a, &child_a.pid)
1646                .expect("setpgid child_a");
1647            let child_a_pgid = child_a.pid.clone();
1648
1649            let mut child_b = task1.clone_task_for_test(0, Some(SIGCHLD));
1650            child_b.set_mm_for_test(init.mm().unwrap().clone());
1651            child_b
1652                .thread_group()
1653                .setpgid(&child_b, &child_b, &child_b.pid)
1654                .expect("setpgid child_b");
1655            let child_b_pgid = child_b.pid.clone();
1656
1657            let mut child_m = task1.clone_task_for_test(0, Some(SIGCHLD));
1658            child_m.set_mm_for_test(init.mm().unwrap().clone());
1659
1660            let fs = new_pts_fs(kernel);
1661            let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx 0");
1662            let opened_replica = open_file(&task1, &fs, "0".into()).expect("open replica 0");
1663            set_controlling_terminal(&task1, &opened_replica, false)
1664                .expect("set controlling terminal 0");
1665            let terminal =
1666                Arc::clone(&task1.thread_group().controlling_terminal().as_ref().unwrap().terminal);
1667
1668            // Create a second session leader and terminal to stress concurrent disassociation.
1669            let mut task_s2 = init.clone_task_for_test(0, Some(SIGCHLD));
1670            task_s2.set_mm_for_test(init.mm().unwrap().clone());
1671            task_s2.thread_group().setsid().expect("setsid task_s2");
1672            task_s2.write().set_signal_mask(SigSet::from(SIGTTOU));
1673            let task_s2_pgid = task_s2.pid.clone();
1674
1675            let _opened_main2 = open_ptmx_and_unlock(init, &fs).expect("ptmx 1");
1676            let opened_replica2 = open_file(&task_s2, &fs, "1".into()).expect("open replica 1");
1677            set_controlling_terminal(&task_s2, &opened_replica2, false)
1678                .expect("set controlling terminal 1");
1679            let terminal2 = Arc::clone(
1680                &task_s2.thread_group().controlling_terminal().as_ref().unwrap().terminal,
1681            );
1682
1683            let running = Arc::new(AtomicBool::new(true));
1684
1685            // Thread 1: Concurrently dispatch signals to the foreground process groups.
1686            let sig_term1 = Arc::clone(&terminal);
1687            let sig_term2 = Arc::clone(&terminal2);
1688            let sig_running = Arc::clone(&running);
1689            let signal_thread = std::thread::spawn(move || {
1690                while sig_running.load(Ordering::Relaxed) {
1691                    sig_term1.send_signals(&[SIGWINCH, SIGCONT]);
1692                    sig_term2.send_signals(&[SIGWINCH, SIGCONT]);
1693                }
1694            });
1695
1696            // Thread 2: Concurrently read the foreground process group and verify PGID validity.
1697            let get_tg1 = Arc::clone(task1.thread_group());
1698            let get_term1 = Arc::clone(&terminal);
1699            let get_running = Arc::clone(&running);
1700            let valid_pgids = [task1_pgid.id, child_a_pgid.id, child_b_pgid.id];
1701            let getter_thread = std::thread::spawn(move || {
1702                while get_running.load(Ordering::Relaxed) {
1703                    if let Ok(pgid) = get_tg1.get_foreground_process_group(&get_term1) {
1704                        assert!(valid_pgids.contains(&pgid));
1705                    }
1706                }
1707            });
1708
1709            // Thread 3: Concurrently invoke setsid on group leaders (acquiring PidTableLock).
1710            let setsid_tg1 = Arc::clone(task1.thread_group());
1711            let setsid_tga = Arc::clone(child_a.thread_group());
1712            let setsid_running = Arc::clone(&running);
1713            let setsid_thread = std::thread::spawn(move || {
1714                while setsid_running.load(Ordering::Relaxed) {
1715                    assert_eq!(setsid_tg1.setsid(), error!(EPERM));
1716                    assert_eq!(setsid_tga.setsid(), error!(EPERM));
1717                }
1718            });
1719
1720            // Thread 4: Concurrently disassociate the controlling session on terminal2.
1721            let disassoc_term2 = Arc::clone(&terminal2);
1722            let disassoc_running = Arc::clone(&running);
1723            let disassoc_thread = std::thread::spawn(move || {
1724                while disassoc_running.load(Ordering::Relaxed) {
1725                    disassoc_term2.disassociate_controlling_session();
1726                }
1727            });
1728
1729            // Main thread: Interleave TIOCSPGRP, setpgid task migration, and terminal attach/detach.
1730            for _ in 0..75 {
1731                let _ = ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &child_a_pgid.id);
1732                task1
1733                    .thread_group()
1734                    .setpgid(&task1, &child_m, &child_a_pgid)
1735                    .expect("migrate child_m to child_a");
1736
1737                let _ = ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &child_b_pgid.id);
1738                task1
1739                    .thread_group()
1740                    .setpgid(&task1, &child_m, &child_b_pgid)
1741                    .expect("migrate child_m to child_b");
1742
1743                let _ = ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task1_pgid.id);
1744
1745                let _ = set_controlling_terminal(&task_s2, &opened_replica2, false);
1746                let _ = task_s2.thread_group().set_foreground_process_group(
1747                    &task_s2,
1748                    &terminal2,
1749                    &task_s2_pgid,
1750                );
1751                let _ = ioctl::<i32>(&task_s2, &opened_replica2, TIOCNOTTY, &0);
1752            }
1753
1754            // Disassociate primary terminal while background threads are still active.
1755            terminal.disassociate_controlling_session();
1756
1757            running.store(false, Ordering::Relaxed);
1758            signal_thread.join().expect("signal_thread join");
1759            getter_thread.join().expect("getter_thread join");
1760            setsid_thread.join().expect("setsid_thread join");
1761            disassoc_thread.join().expect("disassoc_thread join");
1762
1763            assert!(task1.thread_group().controlling_terminal().is_none());
1764            assert!(task_s2.thread_group().controlling_terminal().is_none());
1765        })
1766        .await;
1767    }
1768
1769    #[fuchsia::test]
1770    async fn test_tiocnotty_non_leader_only_detaches_caller() {
1771        spawn_kernel_and_run(async |init| {
1772            let kernel = init.kernel();
1773            tty_device_init(kernel).expect("tty_device_init");
1774
1775            let mut leader = init.clone_task_for_test(0, Some(SIGCHLD));
1776            leader.set_mm_for_test(init.mm().unwrap().clone());
1777            leader.thread_group().setsid().expect("setsid");
1778
1779            let fs = new_pts_fs(kernel);
1780            let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx");
1781            let opened_replica = open_file(&leader, &fs, "0".into()).expect("open replica");
1782            set_controlling_terminal(&leader, &opened_replica, false)
1783                .expect("set controlling terminal");
1784
1785            // Spawn a non-leader process in the same session.
1786            let mut child = leader.clone_task_for_test(0, Some(SIGCHLD));
1787            child.set_mm_for_test(init.mm().unwrap().clone());
1788            assert!(leader.thread_group().controlling_terminal().is_some());
1789            assert!(child.thread_group().controlling_terminal().is_some());
1790
1791            let terminal = Arc::clone(
1792                &leader.thread_group().controlling_terminal().as_ref().unwrap().terminal,
1793            );
1794
1795            // Non-leader calling TIOCNOTTY only detaches its own controlling terminal without
1796            // disassociating the session or signaling the foreground process group.
1797            ioctl::<i32>(&child, &opened_replica, TIOCNOTTY, &0)
1798                .expect("non-leader TIOCNOTTY should succeed");
1799            assert!(child.thread_group().controlling_terminal().is_none());
1800            assert!(leader.thread_group().controlling_terminal().is_some());
1801            assert_eq!(terminal.read().controlling_session(), Some(&leader.pid));
1802            assert!(!leader.thread_group().has_signal_queued(SIGHUP));
1803            assert!(!child.thread_group().has_signal_queued(SIGHUP));
1804
1805            // A second TIOCNOTTY from the already-detached non-leader fails with ENOTTY.
1806            assert_eq!(ioctl::<i32>(&child, &opened_replica, TIOCNOTTY, &0), error!(ENOTTY));
1807
1808            // Session leader calling TIOCNOTTY disassociates the entire session.
1809            ioctl::<i32>(&leader, &opened_replica, TIOCNOTTY, &0)
1810                .expect("leader TIOCNOTTY should succeed");
1811            assert!(leader.thread_group().controlling_terminal().is_none());
1812            assert!(terminal.read().controlling_session().is_none());
1813        })
1814        .await;
1815    }
1816}