summaryrefslogtreecommitdiff
path: root/src/applyperms.c
diff options
context:
space:
mode:
authorMichael Forney <mforney@mforney.org>2017-02-27 09:26:59 -0800
committerMichael Forney <mforney@mforney.org>2017-02-27 09:28:37 -0800
commit2513d3050282a0870eb22c8d394c4b50dca9bd1b (patch)
treec2b0c3dde228fab6ad1b03039528a977c65457b1 /src/applyperms.c
parent858e99ecdbb611c149434bf0381a2155ec3f3bf4 (diff)
Move util -> src
Diffstat (limited to 'src/applyperms.c')
-rw-r--r--src/applyperms.c436
1 files changed, 436 insertions, 0 deletions
diff --git a/src/applyperms.c b/src/applyperms.c
new file mode 100644
index 00000000..f707af34
--- /dev/null
+++ b/src/applyperms.c
@@ -0,0 +1,436 @@
+/*
+See LICENSE file for copyright and license details.
+
+This program is meant to be run by a git hook to fix the permissions of files
+based on a .perms file in the repository
+
+It can also be run with the -d flag on any directory containing .perms in order
+to apply the permissions specified by that file.
+
+Security considerations:
+If the repository previously contained a world or group readable directory which
+has become secret, the names of the new files in that directory will become
+temporarily visible because git checks out the files before this program is run.
+*/
+#define _POSIX_C_SOURCE 200809L
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <spawn.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdnoreturn.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define PERMS_FILE ".perms"
+
+struct perm {
+ mode_t mode;
+ char *name;
+ bool attempted;
+};
+
+struct special {
+ struct perm *perms;
+ int len;
+};
+
+extern char **environ;
+
+static char *prog;
+static dev_t rootdev;
+static int rootfd = AT_FDCWD;
+static struct special oldsp, newsp;
+static int status;
+
+static void
+verror(char *fmt, va_list ap)
+{
+ status = 1;
+ vfprintf(stderr, fmt, ap);
+ if (fmt[0] && fmt[strlen(fmt)-1] == ':')
+ fprintf(stderr, " %s", strerror(errno));
+ fputc('\n', stderr);
+}
+
+static void
+error(char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ verror(fmt, ap);
+ va_end(ap);
+}
+
+static noreturn void
+die(char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ verror(fmt, ap);
+ va_end(ap);
+
+ exit(1);
+}
+
+static FILE *
+spawn(char **argv, pid_t *pid)
+{
+ FILE *f;
+ int fd[2];
+ posix_spawn_file_actions_t actions;
+
+ if (pipe(fd) < 0)
+ die("pipe:");
+ f = fdopen(fd[0], "r");
+ if (!f)
+ die("fdopen:");
+ if ((errno = posix_spawn_file_actions_init(&actions)) > 0)
+ die("posix_spawn_file_actions_init:");
+ if ((errno = posix_spawn_file_actions_addclose(&actions, fd[0])) > 0)
+ die("posix_spawn_file_actions_adddup2:");
+ if ((errno = posix_spawn_file_actions_adddup2(&actions, fd[1], 1)) > 0)
+ die("posix_spawn_file_actions_adddup2:");
+ if ((errno = posix_spawnp(pid, argv[0], &actions, NULL, argv, environ)) > 0)
+ die("posix_spawnp:");
+ posix_spawn_file_actions_destroy(&actions);
+ close(fd[1]);
+
+ return f;
+}
+
+static void
+readspecial(struct special *sp, FILE *f)
+{
+ char *line = NULL, *s, *mode;
+ size_t size = 0;
+ ssize_t n;
+
+ while ((n = getline(&line, &size, f)) >= 0) {
+ if (line[n-1] == '\n')
+ line[--n] = '\0';
+ mode = s = line;
+ s = strchr(s, ' ');
+ if (!s || s == mode)
+ die("malformed permissions file: %s", PERMS_FILE);
+ *s++ = '\0';
+ sp->perms = realloc(sp->perms, (sp->len + 1) * sizeof(*sp->perms));
+ if (!sp->perms)
+ die("realloc:");
+ sp->perms[sp->len].name = strdup(s);
+ if (!sp->perms[sp->len].name)
+ die("strdup:");
+ sp->perms[sp->len].mode = strtoul(mode, &s, 8);
+ sp->perms[sp->len].attempted = false;
+ if (*s)
+ die("invalid mode: %s", mode);
+ ++sp->len;
+ }
+}
+
+static void
+gitspecial(struct special *sp, const char *rev)
+{
+ char object[41 + sizeof(PERMS_FILE)];
+ char *argv[] = {"git", "show", object, 0};
+ FILE *f;
+ pid_t pid;
+ int st;
+
+ if (snprintf(object, sizeof(object), "%s:%s", rev, PERMS_FILE) >= (int)sizeof(object))
+ die("revision is too large: %s", rev);
+ f = spawn(argv, &pid);
+ readspecial(sp, f);
+ fclose(f);
+
+ if (waitpid(pid, &st, 0) < 0)
+ die("waitpid:");
+ if (!(WIFEXITED(st) && WEXITSTATUS(st) == 0))
+ die("child process failed");
+}
+
+static int
+chmod_v(const char *path, mode_t mode)
+{
+ printf("chmod(\"%s\", %#o)\n", path, mode);
+ return fchmodat(rootfd, path, mode, 0);
+}
+
+static int
+mkdir_v(const char *path, mode_t mode)
+{
+ printf("mkdir(\"%s\", %#o)\n", path, mode);
+ return mkdirat(rootfd, path, mode);
+}
+
+static int
+defperm(const char *name)
+{
+ struct stat st;
+ mode_t mode;
+
+ if (fstatat(rootfd, name, &st, AT_SYMLINK_NOFOLLOW) < 0)
+ return -1;
+ if (st.st_dev != rootdev) {
+ errno = EXDEV;
+ return -1;
+ }
+ switch (st.st_mode & S_IFMT) {
+ case S_IFREG:
+ mode = st.st_mode&S_IXUSR ? 0755 : 0644;
+ break;
+ case S_IFDIR:
+ mode = 0755;
+ break;
+ case S_IFLNK:
+ return 0;
+ default:
+ errno = EINVAL;
+ return -1;
+ }
+ if ((st.st_mode&~S_IFMT) == mode)
+ return 0;
+ return chmod_v(name, mode);
+}
+
+static int
+specialperm(struct perm *p)
+{
+ struct stat st;
+
+ if (p->attempted)
+ return 0;
+ p->attempted = true;
+ if (fstatat(rootfd, p->name, &st, AT_SYMLINK_NOFOLLOW) < 0) {
+ if (errno != ENOENT || !S_ISDIR(p->mode))
+ return -1;
+ if (mkdir_v(p->name, p->mode & ~S_IFMT) < 0)
+ return -1;
+ return 0;
+ }
+ if (st.st_dev != rootdev) {
+ errno = EXDEV;
+ return -1;
+ }
+ if (st.st_mode == p->mode)
+ return 0;
+ if ((st.st_mode&S_IFMT) != (p->mode&S_IFMT)) {
+ errno = EINVAL;
+ return -1;
+ }
+ return chmod_v(p->name, p->mode & ~S_IFMT);
+}
+
+static void
+specialperms(void)
+{
+ int i = 0, j = 0, n;
+ char *dirs[oldsp.len], **dir = dirs;
+
+ while (i < oldsp.len || j < newsp.len) {
+ if (i == oldsp.len)
+ n = 1;
+ else if (j == newsp.len)
+ n = -1;
+ else
+ n = strcmp(oldsp.perms[i].name, newsp.perms[j].name);
+ if (n >= 0) {
+ if (specialperm(&newsp.perms[j]) < 0 && errno != EXDEV)
+ error("specialperm:");
+ ++j;
+ if (n == 0)
+ ++i;
+ continue;
+ }
+ switch (oldsp.perms[i].mode&S_IFMT) {
+ case S_IFDIR:
+ *dir++ = oldsp.perms[i].name;
+ break;
+ default:
+ if (defperm(oldsp.perms[i].name) < 0) switch (errno) {
+ case ENOENT:
+ case EXDEV:
+ break;
+ default:
+ error("defperm:");
+ }
+ }
+ ++i;
+ }
+ /* delete directories in reverse order */
+ while (dir > dirs) {
+ --dir;
+ if (rmdir(*dir) < 0) switch (errno) {
+ case ENOENT:
+ case ENOTEMPTY:
+ break;
+ default:
+ error("rmdir:");
+ }
+ }
+}
+
+static int
+setperm(const char *name)
+{
+ int i;
+
+ for (i = 0; i < newsp.len; ++i) {
+ if (strcmp(name, newsp.perms[i].name) == 0)
+ return specialperm(&newsp.perms[i]);
+ }
+ return defperm(name);
+}
+
+static void
+setroot(const char *root)
+{
+ struct stat st;
+
+ rootfd = open(root, O_RDONLY);
+ if (rootfd < 0)
+ die("open %s:", root);
+ if (fstat(rootfd, &st) < 0)
+ die("fstat:", root);
+ rootdev = st.st_dev;
+}
+
+static void
+gitupdate(char *old, char *new)
+{
+ char *argv_diff[] = {"git", "diff", "--name-only", "-z", old, new, 0};
+ char *argv_new[] = {"git", "ls-tree", "--name-only", "--full-tree", "-z", "-r", new, 0};
+ FILE *f;
+ pid_t pid;
+ struct {
+ char *buf;
+ size_t size;
+ } lines[2] = {0};
+ ssize_t n;
+ int cur = 0, st;
+ char *root, *path, *diff, *s;
+
+ root = getenv("GIT_WORK_TREE");
+ setroot(root ? root : ".");
+ if (old)
+ gitspecial(&oldsp, old);
+ gitspecial(&newsp, new);
+
+ f = spawn(old ? argv_diff : argv_new, &pid);
+ umask(0);
+ while ((n = getdelim(&lines[cur].buf, &lines[cur].size, '\0', f)) >= 0) {
+ path = lines[cur].buf;
+ if (strcmp(path, PERMS_FILE) == 0) {
+ specialperms();
+ continue;
+ }
+ if (setperm(path) < 0) switch (errno) {
+ case ENOENT:
+ continue;
+ case EXDEV:
+ break;
+ default:
+ error("setperm %s:", path);
+ }
+ /* find the first difference from the previous path */
+ diff = path;
+ if (lines[!cur].buf)
+ for (s = lines[!cur].buf; *s && *s == *diff; ++s, ++diff);
+ /* set permissions on each parent directory after that difference */
+ for (s = path + n; s >= diff; --s) {
+ if (*s != '/')
+ continue;
+ *s = '\0';
+ if (setperm(path) < 0)
+ error("setperm %s:", path);
+ *s = '/';
+ }
+ cur = !cur;
+ }
+ fclose(f);
+
+ if (waitpid(pid, &st, 0) < 0)
+ die("waitpid:");
+ if (!(WIFEXITED(st) && WEXITSTATUS(st) == 0))
+ die("child process failed");
+}
+
+static void
+applyspecial(void)
+{
+ int fd;
+ FILE *f;
+
+ fd = openat(rootfd, ".perms", O_RDONLY);
+ if (fd < 0)
+ die("open .perms:");
+ f = fdopen(fd, "r");
+ if (!f)
+ die("fdopen:");
+ readspecial(&newsp, f);
+ fclose(f);
+ umask(0);
+ specialperms();
+}
+
+static void
+usage()
+{
+ fprintf(stderr, "usage: %s [[old] new] | %s -d dir\n", prog, prog);
+ exit(2);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int dflag = 0;
+ char *old, *new;
+
+ prog = basename(argv[0]);
+ for (++argv, --argc; argc && (*argv)[0] == '-' && (*argv)[1]; ++argv, --argc) {
+ switch ((*argv)[1]) {
+ case 'd':
+ if (!*++argv)
+ usage();
+ --argc;
+ setroot(*argv);
+ dflag = 1;
+ break;
+ default:
+ usage();
+ }
+ }
+
+ if (dflag) {
+ if (argc)
+ usage();
+ applyspecial();
+ return status;
+ }
+
+ switch (argc) {
+ case 0:
+ old = NULL;
+ new = "HEAD";
+ break;
+ case 1:
+ old = NULL;
+ new = argv[0];
+ break;
+ case 2:
+ old = argv[0];
+ new = argv[1];
+ break;
+ default:
+ usage();
+ }
+ gitupdate(old, new);
+ return status;
+}