opkg

statically linked package installer
git clone anongit@rnpnr.xyz:opkg.git
Log | Files | Refs | Feed | Submodules | README | LICENSE

applyperms.c (8708B)


      1 /*
      2 See LICENSE file for copyright and license details.
      3 
      4 This program is meant to be run by a git hook to fix the permissions of files
      5 based on a .perms file in the repository
      6 
      7 It can also be run with the -d flag on any directory containing .perms in order
      8 to apply the permissions specified by that file.
      9 
     10 Security considerations:
     11 If the repository previously contained a world or group readable directory which
     12 has become secret, the names of the new files in that directory will become
     13 temporarily 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 
     32 struct 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 
     39 struct special {
     40 	struct perm *perms;
     41 	int len;
     42 };
     43 
     44 extern char **environ;
     45 
     46 static char *prog;
     47 static dev_t rootdev;
     48 static int rootfd = AT_FDCWD;
     49 static struct special oldsp, newsp;
     50 static int status;
     51 
     52 static void
     53 verror(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 
     62 static void
     63 error(char *fmt, ...)
     64 {
     65 	va_list ap;
     66 
     67 	va_start(ap, fmt);
     68 	verror(fmt, ap);
     69 	va_end(ap);
     70 }
     71 
     72 static noreturn void
     73 die(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 
     84 static FILE *
     85 spawn(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 
    110 static void
    111 readspecial(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 
    140 static void
    141 gitspecial(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 
    162 static int
    163 chmod_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 
    169 static int
    170 mkdir_v(const char *path, mode_t mode)
    171 {
    172 	printf("mkdir(\"%s\", %#o)\n", path, mode);
    173 	return mkdirat(rootfd, path, mode);
    174 }
    175 
    176 static int
    177 rmdir_v(const char *path)
    178 {
    179 	printf("rmdir(\"%s\")\n", path);
    180 	return unlinkat(rootfd, path, AT_REMOVEDIR);
    181 }
    182 
    183 static int
    184 defperm(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 
    213 static int
    214 specialperm(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 
    239 static void
    240 specialperms(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 
    287 static int
    288 setperm(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 
    299 static void
    300 setroot(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 
    312 static void
    313 gitupdate(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 
    372 static void
    373 applyspecial(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 
    390 static void
    391 usage(void)
    392 {
    393 	fprintf(stderr, "usage: %s [[old] new] | %s -d dir\n", prog, prog);
    394 	exit(2);
    395 }
    396 
    397 int
    398 main(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 }