Pseudo-terminals

My dad recently had an interesting problem at work related to serial ports. Since I use serial ports at work, he asked me for advice. They have third-party software which reads and analyzes sensor data from the serial port. It’s the only method this program has of inputting a stream of data and they’re unable to patch it. Unfortunately, they have another piece of software that needs to massage the data before this final program gets it. The data needs to be intercepted coming on the serial port somehow.

The solution they were aiming for was to create a pair of virtual serial ports. The filter software would read data in on the real serial port, output the filtered data into a virtual serial port which would be virtually connected to a second virtual serial port. The analysis software would then read from this second serial port. They couldn’t figure out how to set this up, short of buying a couple of USB/serial port adapters and plugging them into each other.

It turns out this is very easy to do on Unix-like systems. POSIX defines two functions, posix_openpt(3) and ptsname(3). The first one creates a pseudo-terminal — a virtual serial port — and returns a “master” file descriptor used to talk to it. The second provides the name of the pseudo-terminal device on the filesystem, usually named something like /dev/pts/5.

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main()
{
    int fd = posix_openpt(O_RDWR | O_NOCTTY);
    printf("%s\n", ptsname(fd));
    /* ... read and write to fd ... */
    return 0;
}

The printed device name can be opened by software that’s expecting to access a serial port, such as minicom, and it can be communicated with as if by a pipe. This could be useful in testing a program’s serial port communication logic virtually.

The reason for the unusually long name is because the function wasn’t added to POSIX until 1998 (Unix98). They were probably afraid of name collisions with software already using openpt() as a function name. The GNU C Library provides an extension getpt(3), which is just shorthand for the above.

int fd = getpt();

Pseudo-terminal functionality was available much earlier, of course. It could be done through the poorly designed openpty(3), added in BSD Unix.

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

It accepts NULL for the last three arguments, allowing the user to ignore them. What makes it so bad is that string name. The user would pass it a chunk of allocated space and hope it was long enough for the file name. If not, openpty() would overwrite the end of the string and trash some memory. It’s highly unlikely to ever exceed something like 32 bytes, but it’s still a correctness problem.

The newer ptsname() is only slightly better however. It returns a string that doesn’t need to be free()d, because it’s static memory. However, that means the function is not re-entrant; it has issues in multi-threaded programs, since that string could be trashed at any instant by another call to ptsname(). Consider this case,

int fd0 = getpt();
int fd1 = getpt();
printf("%s %s\n", ptsname(fd0), ptsname(fd1));

ptsname() will be returning the same char * pointer each time it’s called, merely filling the pointed-to space before returning. Rather than printing two different device filenames, the above would print the same filename twice. The GNU C Library provides an extension to correct this flaw, as ptsname_r(), where the user provides the memory as before but also indicates its maximum size.

To make a one-way virtual connection between our pseudo-terminals, create two of them and do the typical buffer thing between the file descriptors (for succinctness, no checking for errors),

while (1) {
    char buffer;
    int in = read(pt0, &buffer, 1);
    write(pt1, &buffer, in);
}

Making a two-way connection would require the use of threads or select(2), but it wouldn’t be much more complicated.

While all this was new and interesting to me, it didn’t help my dad at all because they’re using Windows. These functions don’t exist there and creating virtual serial ports is a highly non-trivial, less-interesting process. Buying the two adapters and connecting them together is my recommended solution for Windows.

Have a comment on this article? Start a discussion in my public inbox by sending an email to ~skeeto/public-inbox@lists.sr.ht [mailing list etiquette] , or see existing discussions.

null program

Chris Wellons

wellons@nullprogram.com (PGP)
~skeeto/public-inbox@lists.sr.ht (view)