ACL evaluation: interaction between "best match" and privilege

Robert Watson rwatson at FreeBSD.org
Fri Sep 22 16:27:09 GMT 2000


On Fri, 22 Sep 2000, Casey Schaufler wrote:

> If a system has audit (What? I though we we talking about ACLs.
> Yes, we are. I said it was a systemic issue, didn't I? now be
> patient) we want to record not only whether an operation
> succeeded of failed, but why. Thus, I want to do a complete
> access control check (in this case, completely resolve the ACL)
> before I check for any capabilities. Why? so that I can mark
> which capabilities are actually used in the audit record.
> If I do capability checks first, or if I intermix them with
> strict policy checking, that information can be incorrect.

I agree with this analysis, but it prompted my concern, as "best match"
results in ambiguity about which privilege to use.

Suppose the request is for VREAD|VWRITE.  There are two matching group
entries, one providing ACL_PERM_READ and one providing ACL_PERM_WRITE.
The process also has CAP_DAC_READ_SEARCH and CAP_DAC_WRITE.  Which
capability do I audit as used? :-)  There is no "best match", as both are
best.  Right now, I do first match when falling back on privilege, but
from the perspective of minimizing privilege used, it's not right unless
you define your privilege minimalization function as first match :-).

I've attached my current vaccess_acl() call, which performs an access
check given an ACL.  Due to the implementation I selected, it gets
ACL_{USER,GROUP}_OBJ and ACL_OTHER information from the inode's
owner/group/mode passed as arguments, and assumes that the only entries in
the ACL are ACL_USER, ACL_GROUP, and ACL_MASK.

Note that as you suggest, checks are generally performed twice: once
without privilege available, and if there is a matched ACL entry, a second
time with privilege.  In the group case, I look for a best match, and if I
don't find it, I fall back and successively try entries until I find one
that matches when all available capabilities are applied.  Note also that
right now, I simply return a flag to the caller indicating that privilege
was used.  Once we resolve which privilege should be recorded as used,
I'll be passing back a mask of the capabilities used, instead.

  Robert N M Watson 

robert at fledge.watson.org              http://www.watson.org/~robert/
PGP key fingerprint: AF B5 5F FF A6 4A 79 37  ED 5F 55 E9 58 04 6A B1
TIS Labs at Network Associates, Safeport Network Services

-------------- next part --------------
/*
 * Implement a version of vaccess() that understands POSIX.1e ACL semantics.
 * Assumes an abbreviated ACL, in which the ACL_USER_OBJ and ACL_GROUP_OBJ
 * entries are passed via the vaccess()-like file_{mode,uid,gid} arguments,
 * and these entries do not appear in the ACL (or are ignored if they do).
 * Should be merged into vaccess() eventually.
 */
int
vaccess_acl(enum vtype type, mode_t file_mode, uid_t file_uid,
    gid_t file_gid, struct acl *acl, mode_t acc_mode, struct ucred *cred,
    int *privused)
{
	struct acl_entry *acl_mask;
	mode_t dac_granted;
	mode_t cap_granted;
	mode_t acl_mask_granted;
	int group_matched, i;

	/*
	 * Look for a normal, non-privileged way to access the file/directory
	 * as requested.  If it exists, go with that.  Otherwise, attempt
	 * to use privileges granted via cap_granted.  In some cases,
	 * which privileges to use may be ambiguous due to "best match",
	 * in which case fall back on first match for the time being.
	 */
	if (privused != NULL)
		*privused = 0;

	/*
	 * Determine privileges now, but don't apply until we've found
	 * a DAC match that has failed to allow access.
	 */
#ifndef CAPABILITIES
	if (suser_xxx(cred, NULL, PRISON_ROOT) == 0)
		cap_granted = (VEXEC | VREAD | VWRITE);
	else
		cap_granted = 0;
#else
	cap_granted = 0;

	if (type == VDIR) {
		if ((acc_mode & VEXEC) && !cap_check(cred, NULL,
		     CAP_DAC_READ_SEARCH, PRISON_ROOT))
			cap_granted |= VEXEC;
	} else {
		if ((acc_mode & VEXEC) && !cap_check(cred, NULL,
		    CAP_DAC_EXECUTE, PRISON_ROOT))
			cap_granted |= VEXEC;
	}

	if ((acc_mode & VREAD) && !cap_check(cred, NULL, CAP_DAC_READ_SEARCH,
	    PRISON_ROOT))
		cap_granted |= VREAD;

	if ((acc_mode & VWRITE) && !cap_check(cred, NULL, CAP_DAC_WRITE,
	    PRISON_ROOT))
		cap_granted |= VWRITE;

#endif /* CAPABILITIES */

	dac_granted = 0;

	/* Check the owner. */
	if (cred->cr_uid == file_uid) {
		/*
		 * Emulate ACL_USER_OBJ entry -- should this check there
		 * is none in the ACL itself?
		 */
		if (file_mode & S_IXUSR)
			dac_granted |= VEXEC;
		if (file_mode & S_IRUSR)
			dac_granted |= VREAD;
		if (file_mode & S_IWUSR)
			dac_granted |= VWRITE;

		if ((acc_mode & dac_granted) == acc_mode)
			return (0);

		if ((acc_mode & (dac_granted | cap_granted)) == acc_mode) {
			if (privused != NULL)
				*privused = 1;
			return (0);
		}
	}

	/*
	 * Checks against remainder of the types include checks against
	 * any ACL_MASK that may be set.  As such, locate and store the
	 * ACL_MASK for safe keeping.  If there is none, assume an
	 * ACL_MASK that permits all activities.
	 */
	acl_mask = NULL;
	for (i = 0; i < acl->acl_cnt; i++)
		if (acl->acl_entry[i].ae_tag == ACL_MASK) {
			acl_mask = &acl->acl_entry[i];
			break;
		}
	if (acl_mask != NULL) {
		acl_mask_granted = 0;
		if (acl_mask->ae_perm & ACL_PERM_EXEC)
			acl_mask_granted |= VEXEC;
		if (acl_mask->ae_perm & ACL_PERM_READ)
			acl_mask_granted |= VREAD;
		if (acl_mask->ae_perm & ACL_PERM_WRITE)
			acl_mask_granted |= VWRITE;
	} else
		acl_mask_granted = VEXEC | VREAD | VWRITE;

	/*
	 * We have to check each type even if we know ACL_MASK will reject,
	 * as we need to know what match there might have been, and
	 * therefore what further types we might be allowed to check.
	 * Do the checks twice -- once without privilege, and a second time
	 * with, if there was a match.
	 */

	/*
	 * Check ACL_USER ACL entries.
	 */
	for (i = 0; i < acl->acl_cnt; i++) {
		dac_granted = 0;
		if (acl->acl_entry[i].ae_tag == ACL_USER &&
		    cred->cr_uid == acl->acl_entry[i].ae_id) {
			if (acl->acl_entry[i].ae_perm & ACL_PERM_EXEC)
				dac_granted |= VEXEC;
			if (acl->acl_entry[i].ae_perm & ACL_PERM_READ)
				dac_granted |= VREAD;
			if (acl->acl_entry[i].ae_perm & ACL_PERM_WRITE)
				dac_granted |= VWRITE;
			dac_granted &= acl_mask_granted;
			if ((acc_mode & dac_granted) == acc_mode)
				return (0);
			if ((acc_mode & (dac_granted | cap_granted)) ==
			    acc_mode) {
				if (privused != NULL)
					*privused = 1;
				return (0);
			}
			return (EACCES);
		}
	}

	/*
	 * Group match is best-match, not first-match, so find a 
	 * "best" match.  Start with ACL_GROUP_OBJ, then move on
	 * if it doesn't match.  Make sure to keep track of whether
	 * or not any matches were found, because if so we must
	 * skip the ACL_OTHER check.
	 */
	group_matched = 0;
	if (groupmember(file_gid, cred)) {
		dac_granted = 0;
		if (file_mode & S_IXGRP)
			dac_granted |= VEXEC;
		if (file_mode & S_IRGRP)
			dac_granted |= VREAD;
		if (file_mode & S_IWGRP)
			dac_granted |= VWRITE;
		dac_granted &= acl_mask_granted;
		if ((acc_mode & dac_granted) == acc_mode)
			return (0);
		group_matched = 1;
	}
	for (i = 0; i < acl->acl_cnt; i++) {
		dac_granted = 0;
		if (acl->acl_entry[i].ae_tag == ACL_GROUP &&
		    groupmember(acl->acl_entry[i].ae_id, cred)) {
			if (acl->acl_entry[i].ae_perm & ACL_PERM_EXEC)
				dac_granted |= VEXEC;
			if (acl->acl_entry[i].ae_perm & ACL_PERM_READ)
				dac_granted |= VREAD;
			if (acl->acl_entry[i].ae_perm & ACL_PERM_WRITE)
				dac_granted |= VWRITE;
			dac_granted  &= acl_mask_granted;

			if ((acc_mode & dac_granted) == acc_mode)
				return (0);

			group_matched = 1;
		}
	}

	if (group_matched == 1) {
		/*
		 * There was a match, but it did not grant rights via
		 * pure DAC.  Try again, this time with privilege.
		 */
		if (groupmember(file_gid, cred)) {
			dac_granted = 0;
			if (file_mode & S_IXGRP)
				dac_granted |= VEXEC;
			if (file_mode & S_IRGRP)
				dac_granted |= VREAD;
			if (file_mode & S_IWGRP)
				dac_granted |= VWRITE;
			dac_granted &= acl_mask_granted;
			if ((acc_mode & (dac_granted | cap_granted)) ==
			    acc_mode) {
				if (privused != NULL)
					*privused = 1;
				return (0);
			}
		}
		for (i = 0; i < acl->acl_cnt; i++) {
			dac_granted = 0;
			if (acl->acl_entry[i].ae_tag == ACL_GROUP &&
			    groupmember(acl->acl_entry[i].ae_id, cred)) {
				if (acl->acl_entry[i].ae_perm & ACL_PERM_EXEC)
					dac_granted |= VEXEC;
				if (acl->acl_entry[i].ae_perm & ACL_PERM_READ)
					dac_granted |= VREAD;
				if (acl->acl_entry[i].ae_perm & ACL_PERM_WRITE)
					dac_granted |= VWRITE;
				dac_granted  &= acl_mask_granted;
				if ((acc_mode & (dac_granted | cap_granted)) ==
				    acc_mode) {
					if (privused != NULL)
						*privused = 1;
					return (0);
				}
			}
		}
		/*
		 * Even with privilege, group membership was not sufficient.
		 * Return failure.
		 */
		return (EACCES);
	}
		
	/*
	 * Fall back on ACL_OTHER, emulated from file mode.
	 * ACL_MASK is not applied to ACL_OTHER.
	 */
	dac_granted = 0;
	if (file_mode & S_IXOTH)
		dac_granted |= VEXEC;
	if (file_mode & S_IROTH)
		dac_granted |= VREAD;
	if (file_mode & S_IWOTH)
		dac_granted |= VWRITE;

	if ((acc_mode & dac_granted) == acc_mode)
		return (0);
	if ((acc_mode & (dac_granted | cap_granted)) == acc_mode) {
		if (privused != NULL)
			*privused = 1;
		return (0);
	}

	return (EACCES);
}


More information about the posix1e mailing list