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, 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, DirectoryEntryType, FdFlags, FileHandle, FileObject, FileObjectState, FileOps,
18    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_device(
144        kernel,
145        "tty".into(),
146        DeviceMetadata::new("tty".into(), DeviceId::TTY, DeviceMode::Char),
147        tty_class.clone(),
148        build_device_directory,
149    )?;
150    registry.add_device(
151        kernel,
152        "ptmx".into(),
153        DeviceMetadata::new("ptmx".into(), DeviceId::PTMX, DeviceMode::Char),
154        tty_class,
155        build_device_directory,
156    )?;
157    Ok(())
158}
159
160struct DevPtsFs {
161    state: Arc<TtyState>,
162    uid: Option<uid_t>,
163    gid: Option<gid_t>,
164    mode: u32,
165    ptmxmode: u32,
166}
167
168impl FileSystemOps for DevPtsFs {
169    fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> {
170        Ok(default_statfs(DEVPTS_SUPER_MAGIC))
171    }
172    fn name(&self) -> &'static FsStr {
173        "devpts".into()
174    }
175
176    fn uses_external_node_ids(&self) -> bool {
177        false
178    }
179}
180
181impl DevPtsFs {
182    fn pty_creds_for(&self, current_task: &CurrentTask) -> FsCred {
183        let creds = current_task.current_creds();
184        let uid = self.uid.unwrap_or_else(|| creds.uid);
185        let gid = self.gid.unwrap_or_else(|| creds.gid);
186        FsCred { uid, gid }
187    }
188}
189
190struct DevPtsRootDir {
191    state: Arc<TtyState>,
192}
193
194impl FsNodeOps for DevPtsRootDir {
195    fs_node_impl_dir_readonly!();
196
197    fn create_file_ops(
198        &self,
199        _node: &FsNode,
200        _current_task: &CurrentTask,
201        _flags: OpenFlags,
202    ) -> Result<Box<dyn FileOps>, Errno> {
203        let mut result = vec![];
204        result.push(VecDirectoryEntry {
205            entry_type: DirectoryEntryType::CHR,
206            name: "ptmx".into(),
207            inode: Some(PTMX_NODE_ID),
208        });
209        for (id, terminal) in self.state.terminals.read().iter() {
210            if let Some(terminal) = terminal.upgrade() {
211                if !terminal.read().is_main_closed() {
212                    result.push(VecDirectoryEntry {
213                        entry_type: DirectoryEntryType::CHR,
214                        name: format!("{id}").into(),
215                        inode: Some((*id as ino_t) + FIRST_PTS_NODE_ID),
216                    });
217                }
218            }
219        }
220        Ok(VecDirectory::new_file(result))
221    }
222
223    fn lookup(
224        &self,
225        node: &FsNode,
226        _current_task: &CurrentTask,
227        name: &FsStr,
228    ) -> Result<FsNodeHandle, Errno> {
229        let fs = node.fs();
230        let devptsfs =
231            fs.downcast_ops::<DevPtsFs>().expect("DevPts should only handle `DevPtsFs`s");
232        let name = std::str::from_utf8(name).map_err(|_| errno!(ENOENT))?;
233        if name == "ptmx" {
234            let mut info = FsNodeInfo::new(mode!(IFCHR, devptsfs.ptmxmode), FsCred::root());
235            info.rdev = DeviceId::PTMX;
236            info.blksize = BLOCK_SIZE;
237            let node = fs.create_node(PTMX_NODE_ID, SpecialNode, info);
238            return Ok(node);
239        }
240        if let Ok(id) = name.parse::<u32>() {
241            let terminal = self.state.terminals.read().get(&id).and_then(Weak::upgrade);
242            if let Some(terminal) = terminal {
243                if !terminal.read().is_main_closed() {
244                    let ino = (id as ino_t) + FIRST_PTS_NODE_ID;
245                    let mut info =
246                        FsNodeInfo::new(mode!(IFCHR, devptsfs.mode), terminal.fscred.clone());
247                    info.rdev = get_device_type_for_pts(id);
248                    info.blksize = BLOCK_SIZE;
249                    let node = fs.create_node(ino, SpecialNode, info);
250                    return Ok(node);
251                }
252            }
253        }
254        error!(ENOENT)
255    }
256}
257
258fn open_dev_pts_device(
259    current_task: &CurrentTask,
260    id: DeviceId,
261    node: &NamespaceNode,
262    flags: OpenFlags,
263) -> Result<Box<dyn FileOps>, Errno> {
264    match id {
265        // /dev/ptmx and /dev/pts/ptmx
266        DeviceId::PTMX => {
267            let fs = node.entry.node.fs();
268            let Some(devpts_fs) = fs.downcast_ops::<DevPtsFs>() else {
269                // The device is not in ptmx, let try to find something at pts/ptmx relative to
270                // the parent of the node
271                let parent = node.parent().ok_or_else(|| errno!(EINVAL))?;
272                let mut lookup_context = LookupContext::new(SymlinkMode::Follow);
273                let ptmx_node =
274                    current_task.lookup_path(&mut lookup_context, parent, "pts/ptmx".into())?;
275                return open_dev_pts_device(current_task, id, &ptmx_node, flags);
276            };
277
278            let creds = devpts_fs.pty_creds_for(current_task);
279            let terminal = devpts_fs.state.get_next_terminal(fs.root().clone(), creds)?;
280            let name = FsString::from(terminal.id.to_string());
281            let replica_dir_entry =
282                fs.root().component_lookup(current_task, &MountInfo::detached(), name.as_ref())?;
283            let replica_node =
284                NamespaceNode { mount: node.mount.clone(), entry: replica_dir_entry };
285            Ok(Box::new(DevPtmxFile::new(terminal, Some(replica_node))))
286        }
287        // /dev/tty
288        DeviceId::TTY => {
289            let controlling_terminal = current_task
290                .thread_group()
291                .read()
292                .process_group
293                .session
294                .read()
295                .controlling_terminal
296                .clone();
297            if let Some(controlling_terminal) = controlling_terminal {
298                if controlling_terminal.is_main {
299                    Ok(Box::new(DevPtmxFile::new(controlling_terminal.terminal, None)))
300                } else {
301                    Ok(Box::new(TtyFile::new(controlling_terminal.terminal)))
302                }
303            } else {
304                error!(ENXIO)
305            }
306        }
307        _ if id.major() < DEVPTS_FIRST_MAJOR
308            || id.major() >= DEVPTS_FIRST_MAJOR + DEVPTS_MAJOR_COUNT =>
309        {
310            error!(ENODEV)
311        }
312        // /dev/pts/??
313        _ => {
314            let fs = node.entry.node.fs();
315            let Some(devpts_fs) = fs.downcast_ops::<DevPtsFs>() else {
316                return error!(ENOTSUP);
317            };
318            let pts_id = (id.major() - DEVPTS_FIRST_MAJOR) * 256 + id.minor();
319            let terminal = devpts_fs
320                .state
321                .terminals
322                .read()
323                .get(&pts_id)
324                .and_then(Weak::upgrade)
325                .ok_or_else(|| errno!(EIO))?;
326            if terminal.read().line_discipline.locked {
327                return error!(EIO);
328            }
329            if !flags.contains(OpenFlags::NOCTTY) {
330                // Opening a replica sets the process' controlling TTY when possible. An error indicates it cannot
331                // be set, and is ignored silently.
332                let _ = current_task.thread_group().set_controlling_terminal(
333                    current_task,
334                    &terminal,
335                    false, /* is_main */
336                    false, /* steal */
337                    flags.can_read(),
338                );
339            }
340            Ok(Box::new(TtyFile::new(terminal)))
341        }
342    }
343}
344
345struct DevPtmxFile {
346    terminal: Arc<Terminal>,
347
348    /// The replica's NamespaceNode, used to implement `TIOCGPTPEER` when opened via `/dev/ptmx`.
349    /// It is `None` if opened via `/dev/tty` redirecting to the main terminal. On Linux, `/dev/tty`
350    /// always redirects to the replica PTY (even if `TIOCSCTTY` is called on the main PTY), where
351    /// `TIOCGPTPEER` is unsupported.
352    replica_node: Option<NamespaceNode>,
353}
354
355impl DevPtmxFile {
356    pub fn new(terminal: Arc<Terminal>, replica_node: Option<NamespaceNode>) -> Self {
357        terminal.main_open();
358        Self { terminal, replica_node }
359    }
360}
361
362impl FileOps for DevPtmxFile {
363    fileops_impl_nonseekable!();
364    fileops_impl_noop_sync!();
365
366    fn close(self: Box<Self>, _file: &FileObjectState, _current_task: &CurrentTask) {
367        let session = {
368            let terminal = self.terminal.read();
369            terminal.controller.as_ref().and_then(|c| c.session.upgrade())
370        };
371        if let Some(session) = session {
372            session.disassociate_controlling_terminal();
373        }
374        self.terminal.main_close();
375    }
376
377    fn read(
378        &self,
379        file: &FileObject,
380        current_task: &CurrentTask,
381        offset: usize,
382        data: &mut dyn OutputBuffer,
383    ) -> Result<usize, Errno> {
384        debug_assert!(offset == 0);
385        file.blocking_op(current_task, FdEvents::POLLIN | FdEvents::POLLHUP, None, || {
386            self.terminal.main_read(data)
387        })
388    }
389
390    fn write(
391        &self,
392        file: &FileObject,
393        current_task: &CurrentTask,
394        offset: usize,
395        data: &mut dyn InputBuffer,
396    ) -> Result<usize, Errno> {
397        debug_assert!(offset == 0);
398        file.blocking_op(current_task, FdEvents::POLLOUT | FdEvents::POLLHUP, None, || {
399            self.terminal.main_write(data)
400        })
401    }
402
403    fn wait_async(
404        &self,
405        _file: &FileObject,
406        _current_task: &CurrentTask,
407        waiter: &Waiter,
408        events: FdEvents,
409        handler: EventHandler,
410    ) -> Option<WaitCanceler> {
411        Some(self.terminal.main_wait_async(waiter, events, handler))
412    }
413
414    fn query_events(
415        &self,
416        _file: &FileObject,
417        _current_task: &CurrentTask,
418    ) -> Result<FdEvents, Errno> {
419        Ok(self.terminal.main_query_events())
420    }
421
422    fn ioctl(
423        &self,
424        file: &FileObject,
425        current_task: &CurrentTask,
426        request: u32,
427        arg: SyscallArg,
428    ) -> Result<SyscallResult, Errno> {
429        let user_addr = UserAddress::from(arg);
430        match request {
431            TIOCGPTN => {
432                // Get the terminal id.
433                let value: u32 = self.terminal.id;
434                current_task.write_object(UserRef::<u32>::new(user_addr), &value)?;
435                Ok(SUCCESS)
436            }
437            TIOCGPTLCK => {
438                // Get the lock status.
439                let value = i32::from(self.terminal.read().line_discipline.locked);
440                current_task.write_object(UserRef::<i32>::new(user_addr), &value)?;
441                Ok(SUCCESS)
442            }
443            TIOCSPTLCK => {
444                // Lock/Unlock the terminal.
445                let value = current_task.read_object(UserRef::<i32>::new(user_addr))?;
446                self.terminal.write().line_discipline.locked = value != 0;
447                Ok(SUCCESS)
448            }
449            TIOCGPTPEER => {
450                let Some(replica_node) = &self.replica_node else {
451                    return error!(ENOTTY);
452                };
453
454                if replica_node.mount.flags().contains(MountFlags::NODEV) {
455                    return error!(EACCES);
456                }
457
458                let flags = OpenFlags::from_bits_truncate(u32::from(arg));
459                let replica_file = replica_node.open(current_task, flags)?;
460
461                let fd_flags = if flags.contains(OpenFlags::CLOEXEC) {
462                    FdFlags::CLOEXEC
463                } else {
464                    FdFlags::empty()
465                };
466                let fd = current_task.add_file(replica_file, fd_flags)?;
467                Ok(fd.into())
468            }
469            _ => shared_ioctl(&self.terminal, true, file, current_task, request, arg),
470        }
471    }
472}
473
474pub struct TtyFile {
475    terminal: Arc<Terminal>,
476}
477
478impl TtyFile {
479    pub fn new(terminal: Arc<Terminal>) -> Self {
480        terminal.replica_open();
481        Self { terminal }
482    }
483}
484
485impl FileOps for TtyFile {
486    fileops_impl_nonseekable!();
487    fileops_impl_noop_sync!();
488
489    fn close(self: Box<Self>, _file: &FileObjectState, _current_task: &CurrentTask) {
490        self.terminal.replica_close();
491    }
492
493    fn read(
494        &self,
495        file: &FileObject,
496        current_task: &CurrentTask,
497        offset: usize,
498        data: &mut dyn OutputBuffer,
499    ) -> Result<usize, Errno> {
500        debug_assert!(offset == 0);
501        file.blocking_op(current_task, FdEvents::POLLIN | FdEvents::POLLHUP, None, || {
502            self.terminal.replica_read(data)
503        })
504    }
505
506    fn write(
507        &self,
508        file: &FileObject,
509        current_task: &CurrentTask,
510        offset: usize,
511        data: &mut dyn InputBuffer,
512    ) -> Result<usize, Errno> {
513        debug_assert!(offset == 0);
514        file.blocking_op(current_task, FdEvents::POLLOUT | FdEvents::POLLHUP, None, || {
515            self.terminal.replica_write(data)
516        })
517    }
518
519    fn wait_async(
520        &self,
521        _file: &FileObject,
522        _current_task: &CurrentTask,
523        waiter: &Waiter,
524        events: FdEvents,
525        handler: EventHandler,
526    ) -> Option<WaitCanceler> {
527        Some(self.terminal.replica_wait_async(waiter, events, handler))
528    }
529
530    fn query_events(
531        &self,
532        _file: &FileObject,
533        _current_task: &CurrentTask,
534    ) -> Result<FdEvents, Errno> {
535        Ok(self.terminal.replica_query_events())
536    }
537
538    fn ioctl(
539        &self,
540        file: &FileObject,
541        current_task: &CurrentTask,
542        request: u32,
543        arg: SyscallArg,
544    ) -> Result<SyscallResult, Errno> {
545        shared_ioctl(&self.terminal, false, file, current_task, request, arg)
546    }
547}
548
549/// The ioctl behaviour common to main and replica terminal file descriptors.
550fn shared_ioctl(
551    terminal: &Terminal,
552    is_main: bool,
553    file: &FileObject,
554    current_task: &CurrentTask,
555    request: u32,
556    arg: SyscallArg,
557) -> Result<SyscallResult, Errno> {
558    let user_addr = UserAddress::from(arg);
559    match request {
560        FIONREAD => {
561            // Get the main terminal available bytes for reading.
562            let value = terminal.read().get_available_read_size(is_main) as u32;
563            current_task.write_object(UserRef::<u32>::new(user_addr), &value)?;
564            Ok(SUCCESS)
565        }
566        TIOCSCTTY => {
567            // Make the given terminal the controlling terminal of the calling process.
568            let steal = bool::from(arg);
569            current_task.thread_group().set_controlling_terminal(
570                current_task,
571                terminal,
572                is_main,
573                steal,
574                file.can_read(),
575            )?;
576            Ok(SUCCESS)
577        }
578        TIOCNOTTY => {
579            // Release the controlling terminal.
580            current_task.thread_group().release_controlling_terminal(
581                current_task,
582                terminal,
583                is_main,
584            )?;
585            Ok(SUCCESS)
586        }
587        TIOCGPGRP => {
588            // Get the foreground process group.
589            let pgid = current_task.thread_group().get_foreground_process_group(terminal)?;
590            current_task.write_object(UserRef::<pid_t>::new(user_addr), &pgid)?;
591            Ok(SUCCESS)
592        }
593        TIOCSPGRP => {
594            // Set the foreground process group.
595            let pgid = current_task.read_object(UserRef::<pid_t>::new(user_addr))?;
596            if pgid < 0 {
597                return error!(EINVAL);
598            }
599            let pgid = current_task.kernel().pids.get(pgid)?;
600            current_task.thread_group().set_foreground_process_group(
601                current_task,
602                terminal,
603                &pgid,
604            )?;
605            Ok(SUCCESS)
606        }
607        TIOCGWINSZ => {
608            // Get the window size
609            current_task.write_object(
610                UserRef::<uapi::winsize>::new(user_addr),
611                &terminal.read().line_discipline.window_size,
612            )?;
613            Ok(SUCCESS)
614        }
615        TIOCSWINSZ => {
616            // If the window is already the required size, do nothing.
617            let new_winsize = current_task.read_object(UserRef::<uapi::winsize>::new(user_addr))?;
618            if terminal.read().line_discipline.window_size == new_winsize {
619                return Ok(SUCCESS);
620            }
621            // Set the window size
622            terminal.write().line_discipline.window_size = new_winsize;
623
624            // Send a SIGWINCH signal to the foreground process group.
625            let foreground_process_group =
626                terminal.read().controller.as_ref().and_then(|terminal_controller| {
627                    terminal_controller.get_foreground_process_group().ok()
628                });
629            if let Some(process_group) = foreground_process_group {
630                process_group.send_signals(&[SIGWINCH]);
631            }
632            Ok(SUCCESS)
633        }
634        TCGETA => {
635            let termio = into_termio(terminal.read().termios());
636            current_task.write_object(UserRef::<uapi::termio>::new(user_addr), &termio)?;
637            Ok(SUCCESS)
638        }
639        TCGETS => {
640            // N.B. TCGETS on the main terminal actually returns the configuration of the replica
641            // end.
642            let termios = termios_from_termios2(terminal.read().termios());
643            current_task.write_object(UserRef::<uapi::termios>::new(user_addr), &termios)?;
644            Ok(SUCCESS)
645        }
646        TCGETS2 => {
647            current_task.write_object(
648                UserRef::<uapi::termios2>::new(user_addr),
649                terminal.read().termios(),
650            )?;
651            Ok(SUCCESS)
652        }
653        TCSETA => {
654            let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?;
655            terminal.set_termios(into_termios2(termio));
656            Ok(SUCCESS)
657        }
658        TCSETS => {
659            // N.B. TCSETS on the main terminal actually affects the configuration of the replica
660            // end.
661            let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?;
662            terminal.set_termios(termios2_from_termios(&termios));
663            Ok(SUCCESS)
664        }
665        TCSETS2 => {
666            let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?;
667            terminal.set_termios(termios2);
668            Ok(SUCCESS)
669        }
670        TCSETAF => {
671            // This should drain the output queue and discard the pending input first.
672            let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?;
673            terminal.set_termios(into_termios2(termio));
674            Ok(SUCCESS)
675        }
676        TCSETSF => {
677            // This should drain the output queue and discard the pending input first.
678            let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?;
679            terminal.set_termios(termios2_from_termios(&termios));
680            Ok(SUCCESS)
681        }
682        TCSETSF2 => {
683            // This should drain the output queue and discard the pending input first.
684            let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?;
685            terminal.set_termios(termios2);
686            Ok(SUCCESS)
687        }
688        TCSETAW => {
689            track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETAW drain output queue first");
690            let termio = current_task.read_object(UserRef::<uapi::termio>::new(user_addr))?;
691            terminal.set_termios(into_termios2(termio));
692            Ok(SUCCESS)
693        }
694        TCSETSW => {
695            track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETSW drain output queue first");
696            let termios = current_task.read_object(UserRef::<uapi::termios>::new(user_addr))?;
697            terminal.set_termios(termios2_from_termios(&termios));
698            Ok(SUCCESS)
699        }
700        TCSETSW2 => {
701            track_stub!(TODO("https://fxbug.dev/322873281"), "TCSETSW2 drain output queue first");
702            let termios2 = current_task.read_object(UserRef::<uapi::termios2>::new(user_addr))?;
703            terminal.set_termios(termios2);
704            Ok(SUCCESS)
705        }
706        TIOCSETD => {
707            track_stub!(
708                TODO("https://fxbug.dev/322874060"),
709                "devpts setting line discipline",
710                is_main
711            );
712            error!(EINVAL)
713        }
714        TCSBRK => Ok(SUCCESS),
715        TCXONC => {
716            track_stub!(TODO("https://fxbug.dev/322892912"), "devpts ioctl TCXONC", is_main);
717            error!(ENOSYS)
718        }
719        TCFLSH => {
720            terminal.flush(is_main, u32::from(arg))?;
721            Ok(SUCCESS)
722        }
723        TIOCEXCL => {
724            track_stub!(TODO("https://fxbug.dev/322893449"), "devpts ioctl TIOCEXCL", is_main);
725            error!(ENOSYS)
726        }
727        TIOCNXCL => {
728            track_stub!(TODO("https://fxbug.dev/322893393"), "devpts ioctl TIOCNXCL", is_main);
729            error!(ENOSYS)
730        }
731        TIOCOUTQ => {
732            track_stub!(TODO("https://fxbug.dev/322893723"), "devpts ioctl TIOCOUTQ", is_main);
733            error!(ENOSYS)
734        }
735        TIOCSTI => {
736            track_stub!(TODO("https://fxbug.dev/322893780"), "devpts ioctl TIOCSTI", is_main);
737            error!(ENOSYS)
738        }
739        TIOCMGET => {
740            track_stub!(TODO("https://fxbug.dev/322893681"), "devpts ioctl TIOCMGET", is_main);
741            error!(ENOSYS)
742        }
743        TIOCMBIS => {
744            track_stub!(TODO("https://fxbug.dev/322893709"), "devpts ioctl TIOCMBIS", is_main);
745            error!(ENOSYS)
746        }
747        TIOCMBIC => {
748            track_stub!(TODO("https://fxbug.dev/322893610"), "devpts ioctl TIOCMBIC", is_main);
749            error!(ENOSYS)
750        }
751        TIOCMSET => {
752            track_stub!(TODO("https://fxbug.dev/322893211"), "devpts ioctl TIOCMSET", is_main);
753            error!(ENOSYS)
754        }
755        TIOCGSOFTCAR => {
756            track_stub!(TODO("https://fxbug.dev/322893365"), "devpts ioctl TIOCGSOFTCAR", is_main);
757            error!(ENOSYS)
758        }
759        TIOCSSOFTCAR => {
760            track_stub!(TODO("https://fxbug.dev/322894074"), "devpts ioctl TIOCSSOFTCAR", is_main);
761            error!(ENOSYS)
762        }
763        TIOCLINUX => {
764            track_stub!(TODO("https://fxbug.dev/322893147"), "devpts ioctl TIOCLINUX", is_main);
765            error!(ENOSYS)
766        }
767        TIOCCONS => {
768            track_stub!(TODO("https://fxbug.dev/322893267"), "devpts ioctl TIOCCONS", is_main);
769            error!(ENOSYS)
770        }
771        TIOCGSERIAL => {
772            track_stub!(TODO("https://fxbug.dev/322893503"), "devpts ioctl TIOCGSERIAL", is_main);
773            error!(ENOSYS)
774        }
775        TIOCSSERIAL => {
776            track_stub!(TODO("https://fxbug.dev/322893663"), "devpts ioctl TIOCSSERIAL", is_main);
777            error!(ENOSYS)
778        }
779        TIOCPKT => {
780            if !is_main {
781                return error!(ENOTTY);
782            }
783            let value = current_task.read_object(UserRef::<i32>::new(user_addr))?;
784            terminal.write().set_packet_mode(value != 0);
785            Ok(SUCCESS)
786        }
787        TIOCGETD => {
788            track_stub!(TODO("https://fxbug.dev/322893974"), "devpts ioctl TIOCGETD", is_main);
789            error!(ENOSYS)
790        }
791        TCSBRKP => Ok(SUCCESS),
792        TIOCSBRK => {
793            track_stub!(TODO("https://fxbug.dev/322893936"), "devpts ioctl TIOCSBRK", is_main);
794            error!(ENOSYS)
795        }
796        TIOCCBRK => {
797            track_stub!(TODO("https://fxbug.dev/322893213"), "devpts ioctl TIOCCBRK", is_main);
798            error!(ENOSYS)
799        }
800        TIOCGSID => {
801            track_stub!(TODO("https://fxbug.dev/322894076"), "devpts ioctl TIOCGSID", is_main);
802            error!(ENOSYS)
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::testing::*;
917    use starnix_core::vfs::buffers::{VecInputBuffer, VecOutputBuffer};
918    use starnix_core::vfs::fs_args::MountParams;
919    use starnix_core::vfs::{MountInfo, NamespaceNode, OpenAccessCheck};
920    use starnix_uapi::auth::Credentials;
921    use starnix_uapi::file_mode::FileMode;
922    use starnix_uapi::signals::{SIGCHLD, SIGTTOU};
923
924    fn new_pts_fs(kernel: &Kernel) -> FileSystemHandle {
925        let mut options = FileSystemOptions::default();
926        options.params = MountParams::parse("ptmxmode=666".into()).expect("parse option");
927        super::new_pts_fs(&kernel, options).expect("create new_pts_fs")
928    }
929
930    fn ioctl<T: zerocopy::IntoBytes + zerocopy::FromBytes + zerocopy::Immutable + Copy>(
931        current_task: &CurrentTask,
932        file: &FileHandle,
933        command: u32,
934        value: &T,
935    ) -> Result<T, Errno> {
936        let address =
937            map_memory(current_task, UserAddress::default(), std::mem::size_of::<T>() as u64);
938        let address_ref = UserRef::<T>::new(address);
939        current_task.write_object(address_ref, value)?;
940        file.ioctl(current_task, command, address.into())?;
941        current_task.read_object(address_ref)
942    }
943
944    fn set_controlling_terminal(
945        current_task: &CurrentTask,
946        file: &FileHandle,
947        steal: bool,
948    ) -> Result<SyscallResult, Errno> {
949        #[allow(clippy::bool_to_int_with_if)]
950        file.ioctl(current_task, TIOCSCTTY, steal.into())
951    }
952
953    fn lookup_node(
954        task: &CurrentTask,
955        fs: &FileSystemHandle,
956        name: &FsStr,
957    ) -> Result<NamespaceNode, Errno> {
958        let root = NamespaceNode::new_anonymous(fs.root().clone());
959        root.lookup_child(task, &mut Default::default(), name)
960    }
961
962    fn open_file_with_flags(
963        current_task: &CurrentTask,
964        fs: &FileSystemHandle,
965        name: &FsStr,
966        flags: OpenFlags,
967    ) -> Result<FileHandle, Errno> {
968        let node = lookup_node(current_task, fs, name)?;
969        node.open(current_task, flags)
970    }
971
972    fn open_file(
973        current_task: &CurrentTask,
974        fs: &FileSystemHandle,
975        name: &FsStr,
976    ) -> Result<FileHandle, Errno> {
977        open_file_with_flags(current_task, fs, name, OpenFlags::RDWR | OpenFlags::NOCTTY)
978    }
979
980    fn open_ptmx_and_unlock(
981        current_task: &CurrentTask,
982        fs: &FileSystemHandle,
983    ) -> Result<FileHandle, Errno> {
984        let file = open_file_with_flags(current_task, fs, "ptmx".into(), OpenFlags::RDWR)?;
985
986        // Unlock terminal
987        ioctl::<i32>(current_task, &file, TIOCSPTLCK, &0)?;
988
989        Ok(file)
990    }
991
992    #[fuchsia::test]
993    async fn opening_ptmx_creates_pts() {
994        spawn_kernel_and_run(async |task| {
995            let kernel = task.kernel();
996            tty_device_init(kernel).expect("tty_device_init");
997            let fs = new_pts_fs(kernel);
998            lookup_node(task, &fs, "0".into()).unwrap_err();
999            let _ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1000            lookup_node(task, &fs, "0".into()).expect("pty");
1001        })
1002        .await;
1003    }
1004
1005    #[fuchsia::test]
1006    async fn closing_ptmx_closes_pts() {
1007        spawn_kernel_and_run(async |task| {
1008            let kernel = task.kernel();
1009            tty_device_init(kernel).expect("tty_device_init");
1010            let fs = new_pts_fs(kernel);
1011            lookup_node(task, &fs, "0".into()).unwrap_err();
1012            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1013            let _pts = open_file(task, &fs, "0".into()).expect("open file");
1014            std::mem::drop(ptmx);
1015            task.trigger_delayed_releaser();
1016            lookup_node(task, &fs, "0".into()).unwrap_err();
1017        })
1018        .await;
1019    }
1020
1021    #[fuchsia::test]
1022    async fn pts_are_reused() {
1023        spawn_kernel_and_run(async |task| {
1024            let kernel = task.kernel();
1025            tty_device_init(kernel).expect("tty_device_init");
1026            let fs = new_pts_fs(kernel);
1027
1028            let _ptmx0 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1029            let mut _ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1030            let _ptmx2 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1031
1032            lookup_node(task, &fs, "0".into()).expect("component_lookup");
1033            lookup_node(task, &fs, "1".into()).expect("component_lookup");
1034            lookup_node(task, &fs, "2".into()).expect("component_lookup");
1035
1036            std::mem::drop(_ptmx1);
1037            task.trigger_delayed_releaser();
1038
1039            lookup_node(task, &fs, "1".into()).unwrap_err();
1040
1041            _ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1042            lookup_node(task, &fs, "1".into()).expect("component_lookup");
1043        })
1044        .await;
1045    }
1046
1047    #[fuchsia::test]
1048    async fn opening_inexistant_replica_fails() {
1049        spawn_kernel_and_run(async |task| {
1050            let kernel = task.kernel();
1051            tty_device_init(kernel).expect("tty_device_init");
1052            // Initialize pts devices
1053            new_pts_fs(kernel);
1054            let fs = TmpFs::new_fs(kernel);
1055            let mount = MountInfo::detached();
1056            let pts = fs
1057                .root()
1058                .create_entry(task, &mount, "custom_pts".into(), |dir, mount, name| {
1059                    dir.create_node(
1060                        task,
1061                        mount,
1062                        name,
1063                        mode!(IFCHR, 0o666),
1064                        DeviceId::new(DEVPTS_FIRST_MAJOR, 0),
1065                        FsCred::root(),
1066                    )
1067                })
1068                .expect("custom_pts");
1069            let node = NamespaceNode::new_anonymous(pts.clone());
1070            assert!(node.open(task, OpenAccessCheck::skip(OpenFlags::RDONLY)).is_err());
1071        })
1072        .await;
1073    }
1074
1075    #[fuchsia::test]
1076    async fn test_open_tty() {
1077        spawn_kernel_and_run(async |task| {
1078            let kernel = task.kernel();
1079            tty_device_init(kernel).expect("tty_device_init");
1080            let fs = new_pts_fs(kernel);
1081            let devfs = starnix_core::fs::devtmpfs::DevTmpFs::from_kernel(kernel);
1082
1083            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1084            set_controlling_terminal(task, &ptmx, false).expect("set_controlling_terminal");
1085            let tty =
1086                open_file_with_flags(task, &devfs, "tty".into(), OpenFlags::RDWR).expect("tty");
1087            // Check that tty is the main terminal by calling the ioctl TIOCGPTN and checking it is
1088            // has the same result as on ptmx.
1089            assert_eq!(
1090                ioctl::<i32>(task, &tty, TIOCGPTN, &0),
1091                ioctl::<i32>(task, &ptmx, TIOCGPTN, &0)
1092            );
1093
1094            // Detach the controlling terminal.
1095            ioctl::<i32>(task, &ptmx, TIOCNOTTY, &0).expect("detach terminal");
1096            let pts = open_file(task, &fs, "0".into()).expect("open file");
1097            set_controlling_terminal(task, &pts, false).expect("set_controlling_terminal");
1098            let tty =
1099                open_file_with_flags(task, &devfs, "tty".into(), OpenFlags::RDWR).expect("tty");
1100            // TIOCGPTN is not implemented on replica terminals
1101            assert!(ioctl::<i32>(task, &tty, TIOCGPTN, &0).is_err());
1102        })
1103        .await;
1104    }
1105
1106    #[fuchsia::test]
1107    async fn test_unknown_ioctl() {
1108        spawn_kernel_and_run(async |task| {
1109            let kernel = task.kernel();
1110            tty_device_init(kernel).expect("tty_device_init");
1111            let fs = new_pts_fs(kernel);
1112
1113            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1114            assert_eq!(ptmx.ioctl(task, 42, Default::default()), error!(ENOTTY));
1115
1116            let pts_file = open_file(task, &fs, "0".into()).expect("open file");
1117            assert_eq!(pts_file.ioctl(task, 42, Default::default()), error!(ENOTTY));
1118        })
1119        .await;
1120    }
1121
1122    #[fuchsia::test]
1123    async fn test_tiocgptn_ioctl() {
1124        spawn_kernel_and_run(async |task| {
1125            let kernel = task.kernel();
1126            tty_device_init(kernel).expect("tty_device_init");
1127            let fs = new_pts_fs(kernel);
1128            let ptmx0 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1129            let ptmx1 = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1130
1131            let pts0 = ioctl::<u32>(task, &ptmx0, TIOCGPTN, &0).expect("ioctl");
1132            assert_eq!(pts0, 0);
1133
1134            let pts1 = ioctl::<u32>(task, &ptmx1, TIOCGPTN, &0).expect("ioctl");
1135            assert_eq!(pts1, 1);
1136        })
1137        .await;
1138    }
1139
1140    #[fuchsia::test]
1141    async fn test_new_terminal_is_locked() {
1142        spawn_kernel_and_run(async |task| {
1143            let kernel = task.kernel();
1144            tty_device_init(kernel).expect("tty_device_init");
1145            let fs = new_pts_fs(kernel);
1146            let _ptmx_file = open_file(task, &fs, "ptmx".into()).expect("open file");
1147
1148            let pts = lookup_node(task, &fs, "0".into()).expect("component_lookup");
1149            assert_eq!(pts.open(task, OpenFlags::RDONLY).map(|_| ()), error!(EIO));
1150        })
1151        .await;
1152    }
1153
1154    #[fuchsia::test]
1155    async fn test_lock_ioctls() {
1156        spawn_kernel_and_run(async |task| {
1157            let kernel = task.kernel();
1158            tty_device_init(kernel).expect("tty_device_init");
1159            let fs = new_pts_fs(kernel);
1160            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1161            let pts = lookup_node(task, &fs, "0".into()).expect("component_lookup");
1162
1163            // Check that the lock is not set.
1164            assert_eq!(ioctl::<i32>(task, &ptmx, TIOCGPTLCK, &0), Ok(0));
1165            // /dev/pts/0 can be opened
1166            pts.open(task, OpenFlags::RDONLY).expect("open");
1167
1168            // Lock the terminal
1169            ioctl::<i32>(task, &ptmx, TIOCSPTLCK, &42).expect("ioctl");
1170            // Check that the lock is set.
1171            assert_eq!(ioctl::<i32>(task, &ptmx, TIOCGPTLCK, &0), Ok(1));
1172            // /dev/pts/0 cannot be opened
1173            assert_eq!(pts.open(task, OpenFlags::RDONLY).map(|_| ()), error!(EIO));
1174        })
1175        .await;
1176    }
1177
1178    #[fuchsia::test]
1179    async fn test_ptmx_stats() {
1180        spawn_kernel_and_run(async |task| {
1181            let kernel = task.kernel();
1182            tty_device_init(kernel).expect("tty_device_init");
1183            task.set_creds(Credentials::with_ids(22, 22));
1184            let fs = new_pts_fs(kernel);
1185            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1186            let ptmx_stat = ptmx.node().stat(task).expect("stat");
1187            assert_eq!(ptmx_stat.st_blksize as usize, BLOCK_SIZE);
1188            let pts = open_file(task, &fs, "0".into()).expect("open file");
1189            let pts_stats = pts.node().stat(task).expect("stat");
1190            assert_eq!(pts_stats.st_mode & FileMode::PERMISSIONS.bits(), 0o600);
1191            assert_eq!(pts_stats.st_uid, 22);
1192            // TODO(qsr): Check that gid is tty.
1193        })
1194        .await;
1195    }
1196
1197    #[fuchsia::test]
1198    async fn test_attach_terminal_when_open() {
1199        spawn_kernel_and_run(async |task| {
1200            let kernel = task.kernel();
1201            tty_device_init(kernel).expect("tty_device_init");
1202            let fs = new_pts_fs(kernel);
1203            let _opened_main = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1204            // Opening the main terminal should not set the terminal of the session.
1205            assert!(
1206                task.thread_group()
1207                    .read()
1208                    .process_group
1209                    .session
1210                    .read()
1211                    .controlling_terminal
1212                    .is_none()
1213            );
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!(
1219                task.thread_group()
1220                    .read()
1221                    .process_group
1222                    .session
1223                    .read()
1224                    .controlling_terminal
1225                    .is_none()
1226            );
1227
1228            // Opening the replica terminal should set the terminal of the session.
1229            let _opened_replica2 =
1230                open_file_with_flags(task, &fs, "0".into(), OpenFlags::RDWR).expect("open file");
1231            assert!(
1232                task.thread_group()
1233                    .read()
1234                    .process_group
1235                    .session
1236                    .read()
1237                    .controlling_terminal
1238                    .is_some()
1239            );
1240        })
1241        .await;
1242    }
1243
1244    #[fuchsia::test]
1245    async fn test_attach_terminal() {
1246        spawn_kernel_and_run(async |task1| {
1247            let kernel = task1.kernel();
1248            tty_device_init(kernel).expect("tty_device_init");
1249            let task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1250            task2.thread_group().setsid().expect("setsid");
1251
1252            let fs = new_pts_fs(kernel);
1253            let opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx");
1254            let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file");
1255
1256            assert_eq!(ioctl::<i32>(task1, &opened_main, TIOCGPGRP, &0), error!(ENOTTY));
1257            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY));
1258
1259            set_controlling_terminal(task1, &opened_main, false).unwrap();
1260            assert_eq!(
1261                ioctl::<i32>(task1, &opened_main, TIOCGPGRP, &0),
1262                Ok(task1.thread_group().read().process_group.leader.id)
1263            );
1264            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY));
1265
1266            // Cannot steal terminal using the replica.
1267            assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM));
1268            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCGPGRP, &0), error!(ENOTTY));
1269        })
1270        .await;
1271    }
1272
1273    #[fuchsia::test]
1274    async fn test_steal_terminal() {
1275        spawn_kernel_and_run(async |task1| {
1276            let kernel = task1.kernel();
1277            tty_device_init(kernel).expect("tty_device_init");
1278            task1.set_creds(Credentials::with_ids(1, 1));
1279
1280            let task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1281
1282            let fs = new_pts_fs(kernel);
1283            let _opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx");
1284            let wo_opened_replica =
1285                open_file_with_flags(task1, &fs, "0".into(), OpenFlags::WRONLY | OpenFlags::NOCTTY)
1286                    .expect("open file");
1287            assert!(!wo_opened_replica.can_read());
1288
1289            // FD must be readable for setting the terminal.
1290            assert_eq!(set_controlling_terminal(task1, &wo_opened_replica, false), error!(EPERM));
1291
1292            let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file");
1293            // Task must be session leader for setting the terminal.
1294            assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EINVAL));
1295
1296            // Associate terminal to task1.
1297            set_controlling_terminal(task1, &opened_replica, false)
1298                .expect("Associate terminal to task1");
1299
1300            // Redundant association of the same terminal should succeed (no-op)
1301            set_controlling_terminal(task1, &opened_replica, false)
1302                .expect("Redundant association should succeed");
1303
1304            task2.thread_group().setsid().expect("setsid");
1305
1306            // One cannot associate a terminal that is already associated with another process.
1307            assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM));
1308
1309            // One cannot steal a terminal without the CAP_SYS_ADMIN capacility
1310            assert_eq!(set_controlling_terminal(&task2, &opened_replica, true), error!(EPERM));
1311
1312            // One can steal a terminal with the CAP_SYS_ADMIN capacility
1313            task2.set_creds(Credentials::with_ids(0, 0));
1314            // But not without specifying that one wants to steal it.
1315            assert_eq!(set_controlling_terminal(&task2, &opened_replica, false), error!(EPERM));
1316            set_controlling_terminal(&task2, &opened_replica, true)
1317                .expect("Associate terminal to task2");
1318
1319            assert!(
1320                task1
1321                    .thread_group()
1322                    .read()
1323                    .process_group
1324                    .session
1325                    .read()
1326                    .controlling_terminal
1327                    .is_none()
1328            );
1329        })
1330        .await;
1331    }
1332
1333    #[fuchsia::test]
1334    async fn test_set_foreground_process() {
1335        spawn_kernel_and_run(async |init| {
1336            let kernel = init.kernel();
1337            tty_device_init(kernel).expect("tty_device_init");
1338            let task1 = init.clone_task_for_test(0, Some(SIGCHLD));
1339            // Share the init task's address space so ioctl usercopy operations can
1340            // access memory while running on this test thread.
1341            task1.running_state().mm.update(Some(init.mm().unwrap().clone()));
1342            task1.thread_group().setsid().expect("setsid");
1343            let task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1344            task2.running_state().mm.update(Some(init.mm().unwrap().clone()));
1345            task2.thread_group().setpgid(&task2, &task2, &task2.pid).expect("setpgid");
1346            let task2_pgid = task2.thread_group().read().process_group.leader.clone();
1347
1348            assert_ne!(task2_pgid, task1.thread_group().read().process_group.leader);
1349
1350            let fs = new_pts_fs(kernel);
1351            let _opened_main = open_ptmx_and_unlock(init, &fs).expect("ptmx");
1352            let opened_replica = open_file(&task2, &fs, "0".into()).expect("open file");
1353
1354            // Cannot change the foreground process group if the terminal is not the controlling
1355            // terminal
1356            assert_eq!(
1357                ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &task2_pgid.id),
1358                error!(ENOTTY)
1359            );
1360
1361            // Attach terminal to task1 and task2 session.
1362            set_controlling_terminal(&task1, &opened_replica, false).unwrap();
1363            // The foreground process group should be the one of task1
1364            assert_eq!(
1365                ioctl::<i32>(&task1, &opened_replica, TIOCGPGRP, &0),
1366                Ok(task1.thread_group().read().process_group.leader.id)
1367            );
1368
1369            // Cannot change the foreground process group to a negative pid.
1370            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &-1), error!(EINVAL));
1371
1372            // Cannot change the foreground process group to a invalid process group.
1373            assert_eq!(ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &255), error!(ESRCH));
1374
1375            // Cannot change the foreground process group to a process group in another session.
1376            let init_pgid = init.thread_group().read().process_group.leader.clone();
1377            assert_eq!(
1378                ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &init_pgid.id),
1379                error!(EPERM)
1380            );
1381
1382            // Changing the foreground process while being in background generates SIGTTOU and fails.
1383            assert_eq!(
1384                ioctl::<i32>(&task2, &opened_replica, TIOCSPGRP, &task2_pgid.id),
1385                error!(EINTR)
1386            );
1387            assert!(task2.read().has_signal_pending(SIGTTOU));
1388
1389            // Set the foreground process to task2 process group
1390            ioctl::<i32>(&task1, &opened_replica, TIOCSPGRP, &task2_pgid.id).unwrap();
1391
1392            // Check that the foreground process has been changed.
1393            let terminal = Arc::clone(
1394                &task1
1395                    .thread_group()
1396                    .read()
1397                    .process_group
1398                    .session
1399                    .read()
1400                    .controlling_terminal
1401                    .as_ref()
1402                    .unwrap()
1403                    .terminal,
1404            );
1405            assert_eq!(
1406                terminal
1407                    .read()
1408                    .controller
1409                    .as_ref()
1410                    .unwrap()
1411                    .session
1412                    .upgrade()
1413                    .unwrap()
1414                    .read()
1415                    .get_foreground_process_group_leader(),
1416                &task2_pgid
1417            );
1418        })
1419        .await;
1420    }
1421
1422    #[fuchsia::test]
1423    async fn test_detach_session() {
1424        spawn_kernel_and_run(async |task1| {
1425            let kernel = task1.kernel();
1426            tty_device_init(kernel).expect("tty_device_init");
1427            let task2 = task1.clone_task_for_test(0, Some(SIGCHLD));
1428            task2.thread_group().setsid().expect("setsid");
1429
1430            let fs = new_pts_fs(kernel);
1431            let _opened_main = open_ptmx_and_unlock(task1, &fs).expect("ptmx");
1432            let opened_replica = open_file(task1, &fs, "0".into()).expect("open file");
1433
1434            // Cannot detach the controlling terminal when none is attached terminal
1435            assert_eq!(ioctl::<i32>(task1, &opened_replica, TIOCNOTTY, &0), error!(ENOTTY));
1436
1437            set_controlling_terminal(&task2, &opened_replica, false)
1438                .expect("set controlling terminal");
1439
1440            // Cannot detach the controlling terminal when not the session leader.
1441            assert_eq!(ioctl::<i32>(task1, &opened_replica, TIOCNOTTY, &0), error!(ENOTTY));
1442
1443            // Detach the terminal
1444            ioctl::<i32>(&task2, &opened_replica, TIOCNOTTY, &0).expect("detach terminal");
1445            assert!(
1446                task2
1447                    .thread_group()
1448                    .read()
1449                    .process_group
1450                    .session
1451                    .read()
1452                    .controlling_terminal
1453                    .is_none()
1454            );
1455        })
1456        .await;
1457    }
1458
1459    #[fuchsia::test]
1460    async fn test_send_data_back_and_forth() {
1461        spawn_kernel_and_run(async |task| {
1462            let kernel = task.kernel();
1463            tty_device_init(kernel).expect("tty_device_init");
1464            let fs = new_pts_fs(kernel);
1465            let ptmx = open_ptmx_and_unlock(task, &fs).expect("ptmx");
1466            let pts = open_file(task, &fs, "0".into()).expect("open file");
1467
1468            let has_data_ready_to_read = |fd: &FileHandle| {
1469                fd.query_events(task).expect("query_events").contains(FdEvents::POLLIN)
1470            };
1471
1472            let write_and_assert = |fd: &FileHandle, data: &[u8]| {
1473                assert_eq!(
1474                    fd.write(task, &mut VecInputBuffer::new(data)).expect("write"),
1475                    data.len()
1476                );
1477            };
1478
1479            let read_and_check = |fd: &FileHandle, data: &[u8]| {
1480                assert!(has_data_ready_to_read(fd));
1481                let mut buffer = VecOutputBuffer::new(data.len() + 1);
1482                assert_eq!(fd.read(task, &mut buffer).expect("read"), data.len());
1483                assert_eq!(data, buffer.data());
1484            };
1485
1486            let hello_buffer = b"hello\n";
1487            let hello_transformed_buffer = b"hello\r\n";
1488
1489            // Main to replica
1490            write_and_assert(&ptmx, hello_buffer);
1491            read_and_check(&pts, hello_buffer);
1492
1493            // Data has been echoed
1494            read_and_check(&ptmx, hello_transformed_buffer);
1495
1496            // Replica to main
1497            write_and_assert(&pts, hello_buffer);
1498            read_and_check(&ptmx, hello_transformed_buffer);
1499
1500            // Data has not been echoed
1501            assert!(!has_data_ready_to_read(&pts));
1502        })
1503        .await;
1504    }
1505}