💾 Archived View for thrig.me › tech › daemon-exec-self.gmi captured on 2024-02-05 at 10:46:16. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-05-24)

-=-=-=-=-=-=-

Daemon Exec Self

Daemons may support the ability to relaunch themselves, perhaps to re-read configuration files, or maybe to clear a cache; the exact reason is not too important here. What is important is that the path to the daemon is fully qualified, or otherwise serviceable to the particular execv(3) call involved. execv(3) or something like that is unix for "replace yourself with some other program", possibly another instance of yourself. There are good reasons to do this, notably for exec wrappers that set environment variables then run something else. But less talk, more example code! Here we have sleepyd that, well, sleeps. Think of it as a bear that both hibernates and estivates. There is also a signal handler so we can exercise some execv(3) function; one might also want user input functionality so a keypress might restart the process, but that's more work, and perhaps is not suitable for an example daemon, unless the goal is to needlessly bloat the code. Maybe there could also be an e-mail interface, an embedded web daemon, ...

    // sleepyd.c -- sleeps, and restarts self if given the HUP signal
    #include <err.h>
    #include <limits.h>
    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    // an exec from a signal handler may be problematic (especially if
    // signal masks and a fork is involved) so instead we only flag that a
    // restart needs to happen
    volatile sig_atomic_t Restart_Self;
    void handler(int sig) {
        Restart_Self = 1;
    }
    int main(int argc, char *argv[]) {
        // probably a daemon should have this
        //if (chdir("/") == -1) err(1, "chdir");
    #ifdef __OpenBSD__
        if (pledge("exec stdio", NULL) == -1) err(1, "pledge failed");
    #endif
        signal(SIGHUP, handler);
        fprintf(stderr, "sleepy time for %d\n", getpid());
        while (1) {
            sleep(INT_MAX);
            if (Restart_Self) {
                execv(*argv, argv);
                err(1, "execv failed '%s'", *argv);
            }
        }
        exit(EXIT_FAILURE);
    }

sleepyd.c

which can be compiled (on OpenBSD 7.3--some linux systems might require various wacky defines be set), run,

    $ make sleepyd && ./sleepyd
    cc -O2 -pipe    -o sleepyd sleepyd.c
    sleepy time for 76613
    sleepy time for 76613
    sleepy time for 76613
    ^C

and then from elsewhere send it a HUP signal a few times. We also check that the process is indeed in a sleep via the state. The [s]leepy thing is a trick to prevent the regular expression from matching itself in the process table; pkill(1) is often a better interface these days.

    $ kill  -HUP 76613
    $ pkill -HUP sleepyd
    $ ps axo pid,command,state | sed -n '1p;/[s]leepy/p'
      PID COMMAND          STAT
     8145 vi sleepyd.c     S+p
    76613 ./sleepyd        S+p

However, there is a problem here. The program accidentally works, which is never a good idea when someone goes all "mission accomplished!!" and heads off on that big backcountry ski trip. The problem could manifest in one of several ways. The program could be moved to a PATH directory, or a chdir("/") call might be added because you do not want sleepyd dangling out on some NFS share that needs to be remounted. (Yes, you could maybe attach gdb(1) to the process, and sent it a chdir(2) call, but that's not covered here. Ah, the folly of youth.)

    $ cp sleepyd ~/bin
    $ sleepyd
    sleepy time for 70086
    sleepy time for 70086

No problem? No, this is also a bad test; the program still only accidentally works.

    ^C
    $ rm sleepyd
    $ sleepyd
    sleepy time for 61100
    sleepyd: execv failed 'sleepyd': No such file or directory

The previous test appeared to work because the execv happened in a directory that had a copy of sleepyd in it. sleepyd would also fail if the chdir line were uncommented before compilation, unless there was a /sleepyd executable on the system.

The general solution here is to always start daemons with a fully qualified path, so one would put something like the following into a hypothetical rc.d(8) startup script, or equivalent.

    daemon="/usr/local/sbin/sleepyd"

Another option would be to use execvp() instead of execv(). This has the advantage and disadvantage of running the first thing it happens to find in PATH, which could be fine, or perhaps a huge security issue if a malicious local user was able to get a binary they control first in PATH, and sleepdy is started as root. Given things like duplicate environment variables and other such problems, it's safer to always start daemons using a fully qualified path, and if you are testing on the command line, to always remember to use the fully qualified path to the binary.

One might also log *argv and getwd(3) at startup, which will show the daemon file and where the process is running. ps(1) or other tools can also expose this information.

http://man.openbsd.org/man1/pkill.1

http://man.openbsd.org/man3/execv.3

http://man.openbsd.org/man8/rc.d.8

Back to tech index

tags #unix