diff mbox series

[bug#54997,04/12] Add (guix least-authority).

Message ID 20220417210453.27884-4-ludo@gnu.org
State Accepted
Headers show
Series Add "least authority" program wrapper | expand

Checks

Context Check Description
cbaines/comparison success View comparision
cbaines/git branch success View Git branch
cbaines/applying patch success View Laminar job
cbaines/issue success View issue

Commit Message

Ludovic Courtès April 17, 2022, 9:04 p.m. UTC
* guix/least-authority.scm: New file.
* Makefile.am (MODULES): Add it.
* gnu/build/shepherd.scm (default-mounts): Make public.
---
 Makefile.am              |   1 +
 gnu/build/shepherd.scm   |   3 +-
 guix/least-authority.scm | 131 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 134 insertions(+), 1 deletion(-)
 create mode 100644 guix/least-authority.scm

Comments

M April 18, 2022, 9:15 a.m. UTC | #1
Ludovic Courtès schreef op zo 17-04-2022 om 23:04 [+0200]:
> +(define %precious-variables
> +  ;; Environment variables preserved by the wrapper by default.
> +  '("HOME" "USER" "LOGNAME" "DISPLAY" "XAUTHORITY" "TERM" "TZ" "PAGER"))

This appears to be duplicated from (guix profiles), so there seems to
be a risk here of them going out-of-sync; would it make send for
(guix profiles) to read (guix least-authority) here?

Greetings,
Maxime.
M April 18, 2022, 9:18 a.m. UTC | #2
Ludovic Courtès schreef op zo 17-04-2022 om 23:04 [+0200]:
> +(define* (least-authority-wrapper program
> +                                  #:key (name "pola-wrapper")
> +                                  (guest-uid 1000)
> +                                  (guest-gid 1000)
> +                                  (mappings '())
> +                                  (namespaces %namespaces)
> +                                  (directory "/")
> +                                  (preserved-environment-variables
> +                                   %precious-variables))

Could there be an option to define environment variables?  E.g. set
GUIX_LOCPATH for Guile packages that need locale data to read non-ASCII
file names.  As is, it seems like an environment-setting wrapper has to
be inserted inside the pola wrapper to do this.

Greetings,
Maxime.
Ludovic Courtès April 19, 2022, 10:04 p.m. UTC | #3
Maxime Devos <maximedevos@telenet.be> skribis:

> Ludovic Courtès schreef op zo 17-04-2022 om 23:04 [+0200]:
>> +(define %precious-variables
>> +  ;; Environment variables preserved by the wrapper by default.
>> +  '("HOME" "USER" "LOGNAME" "DISPLAY" "XAUTHORITY" "TERM" "TZ" "PAGER"))
>
> This appears to be duplicated from (guix profiles), so there seems to
> be a risk here of them going out-of-sync; would it make send for
> (guix profiles) to read (guix least-authority) here?

It is duplicated, but OTOH the use case is different.  So I think it’s
OK to have a different definition here.

(Perhaps eventually we’ll do something fancier, like preserving
XAUTHORITY and DISPLAY if and only if we’re running an X11 program.)

Ludo’.
Ludovic Courtès April 19, 2022, 10:05 p.m. UTC | #4
Maxime Devos <maximedevos@telenet.be> skribis:

> Ludovic Courtès schreef op zo 17-04-2022 om 23:04 [+0200]:
>> +(define* (least-authority-wrapper program
>> +                                  #:key (name "pola-wrapper")
>> +                                  (guest-uid 1000)
>> +                                  (guest-gid 1000)
>> +                                  (mappings '())
>> +                                  (namespaces %namespaces)
>> +                                  (directory "/")
>> +                                  (preserved-environment-variables
>> +                                   %precious-variables))
>
> Could there be an option to define environment variables?  E.g. set
> GUIX_LOCPATH for Guile packages that need locale data to read non-ASCII
> file names.  As is, it seems like an environment-setting wrapper has to
> be inserted inside the pola wrapper to do this.

Yes, good point.  I’m tempted to wait until the first use case comes up
though.  :-)

Thanks for taking a look!
Thiago Jung Bauermann April 22, 2022, 8:10 p.m. UTC | #5
Hello,

Ludovic Courtès <ludo@gnu.org> writes:
> +          (reify-exit-status
> +           (call-with-container mounts
> +             (lambda ()
> +               (chdir #$directory)
> +               (environ variables)
> +               (apply execl #$program #$program (cdr (command-line))))

I'm a bit concerned about running arbitrary commands as PID 1 of process
namespaces. A process running as PID 1 (even in a child namespace) is a
special case and is treated differently by the Linux kernel than any
other process, so it needs to be a program that has been designed to
work in that situation. There are two differences from regular
processes:

1. PID 1 inherits orphan processes and needs to wait() on them when they
   quit, in order to avoid accumulating zombie processes in the system.

2. Unlike regular processes, PID 1 doesn't have default signal handlers.

Both of these aspects are described in more detail here:

https://github.com/krallin/tini/issues/8#issuecomment-146135930

So to avoid an accumulation of zombie processes and other signal-related
problems, I suggest adding a “(init-program ,tini)” parameter to
‘least-authority-wrapper’ and executing ‘program’ as a subprocess of
‘tini’ or whatever was passed as the #:init-program (perhaps #f could
mean running ‘program’ directly as PID 1).

I mention this because I'm currently dealing with a problem that has
exactly this root cause: I'm working on updating the public-inbox
package to the latest version, and the testsuite is failing because it
tests that lei's daemon process is correctly terminated. But that
doesn't work because “guix build” doesn't use a proper init program as
PID 1 and thus the daemon process goes to zombie state and the testsuite
thinks that it didn't go away. I'm hoping to send a patch to fix that
issue.
Ludovic Courtès April 26, 2022, 8:30 p.m. UTC | #6
Hi!

Thiago Jung Bauermann <bauermann@kolabnow.com> skribis:

> I'm a bit concerned about running arbitrary commands as PID 1 of process
> namespaces. A process running as PID 1 (even in a child namespace) is a
> special case and is treated differently by the Linux kernel than any
> other process, so it needs to be a program that has been designed to
> work in that situation. There are two differences from regular
> processes:
>
> 1. PID 1 inherits orphan processes and needs to wait() on them when they
>    quit, in order to avoid accumulating zombie processes in the system.
>
> 2. Unlike regular processes, PID 1 doesn't have default signal handlers.

Good points.

> Both of these aspects are described in more detail here:
>
> https://github.com/krallin/tini/issues/8#issuecomment-146135930
>
> So to avoid an accumulation of zombie processes and other signal-related
> problems, I suggest adding a “(init-program ,tini)” parameter to
> ‘least-authority-wrapper’ and executing ‘program’ as a subprocess of
> ‘tini’ or whatever was passed as the #:init-program (perhaps #f could
> mean running ‘program’ directly as PID 1).

Hmm yes.  It’s not great that the choice is between ‘unshare’—efficient
but the process lives in the parent PID namespace—and ‘clone’—but then
you have to fork twice.

But yeah, you’re right.  I’ll try what you suggest and send a v2.

> I mention this because I'm currently dealing with a problem that has
> exactly this root cause: I'm working on updating the public-inbox
> package to the latest version, and the testsuite is failing because it
> tests that lei's daemon process is correctly terminated. But that
> doesn't work because “guix build” doesn't use a proper init program as
> PID 1 and thus the daemon process goes to zombie state and the testsuite
> thinks that it didn't go away. I'm hoping to send a patch to fix that
> issue.

Now that you mention it, this was discussed before:

  https://issues.guix.gnu.org/30948

I think we should do something about it in gnu-build-system.scm.

Thanks for your feedback!

Ludo’.
Ludovic Courtès April 26, 2022, 8:48 p.m. UTC | #7
Thiago Jung Bauermann <bauermann@kolabnow.com> skribis:

> I'm a bit concerned about running arbitrary commands as PID 1 of process
> namespaces. A process running as PID 1 (even in a child namespace) is a
> special case and is treated differently by the Linux kernel than any
> other process, so it needs to be a program that has been designed to
> work in that situation. There are two differences from regular
> processes:
>
> 1. PID 1 inherits orphan processes and needs to wait() on them when they
>    quit, in order to avoid accumulating zombie processes in the system.
>
> 2. Unlike regular processes, PID 1 doesn't have default signal handlers.

Actually right now ‘make-forkexec-constructor/container’ runs processes
as PID 1.

AFAIK this hasn’t been a problem in practice, probably for two reasons:
(1) we’re wrapping daemons that don’t fork (unlike Jenkins…), and (2)
‘call-with-container’ installs a SIGINT handler and probably daemons
also install SIGTERM and related handlers of their own.

Anyway, it’s a class of problem that would be best avoided in the first
place!

Ludo’.
Thiago Jung Bauermann April 29, 2022, 3:43 a.m. UTC | #8
Hi Ludo!

Ludovic Courtès <ludo@gnu.org> writes:
> Thiago Jung Bauermann <bauermann@kolabnow.com> skribis:
>> So to avoid an accumulation of zombie processes and other signal-related
>> problems, I suggest adding a “(init-program ,tini)” parameter to
>> ‘least-authority-wrapper’ and executing ‘program’ as a subprocess of
>> ‘tini’ or whatever was passed as the #:init-program (perhaps #f could
>> mean running ‘program’ directly as PID 1).
>
> Hmm yes.  It’s not great that the choice is between ‘unshare’—efficient
> but the process lives in the parent PID namespace—and ‘clone’—but then
> you have to fork twice.

Yeah, the signals part of the Unix design isn't great.

> But yeah, you’re right.  I’ll try what you suggest and send a v2.

Thank you for making these changes! I had a look at v2 and it looks
great.

>> I mention this because I'm currently dealing with a problem that has
>> exactly this root cause: I'm working on updating the public-inbox
>> package to the latest version, and the testsuite is failing because it
>> tests that lei's daemon process is correctly terminated. But that
>> doesn't work because “guix build” doesn't use a proper init program as
>> PID 1 and thus the daemon process goes to zombie state and the testsuite
>> thinks that it didn't go away. I'm hoping to send a patch to fix that
>> issue.
>
> Now that you mention it, this was discussed before:
>
>   https://issues.guix.gnu.org/30948
>
> I think we should do something about it in gnu-build-system.scm.

Nice! Thank you for the link. The discussion there was very informative.
I'll try to implement your idea of adding a new build phase to install
the appropriate signal handlers. Probably even steal your child reaping
code from the v2 patches.

> Thanks for your feedback!

Thank you for taking it into account!
Ludovic Courtès May 1, 2022, 8:16 p.m. UTC | #9
Ludovic Courtès <ludo@gnu.org> skribis:

>   gexp: Add 'references-file'.
>   file-systems: Avoid load-time warnings when attempting to load (guix
>     store).
>   linux-container: 'call-with-container' relays SIGTERM and SIGINT.
>   linux-container: Ensure signal-handling asyncs get a chance to run.
>   linux-container: Add #:child-is-pid1? parameter to
>     'call-with-container'.
>   Add (guix least-authority).
>   services: dicod: Rewrite using 'least-authority-wrapper'.
>   services: dicod: Use 'make-inetd-constructor'.
>   services: bitlbee: Use 'make-inetd-constructor'.
>   services: ipfs: Adjust for Shepherd 0.9.
>   services: ipfs: Use 'least-authority-wrapper'.
>   services: wesnothd: Grant write access to /var/run/wesnothd.
>   services: wesnothd: Use 'least-authority-wrapper'.
>   services: quassel: Use 'least-authority-wrapper'.
>   services: opendht: Use 'least-authority-wrapper'.

Pushed as fee06d5aaa71a965ea0bc06c1ff15c138a8bb2c8, thanks again for
reviewing!

Ludo’.
Thiago Jung Bauermann May 2, 2022, 4:25 a.m. UTC | #10
Ludovic Courtès <ludo@gnu.org> writes:

> Ludovic Courtès <ludo@gnu.org> skribis:
>
>>   gexp: Add 'references-file'.
>>   file-systems: Avoid load-time warnings when attempting to load (guix
>>     store).
>>   linux-container: 'call-with-container' relays SIGTERM and SIGINT.
>>   linux-container: Ensure signal-handling asyncs get a chance to run.
>>   linux-container: Add #:child-is-pid1? parameter to
>>     'call-with-container'.
>>   Add (guix least-authority).
>>   services: dicod: Rewrite using 'least-authority-wrapper'.
>>   services: dicod: Use 'make-inetd-constructor'.
>>   services: bitlbee: Use 'make-inetd-constructor'.
>>   services: ipfs: Adjust for Shepherd 0.9.
>>   services: ipfs: Use 'least-authority-wrapper'.
>>   services: wesnothd: Grant write access to /var/run/wesnothd.
>>   services: wesnothd: Use 'least-authority-wrapper'.
>>   services: quassel: Use 'least-authority-wrapper'.
>>   services: opendht: Use 'least-authority-wrapper'.
>
> Pushed as fee06d5aaa71a965ea0bc06c1ff15c138a8bb2c8, thanks again for
> reviewing!

That's great! Thank you for addressing the PID 1 issue!
diff mbox series

Patch

diff --git a/Makefile.am b/Makefile.am
index fecce7c6f7..d0d58da4e3 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -130,6 +130,7 @@  MODULES =					\
   guix/cache.scm				\
   guix/cve.scm					\
   guix/workers.scm				\
+  guix/least-authority.scm			\
   guix/ipfs.scm					\
   guix/build-system.scm				\
   guix/build-system/android-ndk.scm		\
diff --git a/gnu/build/shepherd.scm b/gnu/build/shepherd.scm
index d52e53eb78..f4caefce3c 100644
--- a/gnu/build/shepherd.scm
+++ b/gnu/build/shepherd.scm
@@ -31,7 +31,8 @@  (define-module (gnu build shepherd)
                                  exec-command
                                  %precious-signals)
   #:autoload (shepherd system) (unblock-signals)
-  #:export (make-forkexec-constructor/container
+  #:export (default-mounts
+            make-forkexec-constructor/container
             fork+exec-command/container))
 
 ;;; Commentary:
diff --git a/guix/least-authority.scm b/guix/least-authority.scm
new file mode 100644
index 0000000000..806c47508f
--- /dev/null
+++ b/guix/least-authority.scm
@@ -0,0 +1,131 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix 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.
+;;;
+;;; GNU Guix 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 GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (guix least-authority)
+  #:use-module (guix gexp)
+  #:use-module (guix modules)
+  #:use-module ((guix store) #:select (%store-prefix))
+  #:autoload   (gnu build linux-container) (%namespaces)
+  #:autoload   (gnu system file-systems) (file-system-mapping
+                                          file-system-mapping-source
+                                          spec->file-system
+                                          file-system->spec
+                                          file-system-mapping->bind-mount)
+  #:export (least-authority-wrapper))
+
+;;; Commentary:
+;;;
+;;; This module provides tools to execute programs with the least authority
+;;; necessary, using Linux namespaces.
+;;;
+;;; Code:
+
+(define %precious-variables
+  ;; Environment variables preserved by the wrapper by default.
+  '("HOME" "USER" "LOGNAME" "DISPLAY" "XAUTHORITY" "TERM" "TZ" "PAGER"))
+
+(define* (least-authority-wrapper program
+                                  #:key (name "pola-wrapper")
+                                  (guest-uid 1000)
+                                  (guest-gid 1000)
+                                  (mappings '())
+                                  (namespaces %namespaces)
+                                  (directory "/")
+                                  (preserved-environment-variables
+                                   %precious-variables))
+  "Return a wrapper of PROGRAM that executes it with the least authority.
+
+PROGRAM is executed in separate namespaces according to NAMESPACES, a list of
+symbols; it turns with GUEST-UID and GUEST-GID.  MAPPINGS is a list of
+<file-system-mapping> records indicating directories mirrored inside the
+execution environment of PROGRAM.  DIRECTORY is the working directory of the
+wrapped process.  Each environment listed in PRESERVED-ENVIRONMENT-VARIABLES
+is preserved; other environment variables are erased."
+  (define code
+    (with-imported-modules (source-module-closure
+                            '((gnu system file-systems)
+                              (gnu build shepherd)
+                              (gnu build linux-container)))
+      #~(begin
+          (use-modules (gnu system file-systems)
+                       (gnu build linux-container)
+                       ((gnu build shepherd) #:select (default-mounts))
+                       (srfi srfi-1))
+
+          (define variables
+            (filter-map (lambda (variable)
+                          (let ((value (getenv variable)))
+                            (and value
+                                 (string-append variable "=" value))))
+                        '#$preserved-environment-variables))
+
+          (define (read-file file)
+            (call-with-input-file file read))
+
+          (define references
+            (delete-duplicates
+             (append-map read-file
+                         '#$(map references-file
+                                 (cons program
+                                       (map file-system-mapping-source
+                                            mappings))))))
+
+          (define (store? file-system)
+            (string=? (file-system-mount-point file-system)
+                      #$(%store-prefix)))
+
+          (define mounts
+            (append (map (lambda (item)
+                           (file-system-mapping->bind-mount
+                            (file-system-mapping (source item)
+                                                 (target item))))
+                         references)
+                    (remove store?
+                            (default-mounts
+                              #:namespaces '#$namespaces))
+                    (map spec->file-system
+                         '#$(map (compose file-system->spec
+                                          file-system-mapping->bind-mount)
+                                 mappings))))
+
+          (define (reify-exit-status status)
+            (cond ((status:exit-val status) => exit)
+                  ((or (status:term-sig status)
+                       (status:stop-sig status))
+                   => (lambda (signal)
+                        (format (current-error-port)
+                                "~a terminated with signal ~a~%"
+                                #$program signal)
+                        (exit 126)))))
+
+          ;; Note: 'call-with-container' creates a sub-process that this one
+          ;; waits for.  This might seem suboptimal but unshare(2) isn't
+          ;; really applicable: the process would still run in the same PID
+          ;; namespace.
+
+          (reify-exit-status
+           (call-with-container mounts
+             (lambda ()
+               (chdir #$directory)
+               (environ variables)
+               (apply execl #$program #$program (cdr (command-line))))
+             #:guest-uid #$guest-uid
+             #:guest-gid #$guest-gid
+             #:namespaces '#$namespaces)))))
+
+  (program-file name code))