/* * Copyright 2016 Chris Torek * All rights reserved * * Redistribution and use in source and binary forms, with or without * modification, are permitted providing that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include "lib9p.h" #include "lib9p_impl.h" #include "genacl.h" #include "fid.h" #include "log.h" typedef int econvertfn(acl_entry_t, struct l9p_ace *); #ifndef __APPLE__ static struct l9p_acl *l9p_new_acl(uint32_t acetype, uint32_t aceasize); static struct l9p_acl *l9p_growacl(struct l9p_acl *acl, uint32_t aceasize); static int l9p_count_aces(acl_t sysacl); static struct l9p_acl *l9p_sysacl_to_acl(int, acl_t, econvertfn *); #endif static bool l9p_ingroup(gid_t tid, gid_t gid, gid_t *gids, size_t ngids); static int l9p_check_aces(int32_t mask, struct l9p_acl *acl, struct stat *st, uid_t uid, gid_t gid, gid_t *gids, size_t ngids); void l9p_acl_free(struct l9p_acl *acl) { free(acl); } /* * Is the given group ID tid (test-id) any of the gid's in agids? */ static bool l9p_ingroup(gid_t tid, gid_t gid, gid_t *gids, size_t ngids) { size_t i; if (tid == gid) return (true); for (i = 0; i < ngids; i++) if (tid == gids[i]) return (true); return (false); } /* #define ACE_DEBUG */ /* * Note that NFSv4 tests are done on a "first match" basis. * That is, we check each ACE sequentially until we run out * of ACEs, or find something explicitly denied (DENIED!), * or have cleared out all our attempt-something bits. Once * we come across an ALLOW entry for the bits we're trying, * we clear those from the bits we're still looking for, in * the order they appear. * * The result is either "definitely allowed" (we cleared * all the bits), "definitely denied" (we hit a deny with * some or all of the bits), or "unspecified". We * represent these three states as +1 (positive = yes = allow), * -1 (negative = no = denied), or 0 (no strong answer). * * For our caller's convenience, if we are called with a * mask of 0, we return 0 (no answer). */ static int l9p_check_aces(int32_t mask, struct l9p_acl *acl, struct stat *st, uid_t uid, gid_t gid, gid_t *gids, size_t ngids) { uint32_t i; struct l9p_ace *ace; #ifdef ACE_DEBUG const char *acetype, *allowdeny; bool show_tid; #endif bool match; uid_t tid; if (mask == 0) return (0); for (i = 0; mask != 0 && i < acl->acl_nace; i++) { ace = &acl->acl_aces[i]; switch (ace->ace_type) { case L9P_ACET_ACCESS_ALLOWED: case L9P_ACET_ACCESS_DENIED: break; default: /* audit, alarm - ignore */ continue; } #ifdef ACE_DEBUG show_tid = false; #endif if (ace->ace_flags & L9P_ACEF_OWNER) { #ifdef ACE_DEBUG acetype = "OWNER@"; #endif match = st->st_uid == uid; } else if (ace->ace_flags & L9P_ACEF_GROUP) { #ifdef ACE_DEBUG acetype = "GROUP@"; #endif match = l9p_ingroup(st->st_gid, gid, gids, ngids); } else if (ace->ace_flags & L9P_ACEF_EVERYONE) { #ifdef ACE_DEBUG acetype = "EVERYONE@"; #endif match = true; } else { if (ace->ace_idsize != sizeof(tid)) continue; #ifdef ACE_DEBUG show_tid = true; #endif memcpy(&tid, &ace->ace_idbytes, sizeof(tid)); if (ace->ace_flags & L9P_ACEF_IDENTIFIER_GROUP) { #ifdef ACE_DEBUG acetype = "group"; #endif match = l9p_ingroup(tid, gid, gids, ngids); } else { #ifdef ACE_DEBUG acetype = "user"; #endif match = tid == uid; } } /* * If this ACE applies to us, check remaining bits. * If any of those bits also apply, check the type: * DENY means "stop now", ALLOW means allow these bits * and keep checking. */ #ifdef ACE_DEBUG allowdeny = ace->ace_type == L9P_ACET_ACCESS_DENIED ? "deny" : "allow"; #endif if (match && (ace->ace_mask & (uint32_t)mask) != 0) { #ifdef ACE_DEBUG if (show_tid) L9P_LOG(L9P_DEBUG, "ACE: %s %s %d: mask 0x%x ace_mask 0x%x", allowdeny, acetype, (int)tid, (u_int)mask, (u_int)ace->ace_mask); else L9P_LOG(L9P_DEBUG, "ACE: %s %s: mask 0x%x ace_mask 0x%x", allowdeny, acetype, (u_int)mask, (u_int)ace->ace_mask); #endif if (ace->ace_type == L9P_ACET_ACCESS_DENIED) return (-1); mask &= ~ace->ace_mask; #ifdef ACE_DEBUG L9P_LOG(L9P_DEBUG, "clear 0x%x: now mask=0x%x", (u_int)ace->ace_mask, (u_int)mask); #endif } else { #ifdef ACE_DEBUG if (show_tid) L9P_LOG(L9P_DEBUG, "ACE: SKIP %s %s %d: " "match %d mask 0x%x ace_mask 0x%x", allowdeny, acetype, (int)tid, (int)match, (u_int)mask, (u_int)ace->ace_mask); else L9P_LOG(L9P_DEBUG, "ACE: SKIP %s %s: " "match %d mask 0x%x ace_mask 0x%x", allowdeny, acetype, (int)match, (u_int)mask, (u_int)ace->ace_mask); #endif } } /* Return 1 if access definitely granted. */ #ifdef ACE_DEBUG L9P_LOG(L9P_DEBUG, "ACE: end of ACEs, mask now 0x%x: %s", mask, mask ? "no-definitive-answer" : "ALLOW"); #endif return (mask == 0 ? 1 : 0); } /* * Test against ACLs. * * The return value is normally 0 (access allowed) or EPERM * (access denied), so it could just be a boolean.... * * For "make new dir in dir" and "remove dir in dir", you must * set the mask to test the directory permissions (not ADD_FILE but * ADD_SUBDIRECTORY, and DELETE_CHILD). For "make new file in dir" * you must set the opmask to test file ADD_FILE. * * The L9P_ACE_DELETE flag means "can delete this thing"; it's not * clear whether it should override the parent directory's ACL if * any. In our case it does not, but a caller may try * L9P_ACE_DELETE_CHILD (separately, on its own) and then a * (second, separate) L9P_ACE_DELETE, to make the permissions work * as "or" instead of "and". * * Pass a NULL parent/pstat if they are not applicable, e.g., * for doing operations on an existing file, such as reading or * writing data or attributes. Pass in a null child/cstat if * that's not applicable, such as creating a new file/dir. * * NB: it's probably wise to allow the owner of any file to update * the ACLs of that file, but we leave that test to the caller. */ int l9p_acl_check_access(int32_t opmask, struct l9p_acl_check_args *args) { struct l9p_acl *parent, *child; struct stat *pstat, *cstat; int32_t pop, cop; size_t ngids; uid_t uid; gid_t gid, *gids; int panswer, canswer; assert(opmask != 0); parent = args->aca_parent; pstat = args->aca_pstat; child = args->aca_child; cstat = args->aca_cstat; uid = args->aca_uid; gid = args->aca_gid; gids = args->aca_groups; ngids = args->aca_ngroups; #ifdef ACE_DEBUG L9P_LOG(L9P_DEBUG, "l9p_acl_check_access: opmask=0x%x uid=%ld gid=%ld ngids=%zd", (u_int)opmask, (long)uid, (long)gid, ngids); #endif /* * If caller said "superuser semantics", check that first. * Note that we apply them regardless of ACLs. */ if (uid == 0 && args->aca_superuser) return (0); /* * If told to ignore ACLs and use only stat-based permissions, * discard any non-NULL ACL pointers. * * This will need some fancying up when we support POSIX ACLs. */ if ((args->aca_aclmode & L9P_ACM_NFS_ACL) == 0) parent = child = NULL; assert(parent == NULL || parent->acl_acetype == L9P_ACLTYPE_NFSv4); assert(parent == NULL || pstat != NULL); assert(child == NULL || child->acl_acetype == L9P_ACLTYPE_NFSv4); assert(child == NULL || cstat != NULL); assert(pstat != NULL || cstat != NULL); /* * If the operation is UNLINK we should have either both ACLs * or no ACLs, but we won't require that here. * * If a parent ACL is supplied, it's a directory by definition. * Make sure we're allowed to do this there, whatever this is. * If a child ACL is supplied, check it too. Note that the * DELETE permission only applies in the child though, not * in the parent, and the DELETE_CHILD only applies in the * parent. */ pop = cop = opmask; if (parent != NULL || pstat != NULL) { /* * Remove child-only bits from parent op and * parent-only bits from child op. * * L9P_ACE_DELETE is child-only. * * L9P_ACE_DELETE_CHILD is parent-only, and three data * access bits overlap with three directory access bits. * We should have child==NULL && cstat==NULL, so the * three data bits should be redundant, but it's * both trivial and safest to remove them anyway. */ pop &= ~L9P_ACE_DELETE; cop &= ~(L9P_ACE_DELETE_CHILD | L9P_ACE_LIST_DIRECTORY | L9P_ACE_ADD_FILE | L9P_ACE_ADD_SUBDIRECTORY); } else { /* * Remove child-only bits from parent op. We need * not bother since we just found we have no parent * and no pstat, and hence won't actually *use* pop. * * pop &= ~(L9P_ACE_READ_DATA | L9P_ACE_WRITE_DATA | * L9P_ACE_APPEND_DATA); */ } panswer = 0; canswer = 0; if (parent != NULL) panswer = l9p_check_aces(pop, parent, pstat, uid, gid, gids, ngids); if (child != NULL) canswer = l9p_check_aces(cop, child, cstat, uid, gid, gids, ngids); if (panswer || canswer) { /* * Got a definitive answer from parent and/or * child ACLs. We're not quite done yet though. */ if (opmask == L9P_ACOP_UNLINK) { /* * For UNLINK, we can get an allow from child * and deny from parent, or vice versa. It's * not 100% clear how to handle the two-answer * case. ZFS says that if either says "allow", * we allow, and if both definitely say "deny", * we deny. This makes sense, so we do that * here for all cases, even "strict". */ if (panswer > 0 || canswer > 0) return (0); if (panswer < 0 && canswer < 0) return (EPERM); /* non-definitive answer from one! move on */ } else { /* * Have at least one definitive answer, and * should have only one; obey whichever * one it is. */ if (panswer) return (panswer < 0 ? EPERM : 0); return (canswer < 0 ? EPERM : 0); } } /* * No definitive answer from ACLs alone. Check for ZFS style * permissions checking and an "UNLINK" operation under ACLs. * If so, find write-and-execute permission on parent. * Note that WRITE overlaps with ADD_FILE -- that's ZFS's * way of saying "allow write to dir" -- but EXECUTE is * separate from LIST_DIRECTORY, so that's at least a little * bit cleaner. * * Note also that only a definitive yes (both bits are * explicitly allowed) results in granting unlink, and * a definitive no (at least one bit explicitly denied) * results in EPERM. Only "no answer" moves on. */ if ((args->aca_aclmode & L9P_ACM_ZFS_ACL) && opmask == L9P_ACOP_UNLINK && parent != NULL) { panswer = l9p_check_aces(L9P_ACE_ADD_FILE | L9P_ACE_EXECUTE, parent, pstat, uid, gid, gids, ngids); if (panswer) return (panswer < 0 ? EPERM : 0); } /* * No definitive answer from ACLs. * * Try POSIX style rwx permissions if allowed. This should * be rare, occurring mainly when caller supplied no ACLs * or set the mode to suppress them. * * The stat to check is the parent's if we don't have a child * (i.e., this is a dir op), or if the DELETE_CHILD bit is set * (i.e., this is an unlink or similar). Otherwise it's the * child's. */ if (args->aca_aclmode & L9P_ACM_STAT_MODE) { struct stat *st; int rwx, bits; rwx = l9p_ace_mask_to_rwx(opmask); if ((st = cstat) == NULL || (opmask & L9P_ACE_DELETE_CHILD)) st = pstat; if (uid == st->st_uid) bits = (st->st_mode >> 6) & 7; else if (l9p_ingroup(st->st_gid, gid, gids, ngids)) bits = (st->st_mode >> 3) & 7; else bits = st->st_mode & 7; /* * If all the desired bits are set, we're OK. */ if ((rwx & bits) == rwx) return (0); } /* all methods have failed, return EPERM */ return (EPERM); } /* * Collapse fancy ACL operation mask down to simple Unix bits. * * Directory operations don't map that well. However, listing * a directory really does require read permission, and adding * or deleting files really does require write permission, so * this is probably sufficient. */ int l9p_ace_mask_to_rwx(int32_t opmask) { int rwx = 0; if (opmask & (L9P_ACE_READ_DATA | L9P_ACE_READ_NAMED_ATTRS | L9P_ACE_READ_ATTRIBUTES | L9P_ACE_READ_ACL)) rwx |= 4; if (opmask & (L9P_ACE_WRITE_DATA | L9P_ACE_APPEND_DATA | L9P_ACE_ADD_FILE | L9P_ACE_ADD_SUBDIRECTORY | L9P_ACE_DELETE | L9P_ACE_DELETE_CHILD | L9P_ACE_WRITE_NAMED_ATTRS | L9P_ACE_WRITE_ATTRIBUTES | L9P_ACE_WRITE_ACL)) rwx |= 2; if (opmask & L9P_ACE_EXECUTE) rwx |= 1; return (rwx); } #ifndef __APPLE__ /* * Allocate new ACL holder and ACEs. */ static struct l9p_acl * l9p_new_acl(uint32_t acetype, uint32_t aceasize) { struct l9p_acl *ret; size_t asize, size; asize = aceasize * sizeof(struct l9p_ace); size = sizeof(struct l9p_acl) + asize; ret = malloc(size); if (ret != NULL) { ret->acl_acetype = acetype; ret->acl_nace = 0; ret->acl_aceasize = aceasize; } return (ret); } /* * Expand ACL to accomodate more entries. * * Currently won't shrink, only grow, so it's a fast no-op until * we hit the allocated size. After that, it's best to grow in * big chunks, or this will be O(n**2). */ static struct l9p_acl * l9p_growacl(struct l9p_acl *acl, uint32_t aceasize) { struct l9p_acl *tmp; size_t asize, size; if (acl->acl_aceasize < aceasize) { asize = aceasize * sizeof(struct l9p_ace); size = sizeof(struct l9p_acl) + asize; tmp = realloc(acl, size); if (tmp == NULL) free(acl); acl = tmp; } return (acl); } /* * Annoyingly, there's no POSIX-standard way to count the number * of ACEs in a system ACL other than to walk through them all. * This is silly, but at least 2n is still O(n), and the walk is * short. (If the system ACL mysteriously grows, we'll handle * that OK via growacl(), too.) */ static int l9p_count_aces(acl_t sysacl) { acl_entry_t entry; uint32_t n; int id; id = ACL_FIRST_ENTRY; for (n = 0; acl_get_entry(sysacl, id, &entry) == 1; n++) id = ACL_NEXT_ENTRY; return ((int)n); } /* * Create ACL with ACEs from the given acl_t. We use the given * convert function on each ACE. */ static struct l9p_acl * l9p_sysacl_to_acl(int acetype, acl_t sysacl, econvertfn *convert) { struct l9p_acl *acl; acl_entry_t entry; uint32_t n; int error, id; acl = l9p_new_acl((uint32_t)acetype, (uint32_t)l9p_count_aces(sysacl)); if (acl == NULL) return (NULL); id = ACL_FIRST_ENTRY; for (n = 0;;) { if (acl_get_entry(sysacl, id, &entry) != 1) break; acl = l9p_growacl(acl, n + 1); if (acl == NULL) return (NULL); error = (*convert)(entry, &acl->acl_aces[n]); id = ACL_NEXT_ENTRY; if (error == 0) n++; } acl->acl_nace = n; return (acl); } #endif #if defined(HAVE_POSIX_ACLS) && 0 /* not yet */ struct l9p_acl * l9p_posix_acl_to_acl(acl_t sysacl) { } #endif #if defined(HAVE_FREEBSD_ACLS) static int l9p_frombsdnfs4(acl_entry_t sysace, struct l9p_ace *ace) { acl_tag_t tag; /* e.g., USER_OBJ, GROUP, etc */ acl_entry_type_t entry_type; /* e.g., allow/deny */ acl_permset_t absdperm; acl_flagset_t absdflag; acl_perm_t bsdperm; /* e.g., READ_DATA */ acl_flag_t bsdflag; /* e.g., FILE_INHERIT_ACE */ uint32_t flags, mask; int error; uid_t uid, *aid; error = acl_get_tag_type(sysace, &tag); if (error == 0) error = acl_get_entry_type_np(sysace, &entry_type); if (error == 0) error = acl_get_flagset_np(sysace, &absdflag); if (error == 0) error = acl_get_permset(sysace, &absdperm); if (error) return (error); flags = 0; uid = 0; aid = NULL; /* move user/group/everyone + id-is-group-id into flags */ switch (tag) { case ACL_USER_OBJ: flags |= L9P_ACEF_OWNER; break; case ACL_GROUP_OBJ: flags |= L9P_ACEF_GROUP; break; case ACL_EVERYONE: flags |= L9P_ACEF_EVERYONE; break; case ACL_GROUP: flags |= L9P_ACEF_IDENTIFIER_GROUP; /* FALLTHROUGH */ case ACL_USER: aid = acl_get_qualifier(sysace); /* ugh, this malloc()s */ if (aid == NULL) return (ENOMEM); uid = *(uid_t *)aid; free(aid); aid = &uid; break; default: return (EINVAL); /* can't happen */ } switch (entry_type) { case ACL_ENTRY_TYPE_ALLOW: ace->ace_type = L9P_ACET_ACCESS_ALLOWED; break; case ACL_ENTRY_TYPE_DENY: ace->ace_type = L9P_ACET_ACCESS_DENIED; break; case ACL_ENTRY_TYPE_AUDIT: ace->ace_type = L9P_ACET_SYSTEM_AUDIT; break; case ACL_ENTRY_TYPE_ALARM: ace->ace_type = L9P_ACET_SYSTEM_ALARM; break; default: return (EINVAL); /* can't happen */ } /* transform remaining BSD flags to internal NFS-y form */ bsdflag = *absdflag; if (bsdflag & ACL_ENTRY_FILE_INHERIT) flags |= L9P_ACEF_FILE_INHERIT_ACE; if (bsdflag & ACL_ENTRY_DIRECTORY_INHERIT) flags |= L9P_ACEF_DIRECTORY_INHERIT_ACE; if (bsdflag & ACL_ENTRY_NO_PROPAGATE_INHERIT) flags |= L9P_ACEF_NO_PROPAGATE_INHERIT_ACE; if (bsdflag & ACL_ENTRY_INHERIT_ONLY) flags |= L9P_ACEF_INHERIT_ONLY_ACE; if (bsdflag & ACL_ENTRY_SUCCESSFUL_ACCESS) flags |= L9P_ACEF_SUCCESSFUL_ACCESS_ACE_FLAG; if (bsdflag & ACL_ENTRY_FAILED_ACCESS) flags |= L9P_ACEF_FAILED_ACCESS_ACE_FLAG; ace->ace_flags = flags; /* * Transform BSD permissions to ace_mask. Note that directory * vs file bits are the same in both sets, so we don't need * to worry about that, at least. * * There seem to be no BSD equivalents for WRITE_RETENTION * and WRITE_RETENTION_HOLD. */ mask = 0; bsdperm = *absdperm; if (bsdperm & ACL_READ_DATA) mask |= L9P_ACE_READ_DATA; if (bsdperm & ACL_WRITE_DATA) mask |= L9P_ACE_WRITE_DATA; if (bsdperm & ACL_APPEND_DATA) mask |= L9P_ACE_APPEND_DATA; if (bsdperm & ACL_READ_NAMED_ATTRS) mask |= L9P_ACE_READ_NAMED_ATTRS; if (bsdperm & ACL_WRITE_NAMED_ATTRS) mask |= L9P_ACE_WRITE_NAMED_ATTRS; if (bsdperm & ACL_EXECUTE) mask |= L9P_ACE_EXECUTE; if (bsdperm & ACL_DELETE_CHILD) mask |= L9P_ACE_DELETE_CHILD; if (bsdperm & ACL_READ_ATTRIBUTES) mask |= L9P_ACE_READ_ATTRIBUTES; if (bsdperm & ACL_WRITE_ATTRIBUTES) mask |= L9P_ACE_WRITE_ATTRIBUTES; /* L9P_ACE_WRITE_RETENTION */ /* L9P_ACE_WRITE_RETENTION_HOLD */ /* 0x00800 */ if (bsdperm & ACL_DELETE) mask |= L9P_ACE_DELETE; if (bsdperm & ACL_READ_ACL) mask |= L9P_ACE_READ_ACL; if (bsdperm & ACL_WRITE_ACL) mask |= L9P_ACE_WRITE_ACL; if (bsdperm & ACL_WRITE_OWNER) mask |= L9P_ACE_WRITE_OWNER; if (bsdperm & ACL_SYNCHRONIZE) mask |= L9P_ACE_SYNCHRONIZE; ace->ace_mask = mask; /* fill in variable-size user or group ID bytes */ if (aid == NULL) ace->ace_idsize = 0; else { ace->ace_idsize = sizeof(uid); memcpy(&ace->ace_idbytes[0], aid, sizeof(uid)); } return (0); } struct l9p_acl * l9p_freebsd_nfsv4acl_to_acl(acl_t sysacl) { return (l9p_sysacl_to_acl(L9P_ACLTYPE_NFSv4, sysacl, l9p_frombsdnfs4)); } #endif #if defined(HAVE_DARWIN_ACLS) && 0 /* not yet */ struct l9p_acl * l9p_darwin_nfsv4acl_to_acl(acl_t sysacl) { } #endif