/* Limited tmpfiles.d implementation * * Copyright (c) 2023 Joachim Wiberg * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "config.h" /* Generated by configure script */ #include #include "finit.h" #include "helpers.h" #include "tmpfiles.h" #include "util.h" static int glob_do(const char *path, int (*cb)(const char *)) { int rc = 0; glob_t gl; rc = glob(path, GLOB_NOESCAPE, NULL, &gl); if (rc) { if (rc == GLOB_NOMATCH) { errno = ENOENT; return -1; } return 0; } for (size_t i = 0; i < gl.gl_pathc; i++) rc += cb(gl.gl_pathv[i]); return rc; } static int parse_mm(char *arg, int *major, int *minor) { char *ptr; if (!arg) { inval: errno = EINVAL; return -1; } ptr = strchr(arg, ':'); if (!ptr) goto inval; *ptr++ = 0; *major = atoi(arg); *minor = atoi(ptr); if (!*major || !*minor) return -1; return 0; } static int do_delete(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw) { if (ftw->level == 0) return 1; if (remove(fpath)) warn("Failed removing condition %s", fpath); return 0; } static int rmrf(const char *path) { if (!fisdir(path)) return 0; nftw(path, do_delete, 20, FTW_DEPTH); if (remove(path) && errno != ENOENT) warn("Failed removing path %s", path); return 0; } static void mkparent(char *path, mode_t mode) { mkpath(dirname(strdupa(path)), mode); } static char *write_hex(FILE *fp, char *p) { unsigned char val; char num[4], *ptr; int i, len = 0; for (i = 0; p[i] && len < 2; i++) { char c = p[i]; if ((c >= '0' && c <= '7') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) num[len++] = c; else break; } num[len] = 0; if (len == 0) { fputs("x", fp); return p; } errno = 0; val = strtoul(num, &ptr, 16); if (errno || ptr == num) goto end; fputc(val & 0xff, fp); end: return &p[i]; } static char *write_num(FILE *fp, char *p) { unsigned char val; char num[4], *ptr; int i, len = 0; for (i = 0; p[i] && len < 3; i++) { char c = p[i]; if (c >= '0' && c <= '7') num[len++] = c; else break; } num[len] = 0; if (len == 0) return p; errno = 0; val = strtoul(num, &ptr, 8); if (errno || ptr == num) goto end; fputc(val & 0xff, fp); end: return &p[i]; } static void write_arg(FILE *fp, char *arg) { char *p; if (!arg) return; while (*arg && (p = strchr(arg, '\\'))) { fwrite(arg, sizeof(char), p - arg, fp); *p++ = 0; switch (*p) { case '\\': fputc('\\', fp); arg = p + 1; break; case '\'': fputc('\'', fp); arg = p + 1; break; case '"': fputc('"', fp); arg = p + 1; break; case 'a': fputc('\a', fp); arg = p + 1; break; case 'b': fputc('\b', fp); arg = p + 1; break; case 'e': fputc('\e', fp); arg = p + 1; break; case 'n': fputc('\n', fp); arg = p + 1; break; case 't': fputc('\t', fp); arg = p + 1; break; case 'x': arg = write_hex(fp, &p[1]); break; case '0' ... '7': arg = write_num(fp, p); break; default: fputc(*p, fp); arg = p + 1; break; } } fputs(arg, fp); } /* * The configuration format is one line per path, containing type, path, * mode, ownership, age, and argument fields. The lines are separated by * newlines, the fields by whitespace: * * #Type Path Mode User Group Age Argument… * d /run/user 0755 root root 10d - * L /tmp/foobar - - - - /dev/null * * https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html */ static void tmpfiles(char *line) { char *type, *path, *token, *user, *group, *age, *arg; char *dst = NULL, *opts = ""; struct stat st, ast; int major, minor; int strc, rc = 0; mode_t mode = 0; FILE *fp = NULL; char buf[1024]; glob_t gl; type = strtok(line, "\t "); if (!type) return; path = strtok(NULL, "\t "); if (!path) return; if (strchr(path, '%')) { errx(1, "Path name specifiers unsupported, skipping."); return; } token = strtok(NULL, "\t "); if (token) { errno = 0; mode = strtoul(token, &arg, 8); if (errno || arg == token) mode = 0; } user = strtok(NULL, "\t "); if (!user || !strcmp(user, "-")) user = "root"; group = strtok(NULL, "\t "); if (!group || !strcmp(group, "-")) group = "root"; age = strtok(NULL, "\t "); (void)age; /* unused atm. */ arg = strtok(NULL, "\n"); strc = stat(path, &st); switch (type[0]) { case 'b': rc = parse_mm(arg, &major, &minor); if (rc) break; if (!strc) { if (type[1] != '+') break; erase(path); } mkparent(path, 0755); rc = blkdev(path, mode ?: 0644, major, minor); break; case 'c': rc = parse_mm(arg, &major, &minor); if (rc) break; if (!strc) { if (type[1] != '+') break; erase(path); } mkparent(path, 0755); rc = chardev(path, mode ?: 0644, major, minor); break; case 'C': if (!arg) { paste(buf, sizeof(buf), "/usr/share/factory", path); arg = buf; } if (!strc) { struct dirent **namelist; int num; if (!S_ISDIR(st.st_mode)) break; num = scandir(path, &namelist, NULL, NULL); free(namelist); if (num >= 3) break; /* not empty */ } if (fisdir(arg) && !fisslashdir(arg)) { size_t len = strlen(arg) + 2; dst = malloc(len); if (!dst) break; snprintf(dst, len, "%s/", arg); arg = dst; } mkparent(path, 0755); rc = rsync(arg, path, LITE_FOPT_KEEP_MTIME, NULL); if (rc && errno == ENOENT) rc = 0; break; case 'd': case 'D': mkparent(path, 0755); rc = mksubsys(path, mode ?: 0755, user, group); break; case 'e': if (glob(path, GLOB_NOESCAPE, NULL, &gl)) break; for (size_t i = 0; i < gl.gl_pathc; i++) rc += mksubsys(gl.gl_pathv[i], mode ?: 0755, user, group); break; case 'f': case 'F': mkparent(path, 0755); if (type[1] == '+' || type[0] == 'F') { /* f+/F will create or truncate the file */ if (!arg) { rc = create(path, mode ?: 0644, user, group); break; } fp = fopen(path, "w+"); } else { /* f will create the file if it doesn't exist */ if (strc) fp = fopen(path, "w"); } if (fp) { write_arg(fp, arg); rc = fclose(fp); } break; case 'l': /* Finit extension, like 'L' but only if target exists */ if (!arg) { paste(buf, sizeof(buf), "/usr/share/factory", path); if (stat(buf, &ast)) break; } else if (arg[0] != '/') { char *tmp; tmp = dirname(strdupa(path)); paste(buf, sizeof(buf), tmp, arg); dst = realpath(buf, NULL); if (!dst) break; if (stat(dst, &ast)) break; } else { if (stat(arg, &ast)) break; } /* fallthrough */ case 'L': if (!strc) { if (type[1] != '+') break; rmrf(path); } mkparent(path, 0755); if (!arg) { paste(buf, sizeof(buf), "/usr/share/factory", path); arg = buf; } rc = ln(arg, path); if (rc && errno == EEXIST) rc = 0; break; case 'p': if (!strc) { if (type[1] != '+') break; erase(path); } mkparent(path, 0755); rc = mkfifo(path, mode ?: 0644); break; case 'r': rc = glob_do(path, erase); if (rc && errno == ENOENT) rc = 0; break; case 'R': rc = glob_do(path, rmrf); break; case 'w': if (!arg) break; if (glob(path, GLOB_NOESCAPE, NULL, &gl)) break; for (size_t i = 0; i < gl.gl_pathc; i++) { fp = fopen(gl.gl_pathv[i], type[1] == '+' ? "a" : "w"); if (fp) { write_arg(fp, arg); rc = fclose(fp); } } break; case 'Z': opts = "-R"; /* fallthrough */ case 'z': if (!whichp("restorecon")) break; if (glob(path, GLOB_NOESCAPE, NULL, &gl)) break; for (size_t i = 0; i < gl.gl_pathc; i++) { snprintf(buf, sizeof(buf), "restorecon %s %s", opts, gl.gl_pathv[i]); run(buf, "restorecon"); } break; default: errx(1, "Unsupported tmpfiles command '%s'", type); return; } if (dst) free(dst); if (rc) warn("Failed %s operation on path %s", type, path); } /* * Only the three last tmpfiles.d/ directories are defined in * tmpfiles.d(5) as system search paths. Finit adds two more * before that to have Finit specific ones sorted first, and * a configure prefix specific one after that for user needs. */ void tmpfilesd(void) { /* in priority order */ char *dir[] = { FINIT_TMPFILES "/*.conf", TMPFILES_PATH_ "/*.conf", "/usr/lib/tmpfiles.d/*.conf", "/run/tmpfiles.d/*.conf", "/etc/tmpfiles.d/*.conf", /* local admin overrides */ }; int flags = GLOB_NOESCAPE; glob_t gl; size_t i; for (i = 0; i < NELEMS(dir); i++) { glob(dir[i], flags, NULL, &gl); flags |= GLOB_APPEND; } for (i = 0; i < gl.gl_pathc; i++) { char *fn = gl.gl_pathv[i]; size_t j; FILE *fp; /* check for overrides */ for (j = i + 1; j < gl.gl_pathc; j++) { if (strcmp(basename(fn), basename(gl.gl_pathv[j]))) continue; fn = NULL; break; } if (!fn) continue; /* skip, override exists */ fp = fopen(fn, "r"); if (!fp) continue; // info("Parsing %s ...", fn); while (!feof(fp)) { char *line; line = fparseln(fp, NULL, NULL, NULL, FPARSELN_UNESCCOMM); if (!line) continue; tmpfiles(line); } fclose(fp); } globfree(&gl); } /** * Local Variables: * indent-tabs-mode: t * c-file-style: "linux" * End: */