Subject: Re: [PATCH v3 2/4] terminal: don't assume stdin is /dev/tty

From: Junio C Hamano <[email-masked]>
Date: Tue, 15 Mar 2022 10:42:59 -0700

Phillip Wood <[email-masked]> writes:

> diff --git a/compat/terminal.c b/compat/terminal.c
> index da2f788137..bfbde3c1af 100644
> --- a/compat/terminal.c
> +++ b/compat/terminal.c
> @@ -23,21 +23,28 @@ static void restore_term_on_signal(int sig)
>  static int term_fd = -1;

The variable can be -1 to signal "no valid file descriptor".

>  static struct termios old_term;
>  
> +static void close_term_fd(void)
> +{
> +	if (term_fd)
> +		close(term_fd);
> +	term_fd = -1;
> +}
> +

And we use that negative value after closing it.

The check before closing it is wrong.  It should be

	if (0 <= term_fd)

instead.  Or we could mimick the beginning of restore_term(), i.e.

	if (term_fd < 0)
		return;
	close(term_fd);
	term_fd = -1;

>  void restore_term(void)
>  {
>  	if (term_fd < 0)
>  		return;
>  
>  	tcsetattr(term_fd, TCSAFLUSH, &old_term);
> -	close(term_fd);
> -	term_fd = -1;
> +	close_term_fd();

Because we come this far only when term_fd is valid, this change is
a no-op.  If we are adding more calls to close_term_fd(), it may be
a good abstraction.

>  	sigchain_pop_common();
>  }
>  
>  int save_term(enum save_term_flags flags)
>  {
>  	if (term_fd < 0)
> -		term_fd = open("/dev/tty", O_RDWR);
> +		term_fd = (flags & SAVE_TERM_STDIN) ? 0
> +						    : open("/dev/tty", O_RDWR);

We can avoid overly long line by wrapping differently:

		term_fd = ((flags & SAVE_TERM_STDIN)
			   ? 0
			   : open("/dev/tty", O_RDWR));

We can turn our head sideways to see the parse tree this way.

> @@ -66,8 +73,7 @@ static int disable_bits(enum save_term_flags flags, tcflag_t bits)
>  
>  	sigchain_pop_common();
>  error:
> -	close(term_fd);
> -	term_fd = -1;
> +	close_term_fd();

OK.

> @@ -362,7 +368,7 @@ int read_key_without_echo(struct strbuf *buf)
>  	static int warning_displayed;
>  	int ch;
>  
> -	if (warning_displayed || enable_non_canonical(0) < 0) {
> +	if (warning_displayed || enable_non_canonical(SAVE_TERM_STDIN) < 0) {
>  		if (!warning_displayed) {
>  			warning("reading single keystrokes not supported on "
>  				"this platform; reading line instead");

The validity of this change is harder to see with only what we have
in the patch, but the control flow is

   enable_non_canonical(bits)
   -> disable_bits(bits, ...)
      -> save_term(bits)

And we are passing SAVE_TERM_STDIN to say "reuse fd #0 and save the
terminal settings of it, instead of opening /dev/tty anew".

What happens if FD#0 is *not* connected to a tty, by the way?
tcgetattr() in save_term() would fail, without clearing term_fd
(i.e. term_fd is still 0 when save_term() returns a failure), and
goes to the error code path, where close_term_fd() is called.

Because we have the "if (term_fd)" bug in save_term(), this bug is
hidden, but I think save_term() upon seeing a failure from tcgetattr()
should clear term_fd to limit the damage, especially when it is trying
to futz with caller supplied FD#0, not the tty it opened itself?

> diff --git a/compat/terminal.h b/compat/terminal.h
> index aeb24c9478..79ed00cf61 100644
> --- a/compat/terminal.h
> +++ b/compat/terminal.h
> @@ -4,6 +4,8 @@
>  enum save_term_flags {
>  	/* Save input and output settings */
>  	SAVE_TERM_DUPLEX = 1 << 0,
> +	/* Save stdin rather than /dev/tty (fails if stdin is not a terminal) */
> +	SAVE_TERM_STDIN  = 1 << 1,
>  };
>  
>  /*