12

It seems that the pty driver on Linux is replacing VEOF characters (^D, \4) with NUL bytes (\0) in the data already written from the master side if the terminal settings are changed with tcsetattr(TCSANOW) to non-canonical mode before reading it on the slave side.

Why is this happening? Does it have any justification or it's simply a bug?

Is there any way to avoid it? -- other than waiting for input from the slave on the master side before writing anything, which is not practical, because on the slave side there may be another program -- the routine of setting the terminal into raw mode which I've simplified here is usually what any shell with line-editing capabilities does.

While having eg \r replaced by \n could be expected (because the ICRNL flag was already applied), I cannot make any rationale for those NUL bytes appearing out of nowhere.

Test case below: it will print foo\x00\x00\x00\x00 on Linux, but foo\x04\x04\x04\x04 on *BSD and foo on Solaris.

#define _XOPEN_SOURCE   600
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdio.h>
#include <termios.h>
#include <err.h>

#ifdef __sun
#include <stropts.h>
#define push_streams(fd)\
        if(ioctl(fd, I_PUSH, "ptem")) err(1, "ioctl(I_PUSH, ptem)");\
        if(ioctl(fd, I_PUSH, "ldterm")) err(1, "ioctl(I_PUSH, ldterm)");
#else
#define push_streams(sd)        /* no need */
#endif

int main(void){
        int mt, st; char *sname;

        /* openpty()-like boilerplate */
        if((mt = posix_openpt(O_RDWR|O_NOCTTY)) == -1) err(1, "posix_openpt");
        if(grantpt(mt)) err(1, "grantpt");
        if(unlockpt(mt)) err(1, "unlockpt");
        if(!(sname = ptsname(mt))) err(1, "ptsname");
        if((st = open(sname, O_RDWR|O_NOCTTY)) == -1) err(1, "open %s", sname);
        push_streams(st);

        /* master */ {
                char test[] = "foo\4\4\4\4";
                if(write(mt, test, sizeof test - 1) < sizeof test - 1)
                        err(1, "write");
        }
        /* slave */ {
                unsigned char buf[512]; int i, r;
                struct termios ts;
                usleep(1000);
                if(tcgetattr(st, &ts)) err(1, "tcgetattr");
                ts.c_lflag &= ~ICANON;
                if(tcsetattr(st, TCSANOW, &ts)) err(1, "tcsetattr");
                if((r = read(st, buf, sizeof buf)) < 0)
                        err(1, "read");
                for(i = 0; i < r; i++)
                        if(isprint(buf[i])) putchar(buf[i]);
                        else printf("\\x%02x", buf[i]);
                putchar('\n');
        }
        return 0;
}
  • 1
    I don't know if this really serves as an answer, but this works correctly if the master clears `ICANON` before writing. – Hasturkun Apr 30 '19 at 06:39
  • I cannot do that because the program running in the slave pty may not expect it to be in raw mode. From the point of view of the master side, it doesn't matter if the program running in the terminal is using some line-editing features or not -- sending a ^D to a command line program will work just the same whether it's handled by the tty driver or by the readline/editline library. –  Apr 30 '19 at 08:11
  • It would be easy if I could assume that the program running in the slave will print a prompt (I'd wait for the prompt before writing anything). But I cannot assume that either. –  Apr 30 '19 at 08:15
  • 2
    Another sort of workaround is to set `ts.c_cc[VEOF] = _POSIX_VDISABLE;` in the master, so not fully raw, but has EOF disabled (sort of, NUL will act as EOF and be replaced with a NUL, if I'm reading the kernel code correctly). Btw, if I'm not mistaken, the linux pty code effectively makes input processing happen as a result of the write. – Hasturkun Apr 30 '19 at 09:41

1 Answers1

1

This conversion is done by the line discipline driver when the data is written by the master, not when the slave reads the data. The relevant code is:

https://elixir.bootlin.com/linux/latest/source/drivers/tty/n_tty.c#L1344

user2249675
  • 464
  • 3
  • 13