1
0
mirror of git://git.sv.gnu.org/coreutils.git synced 2026-04-20 18:56:39 +02:00

env: new options --{default,ignore,block}-signal[=SIG]

New options to set signal handlers for the command being executed.
--block-signal suggested by Paul Eggert in http://bugs.gnu.org/34488#71
--default-signal is useful to overcome the POSIX limitation that shell
must not override inherited signal state, e.g. the second 'trap' here is
a no-op:

   trap '' PIPE && sh -c 'trap - PIPE ; seq inf | head -n1'

Instead use:

   trap '' PIPE && sh -c 'env --default-signal=PIPE seq inf | head -n1'

Similarly, the following will prevent CTRL-C from terminating the
program:

   env --ignore-signal=INT seq inf > /dev/null

See https://bugs.gnu.org/34488#8

* NEWS: Mention new options.
* doc/coreutils.texi (env invocation): Document new options.
* man/env.x: Add example of --default-signal=SIG usage.
(SEE ALSO): Mention sigprocmask.
* src/env.c (signals): New global variable.
(longopts): Add new options.
(usage): Print new options.
(parse_signal_params): Parse comma-separated list of signals, store in
signals variable.
(reset_signal_handlers): Set each signal to SIG_DFL/SIG_IGN.
(parse_block_signal_params): Parse command-line options.
(set_signal_proc_mask): Call sigprocmask to block/unblock signals.
(main): Process new options.
* src/local.mk (src_env_SOURCES): Add operand2sig.c.
* tests/misc/env-signal-handler.sh: New test.
* tests/local.mk (all_tests): Add new test.
This commit is contained in:
Assaf Gordon
2019-02-15 12:31:48 -07:00
committed by Pádraig Brady
parent 186896d65f
commit 95adadd9a4
7 changed files with 457 additions and 0 deletions

3
NEWS
View File

@@ -84,6 +84,9 @@ GNU coreutils NEWS -*- outline -*-
test now supports the '-N FILE' unary operator (like e.g. bash) to check
whether FILE exists and has been modified since it was last read.
env now supports '--default-signal[=SIG]', '--ignore-signal[=SIG]', and
'--block-signal[=SIG], to setup signal handling before executing a program.
** New commands
basenc is added to complement existing base64,base32 commands,

View File

@@ -17246,6 +17246,64 @@ chroot /chroot env --chdir=/srv true
env --chdir=/build FOO=bar timeout 5 true
@end example
@item --default-signal[=@var{sig}]
Unblock and reset signal @var{sig} to its default signal handler.
Without @var{sig} all known signals are unblocked and reset to their defaults.
Multiple signals can be comma-separated. The following command runs
@command{seq} with SIGINT and SIGPIPE set to their default
(which is to terminate the program):
@example
env --default-signal=PIPE,INT seq 1000 | head -n1
@end example
In the following example, we see how this is not
possible to do with traditional shells.
Here the first trap command sets SIGPIPE to ignore.
The second trap command ostensibly sets it back to its default,
but POSIX mandates that the shell must not change inherited
state of the signal - so it is a no-op.
@example
trap '' PIPE && sh -c 'trap - PIPE ; seq inf | head -n1'
@end example
Using @option{--default-signal=PIPE} we can
ensure the signal handling is set to its default behavior:
@example
trap '' PIPE && sh -c 'env --default-signal=PIPE seq inf | head -n1'
@end example
@item --ignore-signal[=@var{sig}]
Ignore signal @var{sig} when running a program. Without @var{sig} all
known signals are set to ignore. Multiple signals can be
comma-separated. The following command runs @command{seq} with SIGINT set
to be ignored - pressing @kbd{Ctrl-C} will not terminate it:
@example
env --ignore-signal=INT seq inf > /dev/null
@end example
@samp{SIGCHLD} is special, in that @option{--ignore-signal=CHLD} might have
no effect (POSIX says it's unspecified).
Most operating systems do not allow ignoring @samp{SIGKILL}, @samp{SIGSTOP}
(and possibly other signals). Attempting to ignore these signals will fail.
Multiple (and contradictory) @option{--default-signal=SIG} and
@option{--ignore-signal=SIG} options are processed left-to-right,
with the latter taking precedence. In the following example, @samp{SIGPIPE} is
set to default while @samp{SIGINT} is ignored:
@example
env --default-signal=INT,PIPE --ignore-signal=INT
@end example
@item --block-signal[=@var{sig}]
Block signal(s) @var{sig} from being delivered.
@item -v
@itemx --debug
@opindex -v

View File

@@ -37,3 +37,31 @@ parameter the script will likely fail with:
.RE
.PP
See the full documentation for more details.
.PP
.SS "\-\-default-signal[=SIG]" usage
This option allows setting a signal handler to its default
action, which is not possible using the traditional shell
trap command. The following example ensures that seq
will be terminated by SIGPIPE no matter how this signal
is being handled in the process invoking the command.
.PP
.RS
.nf
sh \-c 'env \-\-default-signal=PIPE seq inf | head \-n1'
.fi
.RE
.PP
[NOTES]
POSIX's exec(2) pages says:
.RS
"many existing applications wrongly assume that they start with certain
signals set to the default action and/or unblocked.... Therefore, it is best
not to block or ignore signals across execs without explicit reason to do so,
and especially not to block signals across execs of arbitrary (not closely
cooperating) programs."
.RE
[SEE ALSO]
sigaction(2), sigprocmask(2), signal(7)

235
src/env.c
View File

@@ -21,12 +21,15 @@
#include <sys/types.h>
#include <getopt.h>
#include <c-ctype.h>
#include <signal.h>
#include <assert.h>
#include "system.h"
#include "die.h"
#include "error.h"
#include "operand2sig.h"
#include "quote.h"
#include "sig2str.h"
/* The official name of this program (e.g., no 'g' prefix). */
#define PROGRAM_NAME "env"
@@ -48,14 +51,45 @@ static bool dev_debug;
static char *varname;
static size_t vnlen;
/* Possible actions on each signal. */
enum SIGNAL_MODE {
UNCHANGED = 0,
DEFAULT, /* Set to default handler (SIG_DFL). */
DEFAULT_NOERR, /* ditto, but ignore sigaction(2) errors. */
IGNORE, /* Set to ignore (SIG_IGN). */
IGNORE_NOERR /* ditto, but ignore sigaction(2) errors. */
};
static enum SIGNAL_MODE signals[SIGNUM_BOUND + 1];
/* Set of signals to block. */
static sigset_t block_signals;
/* Set of signals to unblock. */
static sigset_t unblock_signals;
/* Whether signal mask adjustment requested. */
static bool sig_mask_changed;
static char const shortopts[] = "+C:iS:u:v0 \t";
/* For long options that have no equivalent short option, use a
non-character as a pseudo short option, starting with CHAR_MAX + 1. */
enum
{
DEFAULT_SIGNAL_OPTION = CHAR_MAX + 1,
IGNORE_SIGNAL_OPTION,
BLOCK_SIGNAL_OPTION,
};
static struct option const longopts[] =
{
{"ignore-environment", no_argument, NULL, 'i'},
{"null", no_argument, NULL, '0'},
{"unset", required_argument, NULL, 'u'},
{"chdir", required_argument, NULL, 'C'},
{"default-signal", optional_argument, NULL, DEFAULT_SIGNAL_OPTION},
{"ignore-signal", optional_argument, NULL, IGNORE_SIGNAL_OPTION},
{"block-signal", optional_argument, NULL, BLOCK_SIGNAL_OPTION},
{"debug", no_argument, NULL, 'v'},
{"split-string", required_argument, NULL, 'S'},
{GETOPT_HELP_OPTION_DECL},
@@ -90,6 +124,17 @@ Set each NAME to VALUE in the environment and run COMMAND.\n\
fputs (_("\
-S, --split-string=S process and split S into separate arguments;\n\
used to pass multiple arguments on shebang lines\n\
"), stdout);
fputs (_("\
--block-signal[=SIG] block delivery of SIG signal(s) to COMMAND\n\
"), stdout);
fputs (_("\
--default-signal[=SIG] reset handling of SIG signal(s) to the default\n\
"), stdout);
fputs (_("\
--ignore-signal[=SIG] set handling of SIG signals(s) to do nothing\n\
"), stdout);
fputs (_("\
-v, --debug print verbose information for each processing step\n\
"), stdout);
fputs (HELP_OPTION_DESCRIPTION, stdout);
@@ -97,6 +142,12 @@ Set each NAME to VALUE in the environment and run COMMAND.\n\
fputs (_("\
\n\
A mere - implies -i. If no COMMAND, print the resulting environment.\n\
"), stdout);
fputs (_("\
\n\
SIG may be a signal name like 'PIPE', or a signal number like '13'.\n\
Without SIG, all known signals are included. Multiple signals can be\n\
comma-separated.\n\
"), stdout);
emit_ancillary_info (PROGRAM_NAME);
}
@@ -525,6 +576,176 @@ parse_split_string (const char* str, int /*out*/ *orig_optind,
*orig_optind = 0; /* tell getopt to restart from first argument */
}
static void
parse_signal_action_params (const char* optarg, bool set_default)
{
char signame[SIG2STR_MAX];
char *opt_sig;
char *optarg_writable;
if (! optarg)
{
/* without an argument, reset all signals.
Some signals cannot be set to ignore or default (e.g., SIGKILL,
SIGSTOP on most OSes, and SIGCONT on AIX.) - so ignore errors. */
for (int i = 1 ; i <= SIGNUM_BOUND; i++)
if (sig2str (i, signame) == 0)
signals[i] = set_default ? DEFAULT_NOERR : IGNORE_NOERR;
return;
}
optarg_writable = xstrdup (optarg);
opt_sig = strtok (optarg_writable, ",");
while (opt_sig)
{
int signum = operand2sig (opt_sig, signame);
/* operand2sig accepts signal 0 (EXIT) - but we reject it. */
if (signum == 0)
error (0, 0, _("%s: invalid signal"), quote (opt_sig));
if (signum <= 0)
usage (exit_failure);
signals[signum] = set_default ? DEFAULT : IGNORE;
opt_sig = strtok (NULL, ",");
}
free (optarg_writable);
}
static void
reset_signal_handlers (void)
{
for (int i = 1; i <= SIGNUM_BOUND; i++)
{
struct sigaction act;
if (signals[i] == UNCHANGED)
continue;
bool ignore_errors = (signals[i] == DEFAULT_NOERR
|| signals[i] == IGNORE_NOERR);
bool set_to_default = (signals[i] == DEFAULT
|| signals[i] == DEFAULT_NOERR);
int sig_err = sigaction (i, NULL, &act);
if (sig_err && !ignore_errors)
die (EXIT_CANCELED, errno,
_("failed to get signal action for signal %d"), i);
if (! sig_err)
{
act.sa_handler = set_to_default ? SIG_DFL : SIG_IGN;
if ((sig_err = sigaction (i, &act, NULL)) && !ignore_errors)
die (EXIT_CANCELED, errno,
_("failed to set signal action for signal %d"), i);
}
if (dev_debug)
{
char signame[SIG2STR_MAX];
sig2str (i, signame);
devmsg ("Reset signal %s (%d) to %s%s\n",
signame, i,
set_to_default ? "DEFAULT" : "IGNORE",
sig_err ? " (failure ignored)" : "");
}
}
}
static void
parse_block_signal_params (const char* optarg, bool block)
{
char signame[SIG2STR_MAX];
char *opt_sig;
char *optarg_writable;
if (! optarg)
{
/* without an argument, reset all signals. */
sigfillset (block ? &block_signals : &unblock_signals);
sigemptyset (block ? &unblock_signals : &block_signals);
}
else if (! sig_mask_changed)
{
/* Initialize the sets. */
sigemptyset (&block_signals);
sigemptyset (&unblock_signals);
}
sig_mask_changed = true;
if (! optarg)
return;
optarg_writable = xstrdup (optarg);
opt_sig = strtok (optarg_writable, ",");
while (opt_sig)
{
int signum = operand2sig (opt_sig, signame);
/* operand2sig accepts signal 0 (EXIT) - but we reject it. */
if (signum == 0)
error (0, 0, _("%s: invalid signal"), quote (opt_sig));
if (signum <= 0)
usage (exit_failure);
sigaddset (block ? &block_signals : &unblock_signals, signum);
sigdelset (block ? &unblock_signals : &block_signals, signum);
opt_sig = strtok (NULL, ",");
}
free (optarg_writable);
}
static void
set_signal_proc_mask (void)
{
/* Get the existing signal mask */
sigset_t set;
const char *debug_act;
sigemptyset (&set);
if (sigprocmask (0, NULL, &set))
die (EXIT_CANCELED, errno, _("failed to get signal process mask"));
for (int i = 1; i <= SIGNUM_BOUND; i++)
{
if (sigismember (&block_signals, i))
{
sigaddset (&set, i);
debug_act = "BLOCK";
}
else if (sigismember (&unblock_signals, i))
{
sigdelset (&set, i);
debug_act = "UNBLOCK";
}
else
{
debug_act = NULL;
}
if (dev_debug && debug_act)
{
char signame[SIG2STR_MAX];
sig2str (i, signame);
devmsg ("signal %s (%d) mask set to %s\n",
signame, i, debug_act);
}
}
if (sigprocmask (SIG_SETMASK, &set, NULL))
die (EXIT_CANCELED, errno, _("failed to set signal process mask"));
}
int
main (int argc, char **argv)
{
@@ -558,6 +779,16 @@ main (int argc, char **argv)
case '0':
opt_nul_terminate_output = true;
break;
case DEFAULT_SIGNAL_OPTION:
parse_signal_action_params (optarg, true);
parse_block_signal_params (optarg, false);
break;
case IGNORE_SIGNAL_OPTION:
parse_signal_action_params (optarg, false);
break;
case BLOCK_SIGNAL_OPTION:
parse_block_signal_params (optarg, true);
break;
case 'C':
newdir = optarg;
break;
@@ -633,6 +864,10 @@ main (int argc, char **argv)
return EXIT_SUCCESS;
}
reset_signal_handlers ();
if (sig_mask_changed)
set_signal_proc_mask ();
if (newdir)
{
devmsg ("chdir: %s\n", quoteaf (newdir));

View File

@@ -356,6 +356,7 @@ src_coreutils_SOURCES = src/coreutils.c
src_cp_SOURCES = src/cp.c $(copy_sources) $(selinux_sources)
src_dir_SOURCES = src/ls.c src/ls-dir.c
src_env_SOURCES = src/env.c src/operand2sig.c
src_vdir_SOURCES = src/ls.c src/ls-vdir.c
src_id_SOURCES = src/id.c src/group-list.c
src_groups_SOURCES = src/groups.c src/group-list.c

View File

@@ -240,6 +240,7 @@ all_tests = \
tests/fmt/goal-option.sh \
tests/misc/echo.sh \
tests/misc/env.sh \
tests/misc/env-signal-handler.sh \
tests/misc/ptx.pl \
tests/misc/test.pl \
tests/misc/seq.pl \

131
tests/misc/env-signal-handler.sh Executable file
View File

@@ -0,0 +1,131 @@
#!/bin/sh
# Test env --default-signal=PIPE feature.
# Copyright (C) 2019 Free Software Foundation, Inc.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
print_ver_ env seq test timeout
trap_sigpipe_or_skip_
# Paraphrasing http://bugs.gnu.org/34488#8:
# POSIX requires that sh started with an inherited ignored SIGPIPE must
# silently ignore all attempts from within the shell to restore SIGPIPE
# handling to child processes of the shell:
#
# $ (trap '' PIPE; bash -c 'trap - PIPE; seq inf | head -n1')
# 1
# seq: write error: Broken pipe
#
# With 'env --default-signal=PIPE', the signal handler can be reset to its
# default.
# Baseline Test - default signal handler
# --------------------------------------
# Ensure this results in a "broken pipe" error (the first 'trap'
# sets SIGPIPE to ignore, and the second 'trap' becomes a no-op instead
# of resetting SIGPIPE to its default). Upon a SIGPIPE 'seq' will not be
# terminated, instead its write(2) call will return an error.
(trap '' PIPE; $SHELL -c 'trap - PIPE; seq 999999 2>err1t | head -n1 > out1')
# The exact broken pipe message depends on the operating system, just ensure
# there was a 'write error' message in stderr:
sed 's/^\(seq: write error:\) .*/\1/' err1t > err1 || framework_failure_
printf "1\n" > exp-out || framework_failure_
printf "seq: write error:\n" > exp-err1 || framework_failure_
compare exp-out out1 || framework_failure_
compare exp-err1 err1 || framework_failure_
# env test - default signal handler
# ---------------------------------
# With env resetting the signal handler to its defaults, there should be no
# error message (because the default SIGPIPE action is to terminate the
# 'seq' program):
(trap '' PIPE;
env --default-signal=PIPE \
$SHELL -c 'trap - PIPE; seq 999999 2>err2 | head -n1 > out2')
compare exp-out out2 || fail=1
compare /dev/null err2 || fail=1
# env test - default signal handler (3)
# -------------------------------------
# Repeat the previous test, using --default-signal with no signal names,
# i.e., all signals.
(trap '' PIPE;
env --default-signal \
$SHELL -c 'trap - PIPE; seq 999999 2>err4 | head -n1 > out4')
compare exp-out out4 || fail=1
compare /dev/null err4 || fail=1
# env test - block signal handler
env --block-signal true || fail=1
# Baseline test - ignore signal handler
# -------------------------------------
# Kill 'sleep' after 1 second with SIGINT - it should terminate (as SIGINT's
# default action is to terminate a program).
# (The first 'env' is just to ensure timeout is not the shell's built-in.)
env timeout --verbose --kill-after=.1 --signal=INT .1 \
sleep 10 > /dev/null 2>err5
printf "timeout: sending signal INT to command 'sleep'\n" > exp-err5 \
|| framework_failure_
compare exp-err5 err5 || fail=1
# env test - ignore signal handler
# --------------------------------
# Use env to silence (ignore) SIGINT - "seq" should continue running
# after timeout sends SIGINT, and be killed after 1 second using SIGKILL.
cat>exp-err6 <<EOF
timeout: sending signal INT to command 'env'
timeout: sending signal KILL to command 'env'
EOF
env timeout --verbose --kill-after=.1 --signal=INT .1 \
env --ignore-signal=INT \
sleep 10 > /dev/null 2>err6t
# check only the first two lines from stderr, which are printed by timeout.
# (operating systems might add more messages, like "killed").
sed -n '1,2p' err6t > err6 || framework_failure_
compare exp-err6 err6 || fail=1
# env test - ignore signal handler (2)
# ------------------------------------
# Repeat the previous test with "--ignore-signals" and no signal names,
# i.e., all signals.
env timeout --verbose --kill-after=.1 --signal=INT .1 \
env --ignore-signal \
sleep 10 > /dev/null 2>err7t
# check only the first two lines from stderr, which are printed by timeout.
# (operating systems might add more messages, like "killed").
sed -n '1,2p' err7t > err7 || framework_failure_
compare exp-err6 err7 || fail=1
Exit $fail