diff options
Diffstat (limited to 'zic.c')
| -rw-r--r-- | zic.c | 457 |
1 files changed, 347 insertions, 110 deletions
@@ -18,6 +18,10 @@ #include "tzfile.h" #include <fcntl.h> +#ifndef O_BINARY +# define O_BINARY 0 /* MS-Windows */ +#endif + #include <locale.h> #include <signal.h> #include <stdarg.h> @@ -44,8 +48,8 @@ enum { FORMAT_LEN_GROWTH_BOUND = 5 }; #ifdef HAVE_DIRECT_H # include <direct.h> # include <io.h> -# undef mkdir # define mkdir(name, mode) _mkdir(name) +typedef unsigned short gid_t, mode_t, uid_t; #endif #ifndef HAVE_GETRANDOM @@ -61,13 +65,94 @@ enum { FORMAT_LEN_GROWTH_BOUND = 5 }; # include <sys/random.h> #endif + #if HAVE_SYS_STAT_H # include <sys/stat.h> #endif -#ifdef S_IRUSR -# define MKDIR_UMASK (S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) + +#ifndef S_IRWXU +# define S_IRUSR 0400 +# define S_IWUSR 0200 +# define S_IXUSR 0100 +# define S_IRGRP 0040 +# define S_IWGRP 0020 +# define S_IXGRP 0010 +# define S_IROTH 0004 +# define S_IWOTH 0002 +# define S_IXOTH 0001 +# define S_IRWXU (S_IRUSR | S_IWUSR | S_IXUSR) +# define S_IRWXG (S_IRGRP | S_IWGRP | S_IXGRP) +# define S_IRWXO (S_IROTH | S_IWOTH | S_IXOTH) +#endif + +/* All file permission bits. */ +#define ALL_PERMS (S_IRWXU | S_IRWXG | S_IRWXO) + +/* Troublesome file permission bits. */ +#define TROUBLE_PERMS (S_IWGRP | S_IWOTH) + +/* File permission bits for making directories. + The umask modifies these bits. */ +#define MKDIR_PERMS (ALL_PERMS & ~TROUBLE_PERMS) + +/* File permission bits for making regular files. + The umask modifies these bits. */ +#define CREAT_PERMS (MKDIR_PERMS & ~(S_IXUSR | S_IXGRP | S_IXOTH)) +static mode_t creat_perms = CREAT_PERMS; + +#ifndef HAVE_PWD_H +# ifdef __has_include +# if __has_include(<pwd.h>) && __has_include(<grp.h>) +# define HAVE_PWD_H 1 +# else +# define HAVE_PWD_H 0 +# endif +# endif +#endif +#ifndef HAVE_PWD_H +# define HAVE_PWD_H 1 +#endif +#if HAVE_PWD_H +# include <grp.h> +# include <pwd.h> #else -# define MKDIR_UMASK 0755 +struct group { gid_t gr_gid; }; +struct passwd { uid_t pw_uid; }; +# define getgrnam(arg) NULL +# define getpwnam(arg) NULL +# define fchown(fd, owner, group) ((fd) < 0 ? -1 : 0) +#endif +static gid_t const no_gid = -1; +static uid_t const no_uid = -1; +static gid_t output_group = -1; +static uid_t output_owner = -1; +#ifndef GID_T_MAX +# define GID_T_MAX_NO_PADDING MAXVAL(gid_t, TYPE_BIT(gid_t)) +# if HAVE__GENERIC +# define GID_T_MAX \ + (TYPE_SIGNED(gid_t) \ + ? _Generic((gid_t) 0, \ + signed char: SCHAR_MAX, short: SHRT_MAX, \ + int: INT_MAX, long: LONG_MAX, long long: LLONG_MAX, \ + default: GID_T_MAX_NO_PADDING) \ + : (gid_t) -1) +# else +# define GID_T_MAX GID_T_MAX_NO_PADDING +# endif +#endif +#ifndef UID_T_MAX +# define UID_T_MAX_NO_PADDING MAXVAL(uid_t, TYPE_BIT(uid_t)) +# if HAVE__GENERIC +# define UID_T_MAX \ + (TYPE_SIGNED(uid_t) \ + ? _Generic((uid_t) 0, \ + signed char: SCHAR_MAX, short: SHRT_MAX, \ + int: INT_MAX, long: LONG_MAX, long long: LLONG_MAX, \ + default: UID_T_MAX_NO_PADDING) \ + : (uid_t) -1) +# else +# define UID_T_MAX UID_T_MAX_NO_PADDING +# endif #endif /* The minimum alignment of a type, for pre-C23 platforms. @@ -144,14 +229,6 @@ struct zone { zic_t z_untiltime; }; -#if !HAVE_POSIX_DECLS -extern int getopt(int argc, char * const argv[], - const char * options); -extern int link(const char * target, const char * linkname); -extern char * optarg; -extern int optind; -#endif - #if ! HAVE_SYMLINK static ssize_t readlink(char const *restrict file, char *restrict buf, size_t size) @@ -167,8 +244,8 @@ symlink(char const *target, char const *linkname) } #endif #ifndef AT_SYMLINK_FOLLOW -# define linkat(targetdir, target, linknamedir, linkname, flag) \ - (errno = ENOTSUP, -1) +# define linkat(targetdir, target, linknamedir, linkname, flag) \ + (errno = ENOTSUP, -1) #endif static void addtt(zic_t starttime, int type); @@ -202,6 +279,13 @@ static bool rulesub(struct rule * rp, const char * dayp, const char * timep); static zic_t tadd(zic_t t1, zic_t t2); +/* Is C an ASCII digit? */ +static bool +is_digit(char c) +{ + return '0' <= c && c <= '9'; +} + /* Bound on length of what %z can expand to. */ enum { PERCENT_Z_LEN_BOUND = sizeof "+995959" - 1 }; @@ -219,6 +303,7 @@ static int max_format_len; static zic_t max_year; static zic_t min_year; static bool noise; +static bool skip_mkdir; static int rfilenum; static lineno rlinenum; static const char * progname; @@ -643,14 +728,96 @@ warning(const char *const string, ...) warnings = true; } -/* Close STREAM. If it had an I/O error, report it against DIR/NAME, - remove TEMPNAME if nonnull, and then exit. */ +/* Convert ARG, a string in base BASE, to an unsigned long value no + greater than MAXVAL. On failure, diagnose with MSGID and exit. */ +static unsigned long +arg2num(char const *arg, int base, unsigned long maxval, char const *msgid) +{ + unsigned long n; + char *ep; + errno = 0; + n = strtoul(arg, &ep, base); + if (ep == arg || *ep || maxval < n || errno) { + fprintf(stderr, _(msgid), progname, arg); + exit(EXIT_FAILURE); + } + return n; +} + +#ifndef MODE_T_MAX +# define MODE_T_MAX_NO_PADDING MAXVAL(mode_t, TYPE_BIT(mode_t)) +# if HAVE__GENERIC +# define MODE_T_MAX \ + (TYPE_SIGNED(mode_t) \ + ? _Generic((mode_t) 0, \ + signed char: SCHAR_MAX, short: SHRT_MAX, \ + int: INT_MAX, long: LONG_MAX, long long: LLONG_MAX, \ + default: MODE_T_MAX_NO_PADDING) \ + : (mode_t) -1) +# else +# define MODE_T_MAX MODE_T_MAX_NO_PADDING +# endif +#endif + +#ifndef HAVE_FCHMOD +# define HAVE_FCHMOD 1 +#endif +#if !HAVE_FCHMOD +# define fchmod(fd, mode) 0 +#endif + +#ifndef HAVE_SETMODE +# if (defined __FreeBSD__ || defined __NetBSD__ || defined __OpenBSD__ \ + || (defined __APPLE__ && defined __MACH__)) +# define HAVE_SETMODE 1 +# else +# define HAVE_SETMODE 0 +# endif +#endif + +static mode_t const no_mode = -1; +static mode_t output_mode = -1; + +static mode_t +mode_option(char const *arg) +{ +#if HAVE_SETMODE + void *set = setmode(arg); + if (set) { + mode_t mode = getmode(set, CREAT_PERMS); + free(set); + return mode; + } +#endif + return arg2num(arg, 8, min(MODE_T_MAX, ULONG_MAX), + N_("%s: -m '%s': invalid mode\n")); +} + +static int +chmetadata(FILE *stream) +{ + if (output_owner != no_uid || output_group != no_gid) { + int r = fchown(fileno(stream), output_owner, output_group); + if (r < 0) + return r; + } + return output_mode == no_mode ? 0 : fchmod(fileno(stream), output_mode); +} + +/* Close STREAM. + If it had an I/O error, report it against DIR/NAME, + remove TEMPNAME if nonnull, and then exit. + If TEMPNAME is nonnull, and if requested, + change the stream's metadata before closing. */ static void close_file(FILE *stream, char const *dir, char const *name, char const *tempname) { char const *e = (ferror(stream) ? _("I/O error") - : fclose(stream) != 0 ? strerror(errno) : NULL); + : (fflush(stream) < 0 + || (tempname && chmetadata(stream) < 0) + || fclose(stream) < 0) + ? strerror(errno) : NULL); if (e) { if (name && *name == '/') dir = NULL; @@ -665,14 +832,21 @@ close_file(FILE *stream, char const *dir, char const *name, } ATTRIBUTE_NORETURN static void +duplicate_options(char const *opt) +{ + fprintf(stderr, _("%s: More than one %s option specified\n"), progname, opt); + exit(EXIT_FAILURE); +} + +ATTRIBUTE_NORETURN static void usage(FILE *stream, int status) { fprintf(stream, _("%s: usage is %s [ --version ] [ --help ] [ -v ] \\\n" - "\t[ -b {slim|fat} ] [ -d directory ] [ -l localtime ]" - " [ -L leapseconds ] \\\n" - "\t[ -p posixrules ] [ -r '[@lo][/@hi]' ] [ -R '@hi' ] \\\n" - "\t[ -t localtime-link ] \\\n" + "\t[ -b {slim|fat} ] [ -d directory ] [ -D ] \\\n" + "\t[ -l localtime ] [ -L leapseconds ] [ -m mode ] \\\n" + "\t[ -p posixrules ] [ -r '[@lo][/@hi]' ] [ -R @hi ] \\\n" + "\t[ -t localtime-link ] [ -u 'owner[:group]' ] \\\n" "\t[ filename ... ]\n\n" "Report bugs to %s.\n"), progname, progname, REPORT_BUGS_TO); @@ -681,6 +855,71 @@ usage(FILE *stream, int status) exit(status); } +static void +group_option(char const *arg) +{ + if (*arg) { + if (output_group != no_gid) { + fprintf(stderr, _("multiple groups specified")); + exit(EXIT_FAILURE); + } else { + struct group *gr = getgrnam(arg); + output_group = (gr ? gr->gr_gid + : arg2num(arg, 10, min(GID_T_MAX, ULONG_MAX), + N_("%s: invalid group: %s\n"))); + } + } +} + +static void +owner_option(char const *arg) +{ + if (*arg) { + if (output_owner != no_uid) { + fprintf(stderr, _("multiple owners specified")); + exit(EXIT_FAILURE); + } else { + struct passwd *pw = getpwnam(arg); + output_owner = (pw ? pw->pw_uid + : arg2num(arg, 10, min(UID_T_MAX, ULONG_MAX), + N_("%s: invalid owner: %s\n"))); + } + } +} + +/* If setting owner or group, use temp file permissions that avoid + security races before the fchmod at the end. */ +static void +use_safe_temp_permissions(void) +{ + if (output_owner != no_uid || output_group != no_gid) { + + /* The mode when done with the file. */ + mode_t omode; + if (output_mode == no_mode) { + mode_t cmask = umask(0); + umask(cmask); + omode = CREAT_PERMS & ~cmask; + } else + omode = output_mode; + + /* The mode passed to open+O_CREAT. Do not bother with executable + permissions, as they should not be used and this mode is merely + a nicety (even a mode of 0 still work). */ + creat_perms = ((((omode & (S_IRUSR | S_IRGRP | S_IROTH)) + == (S_IRUSR | S_IRGRP | S_IROTH)) + ? S_IRUSR | S_IRGRP | S_IROTH : 0) + | (((omode & (S_IWUSR | S_IWGRP | S_IWOTH)) + == (S_IWUSR | S_IWGRP | S_IWOTH)) + ? S_IWUSR | S_IWGRP | S_IWOTH : 0)); + + /* If creat_perms is not the final mode, arrange to run + fchmod later, even if -m was not used. */ + if (creat_perms != omode) + output_mode = omode; + } +} + /* Change the working directory to DIR, possibly creating DIR and its ancestors. After this is done, all files are accessed with names relative to DIR. */ @@ -980,9 +1219,6 @@ main(int argc, char **argv) register ptrdiff_t i, j; bool timerange_given = false; -#ifdef S_IWGRP - umask(umask(S_IWGRP | S_IWOTH) | (S_IWGRP | S_IWOTH)); -#endif #if HAVE_GETTEXT setlocale(LC_ALL, ""); # ifdef TZ_DOMAINDIR @@ -1005,8 +1241,7 @@ main(int argc, char **argv) } else if (strcmp(argv[k], "--help") == 0) { usage(stdout, EXIT_SUCCESS); } - while ((c = getopt(argc, argv, "b:d:l:L:p:r:R:st:vy:")) != EOF - && c != -1) + while ((c = getopt(argc, argv, "b:d:Dg:l:L:m:p:r:R:st:u:vy:")) != -1) switch (c) { default: usage(stderr, EXIT_FAILURE); @@ -1023,73 +1258,62 @@ main(int argc, char **argv) error(_("invalid option: -b '%s'"), optarg); break; case 'd': - if (directory == NULL) - directory = optarg; - else { - fprintf(stderr, - _("%s: More than one -d option" - " specified\n"), - progname); - return EXIT_FAILURE; - } + if (directory) + duplicate_options("-d"); + directory = optarg; + break; + case 'D': + skip_mkdir = true; + break; + case 'g': + /* This undocumented option is present for + compatibility with FreeBSD 14. */ + group_option(optarg); break; case 'l': - if (lcltime == NULL) - lcltime = optarg; - else { - fprintf(stderr, - _("%s: More than one -l option" - " specified\n"), - progname); - return EXIT_FAILURE; - } + if (lcltime) + duplicate_options("-l"); + lcltime = optarg; + break; + case 'm': + if (output_mode != no_mode) + duplicate_options("-m"); + output_mode = mode_option(optarg); break; case 'p': - if (psxrules == NULL) - psxrules = optarg; - else { - fprintf(stderr, - _("%s: More than one -p option" - " specified\n"), - progname); - return EXIT_FAILURE; - } + if (psxrules) + duplicate_options("-p"); + psxrules = optarg; break; case 't': - if (tzdefault != NULL) { - fprintf(stderr, - _("%s: More than one -t option" - " specified\n"), - progname); - return EXIT_FAILURE; - } + if (tzdefault) + duplicate_options("-t"); tzdefault = optarg; break; + case 'u': + { + char *colon = strchr(optarg, ':'); + if (colon) + *colon = '\0'; + owner_option(optarg); + if (colon) + group_option(colon + 1); + } + break; case 'y': warning(_("-y ignored")); break; case 'L': - if (leapsec == NULL) - leapsec = optarg; - else { - fprintf(stderr, - _("%s: More than one -L option" - " specified\n"), - progname); - return EXIT_FAILURE; - } + if (leapsec) + duplicate_options("-L"); + leapsec = optarg; break; case 'v': noise = true; break; case 'r': - if (timerange_given) { - fprintf(stderr, - _("%s: More than one -r option" - " specified\n"), - progname); - return EXIT_FAILURE; - } + if (timerange_given) + duplicate_options("-r"); if (! timerange_option(optarg)) { fprintf(stderr, _("%s: invalid time range: %s\n"), @@ -1141,6 +1365,7 @@ main(int argc, char **argv) if (errors) return EXIT_FAILURE; associate(); + use_safe_temp_permissions(); change_directory(directory); directory_ends_in_slash = directory[strlen(directory) - 1] == '/'; catch_signals(); @@ -1327,10 +1552,10 @@ random_dirent(char const **name, char **namealloc) uint_fast64_t unfair_min = - ((UINTMAX_MAX % base__6 + 1) % base__6); if (!dst) { - dst = xmalloc(size_sum(dirlen, prefixlen + suffixlen + 1)); - memcpy(dst, src, dirlen); - memcpy(dst + dirlen, prefix, prefixlen); - dst[dirlen + prefixlen + suffixlen] = '\0'; + char *cp = dst = xmalloc(size_sum(dirlen, prefixlen + suffixlen + 1)); + cp = mempcpy(cp, src, dirlen); + cp = mempcpy(cp, prefix, prefixlen); + cp[suffixlen] = '\0'; *name = *namealloc = dst; } @@ -1367,33 +1592,35 @@ diagslash(char const *filename) static FILE * open_outfile(char const **outname, char **tempname) { -#if __STDC_VERSION__ < 201112 - static char const fopen_mode[] = "wb"; -#else - static char const fopen_mode[] = "wbx"; -#endif - - FILE *fp; bool dirs_made = false; if (!*tempname) random_dirent(outname, tempname); - while (! (fp = fopen(*outname, fopen_mode))) { - int fopen_errno = errno; - if (fopen_errno == ENOENT && !dirs_made) { + while (true) { + int oflags = O_WRONLY | O_BINARY | O_CREAT | O_EXCL; + int fd = open(*outname, oflags, creat_perms); + int err; + if (fd < 0) + err = errno; + else { + FILE *fp = fdopen(fd, "wb"); + if (fp) + return fp; + err = errno; + close(fd); + } + if (err == ENOENT && !dirs_made) { mkdirs(*outname, true); dirs_made = true; - } else if (fopen_errno == EEXIST) + } else if (err == EEXIST) random_dirent(outname, tempname); else { fprintf(stderr, _("%s: Can't create %s%s%s: %s\n"), progname, diagdir(*outname), diagslash(*outname), *outname, - strerror(fopen_errno)); + strerror(err)); exit(EXIT_FAILURE); } } - - return fp; } /* If TEMPNAME, the result is in the temporary file TEMPNAME even @@ -1430,15 +1657,17 @@ relname(char const *target, char const *linkname) if (*linkname == '/') { /* Make F absolute too. */ size_t len = strlen(directory); - size_t lenslash = len + (len && directory[len - 1] != '/'); + bool needs_slash = len && directory[len - 1] != '/'; + size_t lenslash = len + needs_slash; size_t targetsize = strlen(target) + 1; + char *cp; if (*directory != '/') return NULL; linksize = size_sum(lenslash, targetsize); - f = result = xmalloc(linksize); - memcpy(result, directory, len); - result[len] = '/'; - memcpy(result + lenslash, target, targetsize); + f = cp = result = xmalloc(linksize); + cp = mempcpy(cp, directory, len); + *cp = '/'; + memcpy(cp + needs_slash, target, targetsize); } for (i = 0; f[i] && f[i] == linkname[i]; i++) if (f[i] == '/') @@ -1448,11 +1677,13 @@ relname(char const *target, char const *linkname) taillen = strlen(f + dir_len); dotdotetcsize = size_sum(size_product(dotdots, 3), taillen + 1); if (dotdotetcsize <= linksize) { + char *cp; if (!result) result = xmalloc(dotdotetcsize); + cp = result; for (i = 0; i < dotdots; i++) - memcpy(result + 3 * i, "../", 3); - memmove(result + 3 * dotdots, f + dir_len, taillen + 1); + cp = mempcpy(cp, "../", 3); + memmove(cp, f + dir_len, taillen + 1); } return result; } @@ -1820,7 +2051,7 @@ gethms(char const *string, char const *errstring) &hh, &hhx, &mm, &mmx, &ss, &ssx, &tenths, &xr, &xs)) { default: ok = false; break; case 8: - ok = '0' <= xr && xr <= '9'; + ok = is_digit(xr); ATTRIBUTE_FALLTHROUGH; case 7: ok &= ssx == '.'; @@ -2875,11 +3106,11 @@ doabbr(char *abbr, struct zone const *zp, char const *letters, else if (letters == disable_percent_s) return 0; sprintf(abbr, format, letters); - } else if (isdst) { - strcpy(abbr, slashp + 1); - } else { - memcpy(abbr, format, slashp - format); - abbr[slashp - format] = '\0'; + } else if (isdst) + strcpy(abbr, slashp + 1); + else { + char *abbrend = mempcpy(abbr, format, slashp - format); + *abbrend = '\0'; } len = strlen(abbr); if (!doquotes) @@ -3920,7 +4151,7 @@ newabbr(const char *string) cp = string; mp = NULL; - while (is_alpha(*cp) || ('0' <= *cp && *cp <= '9') + while (is_alpha(*cp) || is_digit(*cp) || *cp == '-' || *cp == '+') ++cp; if (noise && cp - string < 3) @@ -3932,7 +4163,7 @@ mp = _("time zone abbreviation differs from POSIX standard"); if (mp != NULL) warning("%s (%s)", mp, string); } - i = strlen(string) + 1; + i = strnlen(string, TZ_MAX_CHARS - charcnt) + 1; if (charcnt + i > TZ_MAX_CHARS) { error(_("too many, or too long, time zone abbreviations")); exit(EXIT_FAILURE); @@ -3948,6 +4179,11 @@ mp = _("time zone abbreviation differs from POSIX standard"); static void mkdirs(char const *argname, bool ancestors) { + /* If -D was specified, do not create directories. + If a file operation's parent directory is missing, + the operation will fail and be diagnosed. */ + if (!skip_mkdir) { + char *name = xstrdup(argname); char *cp = name; @@ -3972,7 +4208,7 @@ mkdirs(char const *argname, bool ancestors) ** not check first whether it already exists, as that ** is checked anyway if the mkdir fails. */ - if (mkdir(name, MKDIR_UMASK) != 0) { + if (mkdir(name, MKDIR_PERMS) < 0) { /* Do not report an error if err == EEXIST, because some other process might have made the directory in the meantime. Likewise for ENOSYS, because @@ -3994,4 +4230,5 @@ mkdirs(char const *argname, bool ancestors) *cp++ = '/'; } free(name); + } } |
