master hovercats/oakiss / pkg / skeleton / applyperms.c
  1/*
  2See LICENSE file for copyright and license details.
  3
  4This program is meant to be run by a git hook to fix the permissions of files
  5based on a .perms file in the repository
  6
  7It can also be run with the -d flag on any directory containing .perms in order
  8to apply the permissions specified by that file.
  9
 10Security considerations:
 11If the repository previously contained a world or group readable directory which
 12has become secret, the names of the new files in that directory will become
 13temporarily visible because git checks out the files before this program is run.
 14*/
 15#define _POSIX_C_SOURCE 200809L
 16#include <errno.h>
 17#include <fcntl.h>
 18#include <libgen.h>
 19#include <spawn.h>
 20#include <stdarg.h>
 21#include <stdbool.h>
 22#include <stdio.h>
 23#include <stdlib.h>
 24#include <stdnoreturn.h>
 25#include <string.h>
 26#include <sys/stat.h>
 27#include <sys/wait.h>
 28#include <unistd.h>
 29
 30#define PERMS_FILE ".perms"
 31
 32struct perm {
 33	char *name;
 34	mode_t mode;
 35	bool attempted;  /* whether we attempted to set permissions for this file */
 36	bool delete;     /* marked for directories that only appear in old .perms */
 37};
 38
 39struct special {
 40	struct perm *perms;
 41	int len;
 42};
 43
 44extern char **environ;
 45
 46static char *prog;
 47static dev_t rootdev;
 48static int rootfd = AT_FDCWD;
 49static struct special oldsp, newsp;
 50static int status;
 51
 52static void
 53verror(char *fmt, va_list ap)
 54{
 55	status = 1;
 56	vfprintf(stderr, fmt, ap);
 57	if (fmt[0] && fmt[strlen(fmt)-1] == ':')
 58		fprintf(stderr, " %s", strerror(errno));
 59	fputc('\n', stderr);
 60}
 61
 62static void
 63error(char *fmt, ...)
 64{
 65	va_list ap;
 66
 67	va_start(ap, fmt);
 68	verror(fmt, ap);
 69	va_end(ap);
 70}
 71
 72static noreturn void
 73die(char *fmt, ...)
 74{
 75	va_list ap;
 76
 77	va_start(ap, fmt);
 78	verror(fmt, ap);
 79	va_end(ap);
 80
 81	exit(1);
 82}
 83
 84static FILE *
 85spawn(char **argv, pid_t *pid)
 86{
 87	FILE *f;
 88	int fd[2];
 89	posix_spawn_file_actions_t actions;
 90
 91	if (pipe(fd) < 0)
 92		die("pipe:");
 93	f = fdopen(fd[0], "r");
 94	if (!f)
 95		die("fdopen:");
 96	if ((errno = posix_spawn_file_actions_init(&actions)) > 0)
 97		die("posix_spawn_file_actions_init:");
 98	if ((errno = posix_spawn_file_actions_addclose(&actions, fd[0])) > 0)
 99		die("posix_spawn_file_actions_adddup2:");
100	if ((errno = posix_spawn_file_actions_adddup2(&actions, fd[1], 1)) > 0)
101		die("posix_spawn_file_actions_adddup2:");
102	if ((errno = posix_spawnp(pid, argv[0], &actions, NULL, argv, environ)) > 0)
103		die("posix_spawnp %s:", argv[0]);
104	posix_spawn_file_actions_destroy(&actions);
105	close(fd[1]);
106
107	return f;
108}
109
110static void
111readspecial(struct special *sp, FILE *f)
112{
113	char *line = NULL, *s, *mode;
114	size_t size = 0;
115	ssize_t n;
116
117	while ((n = getline(&line, &size, f)) >= 0) {
118		if (line[n-1] == '\n')
119			line[--n] = '\0';
120		mode = s = line;
121		s = strchr(s, ' ');
122		if (!s || s == mode)
123			die("malformed permissions file: %s", PERMS_FILE);
124		*s++ = '\0';
125		sp->perms = realloc(sp->perms, (sp->len + 1) * sizeof(*sp->perms));
126		if (!sp->perms)
127			die("realloc:");
128		sp->perms[sp->len] = (struct perm){
129			.name = strdup(s),
130			.mode = strtoul(mode, &s, 8),
131		};
132		if (!sp->perms[sp->len].name)
133			die("strdup:");
134		if (*s)
135			die("invalid mode: %s", mode);
136		++sp->len;
137	}
138}
139
140static void
141gitspecial(struct special *sp, const char *rev)
142{
143	char object[41 + sizeof(PERMS_FILE)];
144	char *argv[] = {"git", "show", object, 0};
145	FILE *f;
146	pid_t pid;
147	int st, n;
148
149	n = snprintf(object, sizeof(object), "%s:%s", rev, PERMS_FILE);
150	if (n < 0 || n >= (int)sizeof(object))
151		die("revision is too large: %s", rev);
152	f = spawn(argv, &pid);
153	readspecial(sp, f);
154	fclose(f);
155
156	if (waitpid(pid, &st, 0) < 0)
157		die("waitpid:");
158	if (!(WIFEXITED(st) && WEXITSTATUS(st) == 0))
159		die("child process failed");
160}
161
162static int
163chmod_v(const char *path, mode_t mode)
164{
165	printf("chmod(\"%s\", %#o)\n", path, mode);
166	return fchmodat(rootfd, path, mode, 0);
167}
168
169static int
170mkdir_v(const char *path, mode_t mode)
171{
172	printf("mkdir(\"%s\", %#o)\n", path, mode);
173	return mkdirat(rootfd, path, mode);
174}
175
176static int
177rmdir_v(const char *path)
178{
179	printf("rmdir(\"%s\")\n", path);
180	return unlinkat(rootfd, path, AT_REMOVEDIR);
181}
182
183static int
184defperm(const char *name)
185{
186	struct stat st;
187	mode_t mode;
188
189	if (fstatat(rootfd, name, &st, AT_SYMLINK_NOFOLLOW) < 0)
190		return -1;
191	if (st.st_dev != rootdev) {
192		errno = EXDEV;
193		return -1;
194	}
195	switch (st.st_mode & S_IFMT) {
196	case S_IFREG:
197		mode = st.st_mode & S_IXUSR ? 0755 : 0644;
198		break;
199	case S_IFDIR:
200		mode = 0755;
201		break;
202	case S_IFLNK:
203		return 0;
204	default:
205		errno = EINVAL;
206		return -1;
207	}
208	if ((st.st_mode & ~S_IFMT) == mode)
209		return 0;
210	return chmod_v(name, mode);
211}
212
213static int
214specialperm(struct perm *p)
215{
216	struct stat st;
217
218	if (p->attempted)
219		return 0;
220	p->attempted = true;
221	if (fstatat(rootfd, p->name, &st, AT_SYMLINK_NOFOLLOW) < 0) {
222		if (errno != ENOENT || !S_ISDIR(p->mode))
223			return -1;
224		return mkdir_v(p->name, p->mode & ~S_IFMT);
225	}
226	if (st.st_dev != rootdev) {
227		errno = EXDEV;
228		return -1;
229	}
230	if (st.st_mode == p->mode)
231		return 0;
232	if ((st.st_mode & S_IFMT) != (p->mode & S_IFMT)) {
233		errno = EINVAL;
234		return -1;
235	}
236	return chmod_v(p->name, p->mode & ~S_IFMT);
237}
238
239static void
240specialperms(void)
241{
242	int i = 0, j = 0, n;
243
244	while (i < oldsp.len || j < newsp.len) {
245		if (i == oldsp.len)
246			n = 1;
247		else if (j == newsp.len)
248			n = -1;
249		else
250			n = strcmp(oldsp.perms[i].name, newsp.perms[j].name);
251		if (n >= 0) {
252			if (specialperm(&newsp.perms[j]) < 0 && errno != EXDEV)
253				error("specialperm:");
254			++j;
255			if (n == 0)
256				++i;
257			continue;
258		}
259		if ((oldsp.perms[i].mode & S_IFMT) == S_IFDIR) {
260			oldsp.perms[i].delete = true;
261		} else if (defperm(oldsp.perms[i].name) < 0) {
262			switch (errno) {
263			case ENOENT:
264			case EXDEV:
265				break;
266			default:
267				error("defperm:");
268			}
269		}
270		++i;
271	}
272	/* delete directories in reverse order */
273	while (i > 0) {
274		--i;
275		if (oldsp.perms[i].delete && rmdir_v(oldsp.perms[i].name) < 0) {
276			switch (errno) {
277			case ENOENT:
278			case ENOTEMPTY:
279				break;
280			default:
281				error("rmdir:");
282			}
283		}
284	}
285}
286
287static int
288setperm(const char *name)
289{
290	int i;
291
292	for (i = 0; i < newsp.len; ++i) {
293		if (strcmp(name, newsp.perms[i].name) == 0)
294			return specialperm(&newsp.perms[i]);
295	}
296	return defperm(name);
297}
298
299static void
300setroot(const char *root)
301{
302	struct stat st;
303
304	rootfd = open(root, O_RDONLY);
305	if (rootfd < 0)
306		die("open %s:", root);
307	if (fstat(rootfd, &st) < 0)
308		die("fstat:", root);
309	rootdev = st.st_dev;
310}
311
312static void
313gitupdate(char *old, char *new)
314{
315	char *argv_diff[] = {"git", "diff", "--name-only", "-z", old, new, 0};
316	char *argv_new[] = {"git", "ls-tree", "--name-only", "--full-tree", "-z", "-r", new, 0};
317	FILE *f;
318	pid_t pid;
319	struct {
320		char *buf;
321		size_t size;
322	} lines[2] = {0};
323	ssize_t n;
324	int cur = 0, st;
325	char *root, *path, *diff, *s;
326
327	root = getenv("GIT_WORK_TREE");
328	setroot(root ? root : ".");
329	if (old)
330		gitspecial(&oldsp, old);
331	gitspecial(&newsp, new);
332
333	f = spawn(old ? argv_diff : argv_new, &pid);
334	umask(0);
335	while ((n = getdelim(&lines[cur].buf, &lines[cur].size, '\0', f)) >= 0) {
336		path = lines[cur].buf;
337		if (strcmp(path, PERMS_FILE) == 0) {
338			specialperms();
339			continue;
340		}
341		if (setperm(path) < 0) switch (errno) {
342		case ENOENT:
343			continue;
344		case EXDEV:
345			break;
346		default:
347			error("setperm %s:", path);
348		}
349		/* find the first difference from the previous path */
350		diff = path;
351		if (lines[!cur].buf)
352			for (s = lines[!cur].buf; *s && *s == *diff; ++s, ++diff);
353		/* set permissions on each parent directory after that difference */
354		for (s = path + n; s >= diff; --s) {
355			if (*s != '/')
356				continue;
357			*s = '\0';
358			if (setperm(path) < 0)
359				error("setperm %s:", path);
360			*s = '/';
361		}
362		cur = !cur;
363	}
364	fclose(f);
365
366	if (waitpid(pid, &st, 0) < 0)
367		die("waitpid:");
368	if (!(WIFEXITED(st) && WEXITSTATUS(st) == 0))
369		die("child process failed");
370}
371
372static void
373applyspecial(void)
374{
375	int fd;
376	FILE *f;
377
378	fd = openat(rootfd, ".perms", O_RDONLY);
379	if (fd < 0)
380		die("open .perms:");
381	f = fdopen(fd, "r");
382	if (!f)
383		die("fdopen:");
384	readspecial(&newsp, f);
385	fclose(f);
386	umask(0);
387	specialperms();
388}
389
390static void
391usage(void)
392{
393	fprintf(stderr, "usage: %s [[old] new] | %s -d dir\n", prog, prog);
394	exit(2);
395}
396
397int
398main(int argc, char *argv[])
399{
400	int dflag = 0;
401	char *old, *new;
402
403	prog = basename(argv[0]);
404	for (++argv, --argc; argc && (*argv)[0] == '-' && (*argv)[1]; ++argv, --argc) {
405		switch ((*argv)[1]) {
406		case 'd':
407			if (!*++argv)
408				usage();
409			--argc;
410			setroot(*argv);
411			dflag = 1;
412			break;
413		default:
414			usage();
415		}
416	}
417
418	if (dflag) {
419		if (argc)
420			usage();
421		applyspecial();
422		return status;
423	}
424
425	switch (argc) {
426	case 0:
427		old = NULL;
428		new = "HEAD";
429		break;
430	case 1:
431		old = NULL;
432		new = argv[0];
433		break;
434	case 2:
435		old = argv[0];
436		new = argv[1];
437		break;
438	default:
439		usage();
440	}
441	gitupdate(old, new);
442	return status;
443}