Subject: bug#76282: Signal handling (like SIGINT) and guix shell --container: PID 1

From: [email-masked]
Date: Fri, 14 Feb 2025 07:30:09 GMT

# Bug and reproducer
There is a discrepancy between (expected) signal handling of programs when running them directly,
vs. using `guix shell --container -- program`.

An example program can be used to illustrate:
```
; test.scm
; Test guix shell to catch ctrl-C signal

(while #t
       (display "In infinite loop...\n")
       (sleep 1))
```

It uses guile, since bash does some extra funky signal-related things, which not every program does.
My usecase is `typst watch`, a program which only exits on ctrl-C, else it loops.

1) Run as `guile test.scm` and use ctrl-C
   It terminates as expected.
   Same when using `guix shell --container guile` and then `guile test.scm` inside the container shell.
2) Run `guix shell --container guile -- guile test.scm` and use ctrl-C
   It doesn't terminate as expected. Only method is to get the PID and use SIGKILL (`kill -KILL <pid>`)

# Reason (as far as I know):
`guix shell --container -- program` starts the program with PID 1 in it's dedicated namespace.
However, PID 1 is handled specially by the kernel when it comes to signal handlers.
Only explicitly registered handlers are used and the default handlers
are not executed for PID 1 (except SIGKILL & SIGSTOP).

You can use `cat /proc/<PID in parent namespace>/status | grep ^NStgid`
to verify the PID seen by the process (rightmost number).

# Sources:
- https://lwn.net/Articles/532748/ ("Signals and the init process")
- In docker: https://ddanilov.me/how-signals-are-handled-in-a-docker-container
- Docker '--init' flag: https://docs.docker.com/reference/cli/docker/container/run/#init
- bash doing funky things: https://colinxy.github.io/computer-science/2017/01/27/bash-handles-signals.html
- Check namespace PID: https://unix.stackexchange.com/questions/625520/is-it-possible-to-check-the-process-id-of-child-in-different-pid-namespace

# Possible fixes:
- Document the behaviour (and nothing more?)
  This is the minimum, and I'll see if I can send a patch.
  This lengthy bugreport is due to undocumented & unexpected behaviour, leading down a rabbit hole ;)
- Add an `--init` argument, like in docker?
  This adds some minimal init process to forward signals and reap processes...
  Not sure how minimal the shepherd is?
- Do this by default, and add a --no-init argument?
- ...

# Workaround:
Add "tini" or "catatonit" as a minimal PID 1 process.
- guix shell --container guile tini -- tini guile test.scm
- guix shell --container guile catatonit -- catatonit guile test.scm

Best regards,
Ingar