]> gitweb.michael.orlitzky.com - apply-default-acl.git/blobdiff - src/libadacl.c
Replace nftw() with manual recursion in apply_default_acl().
[apply-default-acl.git] / src / libadacl.c
index e83f46be630bc40c779f9792786fa2c82e9ff944..38e362e5066fdeb4f086ba19774fd65faae78c99 100644 (file)
@@ -8,6 +8,7 @@
 /* Enables get_current_dir_name() in unistd.h and the O_PATH flag. */
 #define _GNU_SOURCE
 
+#include <dirent.h>     /* readdir(), etc. */
 #include <errno.h>      /* EINVAL, ELOOP, ENOTDIR, etc. */
 #include <fcntl.h>      /* openat() */
 #include <libgen.h>     /* basename(), dirname() */
@@ -622,35 +623,42 @@ int has_default_acl_fd(int fd) {
 }
 
 
+
 /**
- * @brief Apply parent default ACL to a path.
+ * @brief The recursive portion of @c apply_default_acl.
  *
- * This overwrites any existing ACLs on @c path.
+ * The @c apply_default_acl function takes a path, but then opens file
+ * descriptors for the path and its parent. Afterwards, everything is
+ * done using file descriptors, including the recursive application on
+ * the path's children. This function encapsulates the portion of @c
+ * apply_default_acl that uses only file descriptors; for the
+ * recursion, this function ultimately calls itself.
  *
- * @param path
- *   The path whose ACL we would like to reset to its default.
+ * This overwrites any existing ACLs on @c fd and, if @c recursive is
+ * @c true, its children. When @c recursive is @c true, the "worst"
+ * result encountered is returned as the overall result.
+ *
+ * @param parent_fd
+ *   A file descriptor for the parent directory of @c fd.
+ *
+ * @param fd
+ *   The file descriptor that should inherit its parent's default ACL.
  *
  * @param no_exec_mask
  *   The value (either true or false) of the --no-exec-mask flag.
  *
+ * @param recursive
+ *   Should we recurse into subdirectories?
+ *
  * @return
- *   - @c ACL_SUCCESS - The parent default ACL was inherited successfully.
+ *   - @c ACL_SUCCESS - The parent default ACLs were inherited successfully.
  *   - @c ACL_FAILURE - If symlinks or hard links are encountered.
  *   - @c ACL_ERROR - Unexpected library error.
  */
-int apply_default_acl(const char* path, bool no_exec_mask) {
-
-  if (path == NULL) {
-    errno = EINVAL;
-    perror("apply_default_acl (args)");
-    return ACL_ERROR;
-  }
-
-  /* Define these next three variables here because we may have to
-   * jump to the cleanup routine which expects them to exist.
-   */
-
-  /* Our return value. */
+int apply_default_acl_fds(int parent_fd,
+                          int fd,
+                          bool no_exec_mask,
+                          bool recursive) {
   int result = ACL_SUCCESS;
 
   /* The new ACL for this path */
@@ -660,71 +668,6 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
      order to mask the execute bit. */
   acl_t new_acl_unmasked = (acl_t)NULL;
 
-  /* The file descriptor corresponding to "path" */
-  int fd = 0;
-
-  /* The file descriptor for the directory containing "path" */
-  int parent_fd = 0;
-
-  /* dirname() and basename() mangle their arguments, so we need
-     to make copies of "path" before using them. */
-  char* dirname_path_copy = NULL;
-  char* basename_path_copy = NULL;
-
-  /* Get the parent directory of "path" with dirname(), which happens
-   * to murder its argument and necessitates a path_copy. */
-  dirname_path_copy = strdup(path);
-  if (dirname_path_copy == NULL) {
-    perror("apply_default_acl (strdup)");
-    return ACL_ERROR;
-  }
-  char* parent = dirname(dirname_path_copy);
-  parent_fd = safe_open(parent, O_DIRECTORY | O_NOFOLLOW);
-  if (parent_fd == OPEN_ERROR) {
-    if (errno == ELOOP || errno == ENOTDIR) {
-      /* We hit a symlink, either in the last path component (ELOOP)
-         or higher up (ENOTDIR). */
-      result = ACL_FAILURE;
-      goto cleanup;
-    }
-    else {
-      perror("apply_default_acl (open parent fd)");
-      result = ACL_ERROR;
-      goto cleanup;
-    }
-  }
-
-  /* Check to make sure the parent descriptor actually has a default
-     ACL. If it doesn't, then we can "succeed" immediately. */
-  if (has_default_acl_fd(parent_fd) == ACL_FAILURE) {
-    result = ACL_SUCCESS;
-    goto cleanup;
-  }
-
-  /* We already obtained the parent fd safely, so if we use the
-     basename of path here instead of the full thing, then we can get
-     away with using openat() and spare ourselves the slowness of
-     another safe_open(). */
-  basename_path_copy = strdup(path);
-  if (basename_path_copy == NULL) {
-    perror("apply_default_acl (strdup)");
-    return ACL_ERROR;
-  }
-  fd = openat(parent_fd, basename(basename_path_copy), O_NOFOLLOW);
-  if (fd == OPEN_ERROR) {
-    if (errno == ELOOP || errno == ENOTDIR) {
-      /* We hit a symlink, either in the last path component (ELOOP)
-         or higher up (ENOTDIR). */
-      result = ACL_FAILURE;
-      goto cleanup;
-    }
-    else {
-      perror("apply_default_acl (open fd)");
-      result = ACL_ERROR;
-      goto cleanup;
-    }
-  }
-
   /* Refuse to operate on hard links, which can be abused by an
    * attacker to trick us into changing the ACL on a file we didn't
    * intend to; namely the "target" of the hard link. There is TOCTOU
@@ -734,16 +677,33 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
   */
   struct stat s;
   if (fstat(fd, &s) == STAT_ERROR) {
-    perror("apply_default_acl (fstat)");
+    perror("apply_default_acl_fds (fstat)");
+    /* We can't recurse without the stat struct for fd */
     goto cleanup;
   }
 
+
+  /* Check to make sure the parent descriptor actually has a default
+     ACL. If it doesn't, then we can "succeed" immediately, saving a
+     little work, particularly in any_can_execute(). Note that we
+     can't skip the fstat() above, because we need it in case we
+     recurse. */
+  if (has_default_acl_fd(parent_fd) == ACL_FAILURE) {
+    result = ACL_SUCCESS;
+    /* Just because this target can't inherit anything doesn't mean
+       that one of it's children can't. For example, if there's a
+       default on "c" in "a/b/c/d", then we don't want to skip all
+       children of "a"! */
+    goto recurse;
+  }
+
+
   if (!S_ISDIR(s.st_mode)) {
     /* If it's not a directory, make sure it's a regular,
        non-hard-linked file. */
     if (!S_ISREG(s.st_mode) || s.st_nlink != 1) {
       result = ACL_FAILURE;
-      goto cleanup;
+      goto cleanup; /* It's not a directory, so we can skip the recursion. */
     }
   }
 
@@ -759,7 +719,7 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
     int ace_result = any_can_execute(fd,&s) || S_ISDIR(s.st_mode);
 
     if (ace_result == ACL_ERROR) {
-      perror("apply_default_acl (any_can_execute)");
+      perror("apply_default_acl_fds (any_can_execute)");
       result = ACL_ERROR;
       goto cleanup;
     }
@@ -773,7 +733,7 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
                        ACL_TYPE_DEFAULT,
                        fd,
                        ACL_TYPE_DEFAULT) == ACL_ERROR) {
-      perror("apply_default_acl (acl_copy_xattr default)");
+      perror("apply_default_acl_fds (acl_copy_xattr default)");
       result = ACL_ERROR;
       goto cleanup;
     }
@@ -784,7 +744,7 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
                      ACL_TYPE_DEFAULT,
                      fd,
                      ACL_TYPE_ACCESS) == ACL_ERROR) {
-    perror("apply_default_acl (acl_copy_xattr access)");
+    perror("apply_default_acl_fds (acl_copy_xattr access)");
     result = ACL_ERROR;
     goto cleanup;
   }
@@ -803,14 +763,15 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
   /* Now we potentially need to mask the execute permissions in the
      ACL on fd; or maybe not. */
   if (allow_exec) {
-    goto cleanup;
+    /* Skip the mask code for this target, but don't skip its children! */
+    goto recurse;
   }
 
   /* OK, we need to mask some execute permissions. First obtain the
      current ACL... */
   new_acl = acl_get_fd(fd);
   if (new_acl == (acl_t)NULL) {
-    perror("apply_default_acl (acl_get_fd)");
+    perror("apply_default_acl_fds (acl_get_fd)");
     result = ACL_ERROR;
     goto cleanup;
   }
@@ -820,7 +781,7 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
      looping over it no worky). */
   new_acl_unmasked = acl_dup(new_acl);
   if (new_acl_unmasked == (acl_t)NULL) {
-    perror("apply_default_acl (acl_dup)");
+    perror("apply_default_acl_fds (acl_dup)");
     result = ACL_ERROR;
     goto cleanup;
   }
@@ -832,7 +793,7 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
     acl_tag_t tag = ACL_UNDEFINED_TAG;
 
     if (acl_get_tag_type(entry, &tag) == ACL_ERROR) {
-       perror("apply_default_acl (acl_get_tag_type)");
+       perror("apply_default_acl_fds (acl_get_tag_type)");
        result = ACL_ERROR;
        goto cleanup;
     }
@@ -841,7 +802,7 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
     /* We've got an entry/tag from the default ACL. Get its permset. */
     acl_permset_t permset;
     if (acl_get_permset(entry, &permset) == ACL_ERROR) {
-      perror("apply_default_acl (acl_get_permset)");
+      perror("apply_default_acl_fds (acl_get_permset)");
       result = ACL_ERROR;
       goto cleanup;
     }
@@ -855,20 +816,20 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
          minimal ACLs) or acl_other entries, so if execute should be
          masked, we have to do it manually. */
       if (acl_delete_perm(permset, ACL_EXECUTE) == ACL_ERROR) {
-        perror("apply_default_acl (acl_delete_perm)");
+        perror("apply_default_acl_fds (acl_delete_perm)");
         result = ACL_ERROR;
         goto cleanup;
       }
 
       if (acl_set_permset(entry, permset) == ACL_ERROR) {
-        perror("apply_default_acl (acl_set_permset)");
+        perror("apply_default_acl_fds (acl_set_permset)");
         result = ACL_ERROR;
         goto cleanup;
       }
     }
 
     if (acl_update_entry(new_acl, entry) == ACL_ERROR) {
-      perror("apply_default_acl (acl_update_entry)");
+      perror("apply_default_acl_fds (acl_update_entry)");
       result = ACL_ERROR;
       goto cleanup;
     }
@@ -879,30 +840,190 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
   /* Catches the first acl_get_entry as well as the ones at the end of
      the loop. */
   if (ge_result == ACL_ERROR) {
-    perror("apply_default_acl (acl_get_entry)");
+    perror("apply_default_acl_fds (acl_get_entry)");
     result = ACL_ERROR;
     goto cleanup;
   }
 
   if (acl_set_fd(fd, new_acl) == ACL_ERROR) {
-    perror("apply_default_acl (acl_set_fd)");
+    perror("apply_default_acl_fds (acl_set_fd)");
     result = ACL_ERROR;
     goto cleanup;
   }
 
+ recurse:
+  if (recursive && S_ISDIR(s.st_mode)) {
+    /* Recurse into subdirectories. Don't call closedir() on d! It
+       closes the open file descriptor as well, and subsequent calls
+       to close() then throw errors. */
+    DIR* d = fdopendir(fd);
+    if (d == NULL) {
+      perror("apply_default_acl_fds (fdopendir)");
+      result = ACL_ERROR;
+      goto cleanup;
+    }
+
+    struct dirent* de;
+    int new_fd = 0;
+    while ((de = readdir(d)) != NULL) {
+      if (de->d_type != DT_DIR && de->d_type != DT_REG) {
+        /* Hit a symlink or whatever. */
+        result = ACL_FAILURE;
+        continue;
+      }
+      if (strcmp(de->d_name, ".") == 0) { continue; }
+      if (strcmp(de->d_name, "..") == 0) { continue; }
+
+      /* Be careful not to "return" out of this loop and leave the
+         new_fd open! */
+      new_fd = openat(fd, de->d_name, O_NOFOLLOW);
+      if (new_fd == OPEN_ERROR) {
+        if (errno == ELOOP || errno == ENOTDIR) {
+          /* We hit a symlink, either in the last path component (ELOOP)
+             or higher up (ENOTDIR). */
+          if (result == ACL_SUCCESS) {
+            /* Don't overwrite an error result with success/failure. */
+            result = ACL_FAILURE;
+          }
+          continue;
+        }
+        else {
+          perror("apply_default_acl_fds (openat)");
+          result = ACL_ERROR;
+          continue;
+        }
+      }
+      switch (apply_default_acl_fds(fd, new_fd, no_exec_mask, recursive)) {
+        /* Don't overwrite an error result with success/failure. */
+        case ACL_FAILURE:
+          if (result == ACL_SUCCESS) {
+            result = ACL_FAILURE;
+          }
+        case ACL_ERROR:
+          result = ACL_ERROR;
+        default:
+          if (close(new_fd) == CLOSE_ERROR) {
+            perror("apply_default_acl_fds (close)");
+            result = ACL_ERROR;
+          }
+      }
+    }
+  }
+
  cleanup:
-  free(dirname_path_copy);
-  free(basename_path_copy);
   acl_free(new_acl);
   acl_free(new_acl_unmasked);
+  return result;
+}
 
-  if (fd > 0 && close(fd) == CLOSE_ERROR) {
-    perror("apply_default_acl (close fd)");
-    result = ACL_ERROR;
+
+/**
+ * @brief Apply parent default ACL to a path and optionally its children.
+ *
+ * This overwrites any existing ACLs on the target, and, if @c
+ * recursive is @c true, its children. When @c recursive is @c true,
+ * the "worst" result encountered is returned as the overall result.
+ *
+ * @param path
+ *   The path whose ACL we would like to reset to its default.
+ *
+ * @param no_exec_mask
+ *   The value (either true or false) of the --no-exec-mask flag.
+ *
+ * @param recursive
+ *   Should we recurse into subdirectories?
+ *
+ * @return
+ *   - @c ACL_SUCCESS - The parent default ACLs were inherited successfully.
+ *   - @c ACL_FAILURE - If symlinks or hard links are encountered.
+ *   - @c ACL_ERROR - Unexpected library error.
+ */
+int apply_default_acl(const char* path, bool no_exec_mask, bool recursive) {
+
+  if (path == NULL) {
+    errno = EINVAL;
+    perror("apply_default_acl (args)");
+    return ACL_ERROR;
+  }
+
+  /* Define these next three variables here because we may have to
+   * jump to the cleanup routine which expects them to exist.
+   */
+
+  /* Our return value. */
+  int result = ACL_SUCCESS;
+
+  /* The file descriptor corresponding to "path" */
+  int fd = 0;
+
+  /* The file descriptor for the directory containing "path" */
+  int parent_fd = 0;
+
+  /* dirname() and basename() mangle their arguments, so we need
+     to make copies of "path" before using them. */
+  char* dirname_path_copy = NULL;
+  char* basename_path_copy = NULL;
+
+  /* Get the parent directory of "path" with dirname(), which happens
+   * to murder its argument and necessitates a path_copy. */
+  dirname_path_copy = strdup(path);
+  if (dirname_path_copy == NULL) {
+    perror("apply_default_acl (strdup)");
+    return ACL_ERROR;
+  }
+  char* parent = dirname(dirname_path_copy);
+  parent_fd = safe_open(parent, O_DIRECTORY | O_NOFOLLOW);
+  if (parent_fd == OPEN_ERROR) {
+    if (errno == ELOOP || errno == ENOTDIR) {
+      /* We hit a symlink, either in the last path component (ELOOP)
+         or higher up (ENOTDIR). */
+      result = ACL_FAILURE;
+      goto cleanup;
+    }
+    else {
+      perror("apply_default_acl (open parent fd)");
+      result = ACL_ERROR;
+      goto cleanup;
+    }
   }
+
+  /* We already obtained the parent fd safely, so if we use the
+     basename of path here instead of the full thing, then we can get
+     away with using openat() and spare ourselves the slowness of
+     another safe_open(). */
+  basename_path_copy = strdup(path);
+  if (basename_path_copy == NULL) {
+    perror("apply_default_acl (strdup)");
+    return ACL_ERROR;
+  }
+  fd = openat(parent_fd, basename(basename_path_copy), O_NOFOLLOW);
+  if (fd == OPEN_ERROR) {
+    if (errno == ELOOP || errno == ENOTDIR) {
+      /* We hit a symlink, either in the last path component (ELOOP)
+         or higher up (ENOTDIR). */
+      result = ACL_FAILURE;
+      goto cleanup;
+    }
+    else {
+      perror("apply_default_acl (open fd)");
+      result = ACL_ERROR;
+      goto cleanup;
+    }
+  }
+
+  result = apply_default_acl_fds(parent_fd, fd, no_exec_mask, recursive);
+
+ cleanup:
+  free(dirname_path_copy);
+  free(basename_path_copy);
+
   if (parent_fd > 0 && close(parent_fd) == CLOSE_ERROR) {
     perror("apply_default_acl (close parent_fd)");
     result = ACL_ERROR;
   }
+  if (fd > 0 && close(fd) == CLOSE_ERROR) {
+    perror("apply_default_acl (close fd)");
+    result = ACL_ERROR;
+  }
   return result;
 }