11

When bash is invoked as pid 1 directly through the kernel option init=/bin/bash --login, it will issue something like this before prompting:

bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell

And no keyboard-generated signals (e.g ^Z, ^C, ^\) work.

To solve this problem, I wrote a simple program init1.c as following:

/* init1.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main(int argc, char **argv)
{
  char *options[] = {"--login", NULL};
  int tty_fd = -1;

  printf("\n----- Bash Init 1 -----\n\n");

  /* Make bash as session leader. */
  if (setsid() == -1)
    {
      fprintf(stderr, "%s : %d : %s\n", "setsid()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }

  /* Make /dev/tty1 as controlling terminal of Bash. */
  tty_fd = open("/dev/tty1", O_RDWR);
  if (tty_fd == -1)
    {
      fprintf(stderr, "%s : %d : %s\n", "open()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }

  /* Re-connect stdin, stdout, stderr to the controlling terminal. */
  dup2(tty_fd, STDIN_FILENO);
  dup2(tty_fd, STDOUT_FILENO);
  dup2(tty_fd, STDERR_FILENO);
  close(tty_fd);
  
  execv("/bin/bash", options);
}

Compiled it as init1, then invoked it as pid 1 (i.e Bash running as pid 1), the preceding error messages disappear and some signals (e.g ^C, ^\) work, but job control signals (e.g ^Z) still not (unexpected).

So to make job control signals work, I revised the code above as init2.c (just fork()):

/* init2.c */
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main(int argc, char **argv)
{
  char *options[] = {"--login", NULL};
  pid_t pid = -1;
  int tty_fd = -1;
  
  printf("\n----- Bash Init 2 -----\n\n");
  
  pid = fork();
  if (pid < 0)
    {
      fprintf(stderr, "%s : %d : %s\n", "fork()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }
    
  /* Parent. */
  if (pid > 0)
   {
     /* Wait for its child, otherwise all processes would be killed ! */
     while (wait(NULL) > 0)
       ;
     exit(EXIT_SUCCESS);
   }
      
  /* Child. */
  if (setsid() == -1)
    {
      fprintf(stderr, "%s : %d : %s\n", "setsid()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }        
  
  /* Make /dev/tty1 as controlling terminal of Bash. */
  tty_fd = open("/dev/tty1", O_RDWR);
  if (tty_fd == -1)
    {
      fprintf(stderr, "%s : %d : %s\n", "open()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }

  /* Re-connect stdin, stdout, stderr to the controlling terminal. */
  dup2(tty_fd, STDIN_FILENO);
  dup2(tty_fd, STDOUT_FILENO);
  dup2(tty_fd, STDERR_FILENO);
  close(tty_fd);
  
  execv("/bin/bash", options);
}

Compiled it as init2 and invoked as pid 1 (i.e. finally Bash running as arbitrary PID other than 1), and this time, the job control signals work!

But I didn't figure out why the job control signals work in init2 (Bash isn't pid 1) but not init1 (Bash is pid 1), why does foreground job ignore job control signals when Bash is running as PID 1? It seems that there is something special with pid 1.



Update 3/21/2022:

Recently, I found a very simple shell mysh in github which also implements job control, only 949 lines! When I ran it with init1 and init2, this shell also has the same problem! (Thanks to it, I don't have to read the complicated bash source code for figuring out my question. Orz) And the problem lies in waitpid() which doesn't return immediately when SIGTSTP(^Z) reaches. So this issue is not only relative to bash, but also the shells that implement job control. However, I don't understand why does't waitpid() return if SIGTSTP reaches when shell is running as PID 1... 囧

Li-Guangda
  • 341
  • 1
  • 4
  • 14
  • 1
    I'd use `dup2()` to reassign fds 0, 1, and 2 instead of `close()` and `dup()`. RIght now, if `open("/dev/tty1", O_RDWR)` fails your error message is lost. You also missed reopening standard error. – Andrew Henle Mar 12 '22 at 16:10
  • How are you getting bash to run as pid 1? Is it the main process in a docker container? – dbush Mar 12 '22 at 16:17
  • @dbush Not a docker container. I just use kernel parameter `init=`. – Li-Guangda Mar 12 '22 at 16:34
  • @Andrew Henle Thanks for your advice, I have revised my code. :) – Li-Guangda Mar 12 '22 at 17:58
  • 1
    The signals are there, but for PID 1 process, kernel sets the signal handlers for most "terminate by default" signals to be "ignore by default". The process can, of course, override those at its own discretion. – oakad Mar 15 '22 at 01:41
  • **Do not** do this! The `init` process (e.g. `systemd`) has to do many things mount file systems, start networking, etc. And, do a clean system shutdown. `bash` knows nothing about this. To get a shell, boot the system in emergency/singleuser mode. You risk corrupting your system. Do this only in a VM with your method. Or, boot a USB stick – Craig Estey Mar 15 '22 at 01:57
  • @Craig Estey I really do this in VirtualBox. Don't Worry ... :-) – Li-Guangda Mar 15 '22 at 02:06
  • I've done this before in systems shipped to customers but I used `perl` because it's more self contained – Craig Estey Mar 15 '22 at 02:13
  • I learn `GNU\Linux` just for fun, that's all... :-) – Li-Guangda Mar 15 '22 at 02:16
  • To OP: Please see [When should code formatting be used for non-code text?](https://meta.stackoverflow.com/q/254990) on when *not* to use `code` formatting for emphasis. – iBug Mar 21 '22 at 10:49
  • To the OP: did you read `man 2 kill` ? Please do so. – wildplasser Mar 22 '22 at 01:11
  • @wildplasser I have read it just now. But as I known, all keyboard signals are sent to the foreground process group only. In this case, `bash`(as **pid 1**) and its child process are in the different process group, right ? – Li-Guangda Mar 22 '22 at 01:37

1 Answers1

3

In linux, processes are given default signal handlers. A variety of signals (like SIGTERM and SIGINT), have the default behavior of immediately exiting.

For historical and system reasons, pid 1 just does not get these default signal handlers defined, so there's no behavior there. Note that this doesn't stop you from redefining signal handlers yourself.

From the linux kernel man pages

Only signals for which the "init" process has established a signal handler can be sent to the "init" process by other members of the PID namespace. This restriction applies even to privileged processes, and prevents other members of the PID namespace from accidentally killing the "init" process.

Likewise, a process in an ancestor namespace can—subject to the usual permission checks described in kill(2)—send signals to the "init" process of a child PID namespace only if the "init" process has established a handler for that signal. (Within the handler, the siginfo_t si_pid field described in sigaction(2) will be zero.) SIGKILL or SIGSTOP are treated exceptionally: these signals are forcibly delivered when sent from an ancestor PID namespace. Neither of these signals can be caught by the "init" process, and so will result in the usual actions associated with those signals (respectively, terminating and stopping the process).

Carson
  • 2,700
  • 11
  • 24
  • 1
    No. I think this problem isn't relative to the concept of `PID namespaces`. One use of `namespaces` is to implement `containers`. As per [pid_namespaces(7)](https://man7.org/linux/man-pages/man7/pid_namespaces.7.html), the creation of a new `PID namespace` using `clone()` with the `CLONE_NEWPID` flag. I even not use these things to create a process and neither do kernel, let alone a `PID namespace`. – Li-Guangda Mar 15 '22 at 12:49