Read STDIN using syscall READ in Linux: unconsumed input is sent to bash

Viewed 580

The following program (64-bit YASM) reads 4 bytes from the standard input and exits:

    section .data
buf   db "               "      ; Just allocate 16 bytes for string
    section .text
    global _start
_start:
    mov rax, 0                  ; READ syscall
    mov rdi, 0                  ; STDIN
    mov rsi, buf                ; Address of the string
    mov rdx, 4                  ; How many bytes to read
    syscall
    ; Exit:
    mov rax, 60
    mov rdi, 0
    syscall

Once compiled

yasm -f elf64 -l hello.lst -o input.o input.asm
ld -o input input.o

If it is run just as

./input

with, say, 123456\n as user input, it will consume 1234, but the end bit, 56\n is sent to bash. So, bash will try to run the command 56... thankfully unsuccessfully. But imagine if the input were 1234rm -f *. However, if I provide the input using re-direction or piping, for example,

echo "123456" | ./input

56 is not sent to bash.

So, how can I prevent unconsumed input to be sent to bash? Do I need to keep consuming it until EOF in some form is encountered? Is this even expected behavior?

The same thing happens in a C program:

#include <unistd.h>

int main()
{
    char buf[16];
    read(0, buf, 4);
    return 0;
}

(I was just wondering if C runtime was somehow clearing STDIN, but no, it doesn't)

2 Answers

Yep, it's normal behavior. Anything you don't consume is available for the next process. You know how when you're doing something slow you can type ahead, and when the slow thing finishes the shell will run what you typed? Same thing here.

There's no one-size-fits-all solution. It's really about user expectation. How much input did they expect your program to consume? That's how much you should read.

  • Does your program act like a single line prompt like read? Then you should read a full line of input up through the next \n character. The easiest way to do that without over-reading is to read 1 character at a time. If you do bulk reads you might consume part of the next line by mistake.

  • Does your program act like a filter like cat or sed or grep? Then you should read until you reach EOF.

  • Does your program not read from stdin at all like echo or gcc? Then you should leave stdin alone and consume nothing, leaving the input for the next program.

Consuming exactly 4 bytes is unusual but could be reasonable behavior for, say, an interactive program that prompts for a 4-digit PIN and doesn't require the user to press Enter.

"Is sent to" bash (or to any other program) is a suboptimal way to think about it, and perhaps that's contributing to your surprise / confusion. "Made available to" would be a more accurate characterization, whether you're talking about a terminal, a pipe, or any other input source connected to a program's standard input.

When one process forks another, such as a shell does to execute many of the commands you enter, the new process inherits a lot of properties from its parent. Among those are its open file descriptors, and in particular, those of the parent's standard streams. On POSIX systems, this is where a process's standard streams come from in the absence of redirection, and it is also the mechanism by which redirection is implemented.

Thus, when no I/O redirection is involved, of course the parent shell reads input data that programs it launches leave unread. The input could not be available for those programs if it were not also available to the parent shell, because both are reading from the same source. This is also why you can move programs between foreground and background in the same terminal, and each one is able to read from the terminal when it is in the foreground.

Inasmuch as you mention the read() syscall in particular, I suspect your surprise might also have been related to seeing different-seeming behavior from programs that use stdio functions to read from the standard input. This has everything to do with the fact that when the standard input is connected to a terminal, the stdio functions read it in line-buffered mode by default. That is, they transfer data from the underlying source into an internal buffer, thus removing it from the stream, one line at a time (with some caveats).

You can emulate that with read. The simplest way would be to read one byte at a time, until you see a newline or end-of-file, but the C library functions do it more efficiently by configuring the terminal driver appropriately when the standard input is in fact connected to a terminal. You can do that directly, too, but it's a bit complicated.

Related