Race condition with PTY

I've been experimenting with pseudo-terminals and found something I don't quite understand. Writing an EOF character to the master end doesn't work quite as I expect. Once I've written any other data, the master pty seems to treat a single ^D as a seperator, i.e. writing "abcabc" would let cat do one read of six bytes, but "abc\x04abc" would make cat two reads of three bytes. Is this behavior documented anywhere?

Here's the code I test with. I asserted everything in case there were errors I was missing but didn't find any...

#include <unistd.h>
#include <fcntl.h>
#include <pty.h>
#include <errno.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <assert.h>

// My system doesn't declare ptsname for some reason
// but it clearly exists.
#define _XOPEN_SOURCE
#include <stdlib.h>
extern char *ptsname(int fd);


#include <sys/types.h>
#include <sys/wait.h>


/**
 *	popen() and system() create a whole new shell, which
 * 	clutters strace output with tons of garbage.
 *	this just exec's raw and does no such thing.
 */
int system_noshell(const char *cmd, ...);

int main(int argc, char *argv[])
{	// Open a pseudo-terminal, but don't make it our stdin/out/err
	int fd_master=posix_openpt(O_RDWR|O_NOCTTY);
	int ppid=getpid();	// Process ID of parent
	int cpid=-1;		// Get child PID later
	const char *pts;	// slave pseudoterm

	int s;	
	char c;

	assert( fd_master >= 0 );	// Did master terminal open?

	fprintf(stderr, "[P] Opened fd_master = %d\n", fd_master);

	assert( unlockpt(fd_master) >= 0 );	// Unlock PTY

	fprintf(stderr, "[P] Unlocked fd_master\n");

	assert( (pts=ptsname(fd_master)) != NULL ); // Get slave name

	fprintf(stderr, "[P] Name of slave: %s\n", pts);

	// Create a child process to use the slave pty
	assert( (cpid=fork()) >= 0);

	/**
	 *	Following a specific series of events here:
	 *	Parent			Child
	 *  ===================================
	 *	Open Master
	 *	fork
	 *				Open Child
	 *				Close Master
	 *	read			write '|'
	 *	write 'abc'
	 *	write EOF EOF
	 *				read abc
	 *				EOF
	 *				exit
	 *	wait()
	 *	exit
	 */

	if(cpid == 0)	// Child code
	{
		int fd_slave=-1;	// Slave PTY
		// Save real stderr so fprintf writes to it instead of pty
		int STDERR=dup(STDERR_FILENO);
		FILE *stderr=fdopen(STDERR, "w");
		setvbuf(stderr, NULL, _IONBF, 0);

		assert( close(fd_master) >= 0 ); // Ditch master PTY

		assert( (fd_slave=open(pts,O_RDWR)) >= 0); // Open slave PTY

		fprintf(stderr, "\t[C] Opened slave %s\n", pts);

		// This will dup fd over stdin,out,err then close fd
		// This function needs compilation with -lutil
		assert( login_tty(fd_slave) >= 0 );

                // We can't use assert after here, it'll print to the real stderr
                if(system_noshell("/bin/stty", "-echo", NULL) != 0)
                {
                        fprintf(stderr, "\t[C] Couldn't disable echo\n");
                        exit(1);
                }

		fprintf(stderr, "\t[C] Disabled echo\n");

		// We let the parent know the child has control of
		// the slave terminal by writing a char to it.
		if(write(STDOUT_FILENO, "|", 1) != 1 )
                {
                        fprintf(stderr, "\t[C] Couldn't send |\n");
                        exit(1);
                }

		fprintf(stderr, "\t[C] Wrote char\n");

		// We exec cat, to read until EOF.
		execl("/bin/cat", "/bin/cat", NULL);

                fprintf(stderr, \t[C] Couldn't exec: %s\n",
                         strerror(errno));
		exit(1);
	}

	fprintf(stderr, "[P] Created child pid=%d\n", cpid);

	// Parent code
	assert( read(fd_master, &c, 1) == 1 );
	assert( c == '|' );	// Child should have written |

	fprintf(stderr, "[P] Read first char from child\n");

	// Write "abc" to master end of terminal
	assert( write(fd_master, "abc", 3) == 3 );

	fprintf(stderr, "[P] Wrote data to child\n");

	// The child hangs if I don't write two EOF chars.
	assert( write(fd_master, "\x04\x04", 2) == 2 );

	fprintf(stderr, "[P] Wrote EOF\n");

	while(read(fd_master, &c, 1) == 1)	// Read bytes until EOF
	{
		if(c<0x20)	fprintf(stderr, "[P] Read ^%c\n", c+'@');
		else		fprintf(stderr, "[P] Read '%c'\n", c);
	}

	assert( wait(&s) == cpid );	// Wait for child to exit
	fprintf(stderr, "[P] Child has exited\n");
	assert( close(fd_master) >= 0 );
	fprintf(stderr, "[P] All finished.\n");
	return(0);
}

#define MAX_ARGS 16

int system_noshell(const char *cmd, ...)
{
	const char *args[MAX_ARGS]={cmd};
	int n=0, status;
	va_list ap;

	// Assemble varargs into the args array
	va_start(ap, cmd);
		do
			args[++n]=va_arg(ap, void *);
		while((args[n]) && (n<(MAX_ARGS-2)));
	va_end(ap);

	args[++n]=NULL;	// Terminate argument list

	n=fork();
	if(n < 0)
		return(-1);
	else if(n == 0)
	{
		execvp(cmd, args);
		exit(255);
	}

	assert( waitpid(n, &status, 0) == n);
	return(WEXITSTATUS(status));
}

Seems it is documented behavior...see this link which explains why a trailing / embedded EOF acts as a line delimiter.

1 Like