Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/descriptor/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ impl Error for DescriptorError {
}

/// The function `cause` returns the lower-level cause of this error, if any.

fn cause(&self) -> Option<&dyn Error> {
None
}
Expand Down
3 changes: 0 additions & 3 deletions src/fork/pty/master/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::error::Error;
use std::fmt;

/// The alias `Result` learns `MasterError` possibility.

pub type Result<T> = ::std::result::Result<T, MasterError>;

/// The enum `MasterError` defines the possible errors from constructor Master.
Expand All @@ -18,15 +17,13 @@ pub enum MasterError {

impl fmt::Display for MasterError {
/// The function `fmt` formats the value using the given formatter.

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", ::errno::errno())
}
}

impl Error for MasterError {
/// The function `description` returns a short description of the error.

fn description(&self) -> &str {
match *self {
MasterError::BadDescriptor(_) => "the descriptor as occured an error",
Expand Down
16 changes: 13 additions & 3 deletions src/fork/pty/master/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
mod err;

#[cfg(target_os = "macos")]
mod ptsname_r_macos;

use descriptor::Descriptor;

pub use self::err::{MasterError, Result};
Expand Down Expand Up @@ -70,7 +73,14 @@ impl Master {
// of the call
unsafe {
Copy link
Contributor

@ethanpailes ethanpailes Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to maintain the style that every unsafe block has a saftey comment above it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mistakenly moved this one, updated; 559aa75

let data: *mut u8 = &mut buf[0];
match libc::ptsname_r(fd, data as *mut libc::c_char, buf.len()) {

#[cfg(any(target_os = "linux", target_os = "android"))]
let result = libc::ptsname_r(fd, data as *mut libc::c_char, buf.len());

#[cfg(target_os = "macos")]
let result = ptsname_r_macos::ptsname_r(fd, data as *mut libc::c_char, buf.len());

match result {
0 => Ok(()),
_ => Err(MasterError::PtsnameError), // should probably capture errno
}
Expand All @@ -97,7 +107,7 @@ impl io::Read for Master {
}
}
} else {
Err(io::Error::new(io::ErrorKind::Other, "already closed"))
Err(io::Error::other("already closed"))
}
}
}
Expand All @@ -112,7 +122,7 @@ impl io::Write for Master {
}
}
} else {
Err(io::Error::new(io::ErrorKind::Other, "already closed"))
Err(io::Error::other("already closed"))
}
}

Expand Down
153 changes: 153 additions & 0 deletions src/fork/pty/master/ptsname_r_macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! macOS implementation of ptsname_r
//!
//! As `ptsname_r()` is not available on macOS, this provides a compatible
//! implementation using the `TIOCPTYGNAME` ioctl syscall.
//!
//! Based on: https://tarq.net/posts/ptsname-on-osx-with-rust/

#[cfg(target_os = "macos")]
/// # Safety
///
/// Callers must uphold the following invariants:
/// - `fd` must refer to an open master PTY file descriptor.
/// - `buf` must point to a valid allocation with capacity of at least `buflen` bytes and the
/// allocation must remain valid throughout the function call.
pub unsafe fn ptsname_r(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a "Saftey" comment that explains the formal precondtions that any call site must meet in order to use this function correctly (should be pretty easy, just stuff like "buf needs to point to allocated memory" and "buflen can't be longer than the allocation" and "fd must be an open file descriptor").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added; 559aa75

fd: libc::c_int,
buf: *mut libc::c_char,
buflen: libc::size_t,
) -> libc::c_int {
const IOCTL_BUF_SIZE: usize = 128;

if buf.is_null() || buflen == 0 {
return libc::EINVAL;
}

let mut ioctl_buf: [libc::c_char; IOCTL_BUF_SIZE] = [0; IOCTL_BUF_SIZE];

if libc::ioctl(fd, libc::TIOCPTYGNAME as libc::c_ulong, &mut ioctl_buf) != 0 {
return *libc::__error();
}

let null_ptr = libc::memchr(ioctl_buf.as_ptr() as *const libc::c_void, 0, IOCTL_BUF_SIZE);

let len = if null_ptr.is_null() {
IOCTL_BUF_SIZE
} else {
let offset = (null_ptr as usize) - (ioctl_buf.as_ptr() as usize);
offset + 1 // include null terminator.
};

if len > buflen {
return libc::ERANGE;
}

std::ptr::copy(ioctl_buf.as_ptr(), buf, len);

0
}

#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;

#[cfg(target_os = "macos")]
#[test]
fn test_ptsname_r_retrieves_valid_name() {
// Safety: calling libc function with valid flags.
let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a Saftey comment. I know it's just a test, but having them on every block makes auditing easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert!(master_fd >= 0, "Failed to open master PTY");

// Safety: master_fd is a valid open file descriptor.
unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a Saftey comment. I know it's just a test, but having them on every block makes auditing easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated each unsafe section in the tests, not sure about the wording though. How all these sounds like? 559aa75

assert_eq!(libc::grantpt(master_fd), 0, "grantpt failed");
assert_eq!(libc::unlockpt(master_fd), 0, "unlockpt failed");
}

let mut buf = vec![0u8; 1024];
// Safety: master_fd is valid, buf is a properly sized allocation.
let result =
unsafe { ptsname_r(master_fd, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Safety: closing the fd opened above.
unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libc::close(master_fd);
}

assert_eq!(result, 0, "ptsname_r failed with error code: {}", result);

// Safety: buf contains a null-terminated string from ptsname_r.
let name = unsafe { CStr::from_ptr(buf.as_ptr() as *const libc::c_char) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let name_str = name.to_str().expect("Invalid UTF-8 in PTY name");

assert!(
name_str.starts_with("/dev/ttys") || name_str.starts_with("/dev/pty"),
"Unexpected PTY name format: {}",
name_str
);
}

#[cfg(target_os = "macos")]
#[test]
fn test_ptsname_r_buffer_too_small() {
// Safety: calling libc function with valid flags.
let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if master_fd >= 0 {
// Safety: master_fd is a valid open file descriptor.
unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libc::grantpt(master_fd);
libc::unlockpt(master_fd);
}

let mut buf = [0u8; 2];
// Safety: master_fd is valid, buf is a properly sized allocation though intentionally
// too small.
let result =
unsafe { ptsname_r(master_fd, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Safety: closing the fd opened above.
unsafe {
libc::close(master_fd);
}

assert_eq!(result, libc::ERANGE);
}
}

#[cfg(target_os = "macos")]
#[test]
fn test_ptsname_r_invalid_fd() {
let mut buf = vec![0u8; 1024];
// Safety: buf is a properly sized allocation.
let result = unsafe { ptsname_r(-1, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


assert_ne!(result, 0, "Expected non-zero error code for invalid fd");
}

#[cfg(target_os = "macos")]
#[test]
fn test_ptsname_r_null_buffer() {
// Safety: calling libc function with valid flags.
let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if master_fd >= 0 {
// Safety: master_fd is a valid open file descriptor.
unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libc::grantpt(master_fd);
libc::unlockpt(master_fd);
}

// Safety: function handles null buffer safely.
let result = unsafe { ptsname_r(master_fd, std::ptr::null_mut(), 1024) };

// Safety: closing the fd opened above.
unsafe {
libc::close(master_fd);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

assert_eq!(result, libc::EINVAL);
}
}
}
1 change: 0 additions & 1 deletion src/fork/pty/slave/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::error::Error;
use std::fmt;

/// The alias `Result` learns `SlaveError` possibility.

pub type Result<T> = ::std::result::Result<T, SlaveError>;

/// The enum `SlaveError` defines the possible errors from constructor Slave.
Expand Down
12 changes: 11 additions & 1 deletion tests/it_can_read_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ fn it_can_read_write() {
assert_eq!(read_line(&mut master).trim(), "readme!");
let _ = master.write("exit\n".to_string().as_bytes());
} else {
let _ = Command::new("bash").env_clear().status();
let mut cmd = Command::new("bash");
cmd.env_clear();

// On macOS, silence the deprecation warning;
// https://github.com/apple-oss-distributions/bash/blob/e86b2aa8e37a31f8fce56366d1abaf08a3fac7d2/bash-3.2/shell.c#L760-L765
#[cfg(target_os = "macos")]
{
cmd.env("BASH_SILENCE_DEPRECATION_WARNING", "1");
}
Comment on lines +31 to +39
Copy link
Contributor Author

@seruman seruman Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like /bin/sh is just bash in disguise on macOS. Would it be OK to stick to bash and just silence this annoying warning?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that seems fine to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cheers


let _ = cmd.status();
}
}
Loading