Implementing ptsname_r on OSX with Rust

I've been working on a project with rust that requires creating a pseudo-terminal and like many others, I've run into a lot of problems with the functions available to get a pair of master/slave fds for my PTY.

openpty

int openpty(int *amaster, int *aslave, char *name,  
                   const struct termios *termp,
                   const struct winsize *winp);

This function seems like a good fit at first glance but the documentation contains this disclaimer:

Nobody knows how much space should be reserved for name. So, calling openpty() or forkpty() with non-NULL name may not be secure.

Well that's unfortunate. What else we got?

ptsname

char *ptsname(int fd);

This method returns a pointer to a static buffer that is overwritten on each call. This makes it not thread safe without some locking semantics which is also less than ideal.

ptsname_r

int ptsname_r(int fd, char *buf, size_t buflen);  

Now that's more like it! Writes a buffer we choose so we can avoid avoid the thread-safety issues of a static buffer and it let's us specify the length. Signed, sealed, delivered, right?

Hold up a minute there kiddo!

- Posix Standard

Turns out, ptsname_r is a Linux extension. I needed this library to work on my shiny new work MacBook. So what are we to do?

Reinvent the wheel with system calls!

- Author of this blog post

We know that ptsname must be getting that data from somewhere before it writes to that pesky static buffer. So why not call it ourselves? On OSX, we can use ioctl to call TIOCPTYGNAME, which will write the TTY name to a buffer we control. The interface, however, does not let you specify a buffer size.

Lucky for us, the size is defined in sys/ttycom.h as 128. So now all we need to do is implement that in rust:

    #[cfg(target_os="macos")]
    fn get_slave_name(fd: RawFd) -> io::Result<std::path::PathBuf> {
        use std::os::unix::ffi::OsStrExt;
        use std::ffi::{CStr,OsStr};
        use std::os::raw::c_char;
        use std::path::PathBuf;
        // ptsname_r is a linux extension but ptsname isn't thread-safe
        // we could use a static mutex but instead we re-implemented ptsname_r with a syscall
        // ioctl(fd, TIOCPTYGNAME, buf) manually
        // the buffer size on OSX is 128, defined by sys/ttycom.h
        //
        //
        let mut buf : [c_char; 128] = [0;128];

        unsafe {
            match libc::ioctl(fd, libc::TIOCPTYGNAME as u64, &buf) {
                0 => {
                   Ok(PathBuf::from(
                           OsStr::from_bytes(
                               CStr::from_ptr(buf.as_ptr())
                               .to_bytes()
                               )
                           )
                       )
                },
                _ => Err(io::Error::last_os_error()),
            }
        }
    }

Now I'm still finding my bearings when it comes to Rust so if anyone has any critiques of the above code, please let me know.

The above is hastily modified from the application I'm working on so it may not work verbatim but the output of my tests in my library show us that it works!

#[test]
fn create_pty() {  
    let mut master = Master::open().unwrap();
    master.grant_slave_access();
    master.unlock_slave();
    let slavename = master.get_slave_name().unwrap();
    assert!(slavename.starts_with("/dev"));
    println!("slave name {}", slavename.display());
}

Output:

running 1 test  
slave name /dev/ttys010  
test tests::create_pty ... ok  

Perfect! Now that we've got that squared away we can focus our time on appeasing the borrow checker.

Christopher Tarquini

Technical Lead with Linode Professional Services. All posts/opinions/rants/spelling errors are my own.

https://tarq.io
New Jersey

comments powered by Disqus