etxattr, getxattr, removexattr and listxattr in Linux: Complete Guide

 

etxattr, getxattr, removexattr and listxattr in Linux: Complete Guide

Part 3 —etxattr, getxattr, removexattr and listxattr in Linux: Complete Guide
4 Groups
of system calls
3 Variants
per group
sys/xattr.h
Header needed
-lattr
Link flag (some distros)

Key Terms

setxattr lsetxattr fsetxattr getxattr lgetxattr fgetxattr removexattr listxattr XATTR_CREATE XATTR_REPLACE EEXIST ENODATA ERANGE

System Call Families Overview

The EA API is organized into 4 families, each with 3 variants. The variants differ only in how you identify the file:

Variant File identified by Symlink behavior
setxattr() / getxattr() / ... Pathname Follows (dereferences) symlinks
lsetxattr() / lgetxattr() / ... Pathname Does NOT follow symlinks (acts on the link itself)
fsetxattr() / fgetxattr() / ... Open file descriptor (fd) N/A — already resolved when fd was opened
This is the same convention as stat() / lstat() / fstat(). The l prefix = “don’t follow symlinks”, the f prefix = “use file descriptor”.

1. setxattr() Family — Create & Modify EAs

#include <sys/xattr.h>

int setxattr(const char *pathname, const char *name, const void *value, size_t size, int flags);
int lsetxattr(const char *pathname, const char *name, const void *value, size_t size, int flags);
int fsetxattr(int fd, const char *name, const void *value, size_t size, int flags);

// All return 0 on success, or -1 on error

Parameters Explained
Parameter Type Description
pathname / fd char* / int The file to operate on
name const char* Null-terminated EA name, e.g. "user.comment"
value const void* Pointer to the EA value data (can be binary)
size size_t Length of the value buffer in bytes
flags int 0, XATTR_CREATE, or XATTR_REPLACE
The flags Argument — XATTR_CREATE & XATTR_REPLACE

Flag Value EA already exists? EA does NOT exist? Use case
0 (default) ✓ Overwrites ✓ Creates new “Set it no matter what”
XATTR_CREATE ✗ Fails (EEXIST) ✓ Creates new “Only create, never overwrite”
XATTR_REPLACE ✓ Overwrites ✗ Fails (ENODATA) “Only update if it already exists”
#include <sys/xattr.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main() {
    const char *path = "/tmp/demo.txt";
    /* create the file first */
    FILE *f = fopen(path, "w"); fclose(f);

    /* ---- flags = 0 (default): create OR replace ---- */
    setxattr(path, "user.tag", "initial", 7, 0);
    printf("Created user.tag\n");

    /* Calling again with 0 replaces the existing value */
    setxattr(path, "user.tag", "updated", 7, 0);
    printf("Replaced user.tag (flag=0)\n");

    /* ---- XATTR_CREATE: only create (fail if exists) ---- */
    int ret = setxattr(path, "user.tag", "new_value", 9, XATTR_CREATE);
    if (ret == -1 && errno == EEXIST) {
        printf("XATTR_CREATE failed: EA already exists (EEXIST)\n");
    }

    /* ---- XATTR_CREATE on a new name works fine ---- */
    ret = setxattr(path, "user.new_key", "hello", 5, XATTR_CREATE);
    if (ret == 0) printf("XATTR_CREATE succeeded for new key\n");

    /* ---- XATTR_REPLACE: only update (fail if missing) ---- */
    ret = setxattr(path, "user.nonexistent", "val", 3, XATTR_REPLACE);
    if (ret == -1 && errno == ENODATA) {
        printf("XATTR_REPLACE failed: EA doesn't exist (ENODATA)\n");
    }

    /* XATTR_REPLACE on existing key works fine */
    ret = setxattr(path, "user.tag", "final_value", 11, XATTR_REPLACE);
    if (ret == 0) printf("XATTR_REPLACE succeeded for existing key\n");

    return 0;
}
/*
Output:
Created user.tag
Replaced user.tag (flag=0)
XATTR_CREATE failed: EA already exists (EEXIST)
XATTR_CREATE succeeded for new key
XATTR_REPLACE failed: EA doesn't exist (ENODATA)
XATTR_REPLACE succeeded for existing key
*/
fsetxattr() — Using a File Descriptor

When you already have the file open (e.g., after creating it with open()), use fsetxattr() to avoid re-opening by name:

#include <sys/xattr.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main() {
    /* Open the file to get a file descriptor */
    int fd = open("/tmp/myfile.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); return 1; }

    /* Write some content */
    write(fd, "Hello, EA!", 10);

    /* Set EA using the file descriptor — no pathname lookup */
    const char *author = "Ravi Kumar";
    if (fsetxattr(fd, "user.author", author, strlen(author), 0) == -1) {
        perror("fsetxattr");
    } else {
        printf("EA set via fd successfully\n");
    }

    close(fd);
    return 0;
}
Using fsetxattr() is race-condition safe: if the file is renamed between your open() and your EA operation, the fd still refers to the correct file.

2. getxattr() Family — Read EA Values

#include <sys/xattr.h>

ssize_t getxattr(const char *pathname, const char *name, void *value, size_t size);
ssize_t lgetxattr(const char *pathname, const char *name, void *value, size_t size);
ssize_t fgetxattr(int fd, const char *name, void *value, size_t size);

// All return the size of the EA value (bytes) on success, or -1 on error

Basic getxattr() Example
#include <sys/xattr.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    const char *path = "/tmp/myfile.txt";
    char buf[256];

    /* Read the user.author EA we set earlier */
    ssize_t len = getxattr(path, "user.author", buf, sizeof(buf));
    if (len == -1) {
        perror("getxattr");
        return 1;
    }

    /* Note: EA values are NOT null-terminated by default! */
    /* We must use the returned length to print correctly. */
    printf("user.author = %.*s\n", (int)len, buf);
    printf("Value length = %zd bytes\n", len);

    return 0;
}
Common mistake: EA values are not automatically null-terminated. If you store a string like "Ravi Kumar", the kernel stores exactly those 10 bytes. Use the returned ssize_t length to know how many bytes were copied. If you want null-termination, either include the ‘\0’ in the size argument to setxattr(), or add it manually after reading.
Dynamic Buffer Allocation Pattern

When you don’t know the value size in advance, use the two-step pattern: first call with size = 0 to get the required size, then allocate and read.

#include <sys/xattr.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/*
 * Read an EA value into a dynamically allocated, null-terminated string.
 * Caller must free() the returned pointer.
 * Returns NULL on error.
 */
char *read_ea_value(const char *path, const char *ea_name) {
    /* Step 1: Pass size=0 to get the required buffer size */
    ssize_t needed = getxattr(path, ea_name, NULL, 0);
    if (needed == -1) {
        if (errno == ENODATA)
            fprintf(stderr, "EA '%s' does not exist\n", ea_name);
        else
            perror("getxattr size probe");
        return NULL;
    }

    /* Step 2: Allocate buffer (+1 for null terminator we add ourselves) */
    char *buf = malloc(needed + 1);
    if (!buf) { perror("malloc"); return NULL; }

    /* Step 3: Actually read the value */
    ssize_t got = getxattr(path, ea_name, buf, needed);
    if (got == -1) {
        /* Another process may have changed the EA between step 1 and here */
        perror("getxattr read");
        free(buf);
        return NULL;
    }

    /* Null-terminate so we can use it as a C string */
    buf[got] = '\0';
    return buf;
}

int main() {
    char *val = read_ea_value("/tmp/myfile.txt", "user.author");
    if (val) {
        printf("author = [%s]\n", val);
        free(val);
    }
    return 0;
}
Race condition warning: There is a TOCTOU (Time Of Check To Time Of Use) gap between the size probe and the actual read. Another process could change or delete the EA in between. In practice, handle ERANGE (buffer too small) on the second call by retrying with a larger buffer.
Handling ERANGE and Retry
#include <sys/xattr.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

char *read_ea_robust(const char *path, const char *name) {
    size_t size = 256;  /* start with a reasonable guess */
    char  *buf  = NULL;

    for (;;) {
        buf = realloc(buf, size + 1);
        if (!buf) return NULL;

        ssize_t ret = getxattr(path, name, buf, size);
        if (ret >= 0) {
            buf[ret] = '\0';
            return buf;
        }
        if (errno == ERANGE) {
            /* Buffer too small — double it and retry */
            size *= 2;
            continue;
        }
        /* Other error (ENODATA, EACCES, etc.) */
        free(buf);
        perror("getxattr");
        return NULL;
    }
}

int main() {
    char *v = read_ea_robust("/tmp/myfile.txt", "user.author");
    if (v) { printf("%s\n", v); free(v); }
    return 0;
}

3. removexattr() Family — Delete an EA

#include <sys/xattr.h>

int removexattr(const char *pathname, const char *name);
int lremovexattr(const char *pathname, const char *name);
int fremovexattr(int fd, const char *name);

// All return 0 on success, or -1 on error

removexattr() Examples
#include <sys/xattr.h>
#include <stdio.h>
#include <errno.h>

int main() {
    const char *path = "/tmp/myfile.txt";

    /* Remove a specific EA */
    if (removexattr(path, "user.author") == -1) {
        if (errno == ENODATA) {
            printf("EA 'user.author' doesn't exist — nothing to remove\n");
        } else {
            perror("removexattr");
        }
    } else {
        printf("EA 'user.author' removed successfully\n");
    }

    /* Trying to remove again = ENODATA */
    if (removexattr(path, "user.author") == -1) {
        if (errno == ENODATA)
            printf("Second removal: ENODATA (expected)\n");
    }

    return 0;
}
/* Remove ALL user EAs from a file — useful for cleanup */
#include <sys/xattr.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int remove_all_user_eas(const char *path) {
    char list[65536];
    ssize_t listlen = listxattr(path, list, sizeof(list));
    if (listlen == -1) { perror("listxattr"); return -1; }

    int removed = 0;
    for (int pos = 0; pos < listlen; ) {
        char *name = &list[pos];

        /* Only remove user.* EAs */
        if (strncmp(name, "user.", 5) == 0) {
            if (removexattr(path, name) == 0) {
                printf("Removed: %s\n", name);
                removed++;
            }
        }
        pos += strlen(name) + 1;  /* move to next null-terminated name */
    }
    printf("Total user EAs removed: %d\n", removed);
    return 0;
}

int main() {
    remove_all_user_eas("/tmp/myfile.txt");
    return 0;
}

4. listxattr() Family — List All EA Names

#include <sys/xattr.h>

ssize_t listxattr(const char *pathname, char *list, size_t size);
ssize_t llistxattr(const char *pathname, char *list, size_t size);
ssize_t flistxattr(int fd, char *list, size_t size);

// All return total bytes copied into list on success, or -1 on error

Understanding the List Format

The list returned by listxattr() is a series of null-terminated strings packed together in a single buffer. To iterate, move through it name by name.

Buffer returned by listxattr()
user.author \0 user.version \0 user.mime \0 (end)

Each name is separated by a null byte '\0'. To find the next name, advance by strlen(current_name) + 1.

#include <sys/xattr.h>
#include <stdio.h>
#include <string.h>

void list_all_eas(const char *path) {
    char list[65536];
    ssize_t listlen;

    listlen = listxattr(path, list, sizeof(list));
    if (listlen == -1) {
        perror("listxattr");
        return;
    }

    if (listlen == 0) {
        printf("%s: No extended attributes\n", path);
        return;
    }

    printf("EAs on '%s':\n", path);

    /* Iterate through null-terminated names in the buffer */
    for (int pos = 0; pos < listlen; ) {
        char *name = &list[pos];
        printf("  %s\n", name);
        pos += strlen(name) + 1;  /* +1 to skip the \0 */
    }
}

int main() {
    list_all_eas("/tmp/myfile.txt");
    return 0;
}

Complete Program — Display All EA Names & Values

This program (based on the book’s Listing 16-1) retrieves and prints all EAs for files given on the command line. It supports a -x flag to print values as hex.

/*
 * xattr_view.c
 * Usage: ./xattr_view [-x] file1 [file2 ...]
 *   -x : display values as hex instead of text
 *
 * Compile: gcc -o xattr_view xattr_view.c
 */

#include <sys/xattr.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define XATTR_BUF_SIZE  65536   /* 64 KB — VFS maximum for a single EA */

static void usage(const char *progname) {
    fprintf(stderr, "Usage: %s [-x] file...\n", progname);
    exit(EXIT_FAILURE);
}

int main(int argc, char *argv[]) {
    char   list[XATTR_BUF_SIZE];   /* buffer for EA name list */
    char   value[XATTR_BUF_SIZE];  /* buffer for one EA value */
    int    hex_mode = 0;
    int    opt;

    /* Parse -x option */
    while ((opt = getopt(argc, argv, "x")) != -1) {
        switch (opt) {
        case 'x': hex_mode = 1; break;
        default:  usage(argv[0]);
        }
    }

    if (optind >= argc)
        usage(argv[0]);

    /* Process each file on the command line */
    for (int j = optind; j < argc; j++) {
        ssize_t listlen = listxattr(argv[j], list, sizeof(list));
        if (listlen == -1) {
            fprintf(stderr, "%s: listxattr error: ", argv[j]);
            perror("");
            continue;
        }

        printf("%s:\n", argv[j]);

        if (listlen == 0) {
            printf("  (no extended attributes)\n\n");
            continue;
        }

        /* Iterate over each null-terminated name in the list */
        for (int pos = 0; pos < listlen; pos += strlen(&list[pos]) + 1) {
            char *name = &list[pos];
            printf("  name=%-30s ", name);

            ssize_t vlen = getxattr(argv[j], name, value, sizeof(value));
            if (vlen == -1) {
                printf("(error reading value: %m)\n");
                continue;
            }

            if (!hex_mode) {
                /* Print as text — limit to value length (NOT null-terminated) */
                printf("value=%.*s\n", (int)vlen, value);
            } else {
                /* Print as hex bytes */
                printf("value=");
                for (int k = 0; k < vlen; k++)
                    printf("%02x ", (unsigned char)value[k]);
                printf("\n");
            }
        }
        printf("\n");
    }

    return EXIT_SUCCESS;
}

/*
 * Example session:
 *
 * $ touch test.txt
 * $ setfattr -n user.author -v "Ravi Kumar" test.txt
 * $ setfattr -n user.version -v "1.0" test.txt
 * $ ./xattr_view test.txt
 * test.txt:
 *   name=user.author               value=Ravi Kumar
 *   name=user.version              value=1.0
 *
 * $ ./xattr_view -x test.txt
 * test.txt:
 *   name=user.author               value=52 61 76 69 20 4b 75 6d 61 72
 *   name=user.version              value=31 2e 30
 */

Exercise Program — Simple setfattr Clone

This is the solution to Exercise 16-1 from the book: a simple setfattr clone that creates or modifies a user EA via command line arguments.

/*
 * my_setfattr.c
 * Usage: ./my_setfattr filename ea_name value
 * Example: ./my_setfattr /tmp/test.txt user.comment "hello world"
 *
 * Compile: gcc -o my_setfattr my_setfattr.c
 */

#include <sys/xattr.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

static void usage(const char *prog) {
    fprintf(stderr, "Usage: %s <filename> <ea_name> <value>\n", prog);
    fprintf(stderr, "       %s <filename> <ea_name>         (set empty value)\n", prog);
    exit(EXIT_FAILURE);
}

int main(int argc, char *argv[]) {
    if (argc < 3 || argc > 4)
        usage(argv[0]);

    const char *filename = argv[1];
    const char *ea_name  = argv[2];
    const char *value    = (argc == 4) ? argv[3] : "";
    size_t      vlen     = (argc == 4) ? strlen(argv[3]) : 0;

    /* Validate: EA name must start with a known namespace */
    if (strncmp(ea_name, "user.", 5) != 0 &&
        strncmp(ea_name, "trusted.", 8) != 0 &&
        strncmp(ea_name, "system.", 7) != 0 &&
        strncmp(ea_name, "security.", 9) != 0) {
        fprintf(stderr, "Warning: EA name '%s' does not start with a standard namespace\n",
                ea_name);
    }

    printf("Setting EA: %s = \"%s\" on file: %s\n", ea_name, value, filename);

    if (setxattr(filename, ea_name, value, vlen, 0) == -1) {
        switch (errno) {
        case ENOENT:
            fprintf(stderr, "Error: File '%s' does not exist\n", filename);
            break;
        case ENOSPC:
            fprintf(stderr, "Error: EA storage block full (try removing other EAs)\n");
            break;
        case EPERM:
            fprintf(stderr, "Error: Permission denied (symlink? sticky dir? need CAP_SYS_ADMIN?)\n");
            break;
        case ENOTSUP:
            fprintf(stderr, "Error: Filesystem does not support EAs\n");
            break;
        case EACCES:
            fprintf(stderr, "Error: No write permission on file\n");
            break;
        default:
            perror("setxattr");
            break;
        }
        return EXIT_FAILURE;
    }

    printf("Success!\n");

    /* Verify by reading back */
    char readback[256];
    ssize_t rlen = getxattr(filename, ea_name, readback, sizeof(readback));
    if (rlen >= 0) {
        printf("Verified: %s = \"%.*s\" (%zd bytes)\n",
               ea_name, (int)rlen, readback, rlen);
    }

    return EXIT_SUCCESS;
}

Bonus Program — Copy All EAs from One File to Another
/*
 * copy_eas.c — Copy all EAs from source file to destination file
 * Usage: ./copy_eas src_file dst_file
 * Compile: gcc -o copy_eas copy_eas.c
 */

#include <sys/xattr.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFSIZE 65536

int copy_all_eas(const char *src, const char *dst) {
    char   namelist[BUFSIZE];
    char   value[BUFSIZE];
    ssize_t listlen = listxattr(src, namelist, sizeof(namelist));
    if (listlen == -1) { perror("listxattr"); return -1; }

    int copied = 0;
    for (int pos = 0; pos < listlen; ) {
        char *name = &namelist[pos];

        /* Read the EA value from source */
        ssize_t vlen = getxattr(src, name, value, sizeof(value));
        if (vlen == -1) {
            fprintf(stderr, "Skipping %s: getxattr failed\n", name);
        } else {
            /* Write to destination */
            if (setxattr(dst, name, value, vlen, 0) == -1) {
                fprintf(stderr, "Failed to copy %s to dst: ", name);
                perror("");
            } else {
                printf("Copied: %s (%zd bytes)\n", name, vlen);
                copied++;
            }
        }

        pos += strlen(name) + 1;
    }

    printf("Total EAs copied: %d\n", copied);
    return 0;
}

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

Binary EA Values — Storing a Struct

EA values are raw bytes — you can store binary data, including C structs:

#include <sys/xattr.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <time.h>

/* Custom metadata struct stored as binary EA */
struct file_meta {
    uint32_t  version;
    uint32_t  revision;
    time_t    created_at;
    char      author[64];
};

int main() {
    const char *path = "/tmp/binary_ea_test.txt";
    FILE *f = fopen(path, "w"); fclose(f);

    /* Store struct as binary EA */
    struct file_meta meta = {
        .version    = 2,
        .revision   = 5,
        .created_at = time(NULL),
    };
    strncpy(meta.author, "EmbeddedPathashala", sizeof(meta.author));

    if (setxattr(path, "user.file_meta", &meta, sizeof(meta), 0) == -1) {
        perror("setxattr struct"); return 1;
    }
    printf("Stored binary struct EA (%zu bytes)\n", sizeof(meta));

    /* Read it back */
    struct file_meta read_back;
    ssize_t len = getxattr(path, "user.file_meta", &read_back, sizeof(read_back));
    if (len == -1) { perror("getxattr struct"); return 1; }

    printf("Read back: version=%u.%u, author=%s\n",
           read_back.version, read_back.revision, read_back.author);

    return 0;
}
/*
 * Output:
 * Stored binary struct EA (80 bytes)
 * Read back: version=2.5, author=EmbeddedPathashala
 *
 * Note: Binary EAs shown as hex with: getfattr -m - -d /tmp/binary_ea_test.txt
 */
Portability note: Storing structs as binary EAs works but be careful about struct padding and byte-order if the file might be read on a different architecture. Consider serializing to a fixed-format byte array for portability.

Interview Questions
Q1. What is the difference between setxattr(), lsetxattr(), and fsetxattr()?
All three set an EA value. setxattr() takes a pathname and follows symlinks. lsetxattr() takes a pathname but does NOT follow symlinks (acts on the symlink itself). fsetxattr() takes an open file descriptor. The same convention applies to the get, remove, and list families.
Q2. When would you use XATTR_CREATE vs XATTR_REPLACE vs flag=0?
XATTR_CREATE — when you want to guarantee you’re creating a new EA and not accidentally overwriting one (fails with EEXIST if it exists). XATTR_REPLACE — when you want to update an existing EA and fail if it wasn’t previously set (fails with ENODATA if missing). 0 (default) — create or update, whichever is needed.
Q3. Why is getxattr() called twice in the dynamic buffer pattern?
The first call uses size = 0 which makes the kernel return the exact number of bytes needed without reading into any buffer. This is used to allocate the right-sized buffer. The second call does the actual read. This avoids guessing the buffer size or allocating a 64 KB buffer for every EA regardless of actual value size.
Q4. What format does listxattr() return the EA names in?
It returns a flat buffer containing null-terminated strings packed back-to-back. For example: "user.author\0user.version\0user.mime\0". To iterate, start at position 0, process the string, then advance by strlen(name) + 1 to skip the null terminator and reach the next name.
Q5. Are EA values null-terminated when you read them with getxattr()?
No. The kernel stores and returns exactly the bytes you stored — no null terminator is added. This means you must use the returned ssize_t length to determine how many bytes are valid. If you want to use the value as a C string, add the null terminator yourself after reading.
Q6. What happens if you call listxattr() and the returned list mentions an EA that you then cannot read with getxattr()?
This can happen for two reasons: (1) The calling process lacks the privilege to access a trusted or security namespace EA (the file system may include it in the list but deny the read). (2) A race condition — another process deleted the EA between the listxattr() and getxattr() calls. Always check the return value of getxattr() and handle the ENODATA and EACCES errors.
Q7. Write a one-liner to check if a specific EA exists on a file.
int ea_exists = (getxattr(path, name, NULL, 0) != -1 || errno != ENODATA);

If getxattr() with size=0 returns -1 and errno == ENODATA, the EA doesn’t exist. Any other result (including success) means it exists (or there’s a different error like permission denied).

Leave a Reply

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