// SPDX-License-Identifier: GPL-2.0+ /* * erofs-utils/lib/xattr.c * * Originally contributed by an anonymous person, * heavily changed by Li Guifu * and Gao Xiang */ #define _GNU_SOURCE #include #include #ifdef HAVE_LINUX_XATTR_H #include #endif #include #include #include "erofs/print.h" #include "erofs/hashtable.h" #include "erofs/xattr.h" #include "erofs/cache.h" #include "erofs/io.h" #define EA_HASHTABLE_BITS 16 struct xattr_item { const char *kvbuf; unsigned int hash[2], len[2], count; int shared_xattr_id; u8 prefix; struct hlist_node node; }; struct inode_xattr_node { struct list_head list; struct xattr_item *item; }; static DECLARE_HASHTABLE(ea_hashtable, EA_HASHTABLE_BITS); static LIST_HEAD(shared_xattrs_list); static unsigned int shared_xattrs_count, shared_xattrs_size; static struct xattr_prefix { const char *prefix; u16 prefix_len; } xattr_types[] = { [EROFS_XATTR_INDEX_USER] = { XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN }, [EROFS_XATTR_INDEX_POSIX_ACL_ACCESS] = { XATTR_NAME_POSIX_ACL_ACCESS, sizeof(XATTR_NAME_POSIX_ACL_ACCESS) - 1 }, [EROFS_XATTR_INDEX_POSIX_ACL_DEFAULT] = { XATTR_NAME_POSIX_ACL_DEFAULT, sizeof(XATTR_NAME_POSIX_ACL_DEFAULT) - 1 }, [EROFS_XATTR_INDEX_TRUSTED] = { XATTR_TRUSTED_PREFIX, XATTR_TRUSTED_PREFIX_LEN }, [EROFS_XATTR_INDEX_SECURITY] = { XATTR_SECURITY_PREFIX, XATTR_SECURITY_PREFIX_LEN } }; static unsigned int BKDRHash(char *str, unsigned int len) { const unsigned int seed = 131313; unsigned int hash = 0; while (len) { hash = hash * seed + (*str++); --len; } return hash; } static unsigned int xattr_item_hash(u8 prefix, char *buf, unsigned int len[2], unsigned int hash[2]) { hash[0] = BKDRHash(buf, len[0]); /* key */ hash[1] = BKDRHash(buf + len[0], len[1]); /* value */ return prefix ^ hash[0] ^ hash[1]; } static unsigned int put_xattritem(struct xattr_item *item) { if (item->count > 1) return --item->count; free(item); return 0; } static struct xattr_item *get_xattritem(u8 prefix, char *kvbuf, unsigned int len[2]) { struct xattr_item *item; unsigned int hash[2], hkey; hkey = xattr_item_hash(prefix, kvbuf, len, hash); hash_for_each_possible(ea_hashtable, item, node, hkey) { if (prefix == item->prefix && item->len[0] == len[0] && item->len[1] == len[1] && item->hash[0] == hash[0] && item->hash[1] == hash[1] && !memcmp(kvbuf, item->kvbuf, len[0] + len[1])) { free(kvbuf); ++item->count; return item; } } item = malloc(sizeof(*item)); if (!item) { free(kvbuf); return ERR_PTR(-ENOMEM); } INIT_HLIST_NODE(&item->node); item->count = 1; item->kvbuf = kvbuf; item->len[0] = len[0]; item->len[1] = len[1]; item->hash[0] = hash[0]; item->hash[1] = hash[1]; item->shared_xattr_id = -1; item->prefix = prefix; hash_add(ea_hashtable, &item->node, hkey); return item; } static bool match_prefix(const char *key, u8 *index, u16 *len) { struct xattr_prefix *p; for (p = xattr_types; p < xattr_types + ARRAY_SIZE(xattr_types); ++p) { if (p->prefix && !strncmp(p->prefix, key, p->prefix_len)) { *len = p->prefix_len; *index = p - xattr_types; return true; } } return false; } static struct xattr_item *parse_one_xattr(const char *path, const char *key, unsigned int keylen) { ssize_t ret; u8 prefix; u16 prefixlen; unsigned int len[2]; char *kvbuf; erofs_dbg("parse xattr [%s] of %s", path, key); if (!match_prefix(key, &prefix, &prefixlen)) return ERR_PTR(-ENODATA); DBG_BUGON(keylen < prefixlen); /* determine length of the value */ ret = lgetxattr(path, key, NULL, 0); if (ret < 0) return ERR_PTR(-errno); len[1] = ret; /* allocate key-value buffer */ len[0] = keylen - prefixlen; kvbuf = malloc(len[0] + len[1]); if (!kvbuf) return ERR_PTR(-ENOMEM); memcpy(kvbuf, key + prefixlen, len[0]); if (len[1]) { /* copy value to buffer */ ret = lgetxattr(path, key, kvbuf + len[0], len[1]); if (ret < 0) { free(kvbuf); return ERR_PTR(-errno); } if (len[1] != ret) { erofs_err("size of xattr value got changed just now (%u-> %ld)", len[1], (long)ret); len[1] = ret; } } return get_xattritem(prefix, kvbuf, len); } static struct xattr_item *erofs_get_selabel_xattr(const char *srcpath, mode_t mode) { #ifdef HAVE_LIBSELINUX if (cfg.sehnd) { char *secontext; int ret; unsigned int len[2]; char *kvbuf, *fspath; #ifdef WITH_ANDROID if (cfg.mount_point) ret = asprintf(&fspath, "/%s/%s", cfg.mount_point, erofs_fspath(srcpath)); else #endif ret = asprintf(&fspath, "/%s", erofs_fspath(srcpath)); if (ret <= 0) return ERR_PTR(-ENOMEM); ret = selabel_lookup(cfg.sehnd, &secontext, fspath, mode); free(fspath); if (ret) { ret = -errno; if (ret != -ENOENT) { erofs_err("failed to lookup selabel for %s: %s", srcpath, erofs_strerror(ret)); return ERR_PTR(ret); } /* secontext = "u:object_r:unlabeled:s0"; */ return NULL; } len[0] = sizeof("selinux") - 1; len[1] = strlen(secontext); kvbuf = malloc(len[0] + len[1] + 1); if (!kvbuf) { freecon(secontext); return ERR_PTR(-ENOMEM); } sprintf(kvbuf, "selinux%s", secontext); freecon(secontext); return get_xattritem(EROFS_XATTR_INDEX_SECURITY, kvbuf, len); } #endif return NULL; } static int inode_xattr_add(struct list_head *hlist, struct xattr_item *item) { struct inode_xattr_node *node = malloc(sizeof(*node)); if (!node) return -ENOMEM; init_list_head(&node->list); node->item = item; list_add(&node->list, hlist); return 0; } static int shared_xattr_add(struct xattr_item *item) { struct inode_xattr_node *node = malloc(sizeof(*node)); if (!node) return -ENOMEM; init_list_head(&node->list); node->item = item; list_add(&node->list, &shared_xattrs_list); shared_xattrs_size += sizeof(struct erofs_xattr_entry); shared_xattrs_size = EROFS_XATTR_ALIGN(shared_xattrs_size + item->len[0] + item->len[1]); return ++shared_xattrs_count; } static int erofs_xattr_add(struct list_head *ixattrs, struct xattr_item *item) { if (ixattrs) return inode_xattr_add(ixattrs, item); if (item->count == cfg.c_inline_xattr_tolerance + 1) { int ret = shared_xattr_add(item); if (ret < 0) return ret; } return 0; } static bool erofs_is_skipped_xattr(const char *key) { #ifdef HAVE_LIBSELINUX /* if sehnd is valid, selabels will be overridden */ if (cfg.sehnd && !strcmp(key, XATTR_SECURITY_PREFIX "selinux")) return true; #endif return false; } static int read_xattrs_from_file(const char *path, mode_t mode, struct list_head *ixattrs) { ssize_t kllen = llistxattr(path, NULL, 0); int ret; char *keylst, *key, *klend; unsigned int keylen; struct xattr_item *item; if (kllen < 0 && errno != ENODATA) { erofs_err("llistxattr to get the size of names for %s failed", path); return -errno; } ret = 0; if (kllen <= 1) goto out; keylst = malloc(kllen); if (!keylst) return -ENOMEM; /* copy the list of attribute keys to the buffer.*/ kllen = llistxattr(path, keylst, kllen); if (kllen < 0) { erofs_err("llistxattr to get names for %s failed", path); ret = -errno; goto err; } /* * loop over the list of zero terminated strings with the * attribute keys. Use the remaining buffer length to determine * the end of the list. */ klend = keylst + kllen; ret = 0; for (key = keylst; key != klend; key += keylen + 1) { keylen = strlen(key); if (erofs_is_skipped_xattr(key)) continue; item = parse_one_xattr(path, key, keylen); if (IS_ERR(item)) { ret = PTR_ERR(item); goto err; } ret = erofs_xattr_add(ixattrs, item); if (ret < 0) goto err; } free(keylst); out: /* if some selabel is avilable, need to add right now */ item = erofs_get_selabel_xattr(path, mode); if (IS_ERR(item)) return PTR_ERR(item); if (item) ret = erofs_xattr_add(ixattrs, item); return ret; err: free(keylst); return ret; } #ifdef WITH_ANDROID static int erofs_droid_xattr_set_caps(struct erofs_inode *inode) { const u64 capabilities = inode->capabilities; char *kvbuf; unsigned int len[2]; struct vfs_cap_data caps; struct xattr_item *item; if (!capabilities) return 0; len[0] = sizeof("capability") - 1; len[1] = sizeof(caps); kvbuf = malloc(len[0] + len[1]); if (!kvbuf) return -ENOMEM; memcpy(kvbuf, "capability", len[0]); caps.magic_etc = VFS_CAP_REVISION_2 | VFS_CAP_FLAGS_EFFECTIVE; caps.data[0].permitted = (u32) capabilities; caps.data[0].inheritable = 0; caps.data[1].permitted = (u32) (capabilities >> 32); caps.data[1].inheritable = 0; memcpy(kvbuf + len[0], &caps, len[1]); item = get_xattritem(EROFS_XATTR_INDEX_SECURITY, kvbuf, len); if (IS_ERR(item)) return PTR_ERR(item); if (!item) return 0; return erofs_xattr_add(&inode->i_xattrs, item); } #else static int erofs_droid_xattr_set_caps(struct erofs_inode *inode) { return 0; } #endif int erofs_prepare_xattr_ibody(struct erofs_inode *inode) { int ret; struct inode_xattr_node *node; struct list_head *ixattrs = &inode->i_xattrs; /* check if xattr is disabled */ if (cfg.c_inline_xattr_tolerance < 0) return 0; ret = read_xattrs_from_file(inode->i_srcpath, inode->i_mode, ixattrs); if (ret < 0) return ret; ret = erofs_droid_xattr_set_caps(inode); if (ret < 0) return ret; if (list_empty(ixattrs)) return 0; /* get xattr ibody size */ ret = sizeof(struct erofs_xattr_ibody_header); list_for_each_entry(node, ixattrs, list) { const struct xattr_item *item = node->item; if (item->shared_xattr_id >= 0) { ret += sizeof(__le32); continue; } ret += sizeof(struct erofs_xattr_entry); ret = EROFS_XATTR_ALIGN(ret + item->len[0] + item->len[1]); } inode->xattr_isize = ret; return ret; } static int erofs_count_all_xattrs_from_path(const char *path) { int ret; DIR *_dir; struct stat64 st; _dir = opendir(path); if (!_dir) { erofs_err("%s, failed to opendir at %s: %s", __func__, path, erofs_strerror(errno)); return -errno; } ret = 0; while (1) { struct dirent *dp; char buf[PATH_MAX]; /* * set errno to 0 before calling readdir() in order to * distinguish end of stream and from an error. */ errno = 0; dp = readdir(_dir); if (!dp) break; if (is_dot_dotdot(dp->d_name) || !strncmp(dp->d_name, "lost+found", strlen("lost+found"))) continue; ret = snprintf(buf, PATH_MAX, "%s/%s", path, dp->d_name); if (ret < 0 || ret >= PATH_MAX) { /* ignore the too long path */ ret = -ENOMEM; goto fail; } ret = lstat64(buf, &st); if (ret) { ret = -errno; goto fail; } ret = read_xattrs_from_file(buf, st.st_mode, NULL); if (ret) goto fail; if (!S_ISDIR(st.st_mode)) continue; ret = erofs_count_all_xattrs_from_path(buf); if (ret) goto fail; } if (errno) ret = -errno; fail: closedir(_dir); return ret; } static void erofs_cleanxattrs(bool sharedxattrs) { unsigned int i; struct xattr_item *item; struct hlist_node *tmp; hash_for_each_safe(ea_hashtable, i, tmp, item, node) { if (sharedxattrs && item->shared_xattr_id >= 0) continue; hash_del(&item->node); free(item); } if (sharedxattrs) return; shared_xattrs_size = shared_xattrs_count = 0; } static bool erofs_bh_flush_write_shared_xattrs(struct erofs_buffer_head *bh) { void *buf = bh->fsprivate; int err = dev_write(buf, erofs_btell(bh, false), shared_xattrs_size); if (err) return false; free(buf); return erofs_bh_flush_generic_end(bh); } static struct erofs_bhops erofs_write_shared_xattrs_bhops = { .flush = erofs_bh_flush_write_shared_xattrs, }; int erofs_build_shared_xattrs_from_path(const char *path) { int ret; struct erofs_buffer_head *bh; struct inode_xattr_node *node, *n; char *buf; unsigned int p; erofs_off_t off; /* check if xattr or shared xattr is disabled */ if (cfg.c_inline_xattr_tolerance < 0 || cfg.c_inline_xattr_tolerance == INT_MAX) return 0; if (shared_xattrs_size || shared_xattrs_count) { DBG_BUGON(1); return -EINVAL; } ret = erofs_count_all_xattrs_from_path(path); if (ret) return ret; if (!shared_xattrs_size) goto out; buf = calloc(1, shared_xattrs_size); if (!buf) return -ENOMEM; bh = erofs_balloc(XATTR, shared_xattrs_size, 0, 0); if (IS_ERR(bh)) { free(buf); return PTR_ERR(bh); } bh->op = &erofs_skip_write_bhops; erofs_mapbh(bh->block, true); off = erofs_btell(bh, false); sbi.xattr_blkaddr = off / EROFS_BLKSIZ; off %= EROFS_BLKSIZ; p = 0; list_for_each_entry_safe(node, n, &shared_xattrs_list, list) { struct xattr_item *const item = node->item; const struct erofs_xattr_entry entry = { .e_name_index = item->prefix, .e_name_len = item->len[0], .e_value_size = cpu_to_le16(item->len[1]) }; list_del(&node->list); item->shared_xattr_id = (off + p) / sizeof(struct erofs_xattr_entry); memcpy(buf + p, &entry, sizeof(entry)); p += sizeof(struct erofs_xattr_entry); memcpy(buf + p, item->kvbuf, item->len[0] + item->len[1]); p = EROFS_XATTR_ALIGN(p + item->len[0] + item->len[1]); free(node); } bh->fsprivate = buf; bh->op = &erofs_write_shared_xattrs_bhops; out: erofs_cleanxattrs(true); return 0; } char *erofs_export_xattr_ibody(struct list_head *ixattrs, unsigned int size) { struct inode_xattr_node *node, *n; struct erofs_xattr_ibody_header *header; LIST_HEAD(ilst); unsigned int p; char *buf = calloc(1, size); if (!buf) return ERR_PTR(-ENOMEM); header = (struct erofs_xattr_ibody_header *)buf; header->h_shared_count = 0; p = sizeof(struct erofs_xattr_ibody_header); list_for_each_entry_safe(node, n, ixattrs, list) { struct xattr_item *const item = node->item; list_del(&node->list); /* move inline xattrs to the onstack list */ if (item->shared_xattr_id < 0) { list_add(&node->list, &ilst); continue; } *(__le32 *)(buf + p) = cpu_to_le32(item->shared_xattr_id); p += sizeof(__le32); ++header->h_shared_count; free(node); put_xattritem(item); } list_for_each_entry_safe(node, n, &ilst, list) { struct xattr_item *const item = node->item; const struct erofs_xattr_entry entry = { .e_name_index = item->prefix, .e_name_len = item->len[0], .e_value_size = cpu_to_le16(item->len[1]) }; memcpy(buf + p, &entry, sizeof(entry)); p += sizeof(struct erofs_xattr_entry); memcpy(buf + p, item->kvbuf, item->len[0] + item->len[1]); p = EROFS_XATTR_ALIGN(p + item->len[0] + item->len[1]); list_del(&node->list); free(node); put_xattritem(item); } DBG_BUGON(p > size); return buf; }