cvs-permfix-commit.c
/* * CVS permissions fix: commits * * SUMMARY * * Run as loginfo script for all CVS operations. * Propagates ACL entries of parent/container * directories to any subfiles or subdirectories upon * commit. NOTE: does NOT handle tags. See the * companion program for that. * * EXECUTION * * Run as a CVS loginfo filter with %{,,,,,sv} as the * argument format, using regexp ALL. * * DETAILS * * This program is a fix for CVS permissions problems we * are seeing where added files/directories do not in * some circumstances (which are as yet unknown and not * reproducible) inherit the default ACL from the parent * directory. * * Instead of using default ACLs, we are going to * instead always copy the access ACL from the parent * directory, for any new file or directory, or any * modified file. Even though the permission problems * we are trying to fix seem only to show up with adds, * it is necessary to reset permsions for *all* CVS * operations if we are not using default ACLs. This is * because of how CVS modifies files: it makes a * temporary file with a different name in its * destination directory, and then moves it into place * under the real name of the file it is replacing. * Without the use of default ACLs in the destination * directories, this will result in ACL-less files for * modifies, in addition to adds. * * For new directories, we will exactly copy the ACL of * the parent directory. * * For new or modified files, we will copy the ACL of the * parent directory, and remove all write bits. These * are never needed because CVS does not ever modify * files in-place; it replaces them with new copies of * the file upon commit. * * If the file's owner has the execute bit set before we * consider the parent directory ACL, we can conclude * that we should set at least one execute bit in the * new ACL for the file. In this case we give execute * permission to any ACL that also grants read access. * Other than that, we strip the execute bits from the * set of those copied from the parent. This heuristic * might not be exactly right but should work well * enough. * * ASSUMPTIONS * * Not too much care was taken to check for input. * Since it's coming from CVS and we aren't running * privileged, this shouldn't be too big a danger. * * Does not attempt to handle allocation failures. If * we enter real OOM state on our CVS server and have * run out of swap, we have much bigger problems. * * Filename separator used by loginfo is hardcoded. It * is necessary to use one because otherwise, both * filenames and directories with spaces will cause * serious problems: there is no way to tell where the * directory ends and the filenames begin, and there is * no way to determine where the next file begins in a * list of files. * * Note that using an unsupported character in the * format string seems to always result in a comma in * the output, so we just use commas directly E.g., * using `%{.....%sv}' will actually yield a string: * `,,,,,filename,newversion'. * * The filename separator cannot collide with actual * file or directory names, or we screw up. It's * actually impossible to use a separator that can't * also be used in a filename, since one can easily make * a filename with any number of contiguous commas. * Using filenames with the separator string in them * will screw up (although it should only screw up for * those particular files and continue to run ok for the * remainder of the files given by loginfo). * * BUGS * * CVS does not seem to check the result of executing * the loginfo script, so nothing interrupts CVS * operation if we fail. I'm almost thinking we should * raise a SIGTERM to our parent upon failure, or * something like that. But thankfully, we do get * stderr reported, so the user will probably notice an * error as long as we print one. I've endeavored to do * just that, for any error condition. * * CVS should just break apart the files as separate * arguments instead of passing the containing directory * and whole list of files as a single argument, which * forces us to jump through hoops to accomodate spaces * in filenames and figure out where the directory ends * and files begin. We could get over argument stack * space limitations by just using ARG_MAX; I don't know * why they don't do this, it's a dumb way to pass * arguments, as a single string. ARG_MAX should be * used anyways because the arguments are put on the * stack either way. * * AUTHOR: * * Scott Mcdermott, 2004 */ #define _GNU_SOURCE #include <sys/acl.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include "util.h" #include "acl.h" ^L #define FILE_SEPARATOR " ,,,,," #define NEWDIR_SEPARATOR " - New directory" #define VERSION_DELETED "NONE" ^L static char * get_cvsroot_or_exit (void); static char * astrtokstr (char *s, const char *delim); static int goto_repodir_and_open (char *dir); static void consume_stdin (void); ^L int main (int argc, char **argv) { unsigned arglen; int dirfd, parentfd; char *args, *dir, *tok; acl_t acl; /* avoid broken pipe, we don't use stdin */ consume_stdin(); if (--argc != 1) { fprintf(stderr, "%s: bad arg count\n", progname); exit(EXIT_FAILURE); } args = strdup(*++argv); arglen = strlen(args); /* were we given any files? */ dir = astrtokstr(args, FILE_SEPARATOR); if (strlen(dir) == arglen) { /* no; we should be a new directory then */ char *sepbegin = args + arglen - strlen(NEWDIR_SEPARATOR); if (!strcmp(sepbegin, NEWDIR_SEPARATOR)) { /* indeed we are; set our ACL from * the parent dir */ *sepbegin = '\0'; dirfd = goto_repodir_and_open(args); parentfd = xopen("..", O_RDONLY); acl = xacl_get_fd(parentfd); xacl_set_fd(dirfd, acl); /* new directories are given by * themselves, and singly */ exit(EXIT_SUCCESS); } else { /* it's not a new dir either */ fprintf(stderr, "%s: bad loginfo format\n", progname); exit(EXIT_FAILURE); } } /* files are given; go to their base directory */ dirfd = goto_repodir_and_open(dir); /* iterate through each file (we will be invoked * exactly once total with all the new or changed files * for any given directory */ while ((tok = astrtokstr(NULL, FILE_SEPARATOR))) { char *newver; /* the text following the last comma is the * new version; we use this to detect a * deleted file, which obviously does not * need permissions set on it! */ newver = strrchr(tok, ','); *newver++ = '\0'; /* now tok is raw filename */ # if 0 if (!strcmp(newver, VERSION_DELETED)) continue; #endif set_repoacl_from_parent(tok, dirfd); free(tok); } /* if there are additional files in other * directories, we will be invoked again */ exit(EXIT_SUCCESS); } ^L /* * Like strtok(), but uses a fixed string as the delimiter, * instead of a set of arbitrary delimiting characters, so * it's basically a strtok() implemented with strstr() * instead of strspn() and strpbrk(). Because we use * strstr() we have to exactly match the delimiter, so we * take care not to modify the source string, as is usual * with strtok(). To accomplish this we return the token in * a newly allocated string, for which the caller assumes * responsibility. */ static char * astrtokstr (char *s, const char *delim) { static char *olds; char *tok, *buf; size_t len; if (!s) s = olds; if (s == strstr(s, delim)) s += strlen(delim); if (*s == '\0') { olds = s; return NULL; } tok = s; if (!(s = strstr(tok, delim))) /* last token */ olds = rawmemchr(tok, '\0'); else olds = s; len = olds - tok; buf = malloc(len + 1); memcpy(buf, tok, len); *(buf + len) = '\0'; return buf; } static char * get_cvsroot_or_exit (void) { char *val; if (!(val = getenv(ENV_CVSROOT))) { fprintf(stderr, "%s: getenv: %s: %s\n", progname, ENV_CVSROOT, strerror(errno)); exit(EXIT_FAILURE); } return val; } static int goto_repodir_and_open (char *dir) { char *dirname; int dirfd; asprintf(&dirname, "%s/%s", get_cvsroot_or_exit(), dir); xchdir(dirname); dirfd = xopen(".", O_RDONLY); return dirfd; } static void consume_stdin (void) { int ret; char buf[BUFSIZ]; while ((ret = read(STDIN_FILENO, buf, BUFSIZ))) { if (ret == -1) { fprintf(stderr, "%s: read: %s\n", progname, strerror(errno)); exit(EXIT_FAILURE); } } }