Skip to content

Instantly share code, notes, and snippets.

@pskocik
Last active September 6, 2019 21:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pskocik/43e9f807cfd78f16a9df309da4809b59 to your computer and use it in GitHub Desktop.
Save pskocik/43e9f807cfd78f16a9df309da4809b59 to your computer and use it in GitHub Desktop.
Protect against Ctrl+C and Ctrl-/ not hitting processes backgrounded from shellscripts and leaving them running
/*
NAME:
shellguard
Signal forwarding intermediary command executer that translates terminal-generated SIGINT/SIGQUIT (Ctlr+C/Ctrl+/)
into SIGTERM/SIGABRT so that shell-backgrounded procesess don't stay running in the background when
the user presses Ctrl+C or Ctrl+/.
PROBLEM:
Shell scripts don't kill their child processes and instead rely on process-group-wide signals
to do the job.
Frequently the terminal generated SIGINT (terminates the target by default) and
SIGQUIT (terminates the target with a coredump by default) signals are used, which the
controlling terminal generates when Ctlr+C or Ctrl+/ is pressed.
Unfortunately, these 2, and just these two, fail to hit shell-backgrounded (&) processes
because shells are specified to ignore SIGINT and SIGQUIT in their backgrounded children.
(https://stackoverflow.com/questions/45106725/why-do-shells-ignore-sigint-and-sigquit-in-backgrounded-processes)
E.g., pressing Ctrl+C when running
sh -c ' ( sleep 100 ) & sleep 200'
will leave the `sleep 100` process running in the background and this is usually undesirable.
SOLUTION:
This simple, mostly transparent tool executes its argument as a child, waits on it, and while doing so counters
the above behavior by translating any terminal generated SIGINT and SIGQUIT it receives into SIGTERM (causes terminations by default)
and SIGQUIT (causes terminations with coredumps by default) and broadcasting the translated signal to the whole process group.
Signals received from other processes including SIGINT and SIGQUIT (e.g., if you send it with the kill -INT $somePid)
are simply forwarded to the child (not the whole process group) as they are.
When the child exits, the guard emulates the child's mode of death (exit status or death signal).
(SIGINT and SIGQUIT are additionally completely ignored in the child.
This is to prevent the child from exiting on a (terminal-sent) SIGINT/SIGQUIT before the guard gets a
chance to rebroadcast it as SIGTERM/SIGABRT.)
USAGE:
Compile it (and maybe put it in your PATH):
$ cc shellguard.c -Os -o shellguard
Use it:
$ ./shellguard whoami
$ ./shellguard ./some_script_you_wrote
Verify that backgrounded processes get hit when you press Ctr+C (or Ctrl-\):
$ ./shellguard sh -c 'sleep 100 & sleep 200'
^C #press Ctrl+C
$ ps #the `sleep 100` shouldn't have survived
BUGS:
On MacOS, differentiating between terminal-sent SIGINT/SIGQUIT and user-sent SIGINT/SIGQUIT doesn't work.
All SIGINT/SIGQUIT received by the guard on MacOS are translated to SIGTERM/SIQUIT and rebroadcast.
This is unlikely to cause problems in practice.
(Feel free to send bug reports if you find other issues, or money/goodies if you like it. :))
The tool has been lightly tested on Linux, Cygwin, & MacOS.
DISCLAIMER & LICENSE:
Use however you want at your own risk, but keep this note with it.
© Petr Skocik
*/
#undef NDEBUG
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>
#include <string.h>
/*#define fprintf(...) //comment out for debugging*/
#define unreachable() signal(SIGABRT,SIG_DFL),assert(0)
static _Bool proc_generated_eh(siginfo_t *Info)
{
_Bool r= Info->si_code == SI_USER || Info->si_code == SI_QUEUE || Info->si_code <= 0;
if(r)
fprintf(stderr,"proc_sent=%d sig=%d pid=%ld uid=%ld\n", r, Info->si_signo, (long)Info->si_pid, (long)Info->si_uid);
else
fprintf(stderr,"proc_sent=%d sig=%d\n", r, Info->si_signo);
#if __APPLE__
return 0;
#else
return r;
#endif
}
static volatile pid_t CHPID = -1;
static volatile pid_t NEG_PGRP;
static void action (int Sig, siginfo_t *Info, void *Uctx)
{
int wst;
if (!proc_generated_eh(Info)){
switch(Sig){
case SIGCHLD:{
for(;;){
fprintf(stderr, "waitpid\n");
pid_t rc=waitpid(CHPID,&wst,WNOHANG);
fprintf(stderr,"rc=%ld\n", (long)rc);
if(rc==-1){
if(EINTR==errno) continue;
#if !__APPLE__
unreachable();
#endif
continue;
}
else if(0==rc) return;
else break;
}
if(WIFEXITED(wst)) { _exit(WEXITSTATUS(wst)); }
else if(0 && WIFSTOPPED(wst)){
raise(SIGSTOP); return;
} else if(WIFSIGNALED(wst)) {
int sig = WTERMSIG(wst);
signal(sig, SIG_DFL);
fprintf(stderr,"raise=%d\n",sig);
raise(sig);
return;
}
} return;
case SIGINT: kill(NEG_PGRP, SIGTERM); /*no coredump*/ return;
case SIGQUIT: kill(NEG_PGRP, SIGABRT); /*coredump*/ return;
}
}
kill(CHPID, Sig);
return;
}
int main(int C, char **V)
{
NEG_PGRP = -getpgrp();
fprintf(stderr,"pid=%ld\n", (long)getpid());
struct sigaction sa;
sigfillset(&sa.sa_mask); sa.sa_flags=SA_SIGINFO; sa.sa_sigaction = action;
//64 should be enough for all posix systems I know of but let's do 128 just in case
for(int i=1; i<128; i++) (void)sigaction(i, &sa, 0);
//keep the terminal generating stopping signals at their defaults
signal(SIGTTIN, SIG_DFL);
signal(SIGTTOU, SIG_DFL);
signal(SIGTSTP, SIG_DFL);
if (0>(CHPID = fork())) return perror("shellguard: fork()"),1;
if (0==CHPID){
/*Completly ignore INT and QUIT in the child.
Otherwise a first INT/QUIT could cause child death
which could cause the guard to get CHLD and quit
before it would receive the INT/QUIT itself and rebroadcast
it as TERM/ABRT.
*/
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
if(V[1]) execvp(V[1],V+1);
else errno=ENOENT;
fprintf(stderr, "shellguard execvp(%s): %s\n", V[1]?V[1]:"<null>", strerror(errno));
_exit(127);
}
for(;;) pause();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment