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 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>.
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_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. |
/etc/group to bundle users and reference those groups in ACLs. This keeps ACLs maintainable and avoids linear-scan performance issues.4. Coding Examples
#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 */
#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 */
#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 */
#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.
#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
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.#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.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.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.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.Finished all 7 topics? Head to the comprehensive interview Q&A to test everything you’ve learned.
