Skip to content

Commit 6e9d7af

Browse files
committed
Enable ACL support for libfuse3
1 parent dee2798 commit 6e9d7af

2 files changed

Lines changed: 232 additions & 5 deletions

File tree

configure.ac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ AM_CONDITIONAL(BUILD_OS_IS_DARWIN, [test x"$build_os" = darwin])
3636
my_CPPFLAGS="-D_REENTRANT -D_FILE_OFFSET_BITS=64 -D_XOPEN_SOURCE=700 -D__BSD_VISIBLE=1 -D_BSD_SOURCE -D_DEFAULT_SOURCE -D_DARWIN_BETTER_REALPATH"
3737

3838
my_CFLAGS="-std=c99 -Wall -Wpedantic -fno-common"
39-
my_LDFLAGS="-pthread"
39+
my_LDFLAGS="-pthread -lacl"
4040
AC_SUBST([my_CPPFLAGS])
4141
AC_SUBST([my_CFLAGS])
4242
AC_SUBST([my_LDFLAGS])

src/bindfs.c

100644100755
Lines changed: 231 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
#ifdef HAVE_SETXATTR
6464
#include <sys/xattr.h>
6565
#endif
66+
#include <sys/acl.h>
67+
#include <acl/libacl.h>
6668

6769
#ifdef HAVE_FUSE_3
6870
#ifndef __NR_renameat2
@@ -769,6 +771,234 @@ static int bindfs_fgetattr(const char *path, struct stat *stbuf,
769771
}
770772
#endif
771773

774+
/**
775+
* Convert an ACL permset to a bitmask.
776+
*
777+
* @param permset The permset to convert.
778+
*
779+
* @return The permset represented as an `access(2)` compatible bitmask.
780+
*/
781+
unsigned int permset_to_bits(acl_permset_t permset) {
782+
unsigned int bits = 0;
783+
784+
if (acl_get_perm(permset, ACL_READ) == 1)
785+
bits |= R_OK;
786+
787+
if (acl_get_perm(permset, ACL_WRITE) == 1)
788+
bits |= W_OK;
789+
790+
if (acl_get_perm(permset, ACL_EXECUTE) == 1)
791+
bits |= X_OK;
792+
793+
return bits;
794+
}
795+
796+
/**
797+
* Determine whether the effective user has access to a given file system
798+
* object.
799+
*
800+
* Access check algorithm documented here: https://www.usenix.org/legacy/publications/library/proceedings/usenix03/tech/freenix03/full_papers/gruenbacher/gruenbacher_html/main.html
801+
*
802+
* This function performs POSIX ACL access checking, which is a superset of the
803+
* standard Unix permissions.
804+
*
805+
* @param path The path to the file system object.
806+
* @param wants The bitwise-inclusive OR of the access permissions to be
807+
* checked (R_OK, W_OK, X_OK).
808+
*
809+
* @return 0 if the effective user has access, -EACCES otherwise.
810+
*
811+
* */
812+
static int bindfs_access(const char *path, int wants)
813+
{
814+
DPRINTF("Performing access check for '%s'", path);
815+
816+
char *real_path = process_path(path, true);
817+
818+
struct stat st;
819+
if (lstat(real_path, &st) == -1) {
820+
DPRINTF("Could not lstat '%s': %s", real_path, strerror(errno));
821+
free(real_path);
822+
return -errno;
823+
}
824+
825+
uid_t euid = geteuid();
826+
gid_t egid = getegid();
827+
828+
// TODO: There are `root` cases still needing to be accounted for.
829+
// This is not the correct implementation!
830+
//if (euid == 0)
831+
// return 0;
832+
833+
struct passwd *pwd = getpwuid(euid);
834+
835+
// We can perform this check early, Unsubstantiated, but I would imagine
836+
// it's one of the more likely cases, so it makes sense to optimise for it.
837+
//
838+
// If the user ID of the process is the owner, the owner entry determines
839+
// access.
840+
if (euid == st.st_uid) {
841+
return (((st.st_mode & 0700) >> 6) & wants) == wants
842+
? 0
843+
: -EACCES;
844+
}
845+
846+
// Get the groups the effective user is a part of
847+
int ngroups = 8;
848+
gid_t *groups = malloc(sizeof(*groups) * ngroups);
849+
850+
while (getgrouplist(pwd->pw_name, egid, groups, &ngroups) == -1) {
851+
DPRINTF("Reallocating space for groups to %d entries", ngroups);
852+
groups = realloc(groups, sizeof(*groups) * ngroups);
853+
}
854+
855+
// Notes on the below variables:
856+
// We do not need to store the owner permissions as we can exit immediately
857+
// if we find them and do not need to do further processing.
858+
859+
// We do not need to store group ACL permissions separately from the
860+
// standard group permissions. We can merge all the group permissions
861+
// together and treat them as one. TODO: I think, need to thoroughly test.
862+
// NOTE: This only holds if an object that has ACLs where `group_a = r--`,
863+
// `group_b = --x` and the request is for `r-x` (`wants` = 5) we should
864+
// grant access, if we should disallow access, we can't aggregate
865+
// permissions in this way.
866+
867+
unsigned int group_perms = 0;
868+
unsigned int other_perms = 0;
869+
870+
unsigned int user_acl_perms = 0;
871+
unsigned int acl_mask = 0;
872+
873+
unsigned int has_user_acl = 0;
874+
unsigned int has_group = 0;
875+
unsigned int has_mask = 0;
876+
877+
acl_t acl = acl_get_file(real_path, ACL_TYPE_ACCESS);
878+
acl_entry_t acl_entry;
879+
for (
880+
int result = acl_get_entry(acl, ACL_FIRST_ENTRY, &acl_entry);
881+
result > 0;
882+
result = acl_get_entry(acl, ACL_NEXT_ENTRY, &acl_entry)
883+
) {
884+
acl_tag_t acl_tag;
885+
if (acl_get_tag_type(acl_entry, &acl_tag) != 0) {
886+
DPRINTF("Could not get ACL tag type: %s", strerror(errno));
887+
continue;
888+
}
889+
890+
acl_permset_t acl_permset;
891+
if (acl_get_permset(acl_entry, &acl_permset) != 0) {
892+
DPRINTF("Could not get ACL permset: %s", strerror(errno));
893+
continue;
894+
}
895+
896+
unsigned int permset_as_bits = permset_to_bits(acl_permset);
897+
898+
void *acl_qualifier = acl_get_qualifier(acl_entry);
899+
switch(acl_tag) {
900+
case ACL_USER_OBJ:
901+
if (euid == st.st_uid) {
902+
acl_free(acl_qualifier);
903+
acl_free(acl);
904+
free(real_path);
905+
906+
return (permset_as_bits & wants) == wants
907+
? 0
908+
: -EACCES;
909+
}
910+
break;
911+
912+
case ACL_USER:
913+
if (euid == *(uid_t*)acl_qualifier) {
914+
has_user_acl = 1;
915+
user_acl_perms = permset_as_bits;
916+
}
917+
break;
918+
919+
case ACL_GROUP_OBJ:
920+
for (int i = 0; i < ngroups; i++) {
921+
// Aggregate all group permissions, we only care if we have
922+
// the permissions, not where they come from
923+
if (st.st_gid == groups[i]) {
924+
DPRINTF("Has ACL_GROUP_OBJ of %d", groups[i]);
925+
has_group = 1;
926+
group_perms |= permset_as_bits;
927+
}
928+
}
929+
break;
930+
931+
case ACL_GROUP:
932+
for (int i = 0; i < ngroups; i++) {
933+
// Aggregate all group permissions, we only care if we have
934+
// the permissions, not where they come from
935+
if (*(gid_t*)acl_qualifier == groups[i]) {
936+
DPRINTF("Has ACL_GROUP of %d", groups[i]);
937+
has_group = 1;
938+
group_perms |= permset_as_bits;
939+
}
940+
}
941+
break;
942+
943+
case ACL_MASK:
944+
has_mask = 1;
945+
acl_mask = permset_as_bits;
946+
break;
947+
948+
case ACL_OTHER:
949+
other_perms = permset_as_bits;
950+
break;
951+
}
952+
acl_free(acl_qualifier);
953+
}
954+
955+
acl_free(acl);
956+
free(real_path);
957+
958+
// If there is no mask entry, it doesn't restrict anything.
959+
if (has_mask == 0) {
960+
DPRINTF("ACL has no ACL_MASK entry, setting mask to rwx");
961+
acl_mask = R_OK | W_OK | X_OK;
962+
}
963+
964+
//If the user ID of the process is the owner, the owner entry determines
965+
//access - This check is performed above, very early in the function.
966+
967+
// If the user ID of the process matches the qualifier in one
968+
// of the named user entries, this entry determines access
969+
if (has_user_acl == 1) {
970+
DPRINTF("Using ACL_USER entry to determine access");
971+
return (user_acl_perms & acl_mask & wants) == wants
972+
? 0
973+
: -EACCES;
974+
}
975+
976+
// If one of the group IDs of the process matches the owning group and the
977+
// owning group entry contains the requested permissions, this entry
978+
// determines access
979+
980+
// If one of the group IDs of the process matches the qualifier of one of
981+
// the named group entries and this entry contains the requested
982+
// permissions, this entry determines access
983+
984+
// If one of the group IDs of the process matches the owning group or any
985+
// of the named group entries, but neither the owning group entry nor any
986+
// of the matching named group entries contains the requested permissions,
987+
// this determines that access is denied
988+
if (has_group == 1) {
989+
DPRINTF("Using ACL_GROUP_OBJ or ACL_GROUP entry to determine access");
990+
return (group_perms & acl_mask & wants) == wants
991+
? 0
992+
: -EACCES;
993+
}
994+
995+
// Else the other entry determines access.
996+
DPRINTF("Using ACL_OTHER entry to determine access");
997+
return (other_perms & wants) == wants
998+
? 0
999+
: -EACCES;
1000+
}
1001+
7721002
static int bindfs_readlink(const char *path, char *buf, size_t size)
7731003
{
7741004
int res;
@@ -1643,7 +1873,7 @@ static struct fuse_operations bindfs_oper = {
16431873
#ifndef HAVE_FUSE_3
16441874
.fgetattr = bindfs_fgetattr,
16451875
#endif
1646-
/* no access() since we always use -o default_permissions */
1876+
.access = bindfs_access,
16471877
.readlink = bindfs_readlink,
16481878
.readdir = bindfs_readdir,
16491879
.mknod = bindfs_mknod,
@@ -2750,9 +2980,6 @@ int main(int argc, char *argv[])
27502980
fuse_opt_add_arg(&args, "-oallow_other");
27512981
}
27522982

2753-
/* We want the kernel to do our access checks for us based on what getattr gives it. */
2754-
fuse_opt_add_arg(&args, "-odefault_permissions");
2755-
27562983
// With FUSE 3 we set this in bindfs_init
27572984
#ifndef HAVE_FUSE_3
27582985
/* We want to mirror inodes. */

0 commit comments

Comments
 (0)