From 84098dd8b72fbde078845d6526f499ac36c8d94a Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Fri, 16 Jan 2026 16:39:32 -0500 Subject: [PATCH 1/7] tmpfiles: rename flags for clarity Rename the command-line flag variables to be more descriptive: c_flag -> create_flag r_flag -> remove_flag This improves code readability. --- src/tmpfiles.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tmpfiles.c b/src/tmpfiles.c index 621311b0..3a54abde 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -45,8 +45,8 @@ int debug; -int c_flag = 0; -int r_flag = 0; +int create_flag = 0; +int remove_flag = 0; static int is_dir_empty(const char *path) { @@ -318,7 +318,7 @@ static void tmpfiles(char *line) strc = stat(path, &st); // file and directory removal logic - if (r_flag) { + if (remove_flag) { switch (type[0]) { case 'b': case 'c': @@ -361,7 +361,7 @@ static void tmpfiles(char *line) } // file & directory creation logic - if (c_flag) { + if (create_flag) { switch (type[0]) { case 'b': rc = parse_mm(arg, &major, &minor); @@ -562,7 +562,7 @@ int main(int argc, char *argv[]) while ((c = getopt_long(argc, argv, "cdrh?", long_options, NULL)) != EOF) { switch(c) { case 'c': - c_flag = 1; + create_flag = 1; break; case 'd': @@ -570,7 +570,7 @@ int main(int argc, char *argv[]) break; case 'r': - r_flag = 1; + remove_flag = 1; break; case 'h': @@ -582,7 +582,7 @@ int main(int argc, char *argv[]) } } - if (c_flag + r_flag == 0) { + if (create_flag + remove_flag == 0) { fprintf(stderr, "You need to specify at least one of --create or --remove.\n"); return 1; } From 0b8d39329a25d77bf9b6e44a7897326adf350485 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Fri, 16 Jan 2026 16:41:23 -0500 Subject: [PATCH 2/7] tmpfiles: add --clean flag for age-based cleanup Add support for the --clean (-C) flag to remove files and directories older than the age specified in tmpfiles.d configuration entries. The age field (6th column) in tmpfiles.d entries can now be used with 'd', 'D', and 'e' type entries to clean up old files. Supported time suffixes are: s (seconds), m (minutes), h (hours), d (days), w (weeks). Example configuration: d /tmp/cache 0755 root root 10d When run with --clean, files in /tmp/cache older than 10 days will be removed. The directory itself is preserved. Uses a conservative cleanup approach matching systemd-tmpfiles: - Files: kept if ANY of atime, ctime, mtime is recent - Directories: kept if ANY of atime, mtime is recent (ctime excluded because cleanup itself updates directory ctime) A value of "-" or "0" for age disables cleanup for that entry. Note: x/X exclusion patterns are recognized but not yet implemented. --- src/tmpfiles.c | 121 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 6 deletions(-) diff --git a/src/tmpfiles.c b/src/tmpfiles.c index 3a54abde..8e410aaf 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -34,6 +34,7 @@ #include #include +#include #ifdef _LIBITE_LITE # include #else @@ -46,6 +47,7 @@ int debug; int create_flag = 0; +int clean_flag = 0; int remove_flag = 0; static int is_dir_empty(const char *path) @@ -261,6 +263,82 @@ static void write_arg(FILE *fp, char *arg) 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; +} + +/* 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 @@ -312,7 +390,6 @@ static void tmpfiles(char *line) group = "root"; age = strtok(NULL, "\t "); - (void)age; /* unused atm. */ arg = strtok(NULL, "\n"); strc = stat(path, &st); @@ -349,7 +426,7 @@ static void tmpfiles(char *line) break; case 'X': case 'x': - dbg("Unsupported x/X command, ignoring %s, no support for clean at runtime.", path); + /* handled by clean_flag (--clean) */ break; case 'Z': case 'z': @@ -360,6 +437,32 @@ static void tmpfiles(char *line) } } + /* 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]) { @@ -505,7 +608,7 @@ static void tmpfiles(char *line) break; case 'X': case 'x': - dbg("Unsupported x/X command, ignoring %s, no support for clean at runtime.", path); + /* handled by clean_flag (--clean) */ break; case 'Z': opts = "-R"; @@ -538,6 +641,7 @@ static int usage(int rc) "Usage: tmpfiles [COMMAND...]\n" "\n" "Commands:\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" @@ -550,6 +654,7 @@ static int usage(int 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' }, @@ -559,8 +664,12 @@ int main(int argc, char *argv[]) int c; - while ((c = getopt_long(argc, argv, "cdrh?", long_options, NULL)) != EOF) { + 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; @@ -582,8 +691,8 @@ int main(int argc, char *argv[]) } } - if (create_flag + remove_flag == 0) { - fprintf(stderr, "You need to specify at least one of --create or --remove.\n"); + 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; } From 6bf35513c34d3fc3e74b749c4a39c87344399757 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Fri, 16 Jan 2026 16:42:09 -0500 Subject: [PATCH 3/7] tmpfiles: add support for config files on command line Allow specifying one or more configuration files as command line arguments instead of always processing all files in the standard tmpfiles.d directories. This enables targeted operations on specific config files: tmpfiles --create /etc/tmpfiles.d/myapp.conf tmpfiles --clean /tmp/test.conf /tmp/other.conf When no config files are specified, the existing behavior of processing all *.conf files in the standard directories is preserved. Also refactors file processing into a helper function to reduce code duplication. --- src/tmpfiles.c | 61 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/tmpfiles.c b/src/tmpfiles.c index 8e410aaf..3e307162 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -635,17 +635,44 @@ static void tmpfiles(char *line) 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 [COMMAND...]\n" + "Usage: tmpfiles [OPTIONS] [CONFIGFILE...]\n" "\n" - "Commands:\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; @@ -696,7 +723,16 @@ int main(int argc, char *argv[]) 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 @@ -723,7 +759,6 @@ int main(int argc, char *argv[]) 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++) { @@ -736,26 +771,12 @@ int main(int argc, char *argv[]) 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); - free(line); - } - - fclose(fp); + process_file(fn); } globfree(&gl); + + return 0; } /** From 7459ede80653fbb1ee0a689592a61ad46be7ce77 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Fri, 16 Jan 2026 16:48:36 -0500 Subject: [PATCH 4/7] tmpfiles: fix L+ to replace non-directory entries The L+ type should replace existing entries with a symlink. Previously, rmrf() was always called which is only appropriate for directories. Now we check if the path is a directory first, and use erase() for files and symlinks. --- src/tmpfiles.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tmpfiles.c b/src/tmpfiles.c index 3e307162..c2badc5d 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -568,7 +568,10 @@ static void tmpfiles(char *line) if (!strc) { if (type[1] != '+') break; - rmrf(path); + if (fisdir(path)) + rmrf(path); + else + erase(path); } mkparent(path, 0755); if (!arg) { From 25bcde12d4856fad888810d2e2886f3cb4c09513 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Fri, 16 Jan 2026 16:49:37 -0500 Subject: [PATCH 5/7] tmpfiles: add support for numeric uid/gid in config files Add parse_uid() and parse_gid() helper functions that support both numeric IDs and name lookups. Update the d/D directory creation handlers to use these new functions. This allows config files to specify ownership using numeric UIDs and GIDs instead of only usernames and group names, matching systemd-tmpfiles behavior. --- src/tmpfiles.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/tmpfiles.c b/src/tmpfiles.c index c2badc5d..6d7a01ae 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -304,6 +304,46 @@ static time_t parse_age(const char *age) 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; @@ -512,10 +552,27 @@ static void tmpfiles(char *line) rc = 0; break; case 'd': - case 'D': + case 'D': { + int uid, gid; + mode_t omask; + mkparent(path, 0755); - rc = mksubsys(path, mode ?: 0755, user, group); + 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; From 7206f745a2ecb23719598862a6a5c2f0b8fefe3c Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Fri, 16 Jan 2026 16:50:08 -0500 Subject: [PATCH 6/7] tmpfiles: fix 'e' type to only adjust existing directories According to tmpfiles.d(5), the 'e' type adjusts the mode and ownership of existing paths but should not create them. Previously, mksubsys() was used which could create directories. Now we explicitly check if the path is an existing directory before adjusting its permissions. --- src/tmpfiles.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/tmpfiles.c b/src/tmpfiles.c index 6d7a01ae..12ee845f 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -577,8 +577,24 @@ static void tmpfiles(char *line) 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); + 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': From c9fd4418e7b78e42a3a3a8d2b255c5435b74e76c Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Fri, 16 Jan 2026 16:50:39 -0500 Subject: [PATCH 7/7] tmpfiles: fix f/F to apply ownership when writing content When f or F types write content to a file, the mode and ownership specified in the config should be applied. Previously, ownership was only applied when create() was used (i.e., when no argument was specified). Now we explicitly apply mode and ownership after writing content to the file. --- src/tmpfiles.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/tmpfiles.c b/src/tmpfiles.c index 12ee845f..bbc19189 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -601,10 +601,6 @@ static void tmpfiles(char *line) 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 */ @@ -613,8 +609,20 @@ static void tmpfiles(char *line) } if (fp) { + int uid, gid; + write_arg(fp, arg); rc = fclose(fp); + + /* Apply mode and ownership */ + if (mode) + chmod(path, mode); + uid = parse_uid(user); + gid = parse_gid(group); + if (gid < 0) + gid = 0; + if (uid >= 0 && chown(path, uid, gid)) + warn("Failed chown(%s, %d, %d)", path, uid, gid); } break; case 'l': /* Finit extension, like 'L' but only if target exists */