This patch addresses the POSIX compliance aspect explained by Geoff Clare.
A test is included.
From b560e3f911c1eb0095a2c07a461bc28f7335fbd1 Mon Sep 17 00:00:00 2001
From: James Youngman <ja...@youngman.org>
Date: Sun, 19 May 2024 11:05:38 +0100
Subject: [PATCH] [xargs] POSIX requires SIGUSR1/2 to be fatal if not blocked.
To: findutils-patc...@gnu.org

If the user uses -P we're not using only POSIX options, so in that
case it is OK to handle SIGUSR1/2.  Otherwise (without -P) don't
handle these signals, meaning that they could cause xargs to
terminate.

* xargs/xargs.c: set signal handlers for SIGUSR1 and SIGUSR2 only if
the -P option was used.  Warn if SIGUSR1 or SIGUSR2 is not defined.
* tests/xargs/test-sigusr: a new test for this.
* tests/local.mk: build and run the new test.
* NEWS: mention these changes.
---
 NEWS                      |   6 +
 tests/local.mk            |   3 +
 tests/xargs/test-sigusr.c | 352 ++++++++++++++++++++++++++++++++++++++
 xargs/xargs.c             |  42 +++--
 4 files changed, 387 insertions(+), 16 deletions(-)
 create mode 100644 tests/xargs/test-sigusr.c

diff --git a/NEWS b/NEWS
index 762297d1..1ef56c5a 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,12 @@ GNU findutils NEWS - User visible changes.      -*- outline -*- (allout)
 * Noteworthy changes in release ?.? (????-??-??) [?]
 
 ** Bug Fixes
+  If the -P option to xargs is not used, xargs will not change the way
+  in which the SIGUSR1 and SIGUSR2 signals are handled.  This means that
+  they will cause the program to terminate if the signals were not
+  ignored in the process which started xargs.  If you want to depend
+  on xargs not being killed by these signals but don't want
+  parallel execution, use "-P 1".
 
   xargs -P now waits for all its child processes to compete before
   exiting, even if one of them exits with status 255. [#64451]
diff --git a/tests/local.mk b/tests/local.mk
index 9357ccb9..7a7e0aa8 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -82,6 +82,8 @@ EXTRA_DIST += \
 all_root_tests = \
   tests/find/type_list.sh
 
+noinst_PROGRAMS = \
+	tests/xargs/test-sigusr
 
 ALL_RECURSIVE_TARGETS += check-root
 .PHONY: check-root
@@ -108,6 +110,7 @@ all_tests = \
   tests/misc/help-version.sh \
   tests/find/depth-unreadable-dir.sh \
   tests/find/inode-zero.sh \
+  tests/xargs/test-sigusr$(EXEEXT) \
   tests/find/many-dir-entries-vs-OOM.sh \
   tests/find/name-lbracket-literal.sh \
   tests/find/name-slash.sh \
diff --git a/tests/xargs/test-sigusr.c b/tests/xargs/test-sigusr.c
new file mode 100644
index 00000000..616e06bd
--- /dev/null
+++ b/tests/xargs/test-sigusr.c
@@ -0,0 +1,352 @@
+/* test-sigusr -- Verify that xargs SIGUSR1/SIGUSR2 handling is correct.
+   Copyright (C) 2024 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/>.
+
+   Written by James Youngman <j...@gnu.org>
+*/
+
+/* System headers */
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+/* Gnulib modules */
+#include "error.h"
+
+
+enum { FLAGFILE_MAX = 128 };
+static char flagfile_name[FLAGFILE_MAX];
+static const char* tmpdir = NULL;
+
+
+static void
+remove_temporary_files(void)
+{
+  if (tmpdir)
+    {
+      unlink(flagfile_name);
+      rmdir(tmpdir);
+    }
+  tmpdir = NULL;
+}
+
+static void
+wait_for_flagfile(void)
+{
+  struct stat st;
+  for (;;)
+    {
+      if (0 == stat(flagfile_name, &st))
+	{
+	  return;
+	}
+      sleep(1);
+    }
+}
+
+static void
+delete_flagfile(void)
+{
+  if (0 != unlink(flagfile_name))
+    {
+      if (ENOENT != errno)
+	{
+	  error (2, errno, "Failed to unlink %s", flagfile_name);
+	}
+    }
+}
+
+
+static void
+ignore_signal(int signum)
+{
+  struct sigaction sa;
+  sa.sa_handler = SIG_IGN;
+  sa.sa_flags = 0;
+  sigemptyset(&sa.sa_mask);
+  if (0 != sigaction(signum, &sa, (struct sigaction*)NULL))
+    {
+      error(2, errno, "sigaction failed");
+    }
+}
+
+
+static void handler(int sig)
+{
+  /* Actually does nothing. */
+}
+
+static void
+catch_signal(int signum)
+{
+  struct sigaction sa;
+  sa.sa_handler = handler;
+  sa.sa_flags = 0;
+  if (0 != sigaction(signum, &sa, (struct sigaction*)NULL))
+    {
+      error(2, errno, "sigaction failed");
+    }
+}
+
+static void
+run_child(char * const argv[], int errno_fd)
+{
+  int fd;
+  close(0);
+
+  fd = open("/dev/null", O_RDONLY);
+  if (fd < 0)
+    {
+      error(2, errno, "failed to open /dev/null");
+    }
+  else if (fd != 0)
+    {
+      error(2, errno, "failed to close stdin");
+    }
+
+  execvp("xargs", argv);
+
+  /* Exec failed, tell the parent why. */
+  if (write(errno_fd, &errno, sizeof errno) != sizeof errno)
+    {
+      error (2, errno, "child failed to write errno value though pipe");
+    }
+
+  error(2, errno, "unable to exec xargs in child process");
+
+  /*NOTREACHED*/
+}
+
+static void
+set_close_on_exec(int fd)
+{
+  if (0 != fcntl(fd, F_SETFD, FD_CLOEXEC))
+    {
+      error(2, errno, "F_SETFD failed");
+    }
+}
+
+
+
+struct status
+{
+  int retval;
+  int fatalsig;
+};
+
+/* Run xargs and return its exit status */
+static struct status
+run_xargs(const char *option, const char *optarg, int send_signal)
+{
+  int i = 0;
+  enum { ARGV_MAX = 9 };
+  char *argv[ARGV_MAX];
+  pid_t child, dead;
+  int wstatus;
+  struct status result = {0};
+  int pipefd[2];
+  int child_errno;
+
+  argv[i++] = (char*)"xargs";
+  if (option)
+    {
+      argv[i++] = (char*)option;
+    }
+  if (optarg)
+    {
+      argv[i++] = (char*)optarg;
+    }
+  argv[i++] = (char*)"sh";
+  argv[i++] = (char*)"-c";
+  argv[i++] = (char*)"touch \"$1\" && sleep 4";
+  argv[i++] = (char*)"fnord";
+  argv[i++] = flagfile_name;
+  argv[i++] = NULL;
+  assert(i <= ARGV_MAX);
+
+  /* Create a pipe so that we can detect an exec call. */
+  /* read end is pipefd[0], write end is pipefd[1]. */
+  if (0 != pipe(pipefd))
+    {
+      error(2, errno, "pipe");
+    }
+  set_close_on_exec(pipefd[0]);
+  set_close_on_exec(pipefd[1]);
+  delete_flagfile();
+
+  while ((child=fork()) < 0 && errno == EAGAIN)
+    {
+      perror("unable to fork yet, will try again");
+      sleep(1);
+    }
+
+  switch (child)
+    {
+    case -1:
+      error(EXIT_FAILURE, errno, "cannot fork");
+      break;
+    case 0:			/* child */
+      close(pipefd[0]);		/* close read end */
+      /* The child will close the write end of the pipe on successful exec. */
+      run_child(argv, pipefd[1]);
+      abort();
+      break;
+    default:			/* parent */
+      close(pipefd[1]);		/* close write end */
+      if (read(pipefd[0], &child_errno, sizeof child_errno) < sizeof child_errno)
+	{
+	  /* The exec succeded in the child, and its write end of the pipe was closed. */
+	}
+      else
+	{
+	  /* exec failed in the child and its errno value is now in child_errno. */
+	  error(2, child_errno, "execvp failed in the child process");
+	}
+      break;
+    }
+
+  /* We now know that the exec succeeded and xargs is running.  We
+   * should give it a short time to set up its signal handlers.
+   */
+  fputs("exec succeeded in the child...", stdout);
+  fflush(stdout);
+  /* Wait for the child xargs to launch its command (so we know it
+     will have set any signal handlers it is going to set). */
+  wait_for_flagfile();
+
+  if (send_signal)
+    {
+      if (0 != kill (child, send_signal))
+	{
+	  error(2, errno, "kill failed");
+	}
+    }
+
+  wstatus = 0;
+  dead = waitpid(child, &wstatus, 0);
+
+  if (dead < 0)
+    {
+      error(2, errno, "waitpid failed");
+    }
+  else if (dead == 0)
+    {
+      error(2, 0, "test unexpectedly has more than one child");
+    }
+  else
+    {
+      if (WIFEXITED(wstatus))
+	{
+	  result.retval = WEXITSTATUS(wstatus);
+	}
+      else if (WIFSIGNALED(wstatus))
+	{
+	  result.retval = -1;
+	  result.fatalsig = WTERMSIG(wstatus);
+	}
+      else if (WIFSTOPPED(wstatus))
+	{
+	  error(2, 0, "child was unexpectedly stopped");
+	}
+      return result;
+    }
+  /*NOTREACHED*/
+  abort();
+}
+
+
+static void
+verify_signal_ignored(int signum)
+{
+  struct status status;
+  printf("verifying that xargs can ignore signal %d...", signum);
+  fflush(stdout);
+
+  ignore_signal(signum);
+  status = run_xargs(NULL, NULL, signum);
+  if (status.fatalsig)
+    {
+      fprintf(stderr, "xargs should not have exited fatally on receipt of signal %d\n", signum);
+      exit(1);
+    }
+  if (status.retval)
+    {
+      fprintf(stderr, "xargs should not have returned a nonzero exit status %d\n", status.retval);
+      exit(1);
+    }
+  fputs("OK\n", stdout);
+}
+
+static void
+verify_signal_is_fatal(int signum)
+{
+  struct status status;
+  printf("verifying that signal %d will kill xargs (without -P) if it is not blocked when xargs starts...", signum);
+  fflush(stdout);
+  catch_signal(signum);
+  status = run_xargs(NULL, NULL, signum);
+  if (!status.fatalsig)
+    {
+      fprintf(stderr, "xargs should have exited fatally on receipt of signal %d\n", signum);
+      exit(1);
+    }
+  fputs("OK\n", stdout);
+}
+
+static void
+verify_signal_is_nonfatal_with_p(int signum)
+{
+  struct status status;
+  printf("verifying that signal %d will not kill xargs -P if it is not blocked when xargs starts...", signum);
+  fflush(stdout);
+  catch_signal(signum);
+  status = run_xargs("-P", "5", signum);
+  if (status.fatalsig)
+    {
+      fprintf(stderr, "xargs -P should not have exited fatally on receipt of signal %d\n", signum);
+      exit(1);
+    }
+  fputs("OK\n", stdout);
+}
+
+int
+main(int argc, char *argv[])
+{
+  char tmpdir_tmpl[] = "/tmp/test-sigusr.XXXXXX";
+  tmpdir = mkdtemp(tmpdir_tmpl);
+  if (NULL == tmpdir)
+    {
+      error(2, errno, "failed to create temporary directory");
+    }
+  snprintf(flagfile_name, FLAGFILE_MAX, "%s/flagfile", tmpdir);
+  atexit(remove_temporary_files);
+
+  verify_signal_ignored(SIGUSR1);
+  verify_signal_ignored(SIGUSR2);
+  fputs("\n", stdout);
+
+  verify_signal_is_fatal(SIGUSR1);
+  verify_signal_is_fatal(SIGUSR2);
+  fputs("\n", stdout);
+  verify_signal_is_nonfatal_with_p(SIGUSR1);
+  verify_signal_is_nonfatal_with_p(SIGUSR2);
+  return 0;
+}
diff --git a/xargs/xargs.c b/xargs/xargs.c
index 00a5e31c..56425d55 100644
--- a/xargs/xargs.c
+++ b/xargs/xargs.c
@@ -408,7 +408,7 @@ main (int argc, char **argv)
   void (*act_on_init_result)(void) = noop;
   enum BC_INIT_STATUS bcstatus;
   enum { XARGS_POSIX_HEADROOM = 2048u };
-  struct sigaction sigact;
+  bool catch_usr_signals = false;
 
   /* We #define __STDC_LIMIT_MACROS above for its side effect on
    * <limits.h>, but we use it here to avoid getting what would
@@ -671,6 +671,12 @@ main (int argc, char **argv)
 	case 'P':
 	  /* Allow only up to MAX_PROC_MAX child processes. */
 	  proc_max = parse_num (optarg, 'P', 0L, MAX_PROC_MAX, 1);
+#if !(defined(SIGUSR1) && defined(SIGUSR2))
+	  error (0, 0, _("SIGUSR1 and SIGUSR2 are not both defined, so the -P option does nothing."));
+	  proc_max = 1;
+#else
+	  catch_usr_signals = true;
+#endif
 	  break;
 
         case 'a':
@@ -723,25 +729,29 @@ main (int argc, char **argv)
   act_on_init_result ();
   assert (BC_INIT_OK == bcstatus);
 
+  if (catch_usr_signals)
+    {
 #ifdef SIGUSR1
 # ifdef SIGUSR2
-  /* Accept signals to increase or decrease the number of running
-     child processes.  Do this as early as possible after setting
-     proc_max.  */
-  sigact.sa_handler = increment_proc_max;
-  sigemptyset(&sigact.sa_mask);
-  sigact.sa_flags = 0;
-  if (0 != sigaction (SIGUSR1, &sigact, (struct sigaction *)NULL))
-	  error (0, errno, _("Cannot set SIGUSR1 signal handler"));
-
-  sigact.sa_handler = decrement_proc_max;
-  sigemptyset(&sigact.sa_mask);
-  sigact.sa_flags = 0;
-  if (0 != sigaction (SIGUSR2, &sigact, (struct sigaction *)NULL))
-	  error (0, errno, _("Cannot set SIGUSR2 signal handler"));
+      struct sigaction sigact;
+
+      /* Accept signals to increase or decrease the number of running
+	 child processes.  Do this as early as possible after setting
+	 proc_max.  */
+      sigact.sa_handler = increment_proc_max;
+      sigemptyset(&sigact.sa_mask);
+      sigact.sa_flags = 0;
+      if (0 != sigaction (SIGUSR1, &sigact, (struct sigaction *)NULL))
+	error (0, errno, _("Cannot set SIGUSR1 signal handler"));
+
+      sigact.sa_handler = decrement_proc_max;
+      sigemptyset(&sigact.sa_mask);
+      sigact.sa_flags = 0;
+      if (0 != sigaction (SIGUSR2, &sigact, (struct sigaction *)NULL))
+	error (0, errno, _("Cannot set SIGUSR2 signal handler"));
 # endif /* SIGUSR2 */
 #endif /* SIGUSR1 */
-
+    }
 
   if (0 == strcmp (input_file, "-"))
     {
-- 
2.39.2

Reply via email to