/** * @file apply-default-acl.c * * @brief The entire implementation. * */ /* On Linux, ftw.h needs this special voodoo to work. */ #define _XOPEN_SOURCE 500 #include #include /* nftw() et al. */ #include #include /* dirname() */ #include /* PATH_MAX */ #include #include #include #include #include #include /* ACLs */ #include /* acl_get_perm, not portable */ #include #include /* Command-line options */ static bool no_exec_mask = false; /** * @brief Get the mode bits from the given path. * * @param path * The path (file or directory) whose mode we want. * * @return A mode_t (st_mode) structure containing the mode bits. * See sys/stat.h for details. */ mode_t get_mode(const char* path) { if (path == NULL) { errno = ENOENT; return -1; } struct stat s; int result = stat(path, &s); if (result == 0) { return s.st_mode; } else { /* errno will be set already by stat() */ return result; } } /** * @brief Determine whether or not the given path is a regular file. * * @param path * The path to test. * * @return true if @c path is a regular file, false otherwise. */ bool is_regular_file(const char* path) { if (path == NULL) { return false; } struct stat s; int result = stat(path, &s); if (result == 0) { return S_ISREG(s.st_mode); } else { return false; } } /** * @brief Determine whether or not the given path is a directory. * * @param path * The path to test. * * @return true if @c path is a directory, false otherwise. */ bool is_directory(const char* path) { if (path == NULL) { return false; } struct stat s; int result = stat(path, &s); if (result == 0) { return S_ISDIR(s.st_mode); } else { return false; } } int acl_set_entry(acl_t* aclp, acl_entry_t entry) { /* * Update or create the given entry. */ acl_tag_t entry_tag; int gt_result = acl_get_tag_type(entry, &entry_tag); if (gt_result == -1) { perror("acl_set_entry (acl_get_tag_type)"); return -1; } acl_permset_t entry_permset; int ps_result = acl_get_permset(entry, &entry_permset); if (ps_result == -1) { perror("acl_set_entry (acl_get_permset)"); return -1; } acl_entry_t existing_entry; /* Loop through the given ACL looking for matching entries. */ int result = acl_get_entry(*aclp, ACL_FIRST_ENTRY, &existing_entry); while (result == 1) { acl_tag_t existing_tag = ACL_UNDEFINED_TAG; int tag_result = acl_get_tag_type(existing_entry, &existing_tag); if (tag_result == -1) { perror("set_acl_tag_permset (acl_get_tag_type)"); return -1; } if (existing_tag == entry_tag) { if (entry_tag == ACL_USER_OBJ || entry_tag == ACL_GROUP_OBJ || entry_tag == ACL_OTHER) { /* Only update for these three since all other tags will have been wiped. */ acl_permset_t existing_permset; int gep_result = acl_get_permset(existing_entry, &existing_permset); if (gep_result == -1) { perror("acl_set_entry (acl_get_permset)"); return -1; } int s_result = acl_set_permset(existing_entry, entry_permset); if (s_result == -1) { perror("acl_set_entry (acl_set_permset)"); return -1; } return 1; } } result = acl_get_entry(*aclp, ACL_NEXT_ENTRY, &existing_entry); } /* This catches both the initial acl_get_entry and the ones at the end of the loop. */ if (result == -1) { perror("acl_set_entry (acl_get_entry)"); return -1; } /* If we've made it this far, we need to add a new entry to the ACL. */ acl_entry_t new_entry; /* We allocate memory here that we should release! */ int c_result = acl_create_entry(aclp, &new_entry); if (c_result == -1) { perror("acl_set_entry (acl_create_entry)"); return -1; } int st_result = acl_set_tag_type(new_entry, entry_tag); if (st_result == -1) { perror("acl_set_entry (acl_set_tag_type)"); return -1; } int s_result = acl_set_permset(new_entry, entry_permset); if (s_result == -1) { perror("acl_set_entry (acl_set_permset)"); return -1; } if (entry_tag == ACL_USER || entry_tag == ACL_GROUP) { /* We need to set the qualifier too. */ void* entry_qual = acl_get_qualifier(entry); if (entry_qual == (void*)NULL) { perror("acl_set_entry (acl_get_qualifier)"); return -1; } int sq_result = acl_set_qualifier(new_entry, entry_qual); if (sq_result == -1) { perror("acl_set_entry (acl_set_qualifier)"); return -1; } } return 1; } int acl_entry_count(acl_t* acl) { /* * Return the number of entries in acl, or -1 on error. */ acl_entry_t entry; int entry_count = 0; int result = acl_get_entry(*acl, ACL_FIRST_ENTRY, &entry); while (result == 1) { entry_count++; result = acl_get_entry(*acl, ACL_NEXT_ENTRY, &entry); } if (result == -1) { perror("acl_is_minimal (acl_get_entry)"); return -1; } return entry_count; } int acl_is_minimal(acl_t* acl) { /* An ACL is minimal if it has fewer than four entries. Return 0 for * false, 1 for true, and -1 on error. */ int ec = acl_entry_count(acl); if (ec == -1) { perror("acl_is_minimal (acl_entry_count)"); return -1; } if (ec < 4) { return 1; } else { return 0; } } int acl_execute_masked(const char* path) { /* Returns 1 i the given path has an ACL mask which denies execute. Returns 0 if it does not (or if it has no ACL/mask at all), and -1 on error. */ acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS); if (acl == (acl_t)NULL) { return 0; } /* Our return value. */ int result = 0; acl_entry_t entry; int ge_result = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); while (ge_result == 1) { acl_tag_t tag = ACL_UNDEFINED_TAG; int tag_result = acl_get_tag_type(entry, &tag); if (tag_result == -1) { perror("acl_execute_masked (acl_get_tag_type)"); result = -1; goto cleanup; } if (tag == ACL_MASK) { /* This is the mask entry, get its permissions, and see if execute is specified. */ acl_permset_t permset; int ps_result = acl_get_permset(entry, &permset); if (ps_result == -1) { perror("acl_execute_masked (acl_get_permset)"); result = -1; goto cleanup; } int gp_result = acl_get_perm(permset, ACL_EXECUTE); if (gp_result == -1) { perror("acl_execute_masked (acl_get_perm)"); result = -1; goto cleanup; } if (gp_result == 0) { /* No execute bit set in the mask; execute not allowed. */ return 1; } } ge_result = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry); } cleanup: acl_free(acl); return result; } int any_can_execute_or_dir(const char* path) { /* If the given path is a directory, returns 1. Otherwise, returns 1 * if any ACL entry has EFFECTIVE execute access, 0 if none do, and * -1 on error. * * This is meant to somewhat mimic setfacl's handling of the capital * 'X' perm, which allows execute access if the target is a * directory or someone can already execute it. We differ in that we * check the effective execute rather than just the execute bits. */ if (is_directory(path)) { /* That was easy... */ return 1; } acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS); if (acl == (acl_t)NULL) { return 0; } /* Our return value. */ int result = 0; if (acl_is_minimal(&acl)) { mode_t mode = get_mode(path); if (mode & (S_IXUSR | S_IXOTH | S_IXGRP)) { result = 1; goto cleanup; } else { result = 0; goto cleanup; } } acl_entry_t entry; int ge_result = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); while (ge_result == 1) { acl_permset_t permset; int ps_result = acl_get_permset(entry, &permset); if (ps_result == -1) { perror("any_can_execute_or_dir (acl_get_permset)"); result = -1; goto cleanup; } int gp_result = acl_get_perm(permset, ACL_EXECUTE); if (gp_result == -1) { perror("any_can_execute_or_dir (acl_get_perm)"); result = -1; goto cleanup; } if (gp_result == 1) { /* Only return one if this execute bit is not masked. */ if (acl_execute_masked(path) != 1) { result = 1; goto cleanup; } } ge_result = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry); } if (ge_result == -1) { perror("any_can_execute_or_dir (acl_get_entry)"); result = -1; goto cleanup; } cleanup: acl_free(acl); return result; } int inherit_default_acl(const char* path, const char* parent) { /* Inherit the default ACL from parent to path. This overwrites any * existing default ACL. Returns 1 for success, 0 for failure, and * -1 on error. */ /* Our return value. */ int result = 1; if (path == NULL) { errno = ENOENT; return -1; } if (!is_directory(path) || !is_directory(parent)) { return 0; } acl_t parent_acl = acl_get_file(parent, ACL_TYPE_DEFAULT); if (parent_acl == (acl_t)NULL) { return 0; } acl_t path_acl = acl_dup(parent_acl); if (path_acl == (acl_t)NULL) { perror("inherit_default_acl (acl_dup)"); acl_free(parent_acl); return -1; } int sf_result = acl_set_file(path, ACL_TYPE_DEFAULT, path_acl); if (sf_result == -1) { perror("inherit_default_acl (acl_set_file)"); result = -1; goto cleanup; } cleanup: acl_free(path_acl); return result; } int wipe_acls(const char* path) { /* Remove ACL_USER, ACL_GROUP, and ACL_MASK entries from path. Returns 1 for success, 0 for failure, and -1 on error. */ if (path == NULL) { errno = ENOENT; return -1; } /* Finally, remove individual named/mask entries. */ acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS); if (acl == (acl_t)NULL) { perror("wipe_acls (acl_get_file)"); return -1; } /* Our return value. */ int result = 1; acl_entry_t entry; int ge_result = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); while (ge_result == 1) { int d_result = acl_delete_entry(acl, entry); if (d_result == -1) { perror("wipe_acls (acl_delete_entry)"); result = -1; goto cleanup; } ge_result = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry); } /* Catches the first acl_get_entry as well as the ones at the end of the loop. */ if (ge_result == -1) { perror("wipe_acls (acl_get_entry)"); result = -1; goto cleanup; } int sf_result = acl_set_file(path, ACL_TYPE_ACCESS, acl); if (sf_result == -1) { perror("wipe_acls (acl_set_file)"); result = -1; goto cleanup; } cleanup: acl_free(acl); return result; } int apply_default_acl(const char* path) { /* Really apply the default ACL by looping through it. Returns one * for success, zero for failure (i.e. no ACL), and -1 on unexpected * errors. */ if (path == NULL) { return 0; } if (!is_regular_file(path) && !is_directory(path)) { return 0; } /* dirname mangles its argument */ char path_copy[PATH_MAX]; strncpy(path_copy, path, PATH_MAX-1); path_copy[PATH_MAX-1] = 0; char* parent = dirname(path_copy); if (!is_directory(parent)) { /* Make sure dirname() did what we think it did. */ return 0; } /* Default to not masking the exec bit; i.e. applying the default ACL literally. If --no-exec-mask was not specified, then we try to "guess" whether or not to mask the exec bit. */ bool allow_exec = true; if (!no_exec_mask) { int ace_result = any_can_execute_or_dir(path); if (ace_result == -1) { perror("apply_default_acl (any_can_execute_or_dir)"); return -1; } allow_exec = (bool)ace_result; } acl_t defacl = acl_get_file(parent, ACL_TYPE_DEFAULT); if (defacl == (acl_t)NULL) { perror("apply_default_acl (acl_get_file)"); return -1; } /* Our return value. */ int result = 1; int wipe_result = wipe_acls(path); if (wipe_result == -1) { perror("apply_default_acl (wipe_acls)"); result = -1; goto cleanup; } /* Do this after wipe_acls(), otherwise we'll overwrite the wiped ACL with this one. */ acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS); if (acl == (acl_t)NULL) { perror("apply_default_acl (acl_get_file)"); return -1; } /* If it's a directory, inherit the parent's default. */ int inherit_result = inherit_default_acl(path, parent); if (inherit_result == -1) { perror("apply_default_acl (inherit_acls)"); result = -1; goto cleanup; } acl_entry_t entry; int ge_result = acl_get_entry(defacl, ACL_FIRST_ENTRY, &entry); while (ge_result == 1) { acl_tag_t tag = ACL_UNDEFINED_TAG; int tag_result = acl_get_tag_type(entry, &tag); if (tag_result == -1) { perror("apply_default_acl (acl_get_tag_type)"); result = -1; goto cleanup; } /* We've got an entry/tag from the default ACL. Get its permset. */ acl_permset_t permset; int ps_result = acl_get_permset(entry, &permset); if (ps_result == -1) { perror("apply_default_acl (acl_get_permset)"); result = -1; goto cleanup; } /* If this is a default mask, fix it up. */ if (tag == ACL_MASK || tag == ACL_USER_OBJ || tag == ACL_GROUP_OBJ || tag == ACL_OTHER) { if (!allow_exec) { /* The mask doesn't affect acl_user_obj, acl_group_obj (in minimal ACLs) or acl_other entries, so if execute should be masked, we have to do it manually. */ int d_result = acl_delete_perm(permset, ACL_EXECUTE); if (d_result == -1) { perror("apply_default_acl (acl_delete_perm)"); result = -1; goto cleanup; } int sp_result = acl_set_permset(entry, permset); if (sp_result == -1) { perror("apply_default_acl (acl_set_permset)"); result = -1; goto cleanup; } } } /* Finally, add the permset to the access ACL. */ int set_result = acl_set_entry(&acl, entry); if (set_result == -1) { perror("apply_default_acl (acl_set_entry)"); result = -1; goto cleanup; } ge_result = acl_get_entry(defacl, ACL_NEXT_ENTRY, &entry); } /* Catches the first acl_get_entry as well as the ones at the end of the loop. */ if (ge_result == -1) { perror("apply_default_acl (acl_get_entry)"); result = -1; goto cleanup; } int sf_result = acl_set_file(path, ACL_TYPE_ACCESS, acl); if (sf_result == -1) { perror("apply_default_acl (acl_set_file)"); result = -1; goto cleanup; } cleanup: acl_free(defacl); return result; } void usage(char* program_name) { /* * Print usage information. */ printf("Apply any applicable default ACLs to the given files or " "directories.\n\n"); printf("Usage: %s [flags] [ [ ...]]\n\n", program_name); printf("Flags:\n"); printf(" -h, --help Print this help message\n"); printf(" -r, --recursive Act on any given directories recursively\n"); printf(" -x, --no-exec-mask Apply execute permissions unconditionally\n"); return; } int apply_default_acl_nftw(const char *target, const struct stat *s, int info, struct FTW *ftw) { /* A wrapper around the apply_default_acl() function for use with * nftw(). We need to adjust the return value so that nftw() doesn't * think we've failed. */ bool reapp_result = apply_default_acl(target); if (reapp_result) { return 0; } else { return 1; } } bool apply_default_acl_recursive(const char *target) { /* Attempt to apply default ACLs recursively. If target is a * directory, we recurse through its entries. If not, we just * apply the default ACL to target. * * We ignore symlinks for consistency with chmod -r. * */ if (!is_directory(target)) { return apply_default_acl(target); } int max_levels = 256; int flags = FTW_PHYS; /* Don't follow links. */ int nftw_result = nftw(target, apply_default_acl_nftw, max_levels, flags); if (nftw_result == 0) { /* Success */ return true; } /* nftw will return -1 on error, or if the supplied function * (apply_default_acl_nftw) returns a non-zero result, nftw will * return that. */ if (nftw_result == -1) { perror("apply_default_acl_recursive (nftw)"); } return false; } int main(int argc, char* argv[]) { /* * Call apply_default_acl on each command-line argument. */ if (argc < 2) { usage(argv[0]); return EXIT_FAILURE; } bool recursive = false; /* bool no_exec_mask is declared static/global */ struct option long_options[] = { /* These options set a flag. */ {"help", no_argument, NULL, 'h'}, {"recursive", no_argument, NULL, 'r'}, {"no-exec-mask", no_argument, NULL, 'x'}, {NULL, 0, NULL, 0} }; int opt = 0; while ((opt = getopt_long(argc, argv, "hrx", long_options, NULL)) != -1) { switch (opt) { case 'h': usage(argv[0]); return EXIT_SUCCESS; case 'r': recursive = true; break; case 'x': no_exec_mask = true; break; default: usage(argv[0]); return EXIT_FAILURE; } } int result = EXIT_SUCCESS; int arg_index = 1; for (arg_index = optind; arg_index < argc; arg_index++) { const char* target = argv[arg_index]; bool reapp_result = false; if (recursive) { reapp_result = apply_default_acl_recursive(target); } else { /* It's either normal file, or we're not operating recursively. */ reapp_result = apply_default_acl(target); } if (!reapp_result) { result = EXIT_FAILURE; } } return result; }