diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
commit | 55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch) | |
tree | 33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/home/homework-fscrypt.c | |
parent | Initial commit. (diff) | |
download | systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip |
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/home/homework-fscrypt.c')
-rw-r--r-- | src/home/homework-fscrypt.c | 674 |
1 files changed, 674 insertions, 0 deletions
diff --git a/src/home/homework-fscrypt.c b/src/home/homework-fscrypt.c new file mode 100644 index 0000000..6aae1d2 --- /dev/null +++ b/src/home/homework-fscrypt.c @@ -0,0 +1,674 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/fs.h> +#include <openssl/evp.h> +#include <openssl/sha.h> +#include <sys/ioctl.h> +#include <sys/xattr.h> + +#include "errno-util.h" +#include "fd-util.h" +#include "hexdecoct.h" +#include "homework-fscrypt.h" +#include "homework-mount.h" +#include "homework-quota.h" +#include "memory-util.h" +#include "missing_keyctl.h" +#include "missing_syscall.h" +#include "mkdir.h" +#include "mount-util.h" +#include "nulstr-util.h" +#include "openssl-util.h" +#include "parse-util.h" +#include "process-util.h" +#include "random-util.h" +#include "rm-rf.h" +#include "stdio-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "user-util.h" +#include "xattr-util.h" + +static int fscrypt_upload_volume_key( + const uint8_t key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + const void *volume_key, + size_t volume_key_size, + key_serial_t where) { + + _cleanup_free_ char *hex = NULL; + const char *description; + struct fscrypt_key key; + key_serial_t serial; + + assert(key_descriptor); + assert(volume_key); + assert(volume_key_size > 0); + + if (volume_key_size > sizeof(key.raw)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume key too long."); + + hex = hexmem(key_descriptor, FS_KEY_DESCRIPTOR_SIZE); + if (!hex) + return log_oom(); + + description = strjoina("fscrypt:", hex); + + key = (struct fscrypt_key) { + .size = volume_key_size, + }; + memcpy(key.raw, volume_key, volume_key_size); + + CLEANUP_ERASE(key); + + /* Upload to the kernel */ + serial = add_key("logon", description, &key, sizeof(key), where); + if (serial < 0) + return log_error_errno(errno, "Failed to install master key in keyring: %m"); + + log_info("Uploaded encryption key to kernel."); + + return 0; +} + +static void calculate_key_descriptor( + const void *key, + size_t key_size, + uint8_t ret_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE]) { + + uint8_t hashed[512 / 8] = {}, hashed2[512 / 8] = {}; + + /* Derive the key descriptor from the volume key via double SHA512, in order to be compatible with e4crypt */ + + assert_se(SHA512(key, key_size, hashed) == hashed); + assert_se(SHA512(hashed, sizeof(hashed), hashed2) == hashed2); + + assert_cc(sizeof(hashed2) >= FS_KEY_DESCRIPTOR_SIZE); + + memcpy(ret_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE); +} + +static int fscrypt_slot_try_one( + const char *password, + const void *salt, size_t salt_size, + const void *encrypted, size_t encrypted_size, + const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + void **ret_decrypted, size_t *ret_decrypted_size) { + + + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + _cleanup_(erase_and_freep) void *decrypted = NULL; + uint8_t key_descriptor[FS_KEY_DESCRIPTOR_SIZE]; + int decrypted_size_out1, decrypted_size_out2; + uint8_t derived[512 / 8] = {}; + size_t decrypted_size; + const EVP_CIPHER *cc; + int r; + + assert(password); + assert(salt); + assert(salt_size > 0); + assert(encrypted); + assert(encrypted_size > 0); + assert(match_key_descriptor); + + /* Our construction is like this: + * + * 1. In each key slot we store a salt value plus the encrypted volume key + * + * 2. Unlocking is via calculating PBKDF2-HMAC-SHA512 of the supplied password (in combination with + * the salt), then using the first 256 bit of the hash as key for decrypting the encrypted + * volume key in AES256 counter mode. + * + * 3. Writing a password is similar: calculate PBKDF2-HMAC-SHA512 of the supplied password (in + * combination with the salt), then encrypt the volume key in AES256 counter mode with the + * resulting hash. + */ + + CLEANUP_ERASE(derived); + + if (PKCS5_PBKDF2_HMAC( + password, strlen(password), + salt, salt_size, + 0xFFFF, EVP_sha512(), + sizeof(derived), derived) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed"); + + context = EVP_CIPHER_CTX_new(); + if (!context) + return log_oom(); + + /* We use AES256 in counter mode */ + assert_se(cc = EVP_aes_256_ctr()); + + /* We only use the first half of the derived key */ + assert(sizeof(derived) >= (size_t) EVP_CIPHER_key_length(cc)); + + if (EVP_DecryptInit_ex(context, cc, NULL, derived, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context."); + + decrypted_size = encrypted_size + EVP_CIPHER_key_length(cc) * 2; + decrypted = malloc(decrypted_size); + if (!decrypted) + return log_oom(); + + if (EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted, encrypted_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt volume key."); + + assert((size_t) decrypted_size_out1 <= decrypted_size); + + if (EVP_DecryptFinal_ex(context, (uint8_t*) decrypted_size + decrypted_size_out1, &decrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish decryption of volume key."); + + assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size); + decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2; + + calculate_key_descriptor(decrypted, decrypted_size, key_descriptor); + + if (memcmp(key_descriptor, match_key_descriptor, FS_KEY_DESCRIPTOR_SIZE) != 0) + return -ENOANO; /* don't log here */ + + r = fscrypt_upload_volume_key(key_descriptor, decrypted, decrypted_size, KEY_SPEC_THREAD_KEYRING); + if (r < 0) + return r; + + if (ret_decrypted) + *ret_decrypted = TAKE_PTR(decrypted); + if (ret_decrypted_size) + *ret_decrypted_size = decrypted_size; + + return 0; +} + +static int fscrypt_slot_try_many( + char **passwords, + const void *salt, size_t salt_size, + const void *encrypted, size_t encrypted_size, + const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + void **ret_decrypted, size_t *ret_decrypted_size) { + + int r; + + STRV_FOREACH(i, passwords) { + r = fscrypt_slot_try_one(*i, salt, salt_size, encrypted, encrypted_size, match_key_descriptor, ret_decrypted, ret_decrypted_size); + if (r != -ENOANO) + return r; + } + + return -ENOANO; +} + +static int fscrypt_setup( + const PasswordCache *cache, + char **password, + HomeSetup *setup, + void **ret_volume_key, + size_t *ret_volume_key_size) { + + _cleanup_free_ char *xattr_buf = NULL; + int r; + + assert(setup); + assert(setup->root_fd >= 0); + + r = flistxattr_malloc(setup->root_fd, &xattr_buf); + if (r < 0) + return log_error_errno(errno, "Failed to retrieve xattr list: %m"); + + NULSTR_FOREACH(xa, xattr_buf) { + _cleanup_free_ void *salt = NULL, *encrypted = NULL; + _cleanup_free_ char *value = NULL; + size_t salt_size, encrypted_size; + const char *nr, *e; + char **list; + int n; + + /* Check if this xattr has the format 'trusted.fscrypt_slot<nr>' where '<nr>' is a 32-bit unsigned integer */ + nr = startswith(xa, "trusted.fscrypt_slot"); + if (!nr) + continue; + if (safe_atou32(nr, NULL) < 0) + continue; + + n = fgetxattr_malloc(setup->root_fd, xa, &value); + if (n == -ENODATA) /* deleted by now? */ + continue; + if (n < 0) + return log_error_errno(n, "Failed to read %s xattr: %m", xa); + + e = memchr(value, ':', n); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s lacks ':' separator: %m", xa); + + r = unbase64mem(value, e - value, &salt, &salt_size); + if (r < 0) + return log_error_errno(r, "Failed to decode salt of %s: %m", xa); + r = unbase64mem(e+1, n - (e - value) - 1, &encrypted, &encrypted_size); + if (r < 0) + return log_error_errno(r, "Failed to decode encrypted key of %s: %m", xa); + + r = -ENOANO; + FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, password) { + r = fscrypt_slot_try_many( + list, + salt, salt_size, + encrypted, encrypted_size, + setup->fscrypt_key_descriptor, + ret_volume_key, ret_volume_key_size); + if (r != -ENOANO) + break; + } + if (r < 0) { + if (r != -ENOANO) + return r; + } else + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to set up home directory with provided passwords."); +} + +int home_setup_fscrypt( + UserRecord *h, + HomeSetup *setup, + const PasswordCache *cache) { + + _cleanup_(erase_and_freep) void *volume_key = NULL; + struct fscrypt_policy policy = {}; + size_t volume_key_size = 0; + const char *ip; + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + assert(setup); + assert(setup->root_fd < 0); + + assert_se(ip = user_record_image_path(h)); + + setup->root_fd = open(ip, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + if (errno == ENODATA) + return log_error_errno(errno, "Home directory %s is not encrypted.", ip); + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_error_errno(errno, "File system does not support fscrypt: %m"); + return -ENOLINK; /* make recognizable */ + } + return log_error_errno(errno, "Failed to acquire encryption policy of %s: %m", ip); + } + + memcpy(setup->fscrypt_key_descriptor, policy.master_key_descriptor, FS_KEY_DESCRIPTOR_SIZE); + + r = fscrypt_setup( + cache, + h->password, + setup, + &volume_key, + &volume_key_size); + if (r < 0) + return r; + + /* Also install the access key in the user's own keyring */ + + if (uid_is_valid(h->uid)) { + r = safe_fork("(sd-addkey)", + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_REOPEN_LOG, + NULL); + if (r < 0) + return log_error_errno(r, "Failed install encryption key in user's keyring: %m"); + if (r == 0) { + gid_t gid; + + /* Child */ + + gid = user_record_gid(h); + if (setresgid(gid, gid, gid) < 0) { + log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", gid); + _exit(EXIT_FAILURE); + } + + if (setgroups(0, NULL) < 0) { + log_error_errno(errno, "Failed to reset auxiliary groups list: %m"); + _exit(EXIT_FAILURE); + } + + if (setresuid(h->uid, h->uid, h->uid) < 0) { + log_error_errno(errno, "Failed to change UID to " UID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + r = fscrypt_upload_volume_key( + setup->fscrypt_key_descriptor, + volume_key, + volume_key_size, + KEY_SPEC_USER_KEYRING); + if (r < 0) + _exit(EXIT_FAILURE); + + _exit(EXIT_SUCCESS); + } + } + + /* We'll bind mount the image directory to a new mount point where we'll start adjusting it. Only + * once that's complete we'll move the thing to its final place eventually. */ + r = home_unshare_and_mkdir(); + if (r < 0) + return r; + + r = mount_follow_verbose(LOG_ERR, ip, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND, NULL); + if (r < 0) + return r; + + setup->undo_mount = true; + + /* Turn off any form of propagation for this */ + r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_PRIVATE, NULL); + if (r < 0) + return r; + + /* Adjust MS_SUID and similar flags */ + r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL); + if (r < 0) + return r; + + safe_close(setup->root_fd); + setup->root_fd = open(HOME_RUNTIME_WORK_DIR, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + return 0; +} + +static int fscrypt_slot_set( + int root_fd, + const void *volume_key, + size_t volume_key_size, + const char *password, + uint32_t nr) { + + _cleanup_free_ char *salt_base64 = NULL, *encrypted_base64 = NULL, *joined = NULL; + char label[STRLEN("trusted.fscrypt_slot") + DECIMAL_STR_MAX(nr) + 1]; + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + int r, encrypted_size_out1, encrypted_size_out2; + uint8_t salt[64], derived[512 / 8] = {}; + _cleanup_free_ void *encrypted = NULL; + const EVP_CIPHER *cc; + size_t encrypted_size; + ssize_t ss; + + r = crypto_random_bytes(salt, sizeof(salt)); + if (r < 0) + return log_error_errno(r, "Failed to generate salt: %m"); + + CLEANUP_ERASE(derived); + + if (PKCS5_PBKDF2_HMAC( + password, strlen(password), + salt, sizeof(salt), + 0xFFFF, EVP_sha512(), + sizeof(derived), derived) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed"); + + context = EVP_CIPHER_CTX_new(); + if (!context) + return log_oom(); + + /* We use AES256 in counter mode */ + cc = EVP_aes_256_ctr(); + + /* We only use the first half of the derived key */ + assert(sizeof(derived) >= (size_t) EVP_CIPHER_key_length(cc)); + + if (EVP_EncryptInit_ex(context, cc, NULL, derived, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context."); + + encrypted_size = volume_key_size + EVP_CIPHER_key_length(cc) * 2; + encrypted = malloc(encrypted_size); + if (!encrypted) + return log_oom(); + + if (EVP_EncryptUpdate(context, (uint8_t*) encrypted, &encrypted_size_out1, volume_key, volume_key_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt volume key."); + + assert((size_t) encrypted_size_out1 <= encrypted_size); + + if (EVP_EncryptFinal_ex(context, (uint8_t*) encrypted_size + encrypted_size_out1, &encrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of volume key."); + + assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 < encrypted_size); + encrypted_size = (size_t) encrypted_size_out1 + (size_t) encrypted_size_out2; + + ss = base64mem(salt, sizeof(salt), &salt_base64); + if (ss < 0) + return log_oom(); + + ss = base64mem(encrypted, encrypted_size, &encrypted_base64); + if (ss < 0) + return log_oom(); + + joined = strjoin(salt_base64, ":", encrypted_base64); + if (!joined) + return log_oom(); + + xsprintf(label, "trusted.fscrypt_slot%" PRIu32, nr); + if (fsetxattr(root_fd, label, joined, strlen(joined), 0) < 0) + return log_error_errno(errno, "Failed to write xattr %s: %m", label); + + log_info("Written key slot %s.", label); + + return 0; +} + +int home_create_fscrypt( + UserRecord *h, + HomeSetup *setup, + char **effective_passwords, + UserRecord **ret_home) { + + _cleanup_(rm_rf_physical_and_freep) char *temporary = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_(erase_and_freep) void *volume_key = NULL; + _cleanup_close_ int mount_fd = -EBADF; + struct fscrypt_policy policy = {}; + size_t volume_key_size = 512 / 8; + _cleanup_free_ char *d = NULL; + uint32_t nr = 0; + const char *ip; + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + assert(setup); + assert(ret_home); + + assert_se(ip = user_record_image_path(h)); + + r = tempfn_random(ip, "homework", &d); + if (r < 0) + return log_error_errno(r, "Failed to allocate temporary directory: %m"); + + (void) mkdir_parents(d, 0755); + + if (mkdir(d, 0700) < 0) + return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d); + + temporary = TAKE_PTR(d); /* Needs to be destroyed now */ + + r = home_unshare_and_mkdir(); + if (r < 0) + return r; + + setup->root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open temporary home directory: %m"); + + if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_error_errno(errno, "File system does not support fscrypt: %m"); + return -ENOLINK; /* make recognizable */ + } + if (errno != ENODATA) + return log_error_errno(errno, "Failed to get fscrypt policy of directory: %m"); + } else + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Parent of %s already encrypted, refusing.", d); + + volume_key = malloc(volume_key_size); + if (!volume_key) + return log_oom(); + + r = crypto_random_bytes(volume_key, volume_key_size); + if (r < 0) + return log_error_errno(r, "Failed to acquire volume key: %m"); + + log_info("Generated volume key of size %zu.", volume_key_size); + + policy = (struct fscrypt_policy) { + .contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS, + .filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS, + .flags = FS_POLICY_FLAGS_PAD_32, + }; + + calculate_key_descriptor(volume_key, volume_key_size, policy.master_key_descriptor); + + r = fscrypt_upload_volume_key(policy.master_key_descriptor, volume_key, volume_key_size, KEY_SPEC_THREAD_KEYRING); + if (r < 0) + return r; + + log_info("Uploaded volume key to kernel."); + + if (ioctl(setup->root_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy) < 0) + return log_error_errno(errno, "Failed to set fscrypt policy on directory: %m"); + + log_info("Encryption policy set."); + + STRV_FOREACH(i, effective_passwords) { + r = fscrypt_slot_set(setup->root_fd, volume_key, volume_key_size, *i, nr); + if (r < 0) + return r; + + nr++; + } + + (void) home_update_quota_classic(h, temporary); + + r = home_shift_uid(setup->root_fd, HOME_RUNTIME_WORK_DIR, h->uid, h->uid, &mount_fd); + if (r > 0) + setup->undo_mount = true; /* If uidmaps worked we have a mount to undo again */ + + if (mount_fd >= 0) { + /* If we have established a new mount, then we can use that as new root fd to our home directory. */ + safe_close(setup->root_fd); + + setup->root_fd = fd_reopen(mount_fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(setup->root_fd, "Unable to convert mount fd into proper directory fd: %m"); + + mount_fd = safe_close(mount_fd); + } + + r = home_populate(h, setup->root_fd); + if (r < 0) + return r; + + r = home_sync_and_statfs(setup->root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_PERMISSIVE, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + USER_FSCRYPT, + ip, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + setup->root_fd = safe_close(setup->root_fd); + + r = home_setup_undo_mount(setup, LOG_ERR); + if (r < 0) + return r; + + if (rename(temporary, ip) < 0) + return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip); + + temporary = mfree(temporary); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +int home_passwd_fscrypt( + UserRecord *h, + HomeSetup *setup, + const PasswordCache *cache, /* the passwords acquired via PKCS#11/FIDO2 security tokens */ + char **effective_passwords /* new passwords */) { + + _cleanup_(erase_and_freep) void *volume_key = NULL; + _cleanup_free_ char *xattr_buf = NULL; + size_t volume_key_size = 0; + uint32_t slot = 0; + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + assert(setup); + + r = fscrypt_setup( + cache, + h->password, + setup, + &volume_key, + &volume_key_size); + if (r < 0) + return r; + + STRV_FOREACH(p, effective_passwords) { + r = fscrypt_slot_set(setup->root_fd, volume_key, volume_key_size, *p, slot); + if (r < 0) + return r; + + slot++; + } + + r = flistxattr_malloc(setup->root_fd, &xattr_buf); + if (r < 0) + return log_error_errno(errno, "Failed to retrieve xattr list: %m"); + + NULSTR_FOREACH(xa, xattr_buf) { + const char *nr; + uint32_t z; + + /* Check if this xattr has the format 'trusted.fscrypt_slot<nr>' where '<nr>' is a 32-bit unsigned integer */ + nr = startswith(xa, "trusted.fscrypt_slot"); + if (!nr) + continue; + if (safe_atou32(nr, &z) < 0) + continue; + + if (z < slot) + continue; + + if (fremovexattr(setup->root_fd, xa) < 0) + if (errno != ENODATA) + log_warning_errno(errno, "Failed to remove xattr %s: %m", xa); + } + + return 0; +} |