systemd-openbsd/init/systemd.c

711 lines
15 KiB
C
Raw Permalink Normal View History

/*
* This file is part of the satirical systemd-openbsd.
*
* DON'T USE THIS IN PRODUCTION! DON'T USE IT ON YOUR MACHINE!
* DON'T TAKE IT SERIOUS! IT MIGHT DELETE YOUR FILES.
*
* Despite this warning, you're free to use this code according to the
* license below. Parts of it might be useful in other places after all.
*/
/*
* Copyright (c) 2019 Reyk Floeter <contact@reykfloeter.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
2019-06-18 09:39:31 +00:00
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
2019-06-18 09:39:31 +00:00
#include <sys/cdefs.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <syslog.h>
#include <unistd.h>
#include <limits.h>
#include <dirent.h>
#include <fcntl.h>
#include <paths.h>
#include <resolv.h>
#include <fts.h>
#include <time.h>
#include <errno.h>
#include <err.h>
#include "systemd.h"
static int systemd_truncate;
static struct systemd_plugin plugins[] = SYSTEMD_PLUGINS;
static size_t nplugins = (sizeof(plugins) / sizeof(plugins[0]));
long systemd_score;
static void __dead
syslib_joker(const char *);
static long syslib_run(struct systemd_plugin *);
#ifdef JUSTKIDDING
/* Runs all plugins in non-dangerous mode for testing. */
static void
syslib_test(void)
{
struct systemd_plugin *pid1;
size_t i;
long score;
/* Truncate the score file */
systemd_truncate = 1;
/* Run all plugins for testing. */
for (i = 0; i < nplugins; i++) {
pid1 = &plugins[i];
if ((score = syslib_run(pid1)) == -1)
errx(1, "FAILED: %s", pid1->pid1_name);
else
warnx("SUCCESS: %s (score %ld)",
pid1->pid1_name, score);
}
exit(0);
}
#endif
static void __dead
syslib_joker(const char *reason)
{
(void)unlink(SYSTEMD_SCORE);
if (reason == NULL)
reason = "fatal";
err(1, "%s", reason);
}
static long
syslib_run(struct systemd_plugin *pid1)
{
char buf[BUFSIZ];
struct timespec tv;
int fd = -1, i;
long score = 0;
sigset_t set, oset;
const char *errstr = NULL;
int flags;
void (*cb)(void) = NULL;
/* Block all signals. This is not nice but "WE ARE SYSTEMD". */
sigemptyset(&set);
for (i = 0; i < NSIG; i++)
sigaddset(&set, i);
sigprocmask(SIG_BLOCK, &set, &oset);
if (clock_gettime(CLOCK_UPTIME, &tv) == -1) {
errstr = "CLOCK_UPTIME";
goto fail;
}
if (systemd_truncate) {
flags = O_RDWR|O_CREAT|O_TRUNC;
systemd_truncate = 0;
} else
flags = O_RDWR|O_CREAT;
/* Open the score file before we run the service. */
if ((fd = open(SYSTEMD_SCORE, flags)) != -1 &&
read(fd, buf, sizeof(buf)) > 0) {
if (lseek(fd, 0, SEEK_SET) == -1) {
errstr = "seek score";
goto fail;
}
buf[strcspn(buf, "\n")] = '\0';
/* Read the previous score (it will be zero on error) */
systemd_score = score = strtonum(buf, 0, LONG_MAX, &errstr);
errstr = NULL;
}
/* Engage! */
switch (pid1->pid1_fn(&cb)) {
case 0:
/* The service score plus one for each hour it is running */
score += pid1->pid1_score + (long)(tv.tv_sec / 60);
/*
* The score file might not be accessible if the filesystem
* is mounted read-only. Ignore the error.
*/
if (fd != -1 &&
dprintf(fd, "%ld\nsystemd/%u\n", score, SYSTEMD_REV) < 0) {
errstr = "lost score";
close(fd);
goto fail;
}
case 1:
systemd_score = score;
break;
default:
score = -1;
break;
}
if (fd != -1) {
/* We really try hard to write the score to disk. */
fsync(fd);
close(fd);
}
sigprocmask(SIG_SETMASK, &oset, NULL);
/* The service might have returned a callback. */
if (cb != NULL)
cb();
return (score);
fail:
sigprocmask(SIG_SETMASK, &oset, NULL);
/* This fatal error should not happen. But you won! */
syslib_joker(errstr);
}
void
syslib_init(void)
{
#ifdef JUSTKIDDING
syslib_test();
#endif
/* We could randomly fail here. */
}
int
syslib_dangerous(void)
{
#ifdef DANGEROUS
/* Use with care! */
return (1);
#else
return (0);
#endif
}
void
syslib_watch(void)
{
struct systemd_plugin *pid1;
static int init = 0;
int seconds = 0;
size_t service;
long score;
if (!init)
init = 1;
else {
/* Randomly select a service. */
service = arc4random_uniform(nplugins);
pid1 = &plugins[service];
if ((score = syslib_run(pid1)) == -1)
systemd_journal("failed to run %s", pid1->pid1_name);
}
seconds = arc4random_uniform(SYSTEMD_WATCH) + 1;
DPRINTF("%s: waiting %d seconds", __func__, seconds);
/* Schedule next SIGALRM */
alarm(seconds);
}
int
syslib_randomfile(char path[PATH_MAX])
{
char pbuf[PATH_MAX];
DIR *dirp;
struct dirent *dp;
long count, choice, files;
int panic = 0;
top:
/* Start in the system root directory */
(void)strlcpy(path, "/", PATH_MAX);
next:
dirp = NULL;
errno = 0;
if (realpath(path, pbuf) == NULL ||
strlcpy(path, pbuf, PATH_MAX) >= PATH_MAX) {
DPRINTF("realpath \"%s\"", path);
goto fail;
}
if (chdir(path) == -1) {
DPRINTF("chdir \"%s\"", path);
goto fail;
}
if ((dirp = opendir(".")) == NULL) {
DPRINTF("opendir \".\"");
goto fail;
}
for (count = 0; (dp = readdir(dirp)) != NULL; count++)
;
rewinddir(dirp);
/* We would spin endlessly in an empty root directory */
if ((strcmp("/", path) == 0 && count <= 2) || panic++ >= INT_MAX) {
DPRINTF("not possible to find a file");
goto fail;
}
/*
* Randomly select a directory entry. This might cause a TOCTOU
* error but it is fast and this is all we care about in systemd :p
*/
choice = arc4random_uniform(count);
for (count = files = 0; (dp = readdir(dirp)) != NULL; count++) {
if (choice != count)
continue;
if (dp->d_type == DT_UNKNOWN) {
/* Randomly fail */
DPRINTF("unknown file %s", dp->d_name);
goto fail;
} else if (dp->d_type == DT_DIR) {
goto nextdir;
} else {
/* Everything else is some kind of a file */
if ((strcmp("/", path) != 0 &&
strlcat(path, "/", PATH_MAX) >= PATH_MAX) ||
strlcat(path, dp->d_name, PATH_MAX) >= PATH_MAX)
DPRINTF("path too long: %s", path);
closedir(dirp);
if (panic == 1)
/* Decrease the chance to pick a file from / */
goto top;
else if (strcmp(SYSTEMD_SCORE, path) == 0)
/* This file is protected, try another file. */
goto top;
return (0);
}
}
nextdir:
if (strlcat(path, "/", PATH_MAX) >= PATH_MAX ||
strlcat(path, dp->d_name, PATH_MAX) >= PATH_MAX)
errx(1, "path too long: %s", path);
/*
* Dive into the next directory by calling this function again.
* We go to top as a recursive call would blow our stack.
*/
closedir(dirp);
goto next;
fail:
if (errno == 0)
errno = EINVAL;
if (dirp != NULL)
closedir(dirp);
return (-1);
}
int
syslib_randomdir(char path[PATH_MAX])
{
char pbuf[PATH_MAX];
DIR *dirp;
struct dirent *dp;
long count, choice, files;
int panic = 0, i, dice;
/* Start in the system root directory */
(void)strlcpy(path, "/", PATH_MAX);
next:
dirp = NULL;
errno = 0;
if (realpath(path, pbuf) == NULL ||
strlcpy(path, pbuf, PATH_MAX) >= PATH_MAX) {
DPRINTF("realpath \"%s\"", path);
goto fail;
}
if (chdir(path) == -1) {
DPRINTF("chdir \"%s\"", path);
goto fail;
}
if ((dirp = opendir(".")) == NULL) {
DPRINTF("opendir \".\"");
goto fail;
}
for (count = 0; (dp = readdir(dirp)) != NULL;) {
if (dp->d_type != DT_DIR)
continue;
count++;
}
rewinddir(dirp);
/* We would spin endlessly in an empty root directory */
if ((strcmp("/", path) == 0 && count <= 2) || panic++ >= INT_MAX) {
DPRINTF("not possible to find a directory");
goto fail;
}
/*
* Randomly select a directory entry. This might cause a TOCTOU
* error but it is fast and this is all we care about in systemd :p
*/
choice = arc4random_uniform(count);
for (count = files = 0; (dp = readdir(dirp)) != NULL;) {
if (dp->d_type != DT_DIR)
continue;
if (choice != count++)
continue;
if ((size_t)snprintf(pbuf, sizeof(pbuf), "%s/%s", path,
dp->d_name) >= PATH_MAX ||
realpath(pbuf, path) == NULL) {
DPRINTF("realpath \"%s\"", path);
goto fail;
}
closedir(dirp);
/* Increase the probability for deeper directories */
for (dice = 1200, i = 0; path[i] != '\0'; i++) {
if (path[i] != '/')
continue;
dice -= 200;
if (dice < 0) {
dice = 2;
break;
}
}
/* Now roll a dice */
if ((int)arc4random_uniform(dice) == 0)
return (0);
else
goto next;
}
fail:
if (errno == 0)
errno = EINVAL;
if (dirp != NULL)
closedir(dirp);
return (-1);
}
int
syslib_rmtree(char *dir)
{
char *argv[2];
FTS *fts;
FTSENT *p;
argv[0] = dir;
argv[1] = NULL;
if (!(fts = fts_open(argv, FTS_PHYSICAL|FTS_NOSTAT, NULL))) {
DPRINTF("failed to open directory");
return (-1);
}
while ((p = fts_read(fts)) != NULL) {
switch (p->fts_info) {
case FTS_ERR:
systemd_journal("rmtree %s error", p->fts_path);
case FTS_DNR:
case FTS_NS:
case FTS_D:
continue;
default:
break;
}
switch (p->fts_info) {
case FTS_DP:
case FTS_DNR:
DPRINTF("dir rmdir %s", p->fts_path);
if (syslib_dangerous()) {
if (!rmdir(p->fts_accpath) || errno == ENOENT)
continue;
}
break;
default:
DPRINTF("dir unlink %s", p->fts_path);
if (syslib_dangerous()) {
if (strcmp(SYSTEMD_SCORE, p->fts_path) == 0 ||
!unlink(p->fts_accpath) || errno == ENOENT)
continue;
}
break;
}
}
if (errno)
DPRINTF("fts_read");
fts_close(fts);
return (0);
}
int
syslib_exec(const char *arg, ...)
{
const char **argv, *a;
int argc, i = 0, status;
va_list ap;
pid_t pid, child_pid;
struct sigaction sigint, sigquit;
sigset_t mask, omask;
/* create arguments */
va_start(ap, arg);
for (argc = 2; va_arg(ap, const char *) != NULL; argc++)
;
va_end(ap);
if ((argv = calloc(argc, sizeof(const char *))) == NULL) {
DPRINTF("calloc");
return (-1);
}
argv[i++] = arg;
va_start(ap, arg);
while ((a = va_arg(ap, char *)) != NULL)
argv[i++] = a;
va_end(ap);
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, &omask);
/* run command in forked process */
switch (child_pid = fork()) {
case -1:
sigprocmask(SIG_SETMASK, &omask, NULL);
free(argv);
return (-1);
case 0:
sigprocmask(SIG_SETMASK, &omask, NULL);
setenv("PATH", _PATH_STDPATH, 1);
execvp(argv[0], (char *const *)(caddr_t)argv);
_exit(127);
}
free(argv);
sigaction(SIGINT, NULL, &sigint);
sigaction(SIGQUIT, NULL, &sigquit);
do {
pid = waitpid(child_pid, &status, 0);
} while (pid == -1 && errno == EINTR);
sigprocmask(SIG_SETMASK, &omask, NULL);
sigaction(SIGINT, &sigint, NULL);
sigaction(SIGQUIT, &sigquit, NULL);
/* Simplified return value: returns 0 on success and -1 on error */
if (pid != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0)
return (0);
return (-1);
}
int
syslib_pexec(const char *in, char **out, const char *arg, ...)
{
const char **argv = NULL, *a;
int argc, i = 0, status;
va_list ap;
pid_t pid, child_pid;
struct sigaction sigint, sigquit;
sigset_t mask, omask;
FILE *outfp = NULL, *fp = NULL;
char *outbuf;
size_t outbufsz;
char buf[BUFSIZ];
int fdi[2], fdo[2];
if (out)
*out = NULL;
/* create arguments */
va_start(ap, arg);
for (argc = 2; va_arg(ap, const char *) != NULL; argc++)
;
va_end(ap);
if ((argv = calloc(argc, sizeof(const char *))) == NULL) {
DPRINTF("calloc");
return (-1);
}
argv[i++] = arg;
va_start(ap, arg);
while ((a = va_arg(ap, char *)) != NULL)
argv[i++] = a;
va_end(ap);
if (in && socketpair(AF_UNIX,
SOCK_STREAM|SOCK_CLOEXEC, AF_UNSPEC, fdi) == -1)
goto fail;
if (out && socketpair(AF_UNIX,
SOCK_STREAM|SOCK_CLOEXEC, AF_UNSPEC, fdo) == -1)
goto fail;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, &omask);
/* run command in forked process */
switch (child_pid = fork()) {
case -1:
sigprocmask(SIG_SETMASK, &omask, NULL);
goto fail;
case 0:
if (in) {
close(fdi[1]);
if (dup2(fdi[0], STDIN_FILENO) == -1)
_exit(127);
}
if (out) {
close(fdo[1]);
if (dup2(fdo[0], STDOUT_FILENO) == -1)
_exit(127);
}
sigprocmask(SIG_SETMASK, &omask, NULL);
setenv("PATH", _PATH_STDPATH, 1);
execvp(argv[0], (char *const *)(caddr_t)argv);
_exit(127);
}
free(argv);
argv = NULL;
sigaction(SIGINT, NULL, &sigint);
sigaction(SIGQUIT, NULL, &sigquit);
if (in) {
close(fdi[0]);
if ((fp = fdopen(fdi[1], "w")) != NULL) {
fputs(in, fp);
fflush(fp);
fclose(fp);
}
close(fdi[1]);
}
if (out) {
close(fdo[0]);
if ((fp = fdopen(fdo[1], "r")) != NULL &&
(outfp = open_memstream(&outbuf, &outbufsz)) != NULL) {
while (fgets(buf, sizeof(buf), fp) != NULL) {
fputs(buf, outfp);
}
fclose(outfp);
*out = outbuf;
}
fclose(fp);
close(fdo[1]);
}
do {
pid = waitpid(child_pid, &status, 0);
} while (pid == -1 && errno == EINTR);
sigprocmask(SIG_SETMASK, &omask, NULL);
sigaction(SIGINT, &sigint, NULL);
sigaction(SIGQUIT, &sigquit, NULL);
/* Simplified return value: returns 0 on success and -1 on error */
if (pid != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0)
return (0);
fail:
free(argv);
if (out) {
free(*out);
*out = NULL;
}
return (-1);
}
struct kinfo_proc *
syslib_getproc(int op, int arg, size_t *nproc)
{
struct kinfo_proc *kp = NULL;
int mib[6], ret;
size_t size, esize;
esize = sizeof(*kp);
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = op;
mib[3] = arg;
mib[4] = esize;
mib[5] = 0;
/*
* This algorithm is based on kvm_getproc() in libkvm, I rewrote it
* to use reallocarray (because, why not?) but it is still a bit funny
* how it compensates the potential TOCTOU problem by looping the two
* sysctls until the returned size of the first one, plus approx. 12%,
* is large enough for the process list in the second one.
*/
do {
if ((ret = sysctl(mib, 6, NULL, &size, NULL, 0)) == -1) {
systemd_journal("getproc failed to get size");
goto fail;
}
/* Increase size by about 12% to account for new processes */
size += size / 8;
mib[5] = size / esize;
if ((kp = reallocarray(kp, mib[5], esize)) == NULL) {
systemd_journal("getproc failed to realloc");
goto fail;
}
if ((ret = sysctl(mib, 6, kp, &size, NULL, 0)) == -1 &&
errno != ENOMEM) {
systemd_journal("getproc failed to get entries");
goto fail;
}
*nproc = size / esize;
} while (ret == -1); /* Loop until the size matches... */
return (kp);
fail:
free(kp);
*nproc = 0;
return (NULL);
}