Linux ACL Part 7: The ACL C API, Data Types, Functions and Programs

 

Linux ACL Part 7: The ACL C API, Data Types, Functions and Programs

Linux ACL Part 7: The ACL C API, Data Types, Functions and Programs

🧰
acl_t / acl_entry_t
⚙️
All API Functions
📝
acl_view.c walkthrough
📊
Implementation Limits

Linux exposes a rich C library API for reading, writing, and manipulating ACLs programmatically. The API follows the POSIX.1e Draft 17 standard and uses an opaque handle model — you never manipulate ACL data directly. Instead, you use handles (acl_t, acl_entry_t, acl_permset_t) and library functions that operate on them. Programs must link against -lacl and include <sys/acl.h>.

Key Concepts in This Post
acl_t acl_entry_t acl_permset_t acl_tag_t acl_get_file() acl_set_file() acl_get_entry() acl_get_tag_type() acl_get_qualifier() acl_get_permset() acl_get_perm() acl_calc_mask() acl_valid() acl_from_text() acl_to_text() acl_free() Implementation limits

1. ACL Data Types (Handles)

The ACL API uses three opaque handle types. You never directly access or allocate their internals — you only hold pointers to them returned by API functions.

Handle Type What It Refers To NULL on error?
acl_t An entire in-memory ACL (list of entries) Yes
acl_entry_t One ACL entry within an acl_t Yes
acl_permset_t The permission set (r/w/x) within an entry Yes
acl_tag_t Integer type for tag (ACL_USER_OBJ etc.) N/A (integer)

ACL Object Hierarchy
acl_t (in-memory ACL)
acl_entry_t
entry 1
acl_entry_t
entry 2
acl_entry_t
entry 3
acl_entry_t
… more
↓ (each entry has)
acl_tag_t
tag type (e.g. ACL_USER)
void *
qualifier (uid_t* / gid_t*)
acl_permset_t
r / w / x flags

2. ACL API Functions – Complete Reference

The ACL API is large. Here is every commonly used function grouped by purpose.

Group Function Purpose Return
File I/O acl_get_file(path, type) Read ACL from file/dir into memory acl_t or NULL
acl_set_file(path, type, acl) Write in-memory ACL back to disk 0 / -1
Entry Iteration acl_get_entry(acl, id, &entry) Get first (ACL_FIRST_ENTRY) or next (ACL_NEXT_ENTRY) entry 1=ok, 0=end, -1=err
Tag Type acl_get_tag_type(entry, &tag) Read tag type from entry 0 / -1
acl_set_tag_type(entry, tag) Set tag type on entry 0 / -1
Qualifier acl_get_qualifier(entry) Get uid_t* or gid_t* from entry void* or NULL
acl_set_qualifier(entry, qual) Set uid_t* or gid_t* on entry 0 / -1
Permission Set acl_get_permset(entry, &permset) Get permset handle from entry 0 / -1
acl_set_permset(entry, permset) Set permset on entry 0 / -1
acl_get_perm(permset, perm) Check if r/w/x set (Linux ext) 1=yes, 0=no, -1=err
acl_add_perm(permset, perm) Add r/w/x to permset 0 / -1
acl_delete_perm(permset, perm) Remove r/w/x from permset 0 / -1
Entry Management acl_create_entry(&acl, &entry) Add a new entry to ACL 0 / -1
acl_delete_entry(acl, entry) Remove an entry from ACL 0 / -1
ACL Management acl_init(count) Create empty ACL with space hint acl_t or NULL
acl_dup(acl) Deep-copy an ACL acl_t or NULL
acl_free(handle) Free memory (acl_t, char*, qualifier) 0 / -1
acl_calc_mask(&acl) Recalculate/create ACL_MASK entry 0 / -1
acl_valid(acl) Check ACL is structurally valid 0=valid, -1=invalid
Text Conversion acl_from_text(str) Parse text ACL into acl_t acl_t or NULL
acl_to_text(acl, &len) Convert acl_t to long text form string char* or NULL
Default ACL acl_delete_def_file(path) Remove default ACL from directory 0 / -1

3. ACL Implementation Limits by File System

File System Max ACL Entries Reason / Constraint
ext2 / ext3 / ext4 ~500 entries All xattr names+values must fit in one disk block. For 4096-byte block: ~500 entries. Kernels before 2.6.11 had hard limit of 32.
XFS 25 entries XFS enforces a fixed internal limit of 25 entries per ACL.
Reiserfs / JFS 8191 entries VFS limits xattr value size to 64 kB; each entry ~8 bytes.
Btrfs ~500 entries Same block-based constraint as ext4 at time of writing; may change.
Best practice: Even if the file system allows hundreds of entries, keep ACLs small. Use groups in /etc/group to bundle users and reference those groups in ACLs. This keeps ACLs maintainable and avoids linear-scan performance issues.

4. Coding Examples

Example 1 – Print all entries of a file’s ACL
#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>
#include <pwd.h>
#include <grp.h>

/* Helper: print permissions as rwx string */
static void print_perms(acl_permset_t ps) {
    printf("%c", acl_get_perm(ps, ACL_READ)    ? 'r' : '-');
    printf("%c", acl_get_perm(ps, ACL_WRITE)   ? 'w' : '-');
    printf("%c", acl_get_perm(ps, ACL_EXECUTE) ? 'x' : '-');
}

int main(int argc, char *argv[]) {
    if (argc != 2) { fprintf(stderr, "Usage: %s <file>\n", argv[0]); return 1; }

    acl_t acl = acl_get_file(argv[1], ACL_TYPE_ACCESS);
    if (!acl) { perror("acl_get_file"); return 1; }

    acl_entry_t entry;
    int eid;
    printf("%-15s %-10s %s\n", "Tag", "Qualifier", "Perms");
    printf("%-15s %-10s %s\n", "---", "---------", "-----");

    for (eid = ACL_FIRST_ENTRY; acl_get_entry(acl, eid, &entry) == 1;
         eid = ACL_NEXT_ENTRY) {

        acl_tag_t tag;
        acl_get_tag_type(entry, &tag);

        const char *tag_name = (tag == ACL_USER_OBJ)  ? "user_obj"  :
                               (tag == ACL_USER)       ? "user"      :
                               (tag == ACL_GROUP_OBJ)  ? "group_obj" :
                               (tag == ACL_GROUP)      ? "group"     :
                               (tag == ACL_MASK)       ? "mask"      :
                               (tag == ACL_OTHER)      ? "other"     : "???";
        printf("%-15s ", tag_name);

        if (tag == ACL_USER) {
            uid_t *uidp = acl_get_qualifier(entry);
            struct passwd *pw = getpwuid(*uidp);
            printf("%-10s ", pw ? pw->pw_name : "unknown");
            acl_free(uidp);
        } else if (tag == ACL_GROUP) {
            gid_t *gidp = acl_get_qualifier(entry);
            struct group *gr = getgrgid(*gidp);
            printf("%-10s ", gr ? gr->gr_name : "unknown");
            acl_free(gidp);
        } else {
            printf("%-10s ", "-");
        }

        acl_permset_t ps;
        acl_get_permset(entry, &ps);
        print_perms(ps);
        printf("\n");
    }

    acl_free(acl);
    return 0;
}
/* Compile: gcc print_acl.c -lacl -o print_acl */
Example 2 – Build an ACL from scratch and apply it to a file
#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>
#include <pwd.h>

/* Sets ACL on a file: owner=rw, alice=r, group=r, mask=rw, other=--- */
int main(int argc, char *argv[]) {
    if (argc != 2) { fprintf(stderr, "Usage: %s <file>\n", argv[0]); return 1; }

    /* Build from text — easiest approach for known ACLs */
    const char *acl_text = "u::rw-,u:alice:r--,g::r--,m::rw-,o::---";
    acl_t acl = acl_from_text(acl_text);
    if (!acl) { perror("acl_from_text"); return 1; }

    /* Validate */
    if (acl_valid(acl) != 0) {
        fprintf(stderr, "ACL is invalid\n");
        acl_free(acl); return 1;
    }

    /* Apply to file */
    if (acl_set_file(argv[1], ACL_TYPE_ACCESS, acl) == -1) {
        perror("acl_set_file"); acl_free(acl); return 1;
    }

    printf("ACL applied: %s\n", acl_text);
    acl_free(acl);
    return 0;
}
/* Compile: gcc set_acl.c -lacl -o set_acl
   Run:     ./set_acl myfile.txt */
Example 3 – Add a single ACL_USER entry to an existing ACL
#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>
#include <pwd.h>

/* Adds read permission for a named user to a file's ACL.
   Usage: ./add_user_acl <file> <username> */
int main(int argc, char *argv[]) {
    if (argc != 3) { fprintf(stderr, "Usage: %s <file> <user>\n", argv[0]); return 1; }

    const char *path = argv[1];
    const char *username = argv[2];

    /* Look up UID */
    struct passwd *pw = getpwnam(username);
    if (!pw) { fprintf(stderr, "User %s not found\n", username); return 1; }
    uid_t uid = pw->pw_uid;

    /* Fetch current ACL */
    acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS);
    if (!acl) { perror("acl_get_file"); return 1; }

    /* Create a new entry */
    acl_entry_t entry;
    if (acl_create_entry(&acl, &entry) == -1) {
        perror("acl_create_entry"); acl_free(acl); return 1;
    }

    /* Set tag type to ACL_USER */
    if (acl_set_tag_type(entry, ACL_USER) == -1) {
        perror("acl_set_tag_type"); acl_free(acl); return 1;
    }

    /* Set qualifier (UID) */
    if (acl_set_qualifier(entry, &uid) == -1) {
        perror("acl_set_qualifier"); acl_free(acl); return 1;
    }

    /* Set permissions: read only */
    acl_permset_t permset;
    acl_get_permset(entry, &permset);
    acl_clear_perms(permset);
    acl_add_perm(permset, ACL_READ);
    acl_set_permset(entry, permset);

    /* Recalculate mask */
    if (acl_calc_mask(&acl) == -1) {
        perror("acl_calc_mask"); acl_free(acl); return 1;
    }

    /* Write back */
    if (acl_set_file(path, ACL_TYPE_ACCESS, acl) == -1) {
        perror("acl_set_file"); acl_free(acl); return 1;
    }

    printf("Added read access for %s (uid=%d) on %s\n", username, uid, path);
    acl_free(acl);
    return 0;
}
/* Compile: gcc add_user_acl.c -lacl -o add_user_acl */
Example 4 – Check if a specific permission is set for a user
#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>
#include <pwd.h>

/* Check whether a given user has write permission via ACL.
   Usage: ./check_write_acl <file> <username> */
int main(int argc, char *argv[]) {
    if (argc != 3) { fprintf(stderr, "Usage: %s <file> <user>\n", argv[0]); return 1; }

    struct passwd *pw = getpwnam(argv[2]);
    if (!pw) { fprintf(stderr, "User not found\n"); return 1; }
    uid_t target_uid = pw->pw_uid;

    acl_t acl = acl_get_file(argv[1], ACL_TYPE_ACCESS);
    if (!acl) { perror("acl_get_file"); return 1; }

    acl_entry_t entry;
    int eid, found = 0;

    for (eid = ACL_FIRST_ENTRY; acl_get_entry(acl, eid, &entry) == 1;
         eid = ACL_NEXT_ENTRY) {

        acl_tag_t tag;
        acl_get_tag_type(entry, &tag);

        if (tag == ACL_USER) {
            uid_t *uidp = acl_get_qualifier(entry);
            if (uidp && *uidp == target_uid) {
                acl_permset_t ps;
                acl_get_permset(entry, &ps);
                int has_write = acl_get_perm(ps, ACL_WRITE);
                printf("User %s has %s permission on %s\n",
                       argv[2],
                       has_write ? "WRITE" : "NO WRITE",
                       argv[1]);
                found = 1;
                acl_free(uidp);
                break;
            }
            if (uidp) acl_free(uidp);
        }
    }

    if (!found)
        printf("No ACL_USER entry found for %s\n", argv[2]);

    acl_free(acl);
    return 0;
}
/* Compile: gcc check_write_acl.c -lacl -o check_write_acl */

5. Full Program: acl_view.c (Walkthrough)

The book includes a program called acl_view.c that reads and displays a file’s ACL — essentially a subset of getfacl. Below is a clean, annotated version showing every API call used and why.

/* acl_view.c — Display ACL of a file.
   Usage:   ./acl_view [-d] <file>
   -d flag: show default ACL instead of access ACL

   Compile: gcc acl_view.c -lacl -o acl_view
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/acl.h>
#include <acl/libacl.h>   /* Linux extensions: acl_get_perm() */
#include <pwd.h>
#include <grp.h>

static void print_perms(acl_permset_t permset) {
    /* acl_get_perm() is a Linux extension; returns 1/0/-1 */
    int r = acl_get_perm(permset, ACL_READ);
    int w = acl_get_perm(permset, ACL_WRITE);
    int x = acl_get_perm(permset, ACL_EXECUTE);
    printf("%c%c%c", r ? 'r' : '-', w ? 'w' : '-', x ? 'x' : '-');
}

int main(int argc, char *argv[]) {
    int opt;
    acl_type_t type = ACL_TYPE_ACCESS;   /* default: access ACL */

    /* Parse options */
    while ((opt = getopt(argc, argv, "d")) != -1) {
        if (opt == 'd') type = ACL_TYPE_DEFAULT;
        else { fprintf(stderr, "Usage: %s [-d] <file>\n", argv[0]); return 1; }
    }
    if (optind + 1 != argc) {
        fprintf(stderr, "Usage: %s [-d] <file>\n", argv[0]); return 1;
    }

    /* STEP 1: Read the ACL into memory */
    acl_t acl = acl_get_file(argv[optind], type);
    if (acl == NULL) { perror("acl_get_file"); return 1; }

    acl_entry_t entry;
    int entryId;

    /* STEP 2: Walk through all entries */
    for (entryId = ACL_FIRST_ENTRY; ; entryId = ACL_NEXT_ENTRY) {

        /* acl_get_entry returns 1=got entry, 0=no more, -1=error */
        if (acl_get_entry(acl, entryId, &entry) != 1) break;

        /* STEP 3: Get the tag type of this entry */
        acl_tag_t tag;
        if (acl_get_tag_type(entry, &tag) == -1) {
            perror("acl_get_tag_type"); break;
        }

        /* Print tag name */
        const char *tname = (tag == ACL_USER_OBJ)  ? "user_obj"  :
                            (tag == ACL_USER)       ? "user"      :
                            (tag == ACL_GROUP_OBJ)  ? "group_obj" :
                            (tag == ACL_GROUP)      ? "group"     :
                            (tag == ACL_MASK)       ? "mask"      :
                            (tag == ACL_OTHER)      ? "other"     : "???";
        printf("%-12s", tname);

        /* STEP 4: Print qualifier (only for ACL_USER and ACL_GROUP) */
        if (tag == ACL_USER) {
            uid_t *uidp = acl_get_qualifier(entry);   /* returns uid_t* */
            if (!uidp) { perror("acl_get_qualifier"); break; }
            struct passwd *pw = getpwuid(*uidp);
            if (pw) printf("%-10s ", pw->pw_name);
            else    printf("%-10u ", (unsigned)*uidp);
            acl_free(uidp);   /* MUST free the qualifier */
        } else if (tag == ACL_GROUP) {
            gid_t *gidp = acl_get_qualifier(entry);   /* returns gid_t* */
            if (!gidp) { perror("acl_get_qualifier"); break; }
            struct group *gr = getgrgid(*gidp);
            if (gr) printf("%-10s ", gr->gr_name);
            else    printf("%-10u ", (unsigned)*gidp);
            acl_free(gidp);
        } else {
            printf("%-11s", " ");
        }

        /* STEP 5: Get and print the permission set */
        acl_permset_t permset;
        if (acl_get_permset(entry, &permset) == -1) {
            perror("acl_get_permset"); break;
        }
        print_perms(permset);
        printf("\n");
    }

    /* STEP 6: Free the in-memory ACL */
    if (acl_free(acl) == -1) perror("acl_free");

    return 0;
}

Sample run:

$ touch tfile
$ setfacl -m 'u:alice:r,u:bob:rw,g:devteam:r' tfile
$ ./acl_view tfile
user_obj            rw-
user       alice     r--
user       bob       rw-
group_obj           r--
group      devteam   r--
mask                rw-
other               r--

6. acl_calc_mask() and acl_valid()

acl_calc_mask(&acl) computes and sets the ACL_MASK entry as the union of all ACL_USER, ACL_GROUP, and ACL_GROUP_OBJ permissions. It also creates the ACL_MASK entry if it doesn’t exist yet. Always call this after modifying any ACL that has or will have ACL_USER/ACL_GROUP entries.

acl_valid(acl) checks that the ACL is structurally correct — required entries present, no duplicate user/group entries, mask present when needed. Returns 0 if valid, -1 if invalid.

Example 5 – Build ACL manually and use acl_calc_mask / acl_valid
#include <stdio.h>
#include <stdlib.h>
#include <sys/acl.h>

/* Builds the ACL: owner=rwx, alice=r--, group=r-x, other=--- */
int main(int argc, char *argv[]) {
    if (argc != 2) { fprintf(stderr, "Usage: %s <file>\n", argv[0]); return 1; }

    /* Helper macro: add an entry with given tag & perms */
    acl_t acl = acl_init(5);   /* hint: we plan ~5 entries */
    if (!acl) { perror("acl_init"); return 1; }

    acl_entry_t ent;
    acl_permset_t ps;

    /* --- ACL_USER_OBJ (owner) = rwx --- */
    acl_create_entry(&acl, &ent);
    acl_set_tag_type(ent, ACL_USER_OBJ);
    acl_get_permset(ent, &ps); acl_clear_perms(ps);
    acl_add_perm(ps, ACL_READ); acl_add_perm(ps, ACL_WRITE); acl_add_perm(ps, ACL_EXECUTE);
    acl_set_permset(ent, ps);

    /* --- ACL_USER (alice) = r-- --- */
    uid_t alice_uid = 1001;    /* replace with real UID or getpwnam lookup */
    acl_create_entry(&acl, &ent);
    acl_set_tag_type(ent, ACL_USER);
    acl_set_qualifier(ent, &alice_uid);
    acl_get_permset(ent, &ps); acl_clear_perms(ps);
    acl_add_perm(ps, ACL_READ);
    acl_set_permset(ent, ps);

    /* --- ACL_GROUP_OBJ (file group) = r-x --- */
    acl_create_entry(&acl, &ent);
    acl_set_tag_type(ent, ACL_GROUP_OBJ);
    acl_get_permset(ent, &ps); acl_clear_perms(ps);
    acl_add_perm(ps, ACL_READ); acl_add_perm(ps, ACL_EXECUTE);
    acl_set_permset(ent, ps);

    /* --- ACL_OTHER = --- --- */
    acl_create_entry(&acl, &ent);
    acl_set_tag_type(ent, ACL_OTHER);
    acl_get_permset(ent, &ps); acl_clear_perms(ps);
    acl_set_permset(ent, ps);

    /* Compute and create ACL_MASK automatically */
    if (acl_calc_mask(&acl) == -1) { perror("acl_calc_mask"); acl_free(acl); return 1; }

    /* Validate before writing */
    if (acl_valid(acl) != 0) {
        fprintf(stderr, "ACL is not valid!\n"); acl_free(acl); return 1;
    }

    /* Apply */
    if (acl_set_file(argv[1], ACL_TYPE_ACCESS, acl) == -1) {
        perror("acl_set_file"); acl_free(acl); return 1;
    }

    /* Display what we set */
    ssize_t len;
    char *txt = acl_to_text(acl, &len);
    printf("Applied ACL:\n%s\n", txt);
    acl_free(txt);
    acl_free(acl);
    return 0;
}
/* Compile: gcc build_acl.c -lacl -o build_acl */

7. Memory Management – acl_free()

Everything returned by heap-allocating ACL functions must be freed with acl_free(). Missing these calls causes memory leaks, especially in long-running daemons. The following objects must be freed:

Returned by Type Free with
acl_get_file(), acl_init(), acl_dup(), acl_from_text() acl_t acl_free(acl)
acl_to_text() char * acl_free(text)
acl_get_qualifier() void * (uid_t* or gid_t*) acl_free(uidp) or acl_free(gidp)

Interview Questions

Q1. What are the three main opaque handle types in the ACL C API and what does each represent?
acl_t — a handle to an entire in-memory ACL (all entries together). acl_entry_t — a handle to one specific entry within an acl_t. acl_permset_t — a handle to the permission set (read/write/execute flags) within a single ACL entry. You never access their internal structure directly; you only use API functions with these handles.
Q2. How do you iterate through all ACL entries using acl_get_entry()?
Start the loop by calling acl_get_entry(acl, ACL_FIRST_ENTRY, &entry). On each subsequent iteration, call acl_get_entry(acl, ACL_NEXT_ENTRY, &entry). The function returns 1 when an entry is successfully retrieved, 0 when there are no more entries, and -1 on error. Exit the loop when the return value is not 1.
Q3. What header files and linker flag are required for ACL programs on Linux?
Include #include <sys/acl.h> for the standard POSIX.1e API. Include #include <acl/libacl.h> additionally if you use Linux extensions such as acl_get_perm(). Compile and link with the -lacl flag: gcc myprog.c -lacl -o myprog.
Q4. What does acl_calc_mask() do, and when should you call it?
acl_calc_mask(&acl) calculates the ACL_MASK permission value as the union of all permissions in ACL_USER, ACL_GROUP, and ACL_GROUP_OBJ entries, and sets the ACL_MASK entry accordingly. It also creates the ACL_MASK entry if one doesn’t exist. You should call it after adding or modifying any ACL_USER or ACL_GROUP entries to ensure the mask is correct and the ACL remains valid.
Q5. How do you free memory correctly after calling acl_get_qualifier()?
acl_get_qualifier() returns a dynamically allocated pointer (uid_t* for ACL_USER entries, gid_t* for ACL_GROUP entries). After you are done using it, you must call acl_free(pointer) on the returned pointer. Failing to do so causes a memory leak. Note that you use acl_free(), not the standard free(), for all ACL library allocations.
Q6. What is the maximum number of ACL entries on an ext4 file system with a 4096-byte block size?
Approximately 500 entries. This is because on ext2/ext3/ext4, all the names and values of a file’s extended attributes must fit within a single logical disk block. Each ACL entry takes 8 bytes, and there is some overhead for the extended attribute name, so a 4096-byte block accommodates roughly 500 ACL entries. Kernels before 2.6.11 had an arbitrary limit of 32.
Q7. What is the difference between acl_valid() and acl_check()?
acl_valid(acl) is the portable POSIX.1e function that returns 0 if the ACL is structurally valid or -1 if not. acl_check(acl, &first_invalid) is a Linux-specific extension that provides more detail: it returns 0 for a valid ACL, or a positive error code (e.g., ACL_DUPLICATE_ERROR, ACL_MISS_ERROR) indicating what is wrong, and stores the index of the first invalid entry in first_invalid. Use acl_valid() for portable code and acl_check() when you need better diagnostics on Linux.

Series Complete — Test Your Knowledge

Finished all 7 topics? Head to the comprehensive interview Q&A to test everything you’ve learned.

Part 8: Full Interview Q&A → ← Part 6: Default ACLs

Leave a Reply

Your email address will not be published. Required fields are marked *