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 }