etxattr, getxattr, removexattr and listxattr in Linux: Complete Guide
of system calls
per group
Header needed
Link flag (some distros)
Key Terms
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 |
stat() / lstat() / fstat(). The l prefix = “don’t follow symlinks”, the f prefix = “use file descriptor”.#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
| 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 |
| 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
*/
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;
}
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.#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
#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;
}
"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.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;
}
ERANGE (buffer too small) on the second call by retrying with a larger buffer.#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;
}
#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
#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;
}
#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
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;
}
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
*/
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;
}
/*
* 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;
}
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
*/
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.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.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."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.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.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.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).
Continue Learning
← Part 2: Implementation Details Part 4: Access Control Lists →
