/* Limited tmpfiles.d implementation * * Copyright (c) 2025 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 #include #include #include #include #include #include #include #include #include #ifdef _LIBITE_LITE # include #else # include #endif #include "log.h" #include "util.h" int debug; int create_flag = 0; int clean_flag = 0; int remove_flag = 0; static int is_dir_empty(const char *path) { struct dirent **namelist = NULL; int num; num = scandir(path, &namelist, NULL, NULL); if (num < 0) return 0; for (int i = 0; i < num; i++) free(namelist[i]); free(namelist); return num >= 3; } 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) { (void)sb; (void)tflag; if (ftw->level == 0) return 1; if (remove(fpath) && errno != EBUSY) warn("Failed removing %s", fpath); return 0; } static int rmrf(const char *path) { if (!fisdir(path)) return 0; nftw(path, do_delete, 20, FTW_DEPTH | FTW_PHYS); 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); } /** * Parse age string and return age in seconds. * Returns 0 on invalid/empty age ("-" or NULL). * Examples: "10d" = 864000, "1w" = 604800, "2h" = 7200 */ static time_t parse_age(const char *age) { time_t val; char *end; if (!age || !strcmp(age, "-") || !strcmp(age, "0")) return 0; errno = 0; val = strtoul(age, &end, 10); if (errno || end == age) return 0; switch (*end) { case 'w': val *= 7; /* fallthrough */ case 'd': val *= 24; /* fallthrough */ case 'h': val *= 60; /* fallthrough */ case 'm': val *= 60; /* fallthrough */ case 's': case '\0': break; default: return 0; /* unknown suffix */ } return val; } /** * Parse user string - supports both names and numeric UIDs. * Returns UID on success, -1 on failure. */ static int parse_uid(const char *user) { long val; if (!user || !user[0]) return 0; /* Check if it's a numeric UID */ val = atonum(user); if (val >= 0) return val; /* Not numeric, look up by name */ return getuser(user, NULL); } /** * Parse group string - supports both names and numeric GIDs. * Returns GID on success, -1 on failure. */ static int parse_gid(const char *group) { long val; if (!group || !group[0]) return 0; /* Check if it's a numeric GID */ val = atonum(group); if (val >= 0) return val; /* Not numeric, look up by name */ return getgroup(group); } /* Globals for do_clean() callback - nftw doesn't support user data */ static time_t clean_age; static time_t clean_now; static int do_clean(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw) { int is_dir; /* Skip the root directory itself */ if (ftw->level == 0) return 0; is_dir = (tflag == FTW_D || tflag == FTW_DP); /* * Conservative cleanup matching systemd-tmpfiles behavior: * - Files: keep if ANY of atime, ctime, mtime is recent * - Directories: keep if ANY of atime, mtime is recent (ctime * excluded because cleanup itself updates directory ctime) */ if (clean_now - sb->st_mtime < clean_age) return 0; /* mtime is too new, keep it */ if (clean_now - sb->st_atime < clean_age) return 0; /* atime is too new, keep it */ if (!is_dir && clean_now - sb->st_ctime < clean_age) return 0; /* ctime is too new, keep it (files only) */ if (remove(fpath) && errno != ENOENT && errno != EBUSY) warn("Failed cleaning %s", fpath); return 0; } /* * 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 "); arg = strtok(NULL, "\n"); strc = stat(path, &st); // file and directory removal logic if (remove_flag) { switch (type[0]) { case 'b': case 'c': case 'C': case 'd': break; case 'D': if (fisdir(path)) { nftw(path, do_delete, 20, FTW_DEPTH | FTW_PHYS); } break; case 'e': case 'f': case 'F': case 'l': /* Finit extension, like 'L' but only if target exists */ case 'L': case 'p': 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': break; case 'X': case 'x': /* handled by clean_flag (--clean) */ break; case 'Z': case 'z': break; default: errx(1, "Unsupported tmpfiles command '%s'", type); return; } } /* age-based cleanup logic */ if (clean_flag) { time_t max_age = parse_age(age); /* Only process if age is specified */ if (max_age > 0) { switch (type[0]) { case 'd': case 'D': case 'e': if (!fisdir(path)) break; clean_age = max_age; clean_now = time(NULL); nftw(path, do_clean, 20, FTW_DEPTH | FTW_PHYS); break; case 'x': case 'X': /* exclusion patterns - not yet implemented */ break; default: break; } } } // file & directory creation logic if (create_flag) { 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 (fisdir(path) && !is_dir_empty(path)) break; 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': { int uid, gid; mode_t omask; mkparent(path, 0755); omask = umask(0); uid = parse_uid(user); if (uid >= 0) { gid = parse_gid(group); if (gid < 0) gid = 0; rc = makedir(path, mode ?: 0755); if (rc && errno == EEXIST) rc = chmod(path, mode ?: 0755); if (chown(path, uid, gid)) warn("Failed chown(%s, %d, %d)", path, uid, gid); } umask(omask); break; } case 'e': if (glob(path, GLOB_NOESCAPE, NULL, &gl)) break; for (size_t i = 0; i < gl.gl_pathc; i++) { char *p = gl.gl_pathv[i]; int uid, gid; /* e only adjusts existing directories */ if (!fisdir(p)) continue; uid = parse_uid(user); gid = parse_gid(group); if (gid < 0) gid = 0; if (mode) chmod(p, mode); if (uid >= 0 && chown(p, uid, gid)) warn("Failed chown(%s, %d, %d)", p, uid, gid); } 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; if (fisdir(path)) rmrf(path); else erase(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': case 'R': 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 'X': case 'x': /* handled by clean_flag (--clean) */ 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++) systemf("restorecon %s %s", opts, gl.gl_pathv[i]); 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); } static void process_file(const char *fn) { FILE *fp; fp = fopen(fn, "r"); if (!fp) { warn("Failed to open %s", fn); return; } while (!feof(fp)) { char *line; line = fparseln(fp, NULL, NULL, NULL, FPARSELN_UNESCCOMM); if (!line) continue; tmpfiles(line); free(line); } fclose(fp); } static int usage(int rc) { fprintf(stderr, "Usage: tmpfiles [OPTIONS] [CONFIGFILE...]\n" "\n" "Options:\n" " -C, --clean Clean files and directories based on age\n" " -c, --create Create files and directories\n" " -d, --debug Show developer debug messages\n" " -r, --remove Remove files and directories marked for removal\n" " -h, --help This help text\n" "\n" "If no CONFIGFILE is specified, all *.conf files in the standard\n" "tmpfiles.d directories are processed.\n" "\n"); return rc; } int main(int argc, char *argv[]) { struct option long_options[] = { { "clean", 0, NULL, 'C' }, { "create", 0, NULL, 'c' }, { "debug", 0, NULL, 'd' }, { "remove", 0, NULL, 'r' }, { "help", 0, NULL, 'h' }, { NULL, 0, NULL, 0 } }; int c; while ((c = getopt_long(argc, argv, "Ccdrh?", long_options, NULL)) != EOF) { switch(c) { case 'C': clean_flag = 1; break; case 'c': create_flag = 1; break; case 'd': debug = 1; break; case 'r': remove_flag = 1; break; case 'h': case '?': return usage(0); default: return usage(1); } } if (create_flag + clean_flag + remove_flag == 0) { fprintf(stderr, "You need to specify at least one of --clean, --create, or --remove.\n"); return 1; } /* If config files specified on command line, process only those */ if (optind < argc) { for (int i = optind; i < argc; i++) process_file(argv[i]); return 0; } /* * No config files specified, process standard tmpfiles.d directories. * * 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. */ /* 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; /* check for overrides */ for (j = i + 1; j < gl.gl_pathc; j++) { if (strcmp(basenm(fn), basenm(gl.gl_pathv[j]))) continue; fn = NULL; break; } if (!fn) continue; /* skip, override exists */ process_file(fn); } globfree(&gl); return 0; } /** * Local Variables: * indent-tabs-mode: t * c-file-style: "linux" * End: */