about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--NEWS11
-rw-r--r--doc/index.html1
-rw-r--r--doc/overview.html24
-rw-r--r--doc/s6-supervise.html28
-rw-r--r--doc/s6-svperms.html135
-rw-r--r--doc/s6-svscan.html149
-rw-r--r--doc/s6-svscanctl.html75
-rw-r--r--doc/servicedir.html9
-rw-r--r--doc/upgrade.html16
-rw-r--r--package/deps.mak5
-rw-r--r--package/modes1
-rw-r--r--package/targets.mak1
-rw-r--r--src/libs6/ftrigw_fifodir_make.c6
-rw-r--r--src/supervision/deps-exe/s6-supervise2
-rw-r--r--src/supervision/deps-exe/s6-svperms1
-rw-r--r--src/supervision/s6-supervise.c108
-rw-r--r--src/supervision/s6-svc.c3
-rw-r--r--src/supervision/s6-svperms.c272
-rw-r--r--src/supervision/s6-svscan.c2
20 files changed, 620 insertions, 230 deletions
diff --git a/.gitignore b/.gitignore
index 98f7368..93a1802 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
 /s6-svwait
 /s6-svlisten1
 /s6-svlisten
+/s6-svperms
 /s6-notifyoncheck
 /s6-svdt
 /s6-svdt-clear
diff --git a/NEWS b/NEWS
index 52c3405..4998b5b 100644
--- a/NEWS
+++ b/NEWS
@@ -4,7 +4,16 @@ In 2.10.0.0
 -----------
 
  - Bugfixes.
- - Changes to s6-svscan and s6-svscanctl.
+ - Changes to s6-svscan and s6-svscanctl: the list of commands
+that can be sent to s6-svscan has been thoroughly cleaned up.
+s6-svscan now scans and prunes on SIGHUP.
+ - Changes to s6-supervise and s6-svc: -X not supported anymore,
+nosetsid not supported anymore. SIGHUP now properly closes
+stdin/stdout to allow the service to naturally exit on EOF;
+SIGQUIT semantics changed to immediately bail. SIGINT is now
+trapped and forwarded to the service's process group.
+ - New binary: s6-svperms, implementing a split permissions
+model. (By default, everything is the same as before.)
 
 
 In 2.9.2.0
diff --git a/doc/index.html b/doc/index.html
index 9a01f14..ceb854e 100644
--- a/doc/index.html
+++ b/doc/index.html
@@ -161,6 +161,7 @@ a user interface to control those processes and monitor service states.
 <li><a href="s6-svc.html">The <tt>s6-svc</tt> program</a></li>
 <li><a href="s6-svok.html">The <tt>s6-svok</tt> program</a></li>
 <li><a href="s6-svstat.html">The <tt>s6-svstat</tt> program</a></li>
+<li><a href="s6-svperms.html">The <tt>s6-svperms</tt> program</a></li>
 <li><a href="s6-svwait.html">The <tt>s6-svwait</tt> program</a></li>
 <li><a href="s6-svlisten1.html">The <tt>s6-svlisten1</tt> program</a></li>
 <li><a href="s6-svlisten.html">The <tt>s6-svlisten</tt> program</a></li>
diff --git a/doc/overview.html b/doc/overview.html
index 1b4cb5d..9e41824 100644
--- a/doc/overview.html
+++ b/doc/overview.html
@@ -379,6 +379,30 @@ implemented for instance in the
 <a href="//skarnet.org/software/s6-rc/">s6-rc</a> package.
 </p>
 
+<h2> Fine-grained control over services </h2>
+
+<p>
+ s6 provides you with a few more tools to control and monitor your
+services. services. For instance:
+</p>
+
+<ul>
+ <li> <a href="s6-svstat.html">s6-svstat</a> gives you access to
+the detailed state of a service </li>
+ <li> <a href="s6-svperms">s6-svperms</a> allows you to configure
+what users can read that state, what users can send control
+commands to your service, and what users can be notified of
+service start/stop events </li>
+ <li> <a href="s6-svdt.html">s6-svdt</a>
+allows you to see what caused the latest deaths of a supervised
+process </li>
+</ul>
+
+<p>
+ These tools make s6 the most powerful and flexible of the existing
+process supervision suites.
+</p>
+
 <h2> Additional utilities </h2>
 
 <p>
diff --git a/doc/s6-supervise.html b/doc/s6-supervise.html
index 2926f83..94504e3 100644
--- a/doc/s6-supervise.html
+++ b/doc/s6-supervise.html
@@ -108,11 +108,15 @@ daemon as <tt>fdmove -c 2 1 fdmove 1 3 prog...</tt> (in execline), or
 <ul>
  <li> SIGTERM: bring down the service and exit, as if a
 <a href="s6-svc.html">s6-svc -xd</a> command had been received </li>
- <li> SIGHUP: exit as soon as the service stops, as if a
-<a href="s6-svc.html">s6-svc -x</a> command had been received </li>
- <li> SIGQUIT: close stdin, stdout and stderr and exit as soon as
-the service stops, as if a
-<a href="s6-svc.html">s6-svc -X</a> command had been received </li>
+ <li> SIGHUP: close its own stdin and stdout, and exit as soon as the
+service stops, as if a <a href="s6-svc.html">s6-svc -x</a> command
+had been received </li>
+ <li> SIGQUIT: exit immediately without touching the service in any
+way. </li>
+ <li> SIGINT: send a SIGINT to the process group of the service, then
+exit immediately. (The point here is to correctly forward SIGINT
+in the case where s6-supervise is running in a terminal and the user
+sent ^C to interrupt it.) </li>
 </ul>
 
 <a name="#detailed">
@@ -183,17 +187,9 @@ better to have a collection of <a href="servicedir.html">service directories</a>
 single <a href="scandir.html">scan directory</a>, and just run
 <a href="s6-svscan.html">s6-svscan</a> on that scan directory. s6-svscan will spawn
 the necessary s6-supervise processes, and will also take care of logged services. </li>
- <li> s6-supervise is not supposed to have a controlling terminal: it's generally
-launched by a <a href="s6-svscan.html">s6-svscan</a> process that itself does not
-have a controlling terminal. If you run s6-supervise from an interactive shell, be
-warned that typing ^C in the controlling terminal (which sends a SIGINT to
-all processes in the foreground process group in the terminal) will terminate
-s6-supervise, but not the supervised processes - so, the daemon will keep running
-as an orphan. This is by design: supervised processes should be as resilient as
-possible, even when their supervisors die. However, if you want to launch
-s6-supervise from an interactive shell and need your service to die when you ^C it,
-you can obtain this behaviour by creating a <tt>./nosetsid</tt> file in the
-<a href="servicedir.html">service directory</a>. </li>
+ <li> s6-supervise always spawns its child in a new session, as a session leader.
+The goal is to protect the supervision tree from misbehaved services that would
+send signals to their whole process group. </li>
  <li> You can use <a href="s6-svc.html">s6-svc</a> to send commands to the s6-supervise
 process; mostly to change the service state and send signals to the monitored
 process. </li>
diff --git a/doc/s6-svperms.html b/doc/s6-svperms.html
new file mode 100644
index 0000000..5b5d485
--- /dev/null
+++ b/doc/s6-svperms.html
@@ -0,0 +1,135 @@
+<html>
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta http-equiv="Content-Language" content="en" />
+    <title>s6: the s6-svperms program</title>
+    <meta name="Description" content="s6: the s6-svperms program" />
+    <meta name="Keywords" content="s6 command s6-svperms service control permission unix rights events process supervision s6-supervise" />
+    <!-- <link rel="stylesheet" type="text/css" href="//skarnet.org/default.css" /> -->
+  </head>
+<body>
+
+<p>
+<a href="index.html">s6</a><br />
+<a href="//skarnet.org/software/">Software</a><br />
+<a href="//skarnet.org/">skarnet.org</a>
+</p>
+
+<h1> The <tt>s6-svperms</tt> program </h1>
+
+<p>
+<tt>s6-svperms</tt> allows the user to see, or modify, for a given
+list of services: who can read their states, who can send them
+control commands, and who can subscribe to up/down events for those
+services.
+</p>
+
+<h2> Interface </h2>
+
+<pre>
+     s6-svperms [ -v ] [ -u | -g <em>group</em> | -G <em>group</em> | -o | -O <em>group</em> ] [ -e | -E <em>group</em> ] <em>servicedirs...</em>
+</pre>
+
+<p>
+ Without options, or with only the <tt>-v</tt> option,
+<tt>s6-svperms</tt> prints 3 lines to stdout for every service directory
+listed in <em>servicedirs</em>. Every line contains the name
+of the service directory, then the following information:
+</p>
+
+<ul>
+ <li> <tt>status:</tt> - indicates who is allowed to read status
+information on the service, with commands such as
+<a href="s6-svstat.html">s6-svstat</a> or
+<a href="s6-svdt.html">s6-svdt</a>. The values can be <tt>owner</tt>,
+for only the owner of the service; <tt>group: <em>name</em></tt>, for
+the owner and members of group <em>name</em>; or <tt>public</tt>,
+for all users. </li>
+ <li> <tt>control:</tt> - indicates who is allowed to send control
+commands to the service, with commands such as
+<a href="s6-svc.html">s6 -svc</a>. The values can be <tt>owner</tt>,
+for only the owner of the service; or <tt>group: <em>name</em></tt>,
+for the owner and members of group <em>name</em>. </li>
+ <li> <tt>events:</tt> - indicates who is allowed to subscribed to
+events sent by <a href="s6-supervise.html">s6-supervise</a> for this
+service, with commands such as <a href="s6-svwait.html">s6-svwait</a>
+or <a href="s6-svlisten1.html">s6-svlisten1</a>. The values can be
+<tt>group: <em>name</em></tt>, for the owner and members of group
+<em>name</em>, or <tt>public</tt>, for all users.
+</ul>
+
+<p>
+ If something goes wrong while reading a part of the configuration of
+a service directory, <tt>s6-svperms</tt> does not print the corresponding
+line to stdout; instead, it prints a warning message to stderr.
+</p>
+
+<p>
+ When invoked with other options, <tt>s6-svperms</tt> modifies the
+permissions of the service directories listed in <em>servicedirs...</em> as
+specified by the options. The same permissions will be applied to all
+the services listed in <em>servicedirs...</em>.
+</p>
+
+<h2> Options </h2>
+
+<ul>
+ <li> <tt>-v</tt>&nbsp;: re-read the permissions after writing them, and
+print them to stdout.
+ <li> <tt>-u</tt>&nbsp;: restrict the <tt>status:</tt> and <tt>control:</tt>
+permissions to <tt>owner</tt>: only the owner of a service directory will
+be able to read its state or control the service. This is the default when
+<a href="s6-supervise.html">s6-supervise</a> starts a service for the first
+time. </li>
+ <li> <tt>-g&nbsp;<em>group</em></tt>&nbsp;: allow members of group
+<em>group</em> to read the status of the service, but not to control it -
+control will be restricted to the owner. </li>
+ <li> <tt>-G&nbsp;<em>group</em></tt>&nbsp;: allow members of group
+<em>group</em> to read <em>and</em> control the service. </li>
+ <li> <tt>-o</tt>&nbsp;: allow everyone to read the status of the service,
+but restrict <tt>control:</tt> to the owner. </li>
+ <li> <tt>-O&nbsp;<em>group</em></tt>&nbsp;: allow everyone to read the
+status, and allow members of group <em>group</em> to control the
+service. </li>
+ <li> <tt>-e</tt>&nbsp;: allow everyone to subscribe to events. </li>
+ <li> <tt>-E&nbsp;<em>group</em></tt>&nbsp;: only allow members of group
+<em>group</em> to subscribe to events. This is the default when
+<a href="s6-supervise.html">s6-supervise</a> starts a service for the first
+time, with <em>group</em> being the primary group of the s6-supervise
+process (most likely <tt>root</tt>). </li>
+</ul>
+
+<p>
+ <em>group</em> is normally a group name that will be searched in the group
+database. But if it starts with a colon (<tt>:</tt>), the rest of <em>group</em>
+will be interpreted as a numerical gid, and the group database will not be read.
+</p>
+
+<h2> Exit codes </h2>
+
+<ul>
+ <li> 0: success </li>
+ <li> 1: something went wrong when reading permissions in one of the service directories </li>
+ <li> 100: wrong usage </li>
+ <li> 111: system call failed </li>
+</ul>
+
+<h2> Notes </h2>
+
+<ul>
+ <li> The default (restrictive) permissions are safe. </li>
+ <li> Unless operation of a service is restricted information, it is also
+safe to make <tt>status:</tt> more permissive. </li>
+ <li> Opening <tt>control:</tt> to a group can be useful for instance in a
+shared administration situation when individual administrators are not given
+full root powers. </li>
+ <li> Making <tt>events:</tt> public bears a small risk of a local DoS attack
+preventing more subscriptions to events, so it is not recommended for
+supervision trees where such subscriptions are critical to operations - such
+as a set of root services managed by
+<a href="//skarnet.org/software/s6-rc/">s6-rc</a>. </li>
+</ul>
+
+</body>
+</html>
diff --git a/doc/s6-svscan.html b/doc/s6-svscan.html
index 3a93e00..5d92174 100644
--- a/doc/s6-svscan.html
+++ b/doc/s6-svscan.html
@@ -27,7 +27,7 @@ the root or a branch of a <em>supervision tree</em>.
 <h2> Interface </h2>
 
 <pre>
-     s6-svscan [ -S | -s ] [ -d <em>notif</em> ] [ -X <em>consoleholder</em> ] [ -c max ] [ -t <em>rescan</em> ] [ <em>scandir</em> ]
+     s6-svscan [ -d <em>notif</em> ] [ -X <em>consoleholder</em> ] [ -c max ] [ -t <em>rescan</em> ] [ <em>scandir</em> ]
 </pre>
 
 <ul>
@@ -37,9 +37,8 @@ its current directory as the <a href="scandir.html">scan directory</a>. </li>
 <a href="scandir.html">scan directory</a>. </li>
  <li> If the <tt>./.s6-svscan</tt> control directory does not exist,
 s6-svscan creates it. However, it is recommended to already have a <tt>.s6-svscan</tt>
-subdirectory in your scan directory, because s6-svscan may try and execute into the
-<tt>.s6-svscan/crash</tt> or <tt>.s6-svscan/finish</tt> files at some point - so those
-files should exist and be executable. </li>
+subdirectory in your scan directory, because it is used to configure s6-svscan
+operation, see below.
  <li> From this point on, s6-svscan never dies. It tries its best to keep
 control of what's happening. In case of a major system call failure, which means
 that the kernel or hardware is broken in some fashion, it executes into the
@@ -49,19 +48,14 @@ that the kernel or hardware is broken in some fashion, it executes into the
  <li> s6-svscan then occasionally runs <em>scans</em> or <em>reaps</em>,
 see below. </li>
  <li> s6-svscan runs until it is told to stop via <a href="s6-svscanctl.html">
-s6-svscanctl</a>, or a signal.
-Then it executes into the <tt>.s6-svscan/finish</tt> program. The program is
-given an argument that depends on the s6-svscanctl options that were used. </li>
- <li> If that execution fails, s6-svscan falls back on a <tt>.s6-svscan/crash</tt>
-execution. </li>
+s6-svscanctl</a>, or an appropriate signal (see below).
+Then it executes into the <tt>.s6-svscan/finish</tt> program, if present; if
+not, it exits 0.
 </ul>
 
 <h2> Options </h2>
 
 <ul>
- <li> <tt>-S&nbsp;</tt>&nbsp;: do not divert signals. This is the default for now;
-it may change in a future version of s6. </li>
- <li> <tt>-s&nbsp;</tt>&nbsp;: divert signals - see below. </li>
  <li> <tt>-d&nbsp;<em>notif</em></tt>&nbsp;: notify readiness on file descriptor
 <em>notif</em>. When s6-svscan is ready to accept commands from
 <a href="s6-svscanctl.html">s6-svscanctl</a>, it will write a newline to <em>notif</em>.
@@ -94,76 +88,82 @@ of 500 is safe and provides enough room for every reasonable system. </li>
  <li> <tt>-t&nbsp;<em>rescan</em></tt>&nbsp;: perform a scan every <em>rescan</em>
 milliseconds. If <em>rescan</em> is 0 (the default), automatic scans are never performed after
 the first one and s6-svscan will only detect new services when told to via a
-<a href="s6-svscanctl.html">s6-svscanctl -a</a> command.
-It is <em>strongly</em> discouraged to set
-<em>rescan</em> to a positive value under 500. </li>
+<a href="s6-svscanctl.html">s6-svscanctl -a</a> command. Use of this option is
+discouraged; it should only be given to emulate the behaviour of other supervision
+suites. (<tt>-t5000</tt> for daemontools' svscan, <tt>-t14000</tt> for runit's
+runsvdir.) </li>
 </ul>
 
 <h2> Signals </h2>
 
 <p>
- s6-svscan always reacts to the following signals:
+ s6-svscan has special handling for the following signals:
 </p>
 
 <ul>
- <li> SIGCHLD&nbsp;: triggers the reaper. </li>
- <li> SIGALRM&nbsp;: triggers the scanner. </li>
- <li> SIGABRT&nbsp;: acts as if a <tt>s6-svscanctl -b</tt> command had been received. </li>
+ <li> SIGCHLD </li>
+ <li> SIGALRM </li>
+ <li> SIGABRT </li>
+ <li> SIGHUP </li>
+ <li> SIGINT </li>
+ <li> SIGTERM </li>
+ <li> SIGQUIT </li>
+ <li> SIGUSR1 </li>
+ <li> SIGUSR2 </li>
+ <li> SIGPWR (on systems that support it)</li>
+ <li> SIGWINCH (on systems that support it)</li>
 </ul>
 
 <p>
- By default, it also reacts to the following signals:
+ Signals that are not in the above list are not caught by s6-svscan and will
+have the system's default effect.
 </p>
 
-<ul>
- <li> SIGTERM&nbsp;: acts as if a <tt>s6-svscanctl -t</tt> command had been received. </li>
- <li> SIGHUP&nbsp;: acts as if a <tt>s6-svscanctl -h</tt> command had been received. </li>
- <li> SIGQUIT&nbsp;: acts as if a <tt>s6-svscanctl -q</tt> command had been received. </li>
- <li> SIGINT&nbsp;: acts as if a <tt>s6-svscanctl -6</tt> command had been received. </li>
-</ul>
-
 <p>
- But if the <tt>-s</tt> option was given, then instead of those default actions,
-s6-svscan uses configurable handlers: it forks and executes a program every time
-it receives one of the following signals.
+ The behaviour for the first three signals in the list is always fixed:
 </p>
 
 <ul>
- <li> SIGTERM&nbsp;: fork and execute <tt>.s6-svscan/SIGTERM</tt> </li>
- <li> SIGHUP&nbsp;: fork and execute <tt>.s6-svscan/SIGHUP</tt> </li>
- <li> SIGQUIT&nbsp;: fork and execute <tt>.s6-svscan/SIGQUIT</tt> </li>
- <li> SIGINT&nbsp;: fork and execute <tt>.s6-svscan/SIGINT</tt> </li>
- <li> SIGUSR1&nbsp;: fork and execute <tt>.s6-svscan/SIGUSR1</tt> </li>
- <li> SIGUSR2&nbsp;: fork and execute <tt>.s6-svscan/SIGUSR2</tt> </li>
- <li> SIGPWR (on systems that define it)&nbsp;: fork and execute <tt>.s6-svscan/SIGPWR</tt> </li>
- <li> SIGWINCH (on systems that define it)&nbsp;: fork and execute <tt>.s6-svscan/SIGWINCH</tt> </li>
+ <li> SIGCHLD&nbsp;: trigger the reaper. </li>
+ <li> SIGALRM&nbsp;: trigger the scanner. </li>
+ <li> SIGABRT&nbsp;: immediately exec into <tt>.s6-svscan/finish</tt> (or exit 0 if that script does not exist), without waiting for any processes to die </li>
 </ul>
 
 <p>
- If an action cannot be taken (the relevant file doesn't exist, or isn't
-executable, or any kind of error happens), s6-svscan prints a warning
-message to its standard error but does nothing else with the signal.
-</p>
-
-<p>
- The <tt>-s</tt> mechanism is useful, for instance, when s6-svscan is running as
-process 1 and needs to trap signals such as SIGINT (sent on some systems by
-a Ctrl-Alt-Del press) in order to perform some specific work instead of
-executing into <tt>.s6-svscan/finish</tt> on the spot.
+ The behaviour for the rest of the list is configurable: on receipt of a
+<tt>SIG<em>FOO</em></tt>,
+s6-svscan will try to run an executable <tt>.s6-svscan/SIG<em>FOO</em></tt> file. For
+instance, a <tt>.s6-svscan/SIGTERM</tt> executable script will be run on receipt of
+a SIGTERM. If the file cannot be found, or cannot be executed for any reason, the
+default behaviour for the signal will be applied. Default behaviours are:
 </p>
 
-<p>
- s6-svscan will not exit its loop on its own when it receives a signal such as
-SIGINT and the <tt>-s</tt> option has been given. To make it exit its loop,
-invoke a <a href="s6-svscanctl.html">s6-svscanctl</a> command from the signal
-handling script. For instance, a <tt>.s6-svscan/SIGINT</tt> script could look
-like this:
-</p>
-
-<pre>  #!/command/execlineb -P
-  foreground { shutdown-the-services }
-  s6-svscanctl -i .
-</pre>
+<ul>
+ <li> SIGHUP&nbsp;: rescan and prune the supervision tree, i.e. make sure that new
+service directories visible from the scan directory have a
+<a href="s6-supervise.html">s6-supervise</a> process running on them, and instruct
+<a href="s6-supervise.html">s6-supervise</a> processes running on service directories
+that have become invisible from the scan directory to stop their service and exit.
+This behaviour can also be achieved via the
+<tt>s6-svscanctl -an <em>scandir</em> </tt> command. This is the closest that s6-svscan
+can get to the traditional "reload your configuration" behaviour. </li>
+ <li> SIGINT&nbsp;: same as SIGTERM below. </li>
+ <li> SIGTERM&nbsp;: Instruct all the <a href="s6-supervise.html">s6-supervise</a> to
+stop their service and exit; wait for the whole supervision tree to die, without
+losing any logs; then exec into <tt>.s6-svscan/finish</tt> or exit 0. This behaviour
+can also be achieved via the <tt>s6-svscanctl -t <em>scandir</em></tt> command. </li>
+ <li> SIGQUIT&nbsp;: same as SIGTERM above, except that if a service is logged
+(i.e. there is a <em>foo</em> service <em>and</em> a <em>foo</em>/log service)
+then the logging service will also be killed, instead of being allowed to exit
+naturally after its producer has flushed its output and died. This can solve
+problems with badly written logging programs, but it can also cause loss of logs
+since the logger may die before the producer has finished flushing everything. The
+behaviour can also be achieved via the <tt>s6-svscanctl -q <em>scandir</em></tt> command;
+you should only use this if SIGTERM/<tt>-t</tt> fails to properly tear down the
+supervision tree. </li>
+ <li> Others: no effect if an appropriate executable file in <tt>.s6-svscan/</tt>
+cannot be run. </li>
+</ul>
 
 <h2> The reaper </h2>
 
@@ -189,9 +189,10 @@ one second later.
 <h2> The scanner </h2>
 
 <p>
- Every <em>rescan</em> milliseconds, or upon receipt of a SIGALRM or a
+ Upon receipt of a SIGALRM or a
 <a href="s6-svscanctl.html">s6-svscanctl -a</a> command, s6-svscan runs a
-<em>scanner</em> routine.
+<em>scanner</em> routine. (It also runs it every <em>rescan</em> milliseconds
+if the <tt>-t</tt> option has been given.)
 </p>
 
 <p>
@@ -217,8 +218,8 @@ Every service the scanner finds is flagged as "active".
 started in an earlier scan, but the current scan can't find the corresponding
 directory, the service is then flagged as inactive. No command is sent
 to stop inactive s6-supervise processes (unless the administrator
-uses <a href="s6-svscanctl.html">s6-svscanctl -n</a>), but inactive
-s6-supervise processes will not be restarted if they die.
+uses <a href="s6-svscanctl.html">s6-svscanctl -n</a> or a SIGHUP), but
+inactive s6-supervise processes will not be restarted if they die.
 </p>
 
 <h2> Notes </h2>
@@ -238,22 +239,10 @@ process commands at any time, even when the computer is in trouble. </li>
 memory</em>. <small>However, s6-svscan uses opendir(), and most opendir()
 implementations internally use heap memory - so unfortunately, it's impossible
 to guarantee that s6-svscan does not use heap memory at all.</small> </li>
- <li> When run with the <tt>-t0</tt> option, s6-svscan <em>never polls</em>,
-it only wakes up on notifications, just like s6-supervise. The s6 supervision
-tree can be used in energy-critical environments. </li>
- <li> The supervision tree (i.e. the tree of processes made of s6-svscan and
-all its scions) is not supposed to have a controlling terminal; s6-svscan
-generally is either process 1 or a child of process 1, not something that is
-launched from a terminal. If you run s6-svscan from an interactive shell, be
-warned that typing ^C in the controlling terminal (which sends a SIGINT to
-all processes in the foreground process group in the terminal) will terminate
-the supervision tree, but not the supervised processes - so, the supervised
-processes will keep running as orphans. This is by design: supervised
-processes should be as resilient as possible, even when their supervisors
-die. However, if you want to launch s6-svscan from an interactive shell and
-need your services to die with the supervision tree when you ^C it, you can
-obtain this behaviour by creating <tt>./nosetsid</tt> files in every
-<a href="servicedir.html">service directory</a>. </li>
+ <li> Unless run with a nonzero <tt>-t</tt> option, which is only a legacy
+feature used to emulate other supervision suites such as daemontools or runit,
+s6-svscan <em>never polls</em>; it only wakes up on notifications.
+The s6 supervision tree can be used in energy-critical environments. </li>
 </ul>
 
 </body>
diff --git a/doc/s6-svscanctl.html b/doc/s6-svscanctl.html
index ae0f8ee..6340c14 100644
--- a/doc/s6-svscanctl.html
+++ b/doc/s6-svscanctl.html
@@ -26,82 +26,57 @@ process.
 <h2> Interface </h2>
 
 <pre>
-     s6-svscanctl [ -phratszbnNiq0678 ] <em>svscandir</em>
+     s6-svscanctl [ -zabhitqnN ] <em>scandir</em>
 </pre>
 
 <p>
 s6-svscanctl sends the given series of commands to the
 <a href="s6-svscan.html">s6-svscan</a> process monitoring the
-<em>svscandir</em> directory, then exits 0. It exits 111 if it cannot send
-a command, or 100 if no s6-svscan process is running on <em>svscandir</em>.
+<em>scandir</em> directory, then exits 0. It exits 111 if it cannot send
+a command, or 100 if no s6-svscan process is running on <em>scandir</em>.
 </p>
 
 <h2> Options </h2>
 
 <ul>
- <li> <tt>-p</tt>&nbsp;: poweroff mode. s6-svscan will exec into
- <tt>./.s6-svscan/finish poweroff</tt> when it is told to terminate. </li>
- <li> <tt>-h</tt>&nbsp;: Hangup. s6-svscan will send a SIGHUP to all the
-maintained s6-supervise processes, then run its finish procedure. </li>
- <li> <tt>-r</tt>&nbsp;: reboot mode. s6-svscan will exec into
- <tt>./.s6-svscan/finish reboot</tt> when it is told to terminate. This
-is s6-svscan's default mode.</li>
- <li> <tt>-a</tt>&nbsp;: Alarm. s6-svscan will immediately perform a scan
-of <em>svscandir</em> to check for services. </li>
- <li> <tt>-t</tt>&nbsp;: Terminate. s6-svscan will send a
-SIGTERM to all the s6-supervise processes supervising a service and a
-SIGHUP to all the s6-supervise processes supervising a logger, then run its
-finish procedure. </li>
- <li> <tt>-s</tt>&nbsp;: halt mode. s6-svscan will exec into
- <tt>./.s6-svscan/finish halt</tt> when it is told to terminate. </li>
  <li> <tt>-z</tt>&nbsp;: destroy zombies. Immediately triggers s6-svscan's
 reaper mechanism. </li>
+ <li> <tt>-a</tt>&nbsp;: Alarm. s6-svscan will immediately perform a scan
+of <em>scandir</em> to check for services. </li>
  <li> <tt>-b</tt>&nbsp;: abort. s6-svscan will exec into its finishing
 procedure. It will not kill any of the maintained s6-supervise processes. </li>
+ <li> <tt>-h</tt>&nbsp;: Reload configuration. s6-svscan will perform a scan,
+and destroy inactive services. Equivalent to <tt>-an</tt>. </li>
+ <li> <tt>-i</tt>&nbsp;: equivalent to <tt>-t</tt> below. </li>
+ <li> <tt>-t</tt>&nbsp;: Terminate. s6-svscan will send a
+SIGTERM to all the s6-supervise processes supervising a service and a
+SIGHUP to all the s6-supervise processes supervising a logger, then exec into
+its finish procedure. This means that services will be brought down but
+loggers will exit naturally on EOF, and s6-svscan will wait for them to exit
+before exec'ing into <tt>.s6-svscan/finish</tt> or exiting itself: it's a
+clean shutdown with no loss of logs. </li>
+ <li> <tt>-q</tt>&nbsp;: Quit. s6-svscan will send all its s6-supervise processes
+a SIGTERM, then exec into its finish procedure. This is different from <tt>-t</tt>
+in that services <em>and</em> loggers will be forcibly killed, so the quit
+procedure may be faster but in-flight logs may be lost. </li>
  <li> <tt>-n</tt>&nbsp;: nuke. s6-svscan will kill all the
 s6-supervise processes it has launched but that did not match a service
-directory last time <em>svscandir</em> was scanned, i.e. it prunes the
-supervision tree so that it matches exactly what was in <em>svscandir</em>
+directory last time <em>scandir</em> was scanned, i.e. it prunes the
+supervision tree so that it matches exactly what was in <em>scandir</em>
 at the time of the last scan. A SIGTERM is sent to the s6-supervise processes
 supervising services and a SIGHUP is sent to the s6-supervise processes
 supervising loggers. </li>
  <li> <tt>-N</tt>&nbsp;: Really nuke. Does the same thing as <tt>-n</tt>,
 except that SIGTERM is sent to all the relevant s6-supervise processes, even
-if they are supervising loggers. That means that the logger processes will
-be killed with a SIGTERM instead of being allowed to exit at their own pace. </li>
- <li> <tt>-i</tt>&nbsp;: Interrupt. Equivalent to <tt>-rt</tt>&nbsp;: s6-svscan
-will terminate in reboot mode. </li>
- <li> <tt>-q</tt>&nbsp;: Quit. s6-svscan will send all its s6-supervise processes
-a SIGTERM, then exec into its finish procedure. </li>
- <li> <tt>-0</tt>&nbsp;: Halt. Equivalent to <tt>-st</tt>&nbsp;: s6-svscan will
-terminate in halt mode. </li>
- <li> <tt>-6</tt>&nbsp;: Reboot. Equivalent to <tt>-i</tt>. </li>
- <li> <tt>-7</tt>&nbsp;: Poweroff. Equivalent to <tt>-pt</tt>: s6-svscan will
-terminate in poweroff mode. </li>
- <li> <tt>-8</tt>&nbsp;: Other. s6-svscan will terminate in "other" mode. </li>
+if they are supervising loggers. This is not recommended in a situation where
+you do not need to tear down the supervision tree. </li>
 </ul>
 
-<h2> Usage examples </h2>
-
-<pre> s6-svscanctl -an /service </pre>
-<p>
- Updates the process supervision tree
-to exactly match the services listed in <tt>/service</tt>.
-</p>
-
-<pre> s6-svscanctl -6 /service </pre>
-<p>
- Orders the s6-svscan process monitoring <tt>/service</tt> to exit in
-reboot mode: all the supervision tree at <tt>/service</tt> will be terminated,
-and s6-svscan will execute into the <tt>/service/.s6-svscan/finish</tt>
-script with the <tt>reboot</tt> argument.
-</p>
-
 <h2> Internals </h2>
 
 <p>
-s6-svscanctl writes control commands into the <tt><em>svscandir</em>/.s6-svscan/control</tt>
-FIFO. An s6-svscan process running on <em>svscandir</em> will be listening to this FIFO,
+s6-svscanctl writes control commands into the <tt><em>scandir</em>/.s6-svscan/control</tt>
+FIFO. An s6-svscan process running on <em>scandir</em> will be listening to this FIFO,
 and will read and interpret those commands.
 </p>
 
diff --git a/doc/servicedir.html b/doc/servicedir.html
index fa7f974..d8d7c8d 100644
--- a/doc/servicedir.html
+++ b/doc/servicedir.html
@@ -122,15 +122,6 @@ must be writable. </li>
 the default state of the service is considered down, not up: s6-supervise will not
 automatically start it until it receives a <tt>s6-svc -u</tt> command. If no
 <tt>down</tt> file exists, the default state of the service is up. </li>
- <li style="margin-bottom:1em"> An optional, empty, regular file named <tt>nosetsid</tt>.
-If this file exists and starts with the word <tt>setpgrp</tt>, s6-supervise will run the service
-in a new process group (the run script will be a process group leader), but not in a new session.
-If this file exists and <em>does not</em> start with <tt>setpgrp</tt>, 
-s6-supervise will start the service in the same session and process group as itself.
-If no <tt>nosetsid</tt> file exists, the service has its own process group and is started
-as a session leader - which is the default and should normally not be changed. Using the
-<tt>nosetsid</tt> file is a hack; it should only be used in testing environments for
-job control convenience, and probably never outside that use case. </li>
  <li style="margin-bottom:1em"> An optional regular file named <tt>notification-fd</tt>. If such a file
 exists, it means that the service supports
 <a href="notifywhenup.html">readiness notification</a>. The file must only
diff --git a/doc/upgrade.html b/doc/upgrade.html
index 5837345..f49cc0f 100644
--- a/doc/upgrade.html
+++ b/doc/upgrade.html
@@ -25,8 +25,20 @@
 dependency bumped to 2.10.0.0. </li>
  <li> <a href="//skarnet.org/software/execline/">execline</a>
 dependency bumped to 2.7.0.0. </li>
- <li> <a href="s6-svscan.html">s6-svscan</a> and
-<a href="s6-svscanctl.html">s6-svscanctl</a> have changed. </li>
+ <li> Commands received by <a href="s6-svscan.html">s6-svscan</a> and
+sent by <a href="s6-svscanctl.html">s6-svscanctl</a> have changed significantly.
+Different semantics of SIGHUP for <a href="s6-svscan.html">s6-svscan</a>. </li>
+ <li> Commands received by <a href="s6-supervise.html">s6-supervise</a> and
+sent by <a href="s6-svc.html">s6-svc</a> have changed slightly
+(no <tt>s6-svc -X</tt> anymore). Different semantics of SIGHUP and SIGQUIT
+for <a href="s6-supervise.html">s6-supervise</a>. </li>
+ <li> <a href="s6-supervise.html">s6-supervise</a> now handles SIGINT
+appropriately, by forwarding it to its child's process group. </li>
+ <li> The <tt>nosetsid</tt> file is not recognized anymore in service
+directories. <a href="s6-supervise.html">s6-supervise</a> now always starts
+it child as a session leader. </li>
+ <li> Split permissions on service control are now officially supported.
+New binary: <a href="s6-svperms.html">s6-svperms</a>. </li>
 </ul>
 
 <h2> in 2.9.2.0 </h2>
diff --git a/package/deps.mak b/package/deps.mak
index 08b7aec..5c43a12 100644
--- a/package/deps.mak
+++ b/package/deps.mak
@@ -125,6 +125,7 @@ src/supervision/s6-svdt.o src/supervision/s6-svdt.lo: src/supervision/s6-svdt.c
 src/supervision/s6-svlisten.o src/supervision/s6-svlisten.lo: src/supervision/s6-svlisten.c src/supervision/s6-svlisten.h src/include/s6/compat.h
 src/supervision/s6-svlisten1.o src/supervision/s6-svlisten1.lo: src/supervision/s6-svlisten1.c src/supervision/s6-svlisten.h
 src/supervision/s6-svok.o src/supervision/s6-svok.lo: src/supervision/s6-svok.c src/include/s6/s6-supervise.h
+src/supervision/s6-svperms.o src/supervision/s6-svperms.lo: src/supervision/s6-svperms.c src/include/s6/s6-supervise.h
 src/supervision/s6-svscan.o src/supervision/s6-svscan.lo: src/supervision/s6-svscan.c src/include/s6/config.h src/include/s6/s6-supervise.h
 src/supervision/s6-svscanctl.o src/supervision/s6-svscanctl.lo: src/supervision/s6-svscanctl.c src/include/s6/s6-supervise.h
 src/supervision/s6-svstat.o src/supervision/s6-svstat.lo: src/supervision/s6-svstat.c src/include/s6/s6-supervise.h
@@ -228,7 +229,7 @@ s6-notifyoncheck: src/supervision/s6-notifyoncheck.o ${LIBS6}
 s6-permafailon: EXTRA_LIBS := -lskarnet ${SYSCLOCK_LIB}
 s6-permafailon: src/supervision/s6-permafailon.o ${LIBS6}
 s6-supervise: EXTRA_LIBS := -lskarnet ${SYSCLOCK_LIB}
-s6-supervise: src/supervision/s6-supervise.o ${LIBS6}
+s6-supervise: src/supervision/s6-supervise.o libs6.a.xyzzy
 s6-svc: EXTRA_LIBS := -lskarnet
 s6-svc: src/supervision/s6-svc.o ${LIBS6}
 s6-svdt: EXTRA_LIBS := -lskarnet
@@ -241,6 +242,8 @@ s6-svlisten1: EXTRA_LIBS := -lskarnet ${SOCKET_LIB} ${SYSCLOCK_LIB} ${SPAWN_LIB}
 s6-svlisten1: src/supervision/s6-svlisten1.o src/supervision/s6_svlisten_signal_handler.o src/supervision/s6_svlisten_loop.o ${LIBS6}
 s6-svok: EXTRA_LIBS := -lskarnet
 s6-svok: src/supervision/s6-svok.o ${LIBS6}
+s6-svperms: EXTRA_LIBS := -lskarnet
+s6-svperms: src/supervision/s6-svperms.o
 s6-svscan: EXTRA_LIBS := -lskarnet ${SYSCLOCK_LIB} ${SPAWN_LIB}
 s6-svscan: src/supervision/s6-svscan.o
 s6-svscanctl: EXTRA_LIBS := -lskarnet
diff --git a/package/modes b/package/modes
index 851c316..d8690ea 100644
--- a/package/modes
+++ b/package/modes
@@ -19,6 +19,7 @@ s6-svdt-clear			0755
 s6-svwait			0755
 s6-svlisten1			0755
 s6-svlisten			0755
+s6-svperms			0755
 s6-notifyoncheck		0755
 s6-permafailon			0755
 s6-applyuidgid			0700
diff --git a/package/targets.mak b/package/targets.mak
index 1a17cdc..ca447b8 100644
--- a/package/targets.mak
+++ b/package/targets.mak
@@ -20,6 +20,7 @@ s6-permafailon \
 s6-svwait \
 s6-svlisten1 \
 s6-svlisten \
+s6-svperms \
 s6-notifyoncheck \
 s6-envdir \
 s6-envuidgid \
diff --git a/src/libs6/ftrigw_fifodir_make.c b/src/libs6/ftrigw_fifodir_make.c
index 86e9310..df12e25 100644
--- a/src/libs6/ftrigw_fifodir_make.c
+++ b/src/libs6/ftrigw_fifodir_make.c
@@ -8,7 +8,7 @@
 int ftrigw_fifodir_make (char const *path, gid_t gid, int force)
 {
   mode_t m = umask(0) ;
-  if (mkdir(path, 0700) == -1)
+  if (mkdir(path, 0700) < 0)
   {
     struct stat st ;
     umask(m) ;
@@ -19,7 +19,7 @@ int ftrigw_fifodir_make (char const *path, gid_t gid, int force)
     if (!force) return 1 ;
   }
   else umask(m) ;
-  if ((gid != (gid_t)-1) && (chown(path, -1, gid) == -1)) return 0 ;
-  if (chmod(path, (gid != (gid_t)-1) ? 03730 : 01733) == -1) return 0 ;
+  if ((gid != (gid_t)-1) && (chown(path, -1, gid) < 0)) return 0 ;
+  if (chmod(path, gid != (gid_t)-1 ? 03730 : 01733) < 0) return 0 ;
   return 1 ;
 }
diff --git a/src/supervision/deps-exe/s6-supervise b/src/supervision/deps-exe/s6-supervise
index b1e57e4..34dc00e 100644
--- a/src/supervision/deps-exe/s6-supervise
+++ b/src/supervision/deps-exe/s6-supervise
@@ -1,3 +1,3 @@
-${LIBS6}
+libs6.a.xyzzy
 -lskarnet
 ${SYSCLOCK_LIB}
diff --git a/src/supervision/deps-exe/s6-svperms b/src/supervision/deps-exe/s6-svperms
new file mode 100644
index 0000000..e7187fe
--- /dev/null
+++ b/src/supervision/deps-exe/s6-svperms
@@ -0,0 +1 @@
+-lskarnet
diff --git a/src/supervision/s6-supervise.c b/src/supervision/s6-supervise.c
index a8be37d..a175f0a 100644
--- a/src/supervision/s6-supervise.c
+++ b/src/supervision/s6-supervise.c
@@ -40,9 +40,9 @@
 typedef enum trans_e trans_t, *trans_t_ref ;
 enum trans_e
 {
-  V_TIMEOUT, V_CHLD, V_TERM, V_HUP, V_QUIT,
-  V_a, V_b, V_q, V_h, V_k, V_t, V_i, V_1, V_2, V_f, V_F, V_p, V_c, V_y, V_r,
-  V_o, V_d, V_u, V_x, V_O, V_X
+  V_TIMEOUT, V_CHLD, V_TERM, V_HUP, V_QUIT, V_INT,
+  V_a, V_b, V_q, V_h, V_k, V_t, V_i, V_1, V_2, V_p, V_c, V_y, V_r,
+  V_o, V_d, V_u, V_x, V_O
 } ;
 
 typedef enum state_e state_t, *state_t_ref ;
@@ -140,14 +140,22 @@ static void bail (void)
   cont = 0 ;
 }
 
+static void sigint (void)
+{
+  pid_t pgid = getpgid(status.pid) ;
+  if (pgid == -1) strerr_warnwu1sys("getpgid") ;
+  else killpg(pgid, SIGINT) ;
+  bail() ;
+}
+
 static void closethem (void)
 {
-  close(0) ;
-  close(1) ;
-  close(2) ;
-  open_readb("/dev/null") ;
-  open_write("/dev/null") ; ndelay_off(1) ;
-  open_write("/dev/null") ; ndelay_off(2) ;
+  fd_close(0) ;
+  fd_close(1) ;
+  if (open_readb("/dev/null"))
+    strerr_warnwu2sys("open /dev/null for ", "reading") ;
+  else if (open_write("/dev/null") != 1 || ndelay_off(1) < 0)
+      strerr_warnwu2sys("open /dev/null for ", "writing") ;
 }
 
 static void killa (void)
@@ -226,24 +234,6 @@ static void failcoe (int fd)
   errno = e ;
 }
 
-static int maybesetsid (void)
-{
-  char buf[8] = "-------" ;
-  ssize_t r = openreadnclose("nosetsid", buf, 8) ;
-  if (r < 0)
-  {
-    if (errno != ENOENT) return 0 ;
-    setsid() ;
-  }
-  else
-  {
-    if (r == 8 && buf[7] == '\n') buf[--r] = 0 ;
-    if (r == 7 && !strncasecmp(buf, "setpgrp", 7))
-      setpgid(0, 0) ;
-  }
-  return 1 ;
-}
-
 static void trystart (void)
 {
   int p[2] ;
@@ -285,11 +275,7 @@ static void trystart (void)
       failcoe(p[1]) ;
       strerr_diefu1sys(127, "move notification descriptor") ;
     }
-    if (!maybesetsid())
-    {
-      failcoe(p[1]) ;
-      strerr_diefu1sys(127, "access ./nosetsid") ;
-    }
+    setsid() ;
     execv("./run", (char *const *)cargv) ;
     failcoe(p[1]) ;
     strerr_dieexec(127, "run") ;
@@ -407,7 +393,7 @@ static int uplastup_z (void)
     selfpipe_finish() ;
     fmt0[uint_fmt(fmt0, WIFSIGNALED(status.wstat) ? 256 : WEXITSTATUS(status.wstat))] = 0 ;
     fmt1[uint_fmt(fmt1, WTERMSIG(status.wstat))] = 0 ;
-    maybesetsid() ;
+    setsid() ;
     execv("./finish", cargv) ;
     _exit(127) ;
   }
@@ -481,17 +467,12 @@ static void up_u (void)
 static void up_x (void)
 {
   state = LASTUP ;
-}
-
-static void up_X (void)
-{
   closethem() ;
-  up_x() ;
 }
 
 static void up_term (void)
 {
-  up_x() ;
+  state = LASTUP ;
   up_d() ;
 }
 
@@ -522,12 +503,7 @@ static void finish_u (void)
 static void finish_x (void)
 {
   state = LASTFINISH ;
-}
-
-static void finish_X (void)
-{
   closethem() ;
-  finish_x() ;
 }
 
 static void lastfinish_z (void)
@@ -536,23 +512,23 @@ static void lastfinish_z (void)
   bail() ;
 }
 
-static action_t_ref const actions[5][26] =
-{
-  { &downtimeout, &nop, &bail, &bail, &bail,
-    &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop,
-    &down_o, &down_d, &down_u, &bail, &down_O, &bail },
-  { &uptimeout, &up_z, &up_term, &up_x, &up_X,
-    &killa, &killb, &killq, &killh, &killk, &killt, &killi, &kill1, &kill2, &nop, &nop, &killp, &killc, &killy, &killr,
-    &up_o, &up_d, &up_u, &up_x, &up_o, &up_X },
-  { &finishtimeout, &finish_z, &finish_x, &finish_x, &finish_X,
-    &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop,
-    &up_o, &down_d, &finish_u, &finish_x, &up_o, &finish_X },
-  { &uptimeout, &lastup_z, &up_d, &nop, &closethem,
-    &killa, &killb, &killq, &killh, &killk, &killt, &killi, &kill1, &kill2, &nop, &nop, &killp, &killc, &killy, &killr,
-    &up_o, &up_d, &nop, &nop, &up_o, &closethem },
-  { &finishtimeout, &lastfinish_z, &nop, &nop, &closethem,
-    &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop,
-    &nop, &nop, &nop, &nop, &nop, &closethem }
+static action_t_ref const actions[5][24] =
+{
+  { &downtimeout, &nop, &bail, &bail, &bail, &bail,
+    &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop,
+    &down_o, &down_d, &down_u, &bail, &down_O },
+  { &uptimeout, &up_z, &up_term, &up_x, &bail, &sigint,
+    &killa, &killb, &killq, &killh, &killk, &killt, &killi, &kill1, &kill2, &killp, &killc, &killy, &killr,
+    &up_o, &up_d, &up_u, &up_x, &up_o },
+  { &finishtimeout, &finish_z, &finish_x, &finish_x, &bail, &sigint,
+    &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop,
+    &up_o, &down_d, &finish_u, &finish_x, &up_o },
+  { &uptimeout, &lastup_z, &up_d, &closethem, &bail, &sigint,
+    &killa, &killb, &killq, &killh, &killk, &killt, &killi, &kill1, &kill2, &killp, &killc, &killy, &killr,
+    &up_o, &up_d, &nop, &nop, &up_o },
+  { &finishtimeout, &lastfinish_z, &nop, &closethem, &bail, &sigint,
+    &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop, &nop,
+    &nop, &nop, &nop, &nop, &nop }
 } ;
 
 
@@ -615,6 +591,9 @@ static inline void handle_signals (void)
       case SIGQUIT :
         (*actions[state][V_QUIT])() ;
         break ;
+      case SIGINT :
+        (*actions[state][V_INT])() ;
+        break ;
       default :
         strerr_dief1x(101, "internal error: inconsistent signal state. Please submit a bug-report.") ;
     }
@@ -631,8 +610,8 @@ static inline void handle_control (int fd)
     else if (!r) break ;
     else
     {
-      size_t pos = byte_chr("abqhkti12fFpcyroduxOX", 21, c) ;
-      if (pos < 21) (*actions[state][V_a + pos])() ;
+      size_t pos = byte_chr("abqhkti12pcyroduxO", 18, c) ;
+      if (pos < 18) (*actions[state][V_a + pos])() ;
     }
   }
 }
@@ -736,10 +715,11 @@ int main (int argc, char const *const *argv)
     {
       sigset_t set ;
       sigemptyset(&set) ;
+      sigaddset(&set, SIGCHLD) ;
       sigaddset(&set, SIGTERM) ;
       sigaddset(&set, SIGHUP) ;
       sigaddset(&set, SIGQUIT) ;
-      sigaddset(&set, SIGCHLD) ;
+      sigaddset(&set, SIGINT) ;
       if (selfpipe_trapset(&set) < 0) strerr_diefu1sys(111, "trap signals") ;
     }
     
diff --git a/src/supervision/s6-svc.c b/src/supervision/s6-svc.c
index 3e024d6..a189d24 100644
--- a/src/supervision/s6-svc.c
+++ b/src/supervision/s6-svc.c
@@ -28,7 +28,7 @@ int main (int argc, char const *const *argv)
     subgetopt_t l = SUBGETOPT_ZERO ;
     for (;;)
     {
-      int opt = subgetopt_r(argc, argv, "abqhkti12pcyroduxOXT:w:", &l) ;
+      int opt = subgetopt_r(argc, argv, "abqhkti12pcyroduxOT:w:", &l) ;
       if (opt == -1) break ;
       switch (opt)
       {
@@ -50,7 +50,6 @@ int main (int argc, char const *const *argv)
         case 'u' :
         case 'x' :
         case 'O' :
-        case 'X' :
         {
           if (datalen >= DATASIZE) strerr_dief1x(100, "too many commands") ;
           data[datalen++] = opt ;
diff --git a/src/supervision/s6-svperms.c b/src/supervision/s6-svperms.c
new file mode 100644
index 0000000..178ea08
--- /dev/null
+++ b/src/supervision/s6-svperms.c
@@ -0,0 +1,272 @@
+/* ISC license. */
+
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <pwd.h>
+#include <grp.h>
+
+#include <skalibs/types.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr2.h>
+
+#include <s6/s6-supervise.h>
+
+#define USAGE "s6-svperms [ -v ] [ -u | -g group | -G group | -o | -O group ] [ -e | -E group ] servicedir..."
+#define dieusage() strerr_dieusage(100, USAGE)
+
+static gid_t scangid (char const *s)
+{
+  if (s[0] == ':')
+  {
+    gid_t g ;
+    if (!gid0_scan(s+1, &g)) dieusage() ;
+    return g ;
+  }
+  else
+  {
+    struct group *gr ;
+    errno = 0 ;
+    gr = getgrnam(s) ;
+    if (!gr)
+    {
+      if (errno) strerr_diefu1sys(111, "getgrnam") ;
+      else strerr_diefu3x(100, "find entry for ", s, " in group database") ;
+    }
+    return gr->gr_gid ;
+  }
+}
+
+static char *gidname (gid_t gid)
+{
+  struct group *gr ;
+  errno = 0 ;
+  gr = getgrgid(gid) ;
+  if (!gr)
+  {
+    static char fmt[GID_FMT] ;
+    fmt[gid_fmt(fmt, gid)] = 0 ;
+    if (errno) strerr_warnwu2sys("getgrgid ", fmt) ;
+    return fmt ;
+  }
+  return gr->gr_name ;
+}
+
+static void out (char const *s)
+{
+  if (buffer_puts(buffer_1, s) < 0)
+    strerr_diefu1sys(111, "write to stdout") ;
+}
+
+static inline int printsupervise (char const *dir)
+{
+  struct stat st ;
+  size_t len = strlen(dir) ;
+  char fn[len + sizeof(S6_SUPERVISE_CTLDIR) + 9] ;
+  memcpy(fn, dir, len) ;
+  memcpy(fn + len, "/" S6_SUPERVISE_CTLDIR, sizeof(S6_SUPERVISE_CTLDIR) + 1) ;
+  if (stat(fn, &st) < 0)
+  {
+    strerr_warnwu2sys("stat ", fn) ;
+    return 1 ;
+  }
+  if (!S_ISDIR(st.st_mode))
+  {
+    strerr_warnw2x(fn, " is not a directory") ;
+    return 1 ;
+  }
+  if (st.st_mode & 05066 || (st.st_mode & 0700) != 0700 || ((st.st_mode & 0001) && !(st.st_mode & 0010)))
+  {
+    char fmt[UINT_OFMT] ;
+    fmt[uint_ofmt(fmt, st.st_mode & 07777)] = 0 ;
+    strerr_warnw3x(fn, " has incorrect permissions: ", fmt) ;
+    return 1 ;
+  }
+  out(dir) ;
+  out(" status: ") ;
+  if (st.st_mode & 0011)
+  {
+    if (st.st_mode & 0001) buffer_puts(buffer_1, "public") ;
+    else
+    {
+      out("group ") ;
+      out(gidname(st.st_gid)) ;
+    }
+  }
+  else out("owner") ;
+  out("\n") ;
+  memcpy(fn + len + sizeof(S6_SUPERVISE_CTLDIR), "/control", 9) ;
+  if (stat(fn, &st) < 0)
+  {
+    strerr_warnwu2sys("stat ", fn) ;
+    return 1 ;
+  }
+  if (!S_ISFIFO(st.st_mode))
+  {
+    strerr_warnw2x(fn, " is not a named pipe") ;
+    return 1 ;
+  }
+  if (st.st_mode & 0157)
+  {
+    char fmt[UINT_OFMT] ;
+    fmt[uint_ofmt(fmt, st.st_mode & 07777)] = 0 ;
+    strerr_warnw3x(fn, " has incorrect permissions: ", fmt) ;
+    return 1 ;
+  }
+  out(dir) ;
+  out(" control: ") ;
+  if (st.st_mode & 0020)
+  {
+    out("group ") ;
+    out(gidname(st.st_gid)) ;
+  }
+  else out("owner") ;
+  out("\n") ;
+  return 0 ;
+}
+
+static inline int printevent (char const *dir)
+{
+  struct stat st ;
+  size_t len = strlen(dir) ;
+  char fn[len + sizeof(S6_SUPERVISE_EVENTDIR) + 1] ;
+  memcpy(fn, dir, len) ;
+  memcpy(fn + len, "/" S6_SUPERVISE_EVENTDIR, sizeof(S6_SUPERVISE_EVENTDIR) + 1) ;
+  if (stat(fn, &st) < 0)
+  {
+    strerr_warnwu2sys("stat ", fn) ;
+    return 1 ;
+  }
+  if (!S_ISDIR(st.st_mode))
+  {
+    strerr_warnw2x(fn, " is not a directory") ;
+    return 1 ;
+  }
+  if ((st.st_mode & 07777) != 01733 && (st.st_mode & 07777) != 03730)
+  {
+    char fmt[UINT_OFMT] ;
+    fmt[uint_ofmt(fmt, st.st_mode & 07777)] = 0 ;
+    strerr_warnw3x(fn, " has incorrect permissions: ", fmt) ;
+    return 1 ;
+  }
+  out(dir) ;
+  out(" events: ") ;
+  if ((st.st_mode & 07777) == 03730)
+  {
+    out("group ") ;
+    out(gidname(st.st_gid)) ;
+  }
+  else out("public") ;
+  out("\n") ;
+  return 0 ;
+}
+
+static gid_t primarygid (char const *fn)
+{
+  struct passwd *pw ;
+  struct stat st ;
+  if (stat(fn, &st) < 0) strerr_diefu2sys(111, "stat ", fn) ;
+  errno = 0 ;
+  pw = getpwuid(st.st_uid) ;
+  if (!pw)
+  {
+    strerr_warnwu3sys("determine primary gid for the owner of ", fn, " (using root instead)") ;
+    return 0 ;
+  }
+  else return pw->pw_gid ;
+}
+
+static inline void modsupervise (char const *dir, unsigned int what, gid_t gid)
+{
+  size_t len = strlen(dir) ;
+  gid_t cgid  = 0 ;
+  mode_t mode = 0700 ;
+  char fn[len + sizeof(S6_SUPERVISE_CTLDIR) + 9] ;
+  memcpy(fn, dir, len) ;
+  memcpy(fn + len, "/" S6_SUPERVISE_CTLDIR, sizeof(S6_SUPERVISE_CTLDIR) + 1) ;
+  switch (what & 3)
+  {
+    case 0 : cgid = primarygid(fn) ; mode = 0700 ; break ;
+    case 1 : cgid = gid ; mode = 0710 ; break ;
+    case 2 : cgid = primarygid(fn) ; mode = 0711 ; break ;
+  }
+  if (chown(fn, -1, cgid) < 0)
+    strerr_diefu2sys(111, "chown ", fn) ;
+  if (chmod(fn, mode) < 0)
+    strerr_diefu2sys(111, "chmod ", fn) ;
+  memcpy(fn + len + sizeof(S6_SUPERVISE_CTLDIR), "/control", 9) ;
+  if (what & 4) mode = 0620 ;
+  else
+  {
+    gid = primarygid(fn) ;
+    mode = 0600 ;
+  }
+  if (chown(fn, -1, gid) < 0)
+    strerr_diefu2sys(111, "chown ", fn) ;
+  if (chmod(fn, mode) < 0)
+    strerr_diefu2sys(111, "chmod ", fn) ;
+}
+
+static inline void modevent (char const *dir, gid_t gid)
+{
+  size_t len = strlen(dir) ;
+  mode_t mode ;
+  char fn[len + sizeof(S6_SUPERVISE_EVENTDIR) + 1] ;
+  memcpy(fn, dir, len) ;
+  memcpy(fn + len, "/" S6_SUPERVISE_EVENTDIR, sizeof(S6_SUPERVISE_EVENTDIR) + 1) ;
+  if (gid == (gid_t)-1)
+  {
+    gid = primarygid(fn) ;
+    mode = 01733 ;
+  }
+  else mode = 03730 ;
+  if (chown(fn, -1, gid) < 0)
+    strerr_diefu2sys(111, "chown ", fn) ;
+  if (chmod(fn, mode) < 0)
+    strerr_diefu2sys(111, "chmod ", fn) ;
+}
+
+int main (int argc, char const *const *argv)
+{
+  int e = 0 ;
+  gid_t gid = -1 ;
+  gid_t eventgid = -1 ;
+  int rw = 0 ;
+  unsigned int what = 0 ;
+  PROG = "s6-svperms" ;
+  {
+    subgetopt_t l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "vug:G:oO:eE:", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'v' : rw |= 1 ; break ;
+        case 'u' : rw |= 2 ; what = 0 ; break ;
+        case 'g' : rw |= 2 ; what = 1 ; gid = scangid(l.arg) ; break ;
+        case 'G' : rw |= 2 ; what = 5 ; gid = scangid(l.arg) ; break ;
+        case 'o' : rw |= 2 ; what = 2 ; break ;
+        case 'O' : rw |= 2 ; what = 6 ; gid = scangid(l.arg) ; break ;
+        case 'e' : rw |= 4 ; eventgid = -1 ; break ;
+        case 'E' : rw |= 4 ; eventgid = scangid(l.arg) ; break ;
+        default : dieusage() ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+  }
+  if (!argc) dieusage() ;
+
+  if (!rw) rw = 1 ;
+  for (; *argv ; argv++)
+  {
+    if (rw & 2) modsupervise(*argv, what, gid) ;
+    if (rw & 4) modevent(*argv, eventgid) ;
+    if (rw & 1) { e |= printsupervise(*argv) ; e |= printevent(*argv) ; }
+  }
+  if (rw & 1 && !buffer_flush(buffer_1))
+    strerr_diefu1sys(111, "write to stdout") ;
+  return e ;
+}
diff --git a/src/supervision/s6-svscan.c b/src/supervision/s6-svscan.c
index 0a3c286..1d17a3a 100644
--- a/src/supervision/s6-svscan.c
+++ b/src/supervision/s6-svscan.c
@@ -497,7 +497,7 @@ static inline int control_init (void)
       strerr_dief1x(100, S6_SVSCAN_CTLDIR " exists and is not a directory") ;
   }
 
-  fdlck = open(LCK, O_WRONLY | O_NONBLOCK | O_CREAT | O_CLOEXEC, 0644) ;
+  fdlck = open(LCK, O_WRONLY | O_NONBLOCK | O_CREAT | O_CLOEXEC, 0600) ;
   if (fdlck < 0) strerr_diefu1sys(111, "open " LCK) ;
   r = fd_lock(fdlck, 1, 1) ;
   if (r < 0) strerr_diefu1sys(111, "lock " LCK) ;