diff mbox series

jobs: Block signals during tcsetpgrp

Message ID 20210106044512.GA28191@gondor.apana.org.au (mailing list archive)
State Accepted
Delegated to: Herbert Xu
Headers show
Series jobs: Block signals during tcsetpgrp | expand

Commit Message

Herbert Xu Jan. 6, 2021, 4:45 a.m. UTC
Harald van Dijk <harald@gigawatt.nl> wrote:
> On 19/12/2020 22:21, Steffen Nurpmeso wrote:
>> Steffen Nurpmeso wrote in
>>   <20201219172838.1B-WB%steffen@sdaoden.eu>:
>>   |Long story short, after falsely accusing BSD make of not working
>> 
>> After dinner i shortened it a bit more, and attach it again, ok?
>> It is terrible, but now less redundant than before.
>> Sorry for being so terse, that problem crosses my head for about
>> a week, and i was totally mislead and if you bang your head
>> against the wall so many hours bugs or misbehaviours in a handful
>> of other programs is not the expected outcome.
> 
> I think a minimal test case is simply
> 
> all:
>         $(SHELL) -c 'trap "echo TTOU" TTOU; set -m; echo all good'
> 
> unless I accidentally oversimplified.
> 
> The SIGTTOU is caused by setjobctl's xtcsetpgrp(fd, pgrp) call to make 
> its newly started process group the foreground process group when job 
> control is enabled, where xtcsetpgrp is a wrapper for tcsetpgrp. (That's 
> in dash, the other variants may have some small differences.) tcsetpgrp 
> has this little bit in its specification:
> 
>        Attempts to use tcsetpgrp() from a process which is a member of
>        a background process group on a fildes associated with its con‐
>        trolling  terminal  shall  cause the process group to be sent a
>        SIGTTOU signal. If the calling thread is blocking SIGTTOU  sig‐
>        nals  or  the  process is ignoring SIGTTOU signals, the process
>        shall be allowed to perform the operation,  and  no  signal  is
>        sent.
> 
> Ordinarily, when job control is enabled, SIGTTOU is ignored. However, 
> when a trap action is specified for SIGTTOU, the signal is not ignored, 
> and there is no blocking in place either, so the tcsetpgrp() call is not 
> allowed.
> 
> The lowest impact change to make here, the one that otherwise preserves 
> the existing shell behaviour, is to block signals before calling 
> tcsetpgrp and unblocking them afterwards. This ensures SIGTTOU does not 
> get raised here, but also ensures that if SIGTTOU is sent to the shell 
> for another reason, there is no window where it gets silently ignored.
> 
> Another way to fix this is by not trying to make the shell start a new 
> process group, or at least not make it the foreground process group. 
> Most other shells appear to not try to do this.

This patch implements the blocking of SIGTTOU (and everything else)
while we call tcsetpgrp.

Reported-by: Steffen Nurpmeso <steffen@sdaoden.eu>
Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au>

Comments

Harald van Dijk Jan. 6, 2021, 9:16 p.m. UTC | #1
On 06/01/2021 04:45, Herbert Xu wrote:
> This patch implements the blocking of SIGTTOU (and everything else)
> while we call tcsetpgrp.
> 
> Reported-by: Steffen Nurpmeso <steffen@sdaoden.eu>
> Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au>
> 
> diff --git a/src/jobs.c b/src/jobs.c
> index 516786f..809f37c 100644
> --- a/src/jobs.c
> +++ b/src/jobs.c
> @@ -1512,7 +1512,13 @@ showpipe(struct job *jp, struct output *out)
>   STATIC void
>   xtcsetpgrp(int fd, pid_t pgrp)
>   {
> -	if (tcsetpgrp(fd, pgrp))
> +	int err;
> +
> +	sigblockall(NULL);
> +	err = tcsetpgrp(fd, pgrp);
> +	sigclearmask();
> +
> +	if (err)
>   		sh_error("Cannot set tty process group (%s)", strerror(errno));
>   }
>   #endif

While this is a step in the right direction, Jilles has already replied 
with an explanation of why this is not enough: if the terminal is in 
TOSTOP mode, it's not just tcsetpgrp() that needs to be handled, it's 
any write as well that may occur while the shell is not in the 
foreground process group. While it may be working according to design 
for messages written when the shell is not supposed to be in the 
foreground process group, it is another story when the shell is both 
responsible for taking itself out of the foreground process group and 
for writing a message. This is made worse by the fact that there is no 
synchronisation with child processes on errors, so even forcibly 
restoring the foreground process group may not be enough: unfortunate 
scheduling may result in a child process immediately setting the 
foreground process group to the child process after the parent process 
attempted to restore it to itself. I have not yet seen a good solution 
for this.

Cheers,
Harald van Dijk
Jilles Tjoelker Jan. 6, 2021, 10:41 p.m. UTC | #2
On Wed, Jan 06, 2021 at 09:16:58PM +0000, Harald van Dijk wrote:
> On 06/01/2021 04:45, Herbert Xu wrote:
> > This patch implements the blocking of SIGTTOU (and everything else)
> > while we call tcsetpgrp.

> > Reported-by: Steffen Nurpmeso <steffen@sdaoden.eu>
> > Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au>

> > diff --git a/src/jobs.c b/src/jobs.c
> > index 516786f..809f37c 100644
> > --- a/src/jobs.c
> > +++ b/src/jobs.c
> > @@ -1512,7 +1512,13 @@ showpipe(struct job *jp, struct output *out)
> >   STATIC void
> >   xtcsetpgrp(int fd, pid_t pgrp)
> >   {
> > -	if (tcsetpgrp(fd, pgrp))
> > +	int err;
> > +
> > +	sigblockall(NULL);
> > +	err = tcsetpgrp(fd, pgrp);
> > +	sigclearmask();
> > +
> > +	if (err)
> >   		sh_error("Cannot set tty process group (%s)", strerror(errno));
> >   }
> >   #endif

> While this is a step in the right direction, Jilles has already replied with
> an explanation of why this is not enough: if the terminal is in TOSTOP mode,
> it's not just tcsetpgrp() that needs to be handled, it's any write as well
> that may occur while the shell is not in the foreground process group. While
> it may be working according to design for messages written when the shell is
> not supposed to be in the foreground process group, it is another story when
> the shell is both responsible for taking itself out of the foreground
> process group and for writing a message. This is made worse by the fact that
> there is no synchronisation with child processes on errors, so even forcibly
> restoring the foreground process group may not be enough: unfortunate
> scheduling may result in a child process immediately setting the foreground
> process group to the child process after the parent process attempted to
> restore it to itself. I have not yet seen a good solution for this.

Comparing this error situation to the normal case, I think the right
solution is to close any stray pipe ends we have, wait for the remaining
child processes and only then report the error (throwing an exception as
normal). The child processes will probably terminate soon because of a
broken pipe, but even if they stop, they will not change the tty
foreground process group any more. The code in jobs.c will then reset
it.

The same error handling applies to the situation where pipe() fails.
This is a bit easier to test reliably, using ulimit -n.

Adding synchronization with the child processes slows down the normal
case for a rare error case, and the synchronization objects such as
pipe, eventfd, SysV semaphore or MAP_SHARED mapping might cause
unexpected issues in strange use cases.

A related bug: if fork fails for a command substitution, the pipe
created for reading the command output remains open (two descriptors).
This one is also in dash as well as FreeBSD sh. Throwing exceptions from
forkshell() may not be the best idea.
Denys Vlasenko Jan. 7, 2021, 7:36 a.m. UTC | #3
On Wed, Jan 6, 2021 at 10:17 PM Harald van Dijk <harald@gigawatt.nl> wrote:
> On 06/01/2021 04:45, Herbert Xu wrote:
> > This patch implements the blocking of SIGTTOU (and everything else)
> > while we call tcsetpgrp.
> >
> > Reported-by: Steffen Nurpmeso <steffen@sdaoden.eu>
> > Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au>
> >
> > diff --git a/src/jobs.c b/src/jobs.c
> > index 516786f..809f37c 100644
> > --- a/src/jobs.c
> > +++ b/src/jobs.c
> > @@ -1512,7 +1512,13 @@ showpipe(struct job *jp, struct output *out)
> >   STATIC void
> >   xtcsetpgrp(int fd, pid_t pgrp)
> >   {
> > -     if (tcsetpgrp(fd, pgrp))
> > +     int err;
> > +
> > +     sigblockall(NULL);
> > +     err = tcsetpgrp(fd, pgrp);
> > +     sigclearmask();
> > +
> > +     if (err)
> >               sh_error("Cannot set tty process group (%s)", strerror(errno));
> >   }
> >   #endif
>
> While this is a step in the right direction, Jilles has already replied
> with an explanation of why this is not enough: if the terminal is in
> TOSTOP mode, it's not just tcsetpgrp() that needs to be handled, it's
> any write as well that may occur while the shell is not in the
> foreground process group. While it may be working according to design
> for messages written when the shell is not supposed to be in the
> foreground process group, it is another story when the shell is both
> responsible for taking itself out of the foreground process group and
> for writing a message. This is made worse by the fact that there is no
> synchronisation with child processes on errors, so even forcibly
> restoring the foreground process group may not be enough: unfortunate
> scheduling may result in a child process immediately setting the
> foreground process group to the child process after the parent process
> attempted to restore it to itself. I have not yet seen a good solution
> for this.

How about not simply allowing traps on SIGTTOU, like other shells do?

It's not like there is real-world need to allow usage of this signal
- it's not customarily used by userspace for inter-process signaling,
unlike e.g. SIGTERM or SIGUSR1/2.
diff mbox series

Patch

diff --git a/src/jobs.c b/src/jobs.c
index 516786f..809f37c 100644
--- a/src/jobs.c
+++ b/src/jobs.c
@@ -1512,7 +1512,13 @@  showpipe(struct job *jp, struct output *out)
 STATIC void
 xtcsetpgrp(int fd, pid_t pgrp)
 {
-	if (tcsetpgrp(fd, pgrp))
+	int err;
+
+	sigblockall(NULL);
+	err = tcsetpgrp(fd, pgrp);
+	sigclearmask();
+
+	if (err)
 		sh_error("Cannot set tty process group (%s)", strerror(errno));
 }
 #endif