Skip to content

Commit 8c7e5cb

Browse files
ryanbreenclaude
andcommitted
feat(pty): implement POSIX pseudo-terminal support
Add complete PTY (pseudo-terminal) subsystem enabling terminal emulation for remote access (telnet) and terminal multiplexing. Implementation follows POSIX specifications with proper error handling and bidirectional data flow. Key components: - PTY core infrastructure with master/slave pair management - Syscalls: posix_openpt, grantpt, unlockpt, ptsname - devptsfs virtual filesystem for /dev/pts/* device nodes - sys_open integration to route /dev/pts/* paths to devptsfs - libbreenix userspace wrappers for PTY operations - Integration test with data verification - Telnet server skeleton for remote shell access Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d0f80e3 commit 8c7e5cb

File tree

27 files changed

+3292
-9
lines changed

27 files changed

+3292
-9
lines changed

docs/planning/PTY_IMPLEMENTATION_PLAN.md

Lines changed: 851 additions & 0 deletions
Large diffs are not rendered by default.

kernel/src/fs/devptsfs/mod.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
//! DevPTS Filesystem (devpts)
2+
//!
3+
//! Provides a virtual filesystem mounted at /dev/pts containing PTY slave devices.
4+
//! Unlike ext2, devptsfs doesn't use disk storage - all nodes are virtual and
5+
//! dynamically generated based on active PTY pairs.
6+
//!
7+
//! # Supported Entries
8+
//!
9+
//! - `/dev/pts/` - Directory containing active PTY slave devices
10+
//! - `/dev/pts/0` - First PTY slave (if allocated and unlocked)
11+
//! - `/dev/pts/1` - Second PTY slave, etc.
12+
//!
13+
//! # Architecture
14+
//!
15+
//! ```text
16+
//! sys_open("/dev/pts/0")
17+
//! |
18+
//! v
19+
//! devpts_lookup("0")
20+
//! |
21+
//! v
22+
//! Check PTY 0 exists and is unlocked
23+
//! |
24+
//! v
25+
//! Return PtySlave file descriptor
26+
//! ```
27+
28+
use alloc::string::String;
29+
use alloc::vec::Vec;
30+
use spin::Mutex;
31+
32+
use crate::tty::pty;
33+
34+
/// Directory entry for /dev/pts listing
35+
#[derive(Debug, Clone)]
36+
pub struct PtsEntry {
37+
/// PTY number (0, 1, 2, ...)
38+
pub pty_num: u32,
39+
/// Inode number for stat
40+
pub inode: u64,
41+
}
42+
43+
impl PtsEntry {
44+
/// Get the entry name (just the number as string)
45+
pub fn name(&self) -> String {
46+
alloc::format!("{}", self.pty_num)
47+
}
48+
}
49+
50+
/// Global devpts state
51+
struct DevptsState {
52+
/// Whether devpts is initialized
53+
initialized: bool,
54+
}
55+
56+
impl DevptsState {
57+
const fn new() -> Self {
58+
Self {
59+
initialized: false,
60+
}
61+
}
62+
}
63+
64+
static DEVPTS: Mutex<DevptsState> = Mutex::new(DevptsState::new());
65+
66+
/// Initialize devpts filesystem
67+
pub fn init() {
68+
let mut devpts = DEVPTS.lock();
69+
if devpts.initialized {
70+
log::warn!("devpts already initialized");
71+
return;
72+
}
73+
74+
devpts.initialized = true;
75+
log::info!("devpts: initialized at /dev/pts");
76+
77+
// Register mount point
78+
crate::fs::vfs::mount::mount("/dev/pts", "devpts");
79+
}
80+
81+
/// Look up a PTY slave device by name (the number as string, e.g., "0", "1")
82+
///
83+
/// Returns the PTY number if:
84+
/// 1. The name is a valid number
85+
/// 2. A PTY with that number exists
86+
/// 3. The PTY is unlocked (unlockpt was called)
87+
///
88+
/// # Arguments
89+
/// * `name` - The PTY number as a string (without /dev/pts/ prefix)
90+
///
91+
/// # Returns
92+
/// * `Some(pty_num)` if the PTY exists and is unlocked
93+
/// * `None` if the PTY doesn't exist or is still locked
94+
pub fn lookup(name: &str) -> Option<u32> {
95+
// Parse the PTY number from name
96+
let pty_num: u32 = name.parse().ok()?;
97+
98+
// Check if PTY exists and is unlocked
99+
let pair = pty::get(pty_num)?;
100+
if !pair.is_unlocked() {
101+
return None; // PTY exists but hasn't been unlocked yet
102+
}
103+
104+
Some(pty_num)
105+
}
106+
107+
/// Look up a PTY slave by inode number
108+
///
109+
/// For devpts, the inode number is derived from the PTY number.
110+
/// We use a base offset to avoid collision with other filesystem inodes.
111+
pub fn lookup_by_inode(inode: u64) -> Option<u32> {
112+
// Inode = PTY_INODE_BASE + pty_num
113+
const PTY_INODE_BASE: u64 = 0x10000;
114+
115+
if inode < PTY_INODE_BASE {
116+
return None;
117+
}
118+
119+
let pty_num = (inode - PTY_INODE_BASE) as u32;
120+
121+
// Verify the PTY exists and is unlocked
122+
let pair = pty::get(pty_num)?;
123+
if !pair.is_unlocked() {
124+
return None;
125+
}
126+
127+
Some(pty_num)
128+
}
129+
130+
/// Get inode number for a PTY slave
131+
pub fn get_inode(pty_num: u32) -> u64 {
132+
const PTY_INODE_BASE: u64 = 0x10000;
133+
PTY_INODE_BASE + pty_num as u64
134+
}
135+
136+
/// List all active and unlocked PTY slave entries
137+
///
138+
/// Returns entries for all PTYs that:
139+
/// 1. Have been allocated
140+
/// 2. Have been unlocked (unlockpt called)
141+
pub fn list_entries() -> Vec<PtsEntry> {
142+
pty::list_active()
143+
.into_iter()
144+
.filter_map(|pty_num| {
145+
// Only include unlocked PTYs
146+
let pair = pty::get(pty_num)?;
147+
if !pair.is_unlocked() {
148+
return None;
149+
}
150+
Some(PtsEntry {
151+
pty_num,
152+
inode: get_inode(pty_num),
153+
})
154+
})
155+
.collect()
156+
}
157+
158+
/// List all PTY slave names (for directory listing)
159+
pub fn list_names() -> Vec<String> {
160+
list_entries().into_iter().map(|e| e.name()).collect()
161+
}
162+
163+
/// Check if devpts is initialized
164+
pub fn is_initialized() -> bool {
165+
DEVPTS.lock().initialized
166+
}
167+
168+
/// Get device numbers for a PTY slave
169+
///
170+
/// PTY slaves use major number 136 (standard Unix/Linux convention)
171+
/// and minor number = PTY number
172+
pub fn get_device_numbers(pty_num: u32) -> (u32, u32) {
173+
const PTY_SLAVE_MAJOR: u32 = 136;
174+
(PTY_SLAVE_MAJOR, pty_num)
175+
}
176+
177+
/// Get the combined device number (major << 8 | minor) for stat st_rdev
178+
pub fn get_rdev(pty_num: u32) -> u64 {
179+
let (major, minor) = get_device_numbers(pty_num);
180+
((major as u64) << 8) | (minor as u64)
181+
}
182+
183+
#[cfg(test)]
184+
mod tests {
185+
use super::*;
186+
187+
#[test]
188+
fn test_get_inode() {
189+
assert_eq!(get_inode(0), 0x10000);
190+
assert_eq!(get_inode(1), 0x10001);
191+
assert_eq!(get_inode(255), 0x100ff);
192+
}
193+
194+
#[test]
195+
fn test_get_device_numbers() {
196+
let (major, minor) = get_device_numbers(0);
197+
assert_eq!(major, 136);
198+
assert_eq!(minor, 0);
199+
200+
let (major, minor) = get_device_numbers(5);
201+
assert_eq!(major, 136);
202+
assert_eq!(minor, 5);
203+
}
204+
205+
#[test]
206+
fn test_get_rdev() {
207+
// major=136 (0x88), minor=0: rdev = 0x8800
208+
assert_eq!(get_rdev(0), 0x8800);
209+
// major=136 (0x88), minor=5: rdev = 0x8805
210+
assert_eq!(get_rdev(5), 0x8805);
211+
}
212+
}

kernel/src/fs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
#![allow(dead_code)]
1111

1212
pub mod devfs;
13+
pub mod devptsfs;
1314
pub mod ext2;
1415
pub mod vfs;

kernel/src/ipc/fd.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ pub enum FdKind {
104104
Device(crate::fs::devfs::DeviceType),
105105
/// /dev directory (virtual directory for listing devices)
106106
DevfsDirectory { position: u64 },
107+
/// /dev/pts directory (virtual directory for listing PTY slaves)
108+
DevptsDirectory { position: u64 },
109+
/// PTY master file descriptor
110+
/// Allow unused - constructed by posix_openpt syscall in Phase 2
111+
#[allow(dead_code)]
112+
PtyMaster(u32),
113+
/// PTY slave file descriptor
114+
/// Allow unused - constructed when opening /dev/pts/N in Phase 2
115+
#[allow(dead_code)]
116+
PtySlave(u32),
107117
}
108118

109119
impl core::fmt::Debug for FdKind {
@@ -120,6 +130,9 @@ impl core::fmt::Debug for FdKind {
120130
FdKind::Directory(_) => write!(f, "Directory"),
121131
FdKind::Device(dt) => write!(f, "Device({:?})", dt),
122132
FdKind::DevfsDirectory { position } => write!(f, "DevfsDirectory(pos={})", position),
133+
FdKind::DevptsDirectory { position } => write!(f, "DevptsDirectory(pos={})", position),
134+
FdKind::PtyMaster(n) => write!(f, "PtyMaster({})", n),
135+
FdKind::PtySlave(n) => write!(f, "PtySlave({})", n),
123136
}
124137
}
125138
}
@@ -452,6 +465,19 @@ impl Drop for FdTable {
452465
// Devfs directory doesn't need cleanup
453466
log::debug!("FdTable::drop() - releasing devfs directory fd {}", i);
454467
}
468+
FdKind::DevptsDirectory { .. } => {
469+
// Devpts directory doesn't need cleanup
470+
log::debug!("FdTable::drop() - releasing devpts directory fd {}", i);
471+
}
472+
FdKind::PtyMaster(pty_num) => {
473+
// PTY master cleanup - release the PTY pair when master closes
474+
crate::tty::pty::release(pty_num);
475+
log::debug!("FdTable::drop() - released PTY master fd {} (pty {})", i, pty_num);
476+
}
477+
FdKind::PtySlave(_pty_num) => {
478+
// PTY slave doesn't own the pair, just decrement reference
479+
log::debug!("FdTable::drop() - released PTY slave fd {}", i);
480+
}
455481
}
456482
}
457483
}

kernel/src/ipc/poll.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ pub fn poll_fd(fd_entry: &FileDescriptor, events: i16) -> i16 {
149149
revents |= events::POLLIN;
150150
}
151151
}
152+
FdKind::DevptsDirectory { .. } => {
153+
// Devpts directory is always "readable" for getdents purposes
154+
if (events & events::POLLIN) != 0 {
155+
revents |= events::POLLIN;
156+
}
157+
}
152158
FdKind::TcpSocket(_) => {
153159
// Unconnected TCP socket - always writable (for connect attempt)
154160
if (events & events::POLLOUT) != 0 {
@@ -189,6 +195,43 @@ pub fn poll_fd(fd_entry: &FileDescriptor, events: i16) -> i16 {
189195
revents |= events::POLLERR;
190196
}
191197
}
198+
FdKind::PtyMaster(pty_num) => {
199+
// PTY master - check slave_to_master buffer for readable data
200+
if let Some(pair) = crate::tty::pty::get(*pty_num) {
201+
if (events & events::POLLIN) != 0 {
202+
let buffer = pair.slave_to_master.lock();
203+
if !buffer.is_empty() {
204+
revents |= events::POLLIN;
205+
}
206+
}
207+
if (events & events::POLLOUT) != 0 {
208+
// Master can always write (goes through line discipline)
209+
revents |= events::POLLOUT;
210+
}
211+
} else {
212+
revents |= events::POLLERR;
213+
}
214+
}
215+
FdKind::PtySlave(pty_num) => {
216+
// PTY slave - check line discipline for readable data
217+
if let Some(pair) = crate::tty::pty::get(*pty_num) {
218+
if (events & events::POLLIN) != 0 {
219+
let ldisc = pair.ldisc.lock();
220+
if ldisc.has_data() {
221+
revents |= events::POLLIN;
222+
}
223+
}
224+
if (events & events::POLLOUT) != 0 {
225+
let buffer = pair.slave_to_master.lock();
226+
// Can write if buffer not full
227+
if buffer.available() < 4096 {
228+
revents |= events::POLLOUT;
229+
}
230+
}
231+
} else {
232+
revents |= events::POLLERR;
233+
}
234+
}
192235
}
193236

194237
revents

kernel/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ fn kernel_main(boot_info: &'static mut bootloader_api::BootInfo) -> ! {
194194
crate::fs::devfs::init();
195195
log::info!("devfs initialized at /dev");
196196

197+
// Initialize devptsfs (/dev/pts pseudo-terminal slave filesystem)
198+
crate::fs::devptsfs::init();
199+
log::info!("devptsfs initialized at /dev/pts");
200+
197201
// Update IST stacks with per-CPU emergency stacks
198202
gdt::update_ist_stacks();
199203
log::info!("Updated IST stacks with per-CPU emergency and page fault stacks");

kernel/src/process/process.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,19 @@ impl Process {
270270
// Devfs directory doesn't need cleanup
271271
log::debug!("Process::close_all_fds() - released devfs directory fd {}", fd);
272272
}
273+
FdKind::DevptsDirectory { .. } => {
274+
// Devpts directory doesn't need cleanup
275+
log::debug!("Process::close_all_fds() - released devpts directory fd {}", fd);
276+
}
277+
FdKind::PtyMaster(pty_num) => {
278+
// PTY master cleanup - release the PTY pair when master closes
279+
crate::tty::pty::release(pty_num);
280+
log::debug!("Process::close_all_fds() - released PTY master fd {} (pty {})", fd, pty_num);
281+
}
282+
FdKind::PtySlave(_pty_num) => {
283+
// PTY slave doesn't own the pair, just decrement reference
284+
log::debug!("Process::close_all_fds() - released PTY slave fd {}", fd);
285+
}
273286
}
274287
}
275288
}

kernel/src/syscall/dispatcher.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ pub fn dispatch_syscall(
8787
SyscallNumber::Unlink => super::fs::sys_unlink(arg1),
8888
SyscallNumber::Symlink => super::fs::sys_symlink(arg1, arg2),
8989
SyscallNumber::Readlink => super::fs::sys_readlink(arg1, arg2, arg3),
90+
// PTY syscalls
91+
SyscallNumber::PosixOpenpt => super::pty::sys_posix_openpt(arg1),
92+
SyscallNumber::Grantpt => super::pty::sys_grantpt(arg1),
93+
SyscallNumber::Unlockpt => super::pty::sys_unlockpt(arg1),
94+
SyscallNumber::Ptsname => super::pty::sys_ptsname(arg1, arg2, arg3),
9095
// Testing/diagnostic syscalls (Breenix-specific)
9196
SyscallNumber::CowStats => super::handlers::sys_cow_stats(arg1),
9297
SyscallNumber::SimulateOom => super::handlers::sys_simulate_oom(arg1),

kernel/src/syscall/errno.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ pub const EINVAL: i32 = 22;
5252
/// Too many open files
5353
pub const EMFILE: i32 = 24;
5454

55+
/// Not a typewriter (inappropriate ioctl for device)
56+
pub const ENOTTY: i32 = 25;
57+
5558
/// No space left on device
5659
pub const ENOSPC: i32 = 28;
5760

0 commit comments

Comments
 (0)