diff --git a/kmod/src/Makefile b/kmod/src/Makefile index 2666e6df..fa632aa1 100644 --- a/kmod/src/Makefile +++ b/kmod/src/Makefile @@ -9,6 +9,7 @@ CFLAGS_scoutfs_trace.o = -I$(src) # define_trace.h double include scoutfs-y += \ acl.o \ + attr_x.o \ avl.o \ alloc.o \ block.o \ @@ -34,6 +35,7 @@ scoutfs-y += \ options.o \ per_task.o \ quorum.o \ + quota.o \ recov.o \ scoutfs_trace.o \ server.o \ @@ -42,10 +44,12 @@ scoutfs-y += \ srch.o \ super.o \ sysfs.o \ + totl.o \ trans.o \ triggers.o \ tseq.o \ volopt.o \ + wkic.o \ xattr.o # diff --git a/kmod/src/attr_x.c b/kmod/src/attr_x.c new file mode 100644 index 00000000..39e84a36 --- /dev/null +++ b/kmod/src/attr_x.c @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2024 Versity Software, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License v2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + */ +#include +#include + +#include "format.h" +#include "super.h" +#include "inode.h" +#include "ioctl.h" +#include "lock.h" +#include "trans.h" +#include "attr_x.h" + +static int validate_attr_x_input(struct super_block *sb, struct scoutfs_ioctl_inode_attr_x *iax) +{ + int ret; + + if ((iax->x_mask & SCOUTFS_IOC_IAX__UNKNOWN) || + (iax->x_flags & SCOUTFS_IOC_IAX_F__UNKNOWN)) + return -EINVAL; + + if ((iax->x_mask & SCOUTFS_IOC_IAX_RETENTION) && + (ret = scoutfs_fmt_vers_unsupported(sb, SCOUTFS_FORMAT_VERSION_FEAT_RETENTION))) + return ret; + + if ((iax->x_mask & SCOUTFS_IOC_IAX_PROJECT_ID) && + (ret = scoutfs_fmt_vers_unsupported(sb, SCOUTFS_FORMAT_VERSION_FEAT_PROJECT_ID))) + return ret; + + return 0; +} + +/* + * If the mask indicates interest in the given attr then set the field + * to the caller's value and return the new size if it didn't already + * include the attr field. + */ +#define fill_attr(size, iax, bit, field, val) \ +({ \ + __typeof__(iax) _iax = (iax); \ + __typeof__(size) _size = (size); \ + \ + if (_iax->x_mask & (bit)) { \ + _iax->field = (val); \ + _size = max(_size, offsetof(struct scoutfs_ioctl_inode_attr_x, field) + \ + sizeof_field(struct scoutfs_ioctl_inode_attr_x, field)); \ + } \ + \ + _size; \ +}) + +/* + * Returns -errno on error, or >= number of bytes filled by the + * response. 0 can be returned if no attributes are requested in the + * input x_mask. + */ +int scoutfs_get_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *iax) +{ + struct super_block *sb = inode->i_sb; + struct scoutfs_inode_info *si = SCOUTFS_I(inode); + struct scoutfs_lock *lock = NULL; + size_t size = 0; + u64 offline; + u64 online; + u64 bits; + int ret; + + if (iax->x_mask == 0) { + ret = 0; + goto out; + } + + ret = validate_attr_x_input(sb, iax); + if (ret < 0) + goto out; + + inode_lock(inode); + + ret = scoutfs_lock_inode(sb, SCOUTFS_LOCK_READ, SCOUTFS_LKF_REFRESH_INODE, inode, &lock); + if (ret) + goto unlock; + + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_META_SEQ, + meta_seq, scoutfs_inode_meta_seq(inode)); + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_DATA_SEQ, + data_seq, scoutfs_inode_data_seq(inode)); + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_DATA_VERSION, + data_version, scoutfs_inode_data_version(inode)); + if (iax->x_mask & (SCOUTFS_IOC_IAX_ONLINE_BLOCKS | SCOUTFS_IOC_IAX_OFFLINE_BLOCKS)) { + scoutfs_inode_get_onoff(inode, &online, &offline); + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_ONLINE_BLOCKS, + online_blocks, online); + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_OFFLINE_BLOCKS, + offline_blocks, offline); + } + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_CTIME, ctime_sec, inode->i_ctime.tv_sec); + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_CTIME, ctime_nsec, inode->i_ctime.tv_nsec); + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_CRTIME, crtime_sec, si->crtime.tv_sec); + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_CRTIME, crtime_nsec, si->crtime.tv_nsec); + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_SIZE, size, i_size_read(inode)); + if (iax->x_mask & SCOUTFS_IOC_IAX__BITS) { + bits = 0; + if ((iax->x_mask & SCOUTFS_IOC_IAX_RETENTION) && + (scoutfs_inode_get_flags(inode) & SCOUTFS_INO_FLAG_RETENTION)) + bits |= SCOUTFS_IOC_IAX_B_RETENTION; + size = fill_attr(size, iax, SCOUTFS_IOC_IAX__BITS, bits, bits); + } + size = fill_attr(size, iax, SCOUTFS_IOC_IAX_PROJECT_ID, + project_id, scoutfs_inode_get_proj(inode)); + + ret = size; +unlock: + scoutfs_unlock(sb, lock, SCOUTFS_LOCK_READ); + inode_unlock(inode); +out: + return ret; +} + +static bool valid_attr_changes(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *iax) +{ + /* provided data_version must be non-zero */ + if ((iax->x_mask & SCOUTFS_IOC_IAX_DATA_VERSION) && (iax->data_version == 0)) + return false; + + /* can only set size or data version in new regular files */ + if (((iax->x_mask & SCOUTFS_IOC_IAX_SIZE) || + (iax->x_mask & SCOUTFS_IOC_IAX_DATA_VERSION)) && + (!S_ISREG(inode->i_mode) || scoutfs_inode_data_version(inode) != 0)) + return false; + + /* must provide non-zero data_version with non-zero size */ + if (((iax->x_mask & SCOUTFS_IOC_IAX_SIZE) && (iax->size > 0)) && + (!(iax->x_mask & SCOUTFS_IOC_IAX_DATA_VERSION) || (iax->data_version == 0))) + return false; + + /* must provide non-zero size when setting offline extents to that size */ + if ((iax->x_flags & SCOUTFS_IOC_IAX_F_SIZE_OFFLINE) && + (!(iax->x_mask & SCOUTFS_IOC_IAX_SIZE) || (iax->size == 0))) + return false; + + /* the retention bit only applies to regular files */ + if ((iax->x_mask & SCOUTFS_IOC_IAX_RETENTION) && !S_ISREG(inode->i_mode)) + return false; + + return true; +} + +int scoutfs_set_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *iax) +{ + struct super_block *sb = inode->i_sb; + struct scoutfs_inode_info *si = SCOUTFS_I(inode); + struct scoutfs_lock *lock = NULL; + LIST_HEAD(ind_locks); + bool set_data_seq; + int ret; + + /* initially all setting is root only, could loosen with finer grained checks */ + if (!capable(CAP_SYS_ADMIN)) { + ret = -EPERM; + goto out; + } + + if (iax->x_mask == 0) { + ret = 0; + goto out; + } + + ret = validate_attr_x_input(sb, iax); + if (ret < 0) + goto out; + + inode_lock(inode); + + ret = scoutfs_lock_inode(sb, SCOUTFS_LOCK_WRITE, SCOUTFS_LKF_REFRESH_INODE, inode, &lock); + if (ret) + goto unlock; + + /* check for errors before making any changes */ + if (!valid_attr_changes(inode, iax)) { + ret = -EINVAL; + goto unlock; + } + + /* retention prevents modification unless also clearing retention */ + ret = scoutfs_inode_check_retention(inode); + if (ret < 0 && !((iax->x_mask & SCOUTFS_IOC_IAX_RETENTION) && + !(iax->bits & SCOUTFS_IOC_IAX_B_RETENTION))) + goto unlock; + + /* setting only so we don't see 0 data seq with nonzero data_version */ + if ((iax->x_mask & SCOUTFS_IOC_IAX_DATA_VERSION) && (iax->data_version > 0)) + set_data_seq = true; + else + set_data_seq = false; + + ret = scoutfs_inode_index_lock_hold(inode, &ind_locks, set_data_seq, true); + if (ret) + goto unlock; + + ret = scoutfs_dirty_inode_item(inode, lock); + if (ret < 0) + goto release; + + /* creating offline extent first, it might fail */ + if (iax->x_flags & SCOUTFS_IOC_IAX_F_SIZE_OFFLINE) { + ret = scoutfs_data_init_offline_extent(inode, iax->size, lock); + if (ret) + goto release; + } + + /* make all changes once they're all checked and will succeed */ + if (iax->x_mask & SCOUTFS_IOC_IAX_DATA_VERSION) + scoutfs_inode_set_data_version(inode, iax->data_version); + if (iax->x_mask & SCOUTFS_IOC_IAX_SIZE) + i_size_write(inode, iax->size); + if (iax->x_mask & SCOUTFS_IOC_IAX_CTIME) { + inode->i_ctime.tv_sec = iax->ctime_sec; + inode->i_ctime.tv_nsec = iax->ctime_nsec; + } + if (iax->x_mask & SCOUTFS_IOC_IAX_CRTIME) { + si->crtime.tv_sec = iax->crtime_sec; + si->crtime.tv_nsec = iax->crtime_nsec; + } + if (iax->x_mask & SCOUTFS_IOC_IAX_RETENTION) { + scoutfs_inode_set_flags(inode, ~SCOUTFS_INO_FLAG_RETENTION, + (iax->bits & SCOUTFS_IOC_IAX_B_RETENTION) ? + SCOUTFS_INO_FLAG_RETENTION : 0); + } + if (iax->x_mask & SCOUTFS_IOC_IAX_PROJECT_ID) + scoutfs_inode_set_proj(inode, iax->project_id); + + scoutfs_update_inode_item(inode, lock, &ind_locks); + ret = 0; +release: + scoutfs_release_trans(sb); +unlock: + scoutfs_inode_index_unlock(sb, &ind_locks); + scoutfs_unlock(sb, lock, SCOUTFS_LOCK_WRITE); + inode_unlock(inode); +out: + return ret; +} diff --git a/kmod/src/attr_x.h b/kmod/src/attr_x.h new file mode 100644 index 00000000..2aaf8f98 --- /dev/null +++ b/kmod/src/attr_x.h @@ -0,0 +1,11 @@ +#ifndef _SCOUTFS_ATTR_X_H_ +#define _SCOUTFS_ATTR_X_H_ + +#include +#include +#include "ioctl.h" + +int scoutfs_get_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *iax); +int scoutfs_set_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *iax); + +#endif diff --git a/kmod/src/counters.h b/kmod/src/counters.h index 42deed1d..72521b24 100644 --- a/kmod/src/counters.h +++ b/kmod/src/counters.h @@ -199,10 +199,7 @@ EXPAND_COUNTER(srch_read_stale) \ EXPAND_COUNTER(statfs) \ EXPAND_COUNTER(totl_read_copied) \ - EXPAND_COUNTER(totl_read_finalized) \ - EXPAND_COUNTER(totl_read_fs) \ EXPAND_COUNTER(totl_read_item) \ - EXPAND_COUNTER(totl_read_logged) \ EXPAND_COUNTER(trans_commit_data_alloc_low) \ EXPAND_COUNTER(trans_commit_dirty_meta_full) \ EXPAND_COUNTER(trans_commit_fsync) \ diff --git a/kmod/src/data.c b/kmod/src/data.c index 54a1857b..dd10a06f 100644 --- a/kmod/src/data.c +++ b/kmod/src/data.c @@ -586,6 +586,12 @@ static int scoutfs_get_block(struct inode *inode, sector_t iblock, goto out; } + if (create && !si->staging) { + ret = scoutfs_inode_check_retention(inode); + if (ret < 0) + goto out; + } + /* convert unwritten to written, could be staging */ if (create && ext.map && (ext.flags & SEF_UNWRITTEN)) { un.start = iblock; @@ -1104,6 +1110,10 @@ long scoutfs_fallocate(struct file *file, int mode, loff_t offset, loff_t len) while(iblock <= last) { + ret = scoutfs_quota_check_data(sb, inode); + if (ret) + goto out_extent; + ret = scoutfs_inode_index_lock_hold(inode, &ind_locks, false, true); if (ret) goto out_extent; @@ -1155,9 +1165,9 @@ out: * on regular files with no data extents. It's used to restore a file * with an offline extent which can then trigger staging. * - * The caller has taken care of locking the inode. We're updating the - * inode offline count as we create the offline extent so we take care - * of the index locking, updating, and transaction. + * The caller must take care of cluster locking, transactions, inode + * updates, and index updates (so that they can atomically make this + * change along with other metadata changes). */ int scoutfs_data_init_offline_extent(struct inode *inode, u64 size, struct scoutfs_lock *lock) @@ -1171,7 +1181,6 @@ int scoutfs_data_init_offline_extent(struct inode *inode, u64 size, .lock = lock, }; const u64 count = DIV_ROUND_UP(size, SCOUTFS_BLOCK_SM_SIZE); - LIST_HEAD(ind_locks); u64 on; u64 off; int ret; @@ -1184,28 +1193,10 @@ int scoutfs_data_init_offline_extent(struct inode *inode, u64 size, goto out; } - /* we're updating meta_seq with offline block count */ - ret = scoutfs_inode_index_lock_hold(inode, &ind_locks, false, true); - if (ret < 0) - goto out; - - ret = scoutfs_dirty_inode_item(inode, lock); - if (ret < 0) - goto unlock; - down_write(&si->extent_sem); ret = scoutfs_ext_insert(sb, &data_ext_ops, &args, 0, count, 0, SEF_OFFLINE); up_write(&si->extent_sem); - if (ret < 0) - goto unlock; - - scoutfs_update_inode_item(inode, lock, &ind_locks); - -unlock: - scoutfs_release_trans(sb); - scoutfs_inode_index_unlock(sb, &ind_locks); - ret = 0; out: return ret; } @@ -1273,6 +1264,9 @@ int scoutfs_data_move_blocks(struct inode *from, u64 from_off, if (ret) goto out; + if (!is_stage && (ret = scoutfs_inode_check_retention(to))) + goto out; + if ((from_off & SCOUTFS_BLOCK_SM_MASK) || (to_off & SCOUTFS_BLOCK_SM_MASK) || ((byte_len & SCOUTFS_BLOCK_SM_MASK) && diff --git a/kmod/src/dir.c b/kmod/src/dir.c index 4227457f..ca23d3a6 100644 --- a/kmod/src/dir.c +++ b/kmod/src/dir.c @@ -34,6 +34,7 @@ #include "forest.h" #include "acl.h" #include "counters.h" +#include "quota.h" #include "scoutfs_trace.h" /* @@ -651,6 +652,10 @@ static struct inode *lock_hold_create(struct inode *dir, struct dentry *dentry, if (ret) goto out_unlock; + ret = scoutfs_quota_check_inode(sb, dir); + if (ret) + goto out_unlock; + if (orph_lock) { ret = scoutfs_lock_orphan(sb, SCOUTFS_LOCK_WRITE_ONLY, 0, ino, orph_lock); if (ret < 0) @@ -672,6 +677,8 @@ retry: if (ret < 0) goto out; + scoutfs_inode_set_proj(inode, scoutfs_inode_get_proj(dir)); + ret = scoutfs_dirty_inode_item(dir, *dir_lock); out: if (ret) @@ -926,6 +933,10 @@ static int scoutfs_unlink(struct inode *dir, struct dentry *dentry) goto unlock; } + ret = scoutfs_inode_check_retention(inode); + if (ret < 0) + goto unlock; + hash = dirent_name_hash(dentry->d_name.name, dentry->d_name.len); ret = lookup_dirent(sb, scoutfs_ino(dir), dentry->d_name.name, dentry->d_name.len, hash, @@ -1632,6 +1643,10 @@ static int scoutfs_rename_common(struct inode *old_dir, goto out_unlock; } + if ((old_inode && (ret = scoutfs_inode_check_retention(old_inode))) || + (new_inode && (ret = scoutfs_inode_check_retention(new_inode)))) + goto out_unlock; + if (should_orphan(new_inode)) { ret = scoutfs_lock_orphan(sb, SCOUTFS_LOCK_WRITE_ONLY, 0, scoutfs_ino(new_inode), &orph_lock); diff --git a/kmod/src/file.c b/kmod/src/file.c index 55c8b230..d44cd822 100644 --- a/kmod/src/file.c +++ b/kmod/src/file.c @@ -28,6 +28,7 @@ #include "inode.h" #include "per_task.h" #include "omap.h" +#include "quota.h" #ifdef KC_LINUX_HAVE_FOP_AIO_READ /* @@ -108,6 +109,10 @@ retry: if (ret) goto out; + ret = scoutfs_inode_check_retention(inode); + if (ret < 0) + goto out; + ret = scoutfs_complete_truncate(inode, scoutfs_inode_lock); if (ret) goto out; @@ -122,6 +127,10 @@ retry: goto out; } + ret = scoutfs_quota_check_data(sb, inode); + if (ret) + goto out; + /* XXX: remove SUID bit */ ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos); @@ -216,10 +225,18 @@ retry: if (ret <= 0) goto out; + ret = scoutfs_inode_check_retention(inode); + if (ret < 0) + goto out; + ret = scoutfs_complete_truncate(inode, scoutfs_inode_lock); if (ret) goto out; + ret = scoutfs_quota_check_data(sb, inode); + if (ret) + goto out; + if (scoutfs_per_task_add_excl(&si->pt_data_lock, &pt_ent, scoutfs_inode_lock)) { /* data_version is per inode, whole file must be online */ ret = scoutfs_data_wait_check(inode, 0, i_size_read(inode), SEF_OFFLINE, diff --git a/kmod/src/forest.c b/kmod/src/forest.c index f08724d9..0e5c31d3 100644 --- a/kmod/src/forest.c +++ b/kmod/src/forest.c @@ -238,19 +238,16 @@ static int forest_read_items(struct super_block *sb, struct scoutfs_key *key, u6 * We return -ESTALE if we hit stale blocks to give the caller a chance * to reset their state and retry with a newer version of the btrees. */ -int scoutfs_forest_read_items(struct super_block *sb, - struct scoutfs_key *key, - struct scoutfs_key *bloom_key, - struct scoutfs_key *start, - struct scoutfs_key *end, - scoutfs_forest_item_cb cb, void *arg) +int scoutfs_forest_read_items_roots(struct super_block *sb, struct scoutfs_net_roots *roots, + struct scoutfs_key *key, struct scoutfs_key *bloom_key, + struct scoutfs_key *start, struct scoutfs_key *end, + scoutfs_forest_item_cb cb, void *arg) { struct forest_read_items_data rid = { .cb = cb, .cb_arg = arg, }; struct scoutfs_log_trees lt; - struct scoutfs_net_roots roots; struct scoutfs_bloom_block *bb; struct forest_bloom_nrs bloom; SCOUTFS_BTREE_ITEM_REF(iref); @@ -264,18 +261,14 @@ int scoutfs_forest_read_items(struct super_block *sb, scoutfs_inc_counter(sb, forest_read_items); calc_bloom_nrs(&bloom, bloom_key); - ret = scoutfs_client_get_roots(sb, &roots); - if (ret) - goto out; - - trace_scoutfs_forest_using_roots(sb, &roots.fs_root, &roots.logs_root); + trace_scoutfs_forest_using_roots(sb, &roots->fs_root, &roots->logs_root); *start = orig_start; *end = orig_end; /* start with fs root items */ rid.fic |= FIC_FS_ROOT; - ret = scoutfs_btree_read_items(sb, &roots.fs_root, key, start, end, + ret = scoutfs_btree_read_items(sb, &roots->fs_root, key, start, end, forest_read_items, &rid); if (ret < 0) goto out; @@ -283,7 +276,7 @@ int scoutfs_forest_read_items(struct super_block *sb, scoutfs_key_init_log_trees(<k, 0, 0); for (;; scoutfs_key_inc(<k)) { - ret = scoutfs_btree_next(sb, &roots.logs_root, <k, &iref); + ret = scoutfs_btree_next(sb, &roots->logs_root, <k, &iref); if (ret == 0) { if (iref.val_len == sizeof(lt)) { ltk = *iref.key; @@ -340,6 +333,23 @@ out: return ret; } +int scoutfs_forest_read_items(struct super_block *sb, + struct scoutfs_key *key, + struct scoutfs_key *bloom_key, + struct scoutfs_key *start, + struct scoutfs_key *end, + scoutfs_forest_item_cb cb, void *arg) +{ + struct scoutfs_net_roots roots; + int ret; + + ret = scoutfs_client_get_roots(sb, &roots); + if (ret == 0) + ret = scoutfs_forest_read_items_roots(sb, &roots, key, bloom_key, start, end, + cb, arg); + return ret; +} + /* * If the items are deltas then combine the src with the destination * value and store the result in the destination. diff --git a/kmod/src/forest.h b/kmod/src/forest.h index 24753e89..151affb8 100644 --- a/kmod/src/forest.h +++ b/kmod/src/forest.h @@ -24,6 +24,10 @@ int scoutfs_forest_read_items(struct super_block *sb, struct scoutfs_key *start, struct scoutfs_key *end, scoutfs_forest_item_cb cb, void *arg); +int scoutfs_forest_read_items_roots(struct super_block *sb, struct scoutfs_net_roots *roots, + struct scoutfs_key *key, struct scoutfs_key *bloom_key, + struct scoutfs_key *start, struct scoutfs_key *end, + scoutfs_forest_item_cb cb, void *arg); int scoutfs_forest_set_bloom_bits(struct super_block *sb, struct scoutfs_lock *lock); void scoutfs_forest_set_max_seq(struct super_block *sb, u64 max_seq); diff --git a/kmod/src/format.h b/kmod/src/format.h index da1683dd..83e6015c 100644 --- a/kmod/src/format.h +++ b/kmod/src/format.h @@ -8,9 +8,14 @@ */ #define SCOUTFS_FORMAT_VERSION_MIN 1 #define SCOUTFS_FORMAT_VERSION_MIN_STR __stringify(SCOUTFS_FORMAT_VERSION_MIN) -#define SCOUTFS_FORMAT_VERSION_MAX 1 +#define SCOUTFS_FORMAT_VERSION_MAX 2 #define SCOUTFS_FORMAT_VERSION_MAX_STR __stringify(SCOUTFS_FORMAT_VERSION_MAX) +#define SCOUTFS_FORMAT_VERSION_FEAT_RETENTION 2 +#define SCOUTFS_FORMAT_VERSION_FEAT_PROJECT_ID 2 +#define SCOUTFS_FORMAT_VERSION_FEAT_QUOTA 2 +#define SCOUTFS_FORMAT_VERSION_FEAT_INDX_TAG 2 + /* statfs(2) f_type */ #define SCOUTFS_SUPER_MAGIC 0x554f4353 /* "SCOU" */ @@ -175,6 +180,10 @@ struct scoutfs_key { #define sko_rid _sk_first #define sko_ino _sk_second +/* quota rules */ +#define skqr_hash _sk_second +#define skqr_coll_nr _sk_third + /* xattr totl */ #define skxt_a _sk_first #define skxt_b _sk_second @@ -585,7 +594,9 @@ struct scoutfs_log_merge_freeing { */ #define SCOUTFS_INODE_INDEX_ZONE 4 #define SCOUTFS_ORPHAN_ZONE 8 +#define SCOUTFS_QUOTA_ZONE 10 #define SCOUTFS_XATTR_TOTL_ZONE 12 +#define SCOUTFS_XATTR_INDX_ZONE 14 #define SCOUTFS_FS_ZONE 16 #define SCOUTFS_LOCK_ZONE 20 /* Items only stored in server btrees */ @@ -608,6 +619,9 @@ struct scoutfs_log_merge_freeing { /* orphan zone, redundant type used for clarity */ #define SCOUTFS_ORPHAN_TYPE 4 +/* quota zone */ +#define SCOUTFS_QUOTA_RULE_TYPE 4 + /* fs zone */ #define SCOUTFS_INODE_TYPE 4 #define SCOUTFS_XATTR_TYPE 8 @@ -661,6 +675,34 @@ struct scoutfs_xattr_totl_val { __le64 count; }; +#define SQ_RF_TOTL_COUNT (1 << 0) +#define SQ_RF__UNKNOWN (~((1 << 1) - 1)) + +#define SQ_NS_LITERAL 0 +#define SQ_NS_PROJ 1 +#define SQ_NS_UID 2 +#define SQ_NS_GID 3 +#define SQ_NS__NR 4 +#define SQ_NS__NR_SELECT (SQ_NS__NR - 1) /* !literal */ + +#define SQ_NF_SELECT (1 << 0) +#define SQ_NF__UNKNOWN (~((1 << 1) - 1)) + +#define SQ_OP_INODE 0 +#define SQ_OP_DATA 1 +#define SQ_OP__NR 2 + +struct scoutfs_quota_rule_val { + __le64 name_val[3]; + __le64 limit; + __u8 prio; + __u8 op; + __u8 rule_flags; + __u8 name_source[3]; + __u8 name_flags[3]; + __u8 _pad[7]; +}; + /* XXX does this exist upstream somewhere? */ #define member_sizeof(TYPE, MEMBER) (sizeof(((TYPE *)0)->MEMBER)) @@ -859,9 +901,38 @@ struct scoutfs_inode { struct scoutfs_timespec ctime; struct scoutfs_timespec mtime; struct scoutfs_timespec crtime; + __le64 proj; }; -#define SCOUTFS_INO_FLAG_TRUNCATE 0x1 +#define SCOUTFS_INODE_FMT_V1_BYTES offsetof(struct scoutfs_inode, proj) + +/* + * There are so few versions that we don't mind doing this work inline + * so that both utils and kernel can share these. Mounting has already + * checked that the format version is within the supported min and max, + * so these functions only deal with size variance within that band. + */ +/* Returns the native written inode size for the given format version, 0 for bad version */ +static inline int scoutfs_inode_vers_bytes(__u64 fmt_vers) +{ + if (fmt_vers == 1) + return SCOUTFS_INODE_FMT_V1_BYTES; + else + return sizeof(struct scoutfs_inode); +} +/* + * Returns true if bytes is a valid inode size to read from the given + * version. The given version must be greater than the version that + * introduced the size. + */ +static inline int scoutfs_inode_valid_vers_bytes(__u64 fmt_vers, int bytes) +{ + return (bytes == sizeof(struct scoutfs_inode) && fmt_vers == SCOUTFS_FORMAT_VERSION_MAX) || + (bytes == SCOUTFS_INODE_FMT_V1_BYTES); +} + +#define SCOUTFS_INO_FLAG_TRUNCATE 0x1 +#define SCOUTFS_INO_FLAG_RETENTION 0x2 #define SCOUTFS_ROOT_INO 1 diff --git a/kmod/src/inode.c b/kmod/src/inode.c index db76f798..2203a8e9 100644 --- a/kmod/src/inode.c +++ b/kmod/src/inode.c @@ -250,7 +250,7 @@ static void set_item_info(struct scoutfs_inode_info *si, set_item_major(si, SCOUTFS_INODE_INDEX_DATA_SEQ_TYPE, sinode->data_seq); } -static void load_inode(struct inode *inode, struct scoutfs_inode *cinode) +static void load_inode(struct inode *inode, struct scoutfs_inode *cinode, int inode_bytes) { struct scoutfs_inode_info *si = SCOUTFS_I(inode); @@ -278,6 +278,7 @@ static void load_inode(struct inode *inode, struct scoutfs_inode *cinode) si->flags = le32_to_cpu(cinode->flags); si->crtime.tv_sec = le64_to_cpu(cinode->crtime.sec); si->crtime.tv_nsec = le32_to_cpu(cinode->crtime.nsec); + si->proj = le64_to_cpu(cinode->proj); /* * i_blocks is initialized from online and offline and is then @@ -298,6 +299,24 @@ void scoutfs_inode_init_key(struct scoutfs_key *key, u64 ino) }; } +/* + * Read an inode item into the caller's buffer and return the size that + * we read. Returns errors if the inode size is unsupported or doesn't + * make sense for the format version. + */ +static int lookup_inode_item(struct super_block *sb, struct scoutfs_key *key, + struct scoutfs_inode *sinode, struct scoutfs_lock *lock) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + int ret; + + ret = scoutfs_item_lookup_smaller_zero(sb, key, sinode, sizeof(struct scoutfs_inode), lock); + if (ret >= 0 && !scoutfs_inode_valid_vers_bytes(sbi->fmt_vers, ret)) + return -EIO; + + return ret; +} + /* * Refresh the vfs inode fields if the lock indicates that the current * contents could be stale. @@ -333,12 +352,12 @@ int scoutfs_inode_refresh(struct inode *inode, struct scoutfs_lock *lock) mutex_lock(&si->item_mutex); if (atomic64_read(&si->last_refreshed) < refresh_gen) { - ret = scoutfs_item_lookup_exact(sb, &key, &sinode, - sizeof(sinode), lock); - if (ret == 0) { - load_inode(inode, &sinode); + ret = lookup_inode_item(sb, &key, &sinode, lock); + if (ret > 0) { + load_inode(inode, &sinode, ret); atomic64_set(&si->last_refreshed, refresh_gen); scoutfs_lock_add_coverage(sb, lock, &si->ino_lock_cov); + ret = 0; } } else { ret = 0; @@ -486,6 +505,10 @@ retry: if (ret) goto out; + ret = scoutfs_inode_check_retention(inode); + if (ret < 0) + goto out; + attr_size = (attr->ia_valid & ATTR_SIZE) ? attr->ia_size : i_size_read(inode); @@ -686,6 +709,55 @@ void scoutfs_inode_get_onoff(struct inode *inode, s64 *on, s64 *off) } while (read_seqretry(&si->seqlock, seq)); } +/* + * Get our private scoutfs inode flags, not the vfs i_flags. + */ +u32 scoutfs_inode_get_flags(struct inode *inode) +{ + struct scoutfs_inode_info *si = SCOUTFS_I(inode); + unsigned seq; + u32 flags; + + do { + seq = read_seqbegin(&si->seqlock); + flags = si->flags; + } while (read_seqretry(&si->seqlock, seq)); + + return flags; +} + +void scoutfs_inode_set_flags(struct inode *inode, u32 and, u32 or) +{ + struct scoutfs_inode_info *si = SCOUTFS_I(inode); + + write_seqlock(&si->seqlock); + si->flags = (si->flags & and) | or; + write_sequnlock(&si->seqlock); +} + +u64 scoutfs_inode_get_proj(struct inode *inode) +{ + struct scoutfs_inode_info *si = SCOUTFS_I(inode); + unsigned seq; + u64 proj; + + do { + seq = read_seqbegin(&si->seqlock); + proj = si->proj; + } while (read_seqretry(&si->seqlock, seq)); + + return proj; +} + +void scoutfs_inode_set_proj(struct inode *inode, u64 proj) +{ + struct scoutfs_inode_info *si = SCOUTFS_I(inode); + + write_seqlock(&si->seqlock); + si->proj = proj; + write_sequnlock(&si->seqlock); +} + static int scoutfs_iget_test(struct inode *inode, void *arg) { struct scoutfs_inode_info *si = SCOUTFS_I(inode); @@ -795,7 +867,7 @@ out: return inode; } -static void store_inode(struct scoutfs_inode *cinode, struct inode *inode) +static void store_inode(struct scoutfs_inode *cinode, struct inode *inode, int inode_bytes) { struct scoutfs_inode_info *si = SCOUTFS_I(inode); u64 online_blocks; @@ -831,6 +903,7 @@ static void store_inode(struct scoutfs_inode *cinode, struct inode *inode) cinode->crtime.sec = cpu_to_le64(si->crtime.tv_sec); cinode->crtime.nsec = cpu_to_le32(si->crtime.tv_nsec); memset(cinode->crtime.__pad, 0, sizeof(cinode->crtime.__pad)); + cinode->proj = cpu_to_le64(si->proj); } /* @@ -854,15 +927,18 @@ static void store_inode(struct scoutfs_inode *cinode, struct inode *inode) int scoutfs_dirty_inode_item(struct inode *inode, struct scoutfs_lock *lock) { struct super_block *sb = inode->i_sb; + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); struct scoutfs_inode sinode; struct scoutfs_key key; + int inode_bytes; int ret; - store_inode(&sinode, inode); + inode_bytes = scoutfs_inode_vers_bytes(sbi->fmt_vers); + store_inode(&sinode, inode, inode_bytes); scoutfs_inode_init_key(&key, scoutfs_ino(inode)); - ret = scoutfs_item_update(sb, &key, &sinode, sizeof(sinode), lock); + ret = scoutfs_item_update(sb, &key, &sinode, inode_bytes, lock); if (!ret) trace_scoutfs_dirty_inode(inode); return ret; @@ -1064,9 +1140,11 @@ void scoutfs_update_inode_item(struct inode *inode, struct scoutfs_lock *lock, { struct scoutfs_inode_info *si = SCOUTFS_I(inode); struct super_block *sb = inode->i_sb; + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); const u64 ino = scoutfs_ino(inode); - struct scoutfs_key key; struct scoutfs_inode sinode; + struct scoutfs_key key; + int inode_bytes; int ret; int err; @@ -1075,15 +1153,17 @@ void scoutfs_update_inode_item(struct inode *inode, struct scoutfs_lock *lock, /* set the meta version once per trans for any inode updates */ scoutfs_inode_set_meta_seq(inode); + inode_bytes = scoutfs_inode_vers_bytes(sbi->fmt_vers); + /* only race with other inode field stores once */ - store_inode(&sinode, inode); + store_inode(&sinode, inode, inode_bytes); ret = update_indices(sb, si, ino, inode->i_mode, &sinode, lock_list, lock); BUG_ON(ret); scoutfs_inode_init_key(&key, ino); - err = scoutfs_item_update(sb, &key, &sinode, sizeof(sinode), lock); + err = scoutfs_item_update(sb, &key, &sinode, inode_bytes, lock); if (err) { scoutfs_err(sb, "inode %llu update err %d", ino, err); BUG_ON(err); @@ -1451,10 +1531,12 @@ out: int scoutfs_new_inode(struct super_block *sb, struct inode *dir, umode_t mode, dev_t rdev, u64 ino, struct scoutfs_lock *lock, struct inode **inode_ret) { + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); struct scoutfs_inode_info *si; - struct scoutfs_key key; struct scoutfs_inode sinode; + struct scoutfs_key key; struct inode *inode; + int inode_bytes; int ret; inode = new_inode(sb); @@ -1470,6 +1552,7 @@ int scoutfs_new_inode(struct super_block *sb, struct inode *dir, umode_t mode, d si->offline_blocks = 0; si->next_readdir_pos = SCOUTFS_DIRENT_FIRST_POS; si->next_xattr_id = 0; + si->proj = 0; si->have_item = false; atomic64_set(&si->last_refreshed, lock->refresh_gen); scoutfs_lock_add_coverage(sb, lock, &si->ino_lock_cov); @@ -1485,14 +1568,16 @@ int scoutfs_new_inode(struct super_block *sb, struct inode *dir, umode_t mode, d inode->i_rdev = rdev; set_inode_ops(inode); - store_inode(&sinode, inode); + inode_bytes = scoutfs_inode_vers_bytes(sbi->fmt_vers); + + store_inode(&sinode, inode, inode_bytes); scoutfs_inode_init_key(&key, scoutfs_ino(inode)); ret = scoutfs_omap_set(sb, ino); if (ret < 0) goto out; - ret = scoutfs_item_create(sb, &key, &sinode, sizeof(sinode), lock); + ret = scoutfs_item_create(sb, &key, &sinode, inode_bytes, lock); if (ret < 0) scoutfs_omap_clear(sb, ino); out: @@ -1746,7 +1831,7 @@ static int try_delete_inode_items(struct super_block *sb, u64 ino) } scoutfs_inode_init_key(&key, ino); - ret = scoutfs_item_lookup_exact(sb, &key, &sinode, sizeof(sinode), lock); + ret = lookup_inode_item(sb, &key, &sinode, lock); if (ret < 0) { if (ret == -ENOENT) ret = 0; @@ -2135,6 +2220,17 @@ out: return ret; } +/* + * Return an error if the inode has the retention flag set and can not + * be modified. This mimics the errno returned by the vfs whan an + * inode's immutable flag is set. The flag won't be set on older format + * versions so we don't check the mounted format version here. + */ +int scoutfs_inode_check_retention(struct inode *inode) +{ + return (scoutfs_inode_get_flags(inode) & SCOUTFS_INO_FLAG_RETENTION) ? -EPERM : 0; +} + int scoutfs_inode_setup(struct super_block *sb) { struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); diff --git a/kmod/src/inode.h b/kmod/src/inode.h index dd8e33f3..e068cee5 100644 --- a/kmod/src/inode.h +++ b/kmod/src/inode.h @@ -21,6 +21,7 @@ struct scoutfs_inode_info { u64 data_version; u64 online_blocks; u64 offline_blocks; + u64 proj; u32 flags; struct kc_timespec crtime; @@ -120,8 +121,15 @@ u64 scoutfs_inode_meta_seq(struct inode *inode); u64 scoutfs_inode_data_seq(struct inode *inode); u64 scoutfs_inode_data_version(struct inode *inode); void scoutfs_inode_get_onoff(struct inode *inode, s64 *on, s64 *off); +u32 scoutfs_inode_get_flags(struct inode *inode); +void scoutfs_inode_set_flags(struct inode *inode, u32 and, u32 or); +u64 scoutfs_inode_get_proj(struct inode *inode); +void scoutfs_inode_set_proj(struct inode *inode, u64 proj); + int scoutfs_complete_truncate(struct inode *inode, struct scoutfs_lock *lock); +int scoutfs_inode_check_retention(struct inode *inode); + int scoutfs_inode_refresh(struct inode *inode, struct scoutfs_lock *lock); #ifdef KC_LINUX_HAVE_RHEL_IOPS_WRAPPER int scoutfs_getattr(struct vfsmount *mnt, struct dentry *dentry, diff --git a/kmod/src/ioctl.c b/kmod/src/ioctl.c index 6bf70403..cbf442bd 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -42,6 +42,10 @@ #include "alloc.h" #include "server.h" #include "counters.h" +#include "attr_x.h" +#include "totl.h" +#include "wkic.h" +#include "quota.h" #include "scoutfs_trace.h" /* @@ -545,20 +549,41 @@ out: static long scoutfs_ioc_stat_more(struct file *file, unsigned long arg) { struct inode *inode = file_inode(file); - struct scoutfs_inode_info *si = SCOUTFS_I(inode); - struct scoutfs_ioctl_stat_more stm; + struct scoutfs_ioctl_inode_attr_x *iax = NULL; + struct scoutfs_ioctl_stat_more *stm = NULL; + int ret; - stm.meta_seq = scoutfs_inode_meta_seq(inode); - stm.data_seq = scoutfs_inode_data_seq(inode); - stm.data_version = scoutfs_inode_data_version(inode); - scoutfs_inode_get_onoff(inode, &stm.online_blocks, &stm.offline_blocks); - stm.crtime_sec = si->crtime.tv_sec; - stm.crtime_nsec = si->crtime.tv_nsec; + iax = kmalloc(sizeof(struct scoutfs_ioctl_inode_attr_x), GFP_KERNEL); + stm = kmalloc(sizeof(struct scoutfs_ioctl_stat_more), GFP_KERNEL); + if (!iax || !stm) { + ret = -ENOMEM; + goto out; + } - if (copy_to_user((void __user *)arg, &stm, sizeof(stm))) - return -EFAULT; + iax->x_mask = SCOUTFS_IOC_IAX_META_SEQ | SCOUTFS_IOC_IAX_DATA_SEQ | + SCOUTFS_IOC_IAX_DATA_VERSION | SCOUTFS_IOC_IAX_ONLINE_BLOCKS | + SCOUTFS_IOC_IAX_OFFLINE_BLOCKS | SCOUTFS_IOC_IAX_CRTIME; + iax->x_flags = 0; + ret = scoutfs_get_attr_x(inode, iax); + if (ret < 0) + goto out; - return 0; + stm->meta_seq = iax->meta_seq; + stm->data_seq = iax->data_seq; + stm->data_version = iax->data_version; + stm->online_blocks = iax->online_blocks; + stm->offline_blocks = iax->offline_blocks; + stm->crtime_sec = iax->crtime_sec; + stm->crtime_nsec = iax->crtime_nsec; + + if (copy_to_user((void __user *)arg, stm, sizeof(struct scoutfs_ioctl_stat_more))) + ret = -EFAULT; + else + ret = 0; +out: + kfree(iax); + kfree(stm); + return ret; } static bool inc_wrapped(u64 *ino, u64 *iblock) @@ -615,24 +640,19 @@ static long scoutfs_ioc_data_waiting(struct file *file, unsigned long arg) * This is used when restoring files, it lets the caller set all the * inode attributes which are otherwise unreachable. Changing the file * size can only be done for regular files with a data_version of 0. + * + * We unconditionally fill the iax attributes from the sm set and let + * set_attr_x check them. */ static long scoutfs_ioc_setattr_more(struct file *file, unsigned long arg) { - struct inode *inode = file->f_inode; - struct scoutfs_inode_info *si = SCOUTFS_I(inode); - struct super_block *sb = inode->i_sb; + struct inode *inode = file_inode(file); struct scoutfs_ioctl_setattr_more __user *usm = (void __user *)arg; + struct scoutfs_ioctl_inode_attr_x *iax = NULL; struct scoutfs_ioctl_setattr_more sm; - struct scoutfs_lock *lock = NULL; LIST_HEAD(ind_locks); - bool set_data_seq; int ret; - if (!capable(CAP_SYS_ADMIN)) { - ret = -EPERM; - goto out; - } - if (!(file->f_mode & FMODE_WRITE)) { ret = -EBADF; goto out; @@ -643,65 +663,38 @@ static long scoutfs_ioc_setattr_more(struct file *file, unsigned long arg) goto out; } - if ((sm.i_size > 0 && sm.data_version == 0) || - ((sm.flags & SCOUTFS_IOC_SETATTR_MORE_OFFLINE) && !sm.i_size) || - (sm.flags & SCOUTFS_IOC_SETATTR_MORE_UNKNOWN)) { + if (sm.flags & SCOUTFS_IOC_SETATTR_MORE_UNKNOWN) { ret = -EINVAL; goto out; } + iax = kzalloc(sizeof(struct scoutfs_ioctl_inode_attr_x), GFP_KERNEL); + if (!iax) { + ret = -ENOMEM; + goto out; + } + + iax->x_mask = SCOUTFS_IOC_IAX_DATA_VERSION | SCOUTFS_IOC_IAX_CTIME | + SCOUTFS_IOC_IAX_CRTIME | SCOUTFS_IOC_IAX_SIZE; + iax->data_version = sm.data_version; + iax->ctime_sec = sm.ctime_sec; + iax->ctime_nsec = sm.ctime_nsec; + iax->crtime_sec = sm.crtime_sec; + iax->crtime_nsec = sm.crtime_nsec; + iax->size = sm.i_size; + + if (sm.flags & SCOUTFS_IOC_SETATTR_MORE_OFFLINE) + iax->x_flags |= SCOUTFS_IOC_IAX_F_SIZE_OFFLINE; + ret = mnt_want_write_file(file); - if (ret) + if (ret < 0) goto out; - inode_lock(inode); + ret = scoutfs_set_attr_x(inode, iax); - ret = scoutfs_lock_inode(sb, SCOUTFS_LOCK_WRITE, - SCOUTFS_LKF_REFRESH_INODE, inode, &lock); - if (ret) - goto unlock; - - /* can only change size/dv on untouched regular files */ - if ((sm.i_size != 0 || sm.data_version != 0) && - ((!S_ISREG(inode->i_mode) || - scoutfs_inode_data_version(inode) != 0))) { - ret = -EINVAL; - goto unlock; - } - - /* create offline extents in potentially many transactions */ - if (sm.flags & SCOUTFS_IOC_SETATTR_MORE_OFFLINE) { - ret = scoutfs_data_init_offline_extent(inode, sm.i_size, lock); - if (ret) - goto unlock; - } - - /* setting only so we don't see 0 data seq with nonzero data_version */ - set_data_seq = sm.data_version != 0 ? true : false; - ret = scoutfs_inode_index_lock_hold(inode, &ind_locks, set_data_seq, false); - if (ret) - goto unlock; - - if (sm.data_version) - scoutfs_inode_set_data_version(inode, sm.data_version); - if (sm.i_size) - i_size_write(inode, sm.i_size); - inode->i_ctime.tv_sec = sm.ctime_sec; - inode->i_ctime.tv_nsec = sm.ctime_nsec; - si->crtime.tv_sec = sm.crtime_sec; - si->crtime.tv_nsec = sm.crtime_nsec; - - scoutfs_update_inode_item(inode, lock, &ind_locks); - ret = 0; - - scoutfs_release_trans(sb); -unlock: - scoutfs_inode_index_unlock(sb, &ind_locks); - scoutfs_unlock(sb, lock, SCOUTFS_LOCK_WRITE); - inode_unlock(inode); mnt_drop_write_file(file); out: - + kfree(iax); return ret; } @@ -1035,124 +1028,32 @@ out: return ret; } -struct xattr_total_entry { - struct rb_node node; - struct scoutfs_ioctl_xattr_total xt; - u64 fs_seq; - u64 fs_total; - u64 fs_count; - u64 fin_seq; - u64 fin_total; - s64 fin_count; - u64 log_seq; - u64 log_total; - s64 log_count; +struct read_xattr_total_iter_cb_args { + struct scoutfs_ioctl_xattr_total *xt; + unsigned int copied; + unsigned int total; }; -static int cmp_xt_entry_name(const struct xattr_total_entry *a, - const struct xattr_total_entry *b) - -{ - return scoutfs_cmp_u64s(a->xt.name[0], b->xt.name[0]) ?: - scoutfs_cmp_u64s(a->xt.name[1], b->xt.name[1]) ?: - scoutfs_cmp_u64s(a->xt.name[2], b->xt.name[2]); -} - /* - * Record the contribution of the three classes of logged items we can - * see: the item in the fs_root, items from finalized log btrees, and - * items from active log btrees. Once we have the full set the caller - * can decide which of the items contribute to the total it sends to the - * user. + * This is called under an RCU read lock so it can't copy to userspace. */ -static int read_xattr_total_item(struct super_block *sb, struct scoutfs_key *key, - u64 seq, u8 flags, void *val, int val_len, int fic, void *arg) +static int read_xattr_total_iter_cb(struct scoutfs_key *key, void *val, unsigned int val_len, + void *cb_arg) { + struct read_xattr_total_iter_cb_args *cba = cb_arg; struct scoutfs_xattr_totl_val *tval = val; - struct xattr_total_entry *ent; - struct xattr_total_entry rd; - struct rb_root *root = arg; - struct rb_node *parent; - struct rb_node **node; - int cmp; + struct scoutfs_ioctl_xattr_total *xt = &cba->xt[cba->copied]; - rd.xt.name[0] = le64_to_cpu(key->skxt_a); - rd.xt.name[1] = le64_to_cpu(key->skxt_b); - rd.xt.name[2] = le64_to_cpu(key->skxt_c); + xt->name[0] = le64_to_cpu(key->skxt_a); + xt->name[1] = le64_to_cpu(key->skxt_b); + xt->name[2] = le64_to_cpu(key->skxt_c); + xt->total = le64_to_cpu(tval->total); + xt->count = le64_to_cpu(tval->count); - /* find entry matching name */ - node = &root->rb_node; - parent = NULL; - cmp = -1; - while (*node) { - parent = *node; - ent = container_of(*node, struct xattr_total_entry, node); - - /* sort merge items by key then newest to oldest */ - cmp = cmp_xt_entry_name(&rd, ent); - if (cmp < 0) - node = &(*node)->rb_left; - else if (cmp > 0) - node = &(*node)->rb_right; - else - break; - } - - /* allocate and insert new node if we need to */ - if (cmp != 0) { - ent = kzalloc(sizeof(*ent), GFP_KERNEL); - if (!ent) - return -ENOMEM; - - memcpy(&ent->xt.name, &rd.xt.name, sizeof(ent->xt.name)); - - rb_link_node(&ent->node, parent, node); - rb_insert_color(&ent->node, root); - } - - if (fic & FIC_FS_ROOT) { - ent->fs_seq = seq; - ent->fs_total = le64_to_cpu(tval->total); - ent->fs_count = le64_to_cpu(tval->count); - } else if (fic & FIC_FINALIZED) { - ent->fin_seq = seq; - ent->fin_total += le64_to_cpu(tval->total); - ent->fin_count += le64_to_cpu(tval->count); - } else { - ent->log_seq = seq; - ent->log_total += le64_to_cpu(tval->total); - ent->log_count += le64_to_cpu(tval->count); - } - - scoutfs_inc_counter(sb, totl_read_item); - - return 0; -} - -/* these are always _safe, node stores next */ -#define for_each_xt_ent(ent, node, root) \ - for (node = rb_first(root); \ - node && (ent = rb_entry(node, struct xattr_total_entry, node), \ - node = rb_next(node), 1); ) - -#define for_each_xt_ent_reverse(ent, node, root) \ - for (node = rb_last(root); \ - node && (ent = rb_entry(node, struct xattr_total_entry, node), \ - node = rb_prev(node), 1); ) - -static void free_xt_ent(struct rb_root *root, struct xattr_total_entry *ent) -{ - rb_erase(&ent->node, root); - kfree(ent); -} - -static void free_all_xt_ents(struct rb_root *root) -{ - struct xattr_total_entry *ent; - struct rb_node *node; - - for_each_xt_ent(ent, node, root) - free_xt_ent(root, ent); + if (++cba->copied < cba->total) + return -EAGAIN; + else + return 0; } /* @@ -1162,30 +1063,6 @@ static void free_all_xt_ents(struct rb_root *root) * have been committed. It doesn't use locking to force commits and * block writers so it can be a little bit out of date with respect to * dirty xattrs in memory across the system. - * - * Our reader has to be careful because the log btree merging code can - * write partial results to the fs_root. This means that a reader can - * see both cases where new finalized logs should be applied to the old - * fs items and where old finalized logs have already been applied to - * the partially merged fs items. Currently active logged items are - * always applied on top of all cases. - * - * These cases are differentiated with a combination of sequence numbers - * in items, the count of contributing xattrs, and a flag - * differentiating finalized and active logged items. This lets us - * recognize all cases, including when finalized logs were merged and - * deleted the fs item. - * - * We're allocating a tracking struct for each totl name we see while - * traversing the item btrees. The forest reader is providing the items - * it finds in leaf blocks that contain the search key. In the worst - * case all of these blocks are full and none of the items overlap. At - * most, figure order a thousand names per mount. But in practice many - * of these factors fall away: leaf blocks aren't fill, leaf items - * overlap, there aren't finalized log btrees, and not all mounts are - * actively changing totals. We're much more likely to only read a - * leaf block's worth of totals that have been long since merged into - * the fs_root. */ static long scoutfs_ioc_read_xattr_totals(struct file *file, unsigned long arg) { @@ -1193,14 +1070,13 @@ static long scoutfs_ioc_read_xattr_totals(struct file *file, unsigned long arg) struct scoutfs_ioctl_read_xattr_totals __user *urxt = (void __user *)arg; struct scoutfs_ioctl_read_xattr_totals rxt; struct scoutfs_ioctl_xattr_total __user *uxt; - struct xattr_total_entry *ent; + struct read_xattr_total_iter_cb_args cba = {NULL, }; + struct scoutfs_key range_start; + struct scoutfs_key range_end; struct scoutfs_key key; - struct scoutfs_key bloom_key; - struct scoutfs_key start; - struct scoutfs_key end; - struct rb_root root = RB_ROOT; - struct rb_node *node; - int count = 0; + unsigned int copied = 0; + unsigned int total; + unsigned int ready; int ret; if (!(file->f_mode & FMODE_READ)) { @@ -1213,6 +1089,13 @@ static long scoutfs_ioc_read_xattr_totals(struct file *file, unsigned long arg) goto out; } + cba.xt = (void *)__get_free_page(GFP_KERNEL); + if (!cba.xt) { + ret = -ENOMEM; + goto out; + } + cba.total = PAGE_SIZE / sizeof(struct scoutfs_ioctl_xattr_total); + if (copy_from_user(&rxt, urxt, sizeof(rxt))) { ret = -EFAULT; goto out; @@ -1225,101 +1108,40 @@ static long scoutfs_ioc_read_xattr_totals(struct file *file, unsigned long arg) goto out; } - scoutfs_key_set_zeros(&bloom_key); - bloom_key.sk_zone = SCOUTFS_XATTR_TOTL_ZONE; - scoutfs_xattr_init_totl_key(&start, rxt.pos_name); + total = div_u64(min_t(u64, rxt.totals_bytes, INT_MAX), + sizeof(struct scoutfs_ioctl_xattr_total)); - while (rxt.totals_bytes >= sizeof(struct scoutfs_ioctl_xattr_total)) { + scoutfs_totl_set_range(&range_start, &range_end); + scoutfs_xattr_init_totl_key(&key, rxt.pos_name); - scoutfs_key_set_ones(&end); - end.sk_zone = SCOUTFS_XATTR_TOTL_ZONE; - if (scoutfs_key_compare(&start, &end) > 0) + while (copied < total) { + cba.copied = 0; + ret = scoutfs_wkic_iterate(sb, &key, &range_end, &range_start, &range_end, + read_xattr_total_iter_cb, &cba); + if (ret < 0) + goto out; + + if (cba.copied == 0) break; - key = start; - ret = scoutfs_forest_read_items(sb, &key, &bloom_key, &start, &end, - read_xattr_total_item, &root); - if (ret < 0) { - if (ret == -ESTALE) { - free_all_xt_ents(&root); - continue; - } + ready = min(total - copied, cba.copied); + + if (copy_to_user(&uxt[copied], cba.xt, ready * sizeof(cba.xt[0]))) { + ret = -EFAULT; goto out; } - if (RB_EMPTY_ROOT(&root)) - break; - - /* trim totals that fall outside of the consistent range */ - for_each_xt_ent(ent, node, &root) { - scoutfs_xattr_init_totl_key(&key, ent->xt.name); - if (scoutfs_key_compare(&key, &start) < 0) { - free_xt_ent(&root, ent); - } else { - break; - } - } - for_each_xt_ent_reverse(ent, node, &root) { - scoutfs_xattr_init_totl_key(&key, ent->xt.name); - if (scoutfs_key_compare(&key, &end) > 0) { - free_xt_ent(&root, ent); - } else { - break; - } - } - - /* copy resulting unique non-zero totals to userspace */ - for_each_xt_ent(ent, node, &root) { - if (rxt.totals_bytes < sizeof(ent->xt)) - break; - - /* start with the fs item if we have it */ - if (ent->fs_seq != 0) { - ent->xt.total = ent->fs_total; - ent->xt.count = ent->fs_count; - scoutfs_inc_counter(sb, totl_read_fs); - } - - /* apply finalized logs if they're newer or creating */ - if (((ent->fs_seq != 0) && (ent->fin_seq > ent->fs_seq)) || - ((ent->fs_seq == 0) && (ent->fin_count > 0))) { - ent->xt.total += ent->fin_total; - ent->xt.count += ent->fin_count; - scoutfs_inc_counter(sb, totl_read_finalized); - } - - /* always apply active logs which must be newer than fs and finalized */ - if (ent->log_seq > 0) { - ent->xt.total += ent->log_total; - ent->xt.count += ent->log_count; - scoutfs_inc_counter(sb, totl_read_logged); - } - - if (ent->xt.total != 0 || ent->xt.count != 0) { - if (copy_to_user(uxt, &ent->xt, sizeof(ent->xt))) { - ret = -EFAULT; - goto out; - } - - uxt++; - rxt.totals_bytes -= sizeof(ent->xt); - count++; - scoutfs_inc_counter(sb, totl_read_copied); - } - - free_xt_ent(&root, ent); - } - - /* continue after the last possible key read */ - start = end; - scoutfs_key_inc(&start); + scoutfs_xattr_init_totl_key(&key, cba.xt[ready - 1].name); + scoutfs_key_inc(&key); + copied += ready; } ret = 0; out: - free_all_xt_ents(&root); + if (cba.xt) + free_page((long)cba.xt); - return ret ?: count; + return ret ?: copied; } static long scoutfs_ioc_get_allocated_inos(struct file *file, unsigned long arg) @@ -1504,6 +1326,265 @@ out: return nr ?: ret; } +static long scoutfs_ioc_get_attr_x(struct file *file, unsigned long arg) +{ + struct inode *inode = file_inode(file); + struct scoutfs_ioctl_inode_attr_x __user *uiax = (void __user *)arg; + struct scoutfs_ioctl_inode_attr_x *iax = NULL; + int ret; + + iax = kmalloc(sizeof(struct scoutfs_ioctl_inode_attr_x), GFP_KERNEL); + if (!iax) { + ret = -ENOMEM; + goto out; + } + + ret = get_user(iax->x_mask, &uiax->x_mask) ?: + get_user(iax->x_flags, &uiax->x_flags); + if (ret < 0) + goto out; + + ret = scoutfs_get_attr_x(inode, iax); + if (ret < 0) + goto out; + + /* only copy results after dropping cluster locks (could fault) */ + if (ret > 0 && copy_to_user(uiax, iax, ret) != 0) + ret = -EFAULT; + else + ret = 0; +out: + kfree(iax); + return ret; +} + +static long scoutfs_ioc_set_attr_x(struct file *file, unsigned long arg) +{ + struct inode *inode = file_inode(file); + struct scoutfs_ioctl_inode_attr_x __user *uiax = (void __user *)arg; + struct scoutfs_ioctl_inode_attr_x *iax = NULL; + int ret; + + iax = kmalloc(sizeof(struct scoutfs_ioctl_inode_attr_x), GFP_KERNEL); + if (!iax) { + ret = -ENOMEM; + goto out; + } + + if (copy_from_user(iax, uiax, sizeof(struct scoutfs_ioctl_inode_attr_x))) { + ret = -EFAULT; + goto out; + } + + ret = mnt_want_write_file(file); + if (ret < 0) + goto out; + + ret = scoutfs_set_attr_x(inode, iax); + + mnt_drop_write_file(file); +out: + kfree(iax); + return ret; +} + +static long scoutfs_ioc_get_quota_rules(struct file *file, unsigned long arg) +{ + struct super_block *sb = file_inode(file)->i_sb; + struct scoutfs_ioctl_get_quota_rules __user *ugqr = (void __user *)arg; + struct scoutfs_ioctl_get_quota_rules gqr; + struct scoutfs_ioctl_quota_rule __user *uirules; + struct scoutfs_ioctl_quota_rule *irules; + struct page *page = NULL; + int copied = 0; + int nr; + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (copy_from_user(&gqr, ugqr, sizeof(gqr))) + return -EFAULT; + + if (gqr.rules_nr == 0) + return 0; + + uirules = (void __user *)gqr.rules_ptr; + /* limit rules copied per call */ + gqr.rules_nr = min_t(u64, gqr.rules_nr, INT_MAX); + + page = alloc_page(GFP_KERNEL | __GFP_ZERO); + if (!page) { + ret = -ENOMEM; + goto out; + } + irules = page_address(page); + + while (copied < gqr.rules_nr) { + nr = min_t(u64, gqr.rules_nr - copied, + PAGE_SIZE / sizeof(struct scoutfs_ioctl_quota_rule)); + ret = scoutfs_quota_get_rules(sb, gqr.iterator, page_address(page), nr); + if (ret <= 0) + goto out; + + if (copy_to_user(&uirules[copied], irules, ret * sizeof(irules[0]))) { + ret = -EFAULT; + goto out; + } + + copied += ret; + } + + ret = 0; +out: + if (page) + __free_page(page); + + if (ret == 0 && copy_to_user(ugqr->iterator, gqr.iterator, sizeof(gqr.iterator))) + ret = -EFAULT; + + return ret ?: copied; +} + +static long scoutfs_ioc_mod_quota_rule(struct file *file, unsigned long arg, bool is_add) +{ + struct super_block *sb = file_inode(file)->i_sb; + struct scoutfs_ioctl_quota_rule __user *uirule = (void __user *)arg; + struct scoutfs_ioctl_quota_rule irule; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (copy_from_user(&irule, uirule, sizeof(irule))) + return -EFAULT; + + return scoutfs_quota_mod_rule(sb, is_add, &irule); +} + +struct read_index_buf { + int nr; + int size; + struct scoutfs_ioctl_xattr_index_entry ents[0]; +}; + +#define READ_INDEX_BUF_MAX_ENTS \ + ((PAGE_SIZE - sizeof(struct read_index_buf)) / \ + sizeof(struct scoutfs_ioctl_xattr_index_entry)) + +/* + * This doesn't filter out duplicates, the caller filters them out to + * catch duplicates between iteration calls. + */ +static int read_index_cb(struct scoutfs_key *key, void *val, unsigned int val_len, void *cb_arg) +{ + struct read_index_buf *rib = cb_arg; + struct scoutfs_ioctl_xattr_index_entry *ent = &rib->ents[rib->nr]; + u64 xid; + + if (val_len != 0) + return -EIO; + + /* discard the xid, they're not exposed to ioctl callers */ + scoutfs_xattr_get_indx_key(key, &ent->major, &ent->minor, &ent->ino, &xid); + + if (++rib->nr == rib->size) + return rib->nr; + + return -EAGAIN; +} + +static long scoutfs_ioc_read_xattr_index(struct file *file, unsigned long arg) +{ + struct super_block *sb = file_inode(file)->i_sb; + struct scoutfs_ioctl_read_xattr_index __user *urxi = (void __user *)arg; + struct scoutfs_ioctl_xattr_index_entry __user *uents; + struct scoutfs_ioctl_xattr_index_entry *ent; + struct scoutfs_ioctl_xattr_index_entry prev; + struct scoutfs_ioctl_read_xattr_index rxi; + struct read_index_buf *rib; + struct page *page = NULL; + struct scoutfs_key first; + struct scoutfs_key last; + struct scoutfs_key start; + struct scoutfs_key end; + int copied = 0; + int ret; + int i; + + if (!capable(CAP_SYS_ADMIN)) { + ret = -EPERM; + goto out; + } + + if (copy_from_user(&rxi, urxi, sizeof(rxi))) { + ret = -EFAULT; + goto out; + } + uents = (void __user *)rxi.entries_ptr; + rxi.entries_nr = min_t(u64, rxi.entries_nr, INT_MAX); + + page = alloc_page(GFP_KERNEL); + if (!page) { + ret = -ENOMEM; + goto out; + } + rib = page_address(page); + + scoutfs_xattr_init_indx_key(&first, rxi.first.major, rxi.first.minor, rxi.first.ino, 0); + scoutfs_xattr_init_indx_key(&last, rxi.last.major, rxi.last.minor, rxi.last.ino, U64_MAX); + scoutfs_xattr_indx_get_range(&start, &end); + + if (scoutfs_key_compare(&first, &last) > 0) { + ret = -EINVAL; + goto out; + } + + /* 0 ino doesn't exist, can't ever match entry to return */ + memset(&prev, 0, sizeof(prev)); + + while (copied < rxi.entries_nr) { + rib->nr = 0; + rib->size = min_t(u64, rxi.entries_nr - copied, READ_INDEX_BUF_MAX_ENTS); + ret = scoutfs_wkic_iterate(sb, &first, &last, &start, &end, + read_index_cb, rib); + if (ret < 0) + goto out; + if (rib->nr == 0) + break; + + /* + * Copy entries to userspace, skipping duplicate entries + * that can result from multiple xattrs indexing an + * inode at the same position and which can span + * multiple cache iterations. (Comparing in order of + * most likely to change to fail fast.) + */ + for (i = 0, ent = rib->ents; i < rib->nr; i++, ent++) { + if (ent->ino == prev.ino && ent->minor == prev.minor && + ent->major == prev.major) + continue; + + if (copy_to_user(&uents[copied], ent, sizeof(*ent))) { + ret = -EFAULT; + goto out; + } + + prev = *ent; + copied++; + } + + scoutfs_xattr_init_indx_key(&first, prev.major, prev.minor, prev.ino, U64_MAX); + scoutfs_key_inc(&first); + } + + ret = copied; +out: + if (page) + __free_page(page); + + return ret; +} + long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { @@ -1541,6 +1622,18 @@ long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg) return scoutfs_ioc_get_allocated_inos(file, arg); case SCOUTFS_IOC_GET_REFERRING_ENTRIES: return scoutfs_ioc_get_referring_entries(file, arg); + case SCOUTFS_IOC_GET_ATTR_X: + return scoutfs_ioc_get_attr_x(file, arg); + case SCOUTFS_IOC_SET_ATTR_X: + return scoutfs_ioc_set_attr_x(file, arg); + case SCOUTFS_IOC_GET_QUOTA_RULES: + return scoutfs_ioc_get_quota_rules(file, arg); + case SCOUTFS_IOC_ADD_QUOTA_RULE: + return scoutfs_ioc_mod_quota_rule(file, arg, true); + case SCOUTFS_IOC_DEL_QUOTA_RULE: + return scoutfs_ioc_mod_quota_rule(file, arg, false); + case SCOUTFS_IOC_READ_XATTR_INDEX: + return scoutfs_ioc_read_xattr_index(file, arg); } return -ENOTTY; diff --git a/kmod/src/ioctl.h b/kmod/src/ioctl.h index 7be22dbb..9f79839e 100644 --- a/kmod/src/ioctl.h +++ b/kmod/src/ioctl.h @@ -673,4 +673,174 @@ struct scoutfs_ioctl_dirent { #define SCOUTFS_IOC_GET_REFERRING_ENTRIES \ _IOW(SCOUTFS_IOCTL_MAGIC, 17, struct scoutfs_ioctl_get_referring_entries) +struct scoutfs_ioctl_inode_attr_x { + __u64 x_mask; + __u64 x_flags; + __u64 meta_seq; + __u64 data_seq; + __u64 data_version; + __u64 online_blocks; + __u64 offline_blocks; + __u64 ctime_sec; + __u32 ctime_nsec; + __u32 crtime_nsec; + __u64 crtime_sec; + __u64 size; + __u64 bits; + __u64 project_id; +}; + +/* + * Behavioral flags set in the x_flags field. These flags don't + * necessarily correspond to specific attributes, but instead change the + * behaviour of a _get_ or _set_ operation. + * + * @SCOUTFS_IOC_IAX_F_SIZE_OFFLINE: When setting i_size, also create + * extents which are marked offline for the region of the file from + * offset 0 to the new set size. This can only be set when setting the + * size and has no effect if setting the size fails. + */ +#define SCOUTFS_IOC_IAX_F_SIZE_OFFLINE (1ULL << 0) +#define SCOUTFS_IOC_IAX_F__UNKNOWN (U64_MAX << 1) + +/* + * Single-bit values stored in the @bits field. These indicate whether + * the bit is set, or not. The main _IAX_ bits set in the mask indicate + * whether this value bit is populated by _get or stored by _set. + */ +#define SCOUTFS_IOC_IAX_B_RETENTION (1ULL << 0) + +/* + * x_mask bits which indicate which attributes of the inode to populate + * on return for _get or to set on the inode for _set. Each mask bit + * corresponds to the matching named field in the attr_x struct passed + * to the _get_ and _set_ calls. + * + * Each field can have different permissions or other attribute + * requirements which can cause calls to fail. If _set_ fails then no + * other attribute changes will have been made by the same call. + * + * @SCOUTFS_IOC_IAX_RETENTION: Mark a file for retention. When marked, + * no modification can be made to the file other than changing extended + * attributes outside the "user." prefix and clearing the retention + * mark. This can only be set on regular files and requires root (the + * CAP_SYS_ADMIN capability). Other attributes can be set with a + * set_attr_x call on a retention inode as long as that call also + * successfully clears the retention mark. + */ +#define SCOUTFS_IOC_IAX_META_SEQ (1ULL << 0) +#define SCOUTFS_IOC_IAX_DATA_SEQ (1ULL << 1) +#define SCOUTFS_IOC_IAX_DATA_VERSION (1ULL << 2) +#define SCOUTFS_IOC_IAX_ONLINE_BLOCKS (1ULL << 3) +#define SCOUTFS_IOC_IAX_OFFLINE_BLOCKS (1ULL << 4) +#define SCOUTFS_IOC_IAX_CTIME (1ULL << 5) +#define SCOUTFS_IOC_IAX_CRTIME (1ULL << 6) +#define SCOUTFS_IOC_IAX_SIZE (1ULL << 7) +#define SCOUTFS_IOC_IAX_RETENTION (1ULL << 8) +#define SCOUTFS_IOC_IAX_PROJECT_ID (1ULL << 9) + +/* single bit attributes that are packed in the bits field as _B_ */ +#define SCOUTFS_IOC_IAX__BITS (SCOUTFS_IOC_IAX_RETENTION) +/* inverse of all the bits we understand */ +#define SCOUTFS_IOC_IAX__UNKNOWN (U64_MAX << 10) + +#define SCOUTFS_IOC_GET_ATTR_X \ + _IOW(SCOUTFS_IOCTL_MAGIC, 18, struct scoutfs_ioctl_inode_attr_x) + +#define SCOUTFS_IOC_SET_ATTR_X \ + _IOW(SCOUTFS_IOCTL_MAGIC, 19, struct scoutfs_ioctl_inode_attr_x) + +/* + * (These fields are documented in the order that they're displayed by + * the scoutfs cli utility which matches the sort order of the rules.) + * + * @prio: The priority of the rule. Rules are sorted by their fields + * with prio at the highest magnitude. When multiple rules match the + * rule with the highest sort order is enforced. The priority field + * lets rules override the default field sort order. + * + * @name_val[3]: The three 64bit values that make up the name of the + * totl xattr whose total will be checked against the rule's limit to + * see if the quota rule has been exceeded. The behavior of the values + * can be changed by their corresponding name_source and name_flags. + * + * @name_source[3]: The SQ_NS_ enums that control where the value comes + * from. _LITERAL uses the value from name_val. Inode attribute + * sources (_PROJ, _UID, _GID) are taken from the inode of the operation + * that is being checked against the rule. + * + * @name_flags[3]: The SQ_NF_ enums that alter the name values. _SELECT + * makes the rule only match if the inode attribute of the operation + * matches the attribute value stored in name_val. This lets rules + * match a specific value of an attribute rather than mapping all + * attribute values of to totl names. + * + * @op: The SQ_OP_ enums which specify the operation that can't exceed + * the rule's limit. _INODE checks inode creation and the inode + * attributes are taken from the inode that would be created. _DATA + * checks file data block allocation and the inode fields come from the + * inode that is allocating the blocks. + * + * @limit: The 64bit value that is checked against the totl value + * described by the rule. If the totl value is greater than or equal to + * this value of the matching rule then the operation will return + * -EDQUOT. + * + * @rule_flags: SQ_RF_TOTL_COUNT indicates that the rule's limit should + * be checked against the number of xattrs contributing to a totl value + * instead of the sum of the xattrs. + */ +struct scoutfs_ioctl_quota_rule { + __u64 name_val[3]; + __u64 limit; + __u8 prio; + __u8 op; + __u8 rule_flags; + __u8 name_source[3]; + __u8 name_flags[3]; + __u8 _pad[7]; +}; + +struct scoutfs_ioctl_get_quota_rules { + __u64 iterator[2]; + __u64 rules_ptr; + __u64 rules_nr; +}; + +/* + * Rules are uniquely identified by their non-padded fields. Addition will fail + * with -EEXIST if the specified rule already exists and deletion must find a rule + * with all matching fields to delete. + */ +#define SCOUTFS_IOC_GET_QUOTA_RULES \ + _IOR(SCOUTFS_IOCTL_MAGIC, 20, struct scoutfs_ioctl_get_quota_rules) +#define SCOUTFS_IOC_ADD_QUOTA_RULE \ + _IOW(SCOUTFS_IOCTL_MAGIC, 21, struct scoutfs_ioctl_quota_rule) +#define SCOUTFS_IOC_DEL_QUOTA_RULE \ + _IOW(SCOUTFS_IOCTL_MAGIC, 22, struct scoutfs_ioctl_quota_rule) + +/* + * Inodes can be indexed in a global key space at a position determined + * by a .indx. tagged xattr. The xattr name specifies the two index + * position values, with major having the more significant comparison + * order. + */ +struct scoutfs_ioctl_xattr_index_entry { + __u64 minor; + __u64 ino; + __u8 major; + __u8 _pad[7]; +}; + +struct scoutfs_ioctl_read_xattr_index { + __u64 flags; + struct scoutfs_ioctl_xattr_index_entry first; + struct scoutfs_ioctl_xattr_index_entry last; + __u64 entries_ptr; + __u64 entries_nr; +}; + +#define SCOUTFS_IOC_READ_XATTR_INDEX \ + _IOR(SCOUTFS_IOCTL_MAGIC, 23, struct scoutfs_ioctl_read_xattr_index) + #endif diff --git a/kmod/src/item.c b/kmod/src/item.c index a99202cf..9ec5aad0 100644 --- a/kmod/src/item.c +++ b/kmod/src/item.c @@ -1720,8 +1720,8 @@ static int copy_val(void *dst, int dst_len, void *src, int src_len) * The amount of bytes copied is returned which can be 0 or truncated if * the caller's buffer isn't big enough. */ -int scoutfs_item_lookup(struct super_block *sb, struct scoutfs_key *key, - void *val, int val_len, struct scoutfs_lock *lock) +static int item_lookup(struct super_block *sb, struct scoutfs_key *key, + void *val, int val_len, int len_limit, struct scoutfs_lock *lock) { DECLARE_ITEM_CACHE_INFO(sb, cinf); struct cached_item *item; @@ -1741,6 +1741,8 @@ int scoutfs_item_lookup(struct super_block *sb, struct scoutfs_key *key, item = item_rbtree_walk(&pg->item_root, key, NULL, NULL, NULL); if (!item || item->deletion) ret = -ENOENT; + else if (len_limit > 0 && item->val_len > len_limit) + ret = -EIO; else ret = copy_val(val, val_len, item->val, item->val_len); @@ -1749,13 +1751,38 @@ out: return ret; } +int scoutfs_item_lookup(struct super_block *sb, struct scoutfs_key *key, + void *val, int val_len, struct scoutfs_lock *lock) +{ + return item_lookup(sb, key, val, val_len, 0, lock); +} + +/* + * Copy an item's value into the caller's buffer. If the item's value + * is larger than the caller's buffer then -EIO is returned. If the + * item is smaller then the bytes from the end of the copied value to + * the end of the buffer are zeroed. The number of value bytes copied + * is returned, and 0 can be returned for an item with no value. + */ +int scoutfs_item_lookup_smaller_zero(struct super_block *sb, struct scoutfs_key *key, + void *val, int val_len, struct scoutfs_lock *lock) +{ + int ret; + + ret = item_lookup(sb, key, val, val_len, val_len, lock); + if (ret >= 0 && ret < val_len) + memset(val + ret, 0, val_len - ret); + + return ret; +} + int scoutfs_item_lookup_exact(struct super_block *sb, struct scoutfs_key *key, void *val, int val_len, struct scoutfs_lock *lock) { int ret; - ret = scoutfs_item_lookup(sb, key, val, val_len, lock); + ret = item_lookup(sb, key, val, val_len, 0, lock); if (ret == val_len) ret = 0; else if (ret >= 0) diff --git a/kmod/src/item.h b/kmod/src/item.h index d7fe6a2e..abadc90a 100644 --- a/kmod/src/item.h +++ b/kmod/src/item.h @@ -3,6 +3,8 @@ int scoutfs_item_lookup(struct super_block *sb, struct scoutfs_key *key, void *val, int val_len, struct scoutfs_lock *lock); +int scoutfs_item_lookup_smaller_zero(struct super_block *sb, struct scoutfs_key *key, + void *val, int val_len, struct scoutfs_lock *lock); int scoutfs_item_lookup_exact(struct super_block *sb, struct scoutfs_key *key, void *val, int val_len, struct scoutfs_lock *lock); diff --git a/kmod/src/lock.c b/kmod/src/lock.c index db4c384f..a6ae9a6e 100644 --- a/kmod/src/lock.c +++ b/kmod/src/lock.c @@ -36,6 +36,8 @@ #include "item.h" #include "omap.h" #include "util.h" +#include "totl.h" +#include "quota.h" /* * scoutfs uses a lock service to manage item cache consistency between @@ -185,6 +187,9 @@ static int lock_invalidate(struct super_block *sb, struct scoutfs_lock *lock, return ret; } + if (lock->start.sk_zone == SCOUTFS_QUOTA_ZONE && !lock_mode_can_read(mode)) + scoutfs_quota_invalidate(sb); + /* have to invalidate if we're not in the only usable case */ if (!(prev == SCOUTFS_LOCK_WRITE && mode == SCOUTFS_LOCK_READ)) { retry: @@ -1244,10 +1249,29 @@ int scoutfs_lock_xattr_totl(struct super_block *sb, enum scoutfs_lock_mode mode, struct scoutfs_key start; struct scoutfs_key end; - scoutfs_key_set_zeros(&start); - start.sk_zone = SCOUTFS_XATTR_TOTL_ZONE; - scoutfs_key_set_ones(&end); - end.sk_zone = SCOUTFS_XATTR_TOTL_ZONE; + scoutfs_totl_set_range(&start, &end); + + return lock_key_range(sb, mode, flags, &start, &end, lock); +} + +int scoutfs_lock_xattr_indx(struct super_block *sb, enum scoutfs_lock_mode mode, int flags, + struct scoutfs_lock **lock) +{ + struct scoutfs_key start; + struct scoutfs_key end; + + scoutfs_xattr_indx_get_range(&start, &end); + + return lock_key_range(sb, mode, flags, &start, &end, lock); +} + +int scoutfs_lock_quota(struct super_block *sb, enum scoutfs_lock_mode mode, int flags, + struct scoutfs_lock **lock) +{ + struct scoutfs_key start; + struct scoutfs_key end; + + scoutfs_quota_get_lock_range(&start, &end); return lock_key_range(sb, mode, flags, &start, &end, lock); } diff --git a/kmod/src/lock.h b/kmod/src/lock.h index 4ba50bcd..07908d62 100644 --- a/kmod/src/lock.h +++ b/kmod/src/lock.h @@ -86,6 +86,10 @@ int scoutfs_lock_orphan(struct super_block *sb, enum scoutfs_lock_mode mode, int u64 ino, struct scoutfs_lock **lock); int scoutfs_lock_xattr_totl(struct super_block *sb, enum scoutfs_lock_mode mode, int flags, struct scoutfs_lock **lock); +int scoutfs_lock_xattr_indx(struct super_block *sb, enum scoutfs_lock_mode mode, int flags, + struct scoutfs_lock **lock); +int scoutfs_lock_quota(struct super_block *sb, enum scoutfs_lock_mode mode, int flags, + struct scoutfs_lock **lock); void scoutfs_unlock(struct super_block *sb, struct scoutfs_lock *lock, enum scoutfs_lock_mode mode); diff --git a/kmod/src/quota.c b/kmod/src/quota.c new file mode 100644 index 00000000..c3e3839b --- /dev/null +++ b/kmod/src/quota.c @@ -0,0 +1,1261 @@ +/* + * Copyright (C) 2023 Versity Software, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License v2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "format.h" +#include "super.h" +#include "lock.h" +#include "hash.h" +#include "inode.h" +#include "item.h" +#include "ioctl.h" +#include "cmp.h" +#include "wkic.h" +#include "xattr.h" +#include "totl.h" +#include "util.h" +#include "quota.h" +#include "scoutfs_trace.h" + +/* + * scoutfs quotas let userspace manage accounting and rules which + * specify when operations should fail because a quota is exceeded. + * + * Userspace is responsible for managing the .totl. xattrs that + * accumulate counts and totals that can be checked to enforce quotas. + * Userspace then builds quota rules that map operations to totl names + * and limits. This puts userspace entirely in control of the quota + * policy. + * + * The quota checks are specifically allowed to use slightly stale data + * to avoid global locking bottlenecks. + * + * Rules are stored as items in the main fs btree and are subject strict + * consistency cluster locking. After any change to rules all the rules + * will be read in again and processed for checking. + * + * The .totl. xattrs are not read under cluster locking to avoid lock + * contention. They're read using the weak item cache which expires + * only on a timeout. This leads to a regular background load of weak + * reads of the item totls as they're updated at the frequency of the + * cache expiration. + */ + +#define CACHE_AGE_MS (5 * MSEC_PER_SEC) + +/* + * Rules are stored in trees whose nodes are keyed by their input + * matching criteria. The trees are not modified once they're visible + * to readers. RCU is used to free the trees once all the readers have + * finished. + */ +struct squota_ruleset { + struct rcu_head rcu; + struct rb_root roots[SQ_NS__NR_SELECT]; + struct squota_rule *defaults[SQ_OP__NR]; +}; + +struct squota_info { + struct super_block *sb; + struct squota_ruleset __rcu *ruleset; /* ENOENT, EINVAL, EBUSY, or valid ptr */ + struct rhashtable check_ht; + atomic64_t nr_checks; + + struct rw_semaphore rwsem; + spinlock_t lock; + wait_queue_head_t waitq; + KC_DEFINE_SHRINKER(shrinker); + struct dentry *drop_dentry; +}; + +#define DECLARE_QUOTA_INFO(sb, name) \ + struct squota_info *name = SCOUTFS_SB(sb)->squota_info + +static inline int quota_unsupported(struct super_block *sb) +{ + return scoutfs_fmt_vers_unsupported(sb, SCOUTFS_FORMAT_VERSION_FEAT_QUOTA); +} + +struct squota_check { + struct rcu_head rcu; + struct rhash_head head; + struct squota_input inp; + ktime_t expiration; + int result; +}; + +static const struct rhashtable_params check_ht_params = { + .key_len = member_sizeof(struct squota_check, inp), + .key_offset = offsetof(struct squota_check, inp), + .head_offset = offsetof(struct squota_check, head), +}; + +static bool get_cached_check(struct squota_info *qtinf, struct squota_input *inp, int *result) +{ + struct squota_check *chk; + bool got; + + if (WARN_ON_ONCE(!rcu_read_lock_held())) + return false; + + chk = rhashtable_lookup(&qtinf->check_ht, inp, check_ht_params); + if (chk && ktime_after(chk->expiration, ktime_get_raw())) { + *result = chk->result; + got = true; + } else { + *result = 0; + got = false; + } + + return got; +} + +/* + * Insert a new cached check. If a cached check already exists its + * either timed out or was inserted very recently so either can be used. + * We abandon the insertion attempt on other errors, including + * allocation failures and insertion failure from a pending hash table + * resize. + */ +static void insert_cached_check(struct squota_info *qtinf, struct squota_input *inp, int result) +{ + struct squota_check *found; + struct squota_check *chk; + int ret; + + /* zero full size for hash table memcmp */ + chk = kzalloc(sizeof(struct squota_check), GFP_NOFS); + if (!chk) + return; + + chk->inp = *inp; + chk->expiration = ktime_add_ms(ktime_get_raw(), CACHE_AGE_MS); + chk->result = result; + + while (chk) { + ret = rhashtable_lookup_insert_fast(&qtinf->check_ht, &chk->head, + check_ht_params); + if (ret == 0) { + atomic64_inc(&qtinf->nr_checks); + chk = NULL; + + } else if (ret == -EEXIST) { + /* try to free older insertion or existing */ + rcu_read_lock(); + found = rhashtable_lookup(&qtinf->check_ht, inp, check_ht_params); + if (found) { + if (ktime_before(found->expiration, chk->expiration)) { + if (rhashtable_remove_fast(&qtinf->check_ht, + &found->head, + check_ht_params) == 0) { + kfree_rcu(found, rcu); + atomic64_dec(&qtinf->nr_checks); + } + } else { + kfree(chk); + chk = NULL; + } + } + rcu_read_unlock(); + + } else { + kfree(chk); + chk = NULL; + } + } +} + +/* + * Return a random cached check from the hash table. We sweep the + * buckets from a random starting point and return the first we find, + * continuing from the next table if it's resizing. This is sort of + * like the _walk_ api but we can set the starting point and it doesn't + * return -EAGAIN while resizing. + */ +static struct squota_check *lookup_random_check(struct rhashtable *rht) +{ + struct bucket_table *tbl; + struct squota_check *chk; + struct rhash_head *pos; + unsigned long s; + unsigned long i; + + WARN_ON_ONCE(!rcu_read_lock_held()); + + tbl = rht_dereference_rcu(rht->tbl, rht); + do { + for (s = 0, i = prandom_u32_max(tbl->size); + s < tbl->size; + s++, i = (i + 1) % tbl->size) { + rht_for_each_entry_rcu(chk, pos, tbl, i, head) { + return chk; + } + } + } while (!IS_ERR_OR_NULL((tbl = rht_dereference_rcu(tbl->future_tbl, rht)))); + + return NULL; +} + +static unsigned long count_cached_checks(struct shrinker *shrink, struct shrink_control *sc) +{ + struct squota_info *qtinf = KC_SHRINKER_CONTAINER_OF(shrink, struct squota_info); + + return shrinker_min_long(atomic64_read(&qtinf->nr_checks)); +} + +/* + * We don't bother with any precise replacement mechanism. We choose + * cached check results to drop at random. If the cache is large then + * random choices are unlikely to have been used again. If the cache is + * small then any choices end up blowing away most of the cache. + */ +static unsigned long scan_cached_checks(struct shrinker *shrink, struct shrink_control *sc) +{ + struct squota_info *qtinf = KC_SHRINKER_CONTAINER_OF(shrink, struct squota_info); + unsigned long nr = sc->nr_to_scan; + unsigned int retries = 10; + unsigned long freed = 0; + struct squota_check *chk; + int err; + + rcu_read_lock(); + + while (nr > 0 && retries > 0 && (chk = lookup_random_check(&qtinf->check_ht))) { + err = rhashtable_remove_fast(&qtinf->check_ht, &chk->head, check_ht_params); + if (err) { + retries--; + continue; + } + + kfree_rcu(chk, rcu); + atomic64_dec(&qtinf->nr_checks); + freed++; + nr--; + } + + rcu_read_unlock(); + + if (retries == 0 && freed == 0) + freed = SHRINK_STOP; + + return freed; +} + +static void shrink_all_cached_checks(struct squota_info *qtinf) +{ + struct shrink_control sc = { .nr_to_scan = LONG_MAX, }; + + scan_cached_checks(KC_SHRINKER_FN(&qtinf->shrinker), &sc); +} + +static u8 ns_is_attr(u8 ns) +{ + switch (ns) { + case SQ_NS_PROJ: + case SQ_NS_UID: + case SQ_NS_GID: + return true; + default: + return false; + } +} + +/* rule validation has made sure these derefs are safe */ +static u8 ns_to_attr(u8 ns) +{ + static u8 ind[] = { + [SQ_NS_PROJ] = 0, + [SQ_NS_UID] = 1, + [SQ_NS_GID] = 2, + }; + + return ind[ns]; +} + +static void rule_to_rule_val(struct scoutfs_quota_rule_val *rv, struct squota_rule *rule) +{ + rv->limit = cpu_to_le64(rule->limit); + rv->prio = rule->prio; + rv->op = rule->op; + rv->rule_flags = rule->rule_flags; + rv->name_val[0] = cpu_to_le64(rule->names[0].val); + rv->name_source[0] = rule->names[0].source; + rv->name_flags[0] = rule->names[0].flags; + rv->name_val[1] = cpu_to_le64(rule->names[1].val); + rv->name_source[1] = rule->names[1].source; + rv->name_flags[1] = rule->names[1].flags; + rv->name_val[2] = cpu_to_le64(rule->names[2].val); + rv->name_source[2] = rule->names[2].source; + rv->name_flags[2] = rule->names[2].flags; + memset(&rv->_pad, 0, sizeof(rv->_pad)); +} + +static void rule_to_irule(struct scoutfs_ioctl_quota_rule *irule, struct squota_rule *rule) +{ + irule->limit = rule->limit; + irule->prio = rule->prio; + irule->op = rule->op; + irule->rule_flags = rule->rule_flags; + irule->name_val[0] = rule->names[0].val; + irule->name_source[0] = rule->names[0].source; + irule->name_flags[0] = rule->names[0].flags; + irule->name_val[1] = rule->names[1].val; + irule->name_source[1] = rule->names[1].source; + irule->name_flags[1] = rule->names[1].flags; + irule->name_val[2] = rule->names[2].val; + irule->name_source[2] = rule->names[2].source; + irule->name_flags[2] = rule->names[2].flags; + memset(&irule->_pad, 0, sizeof(irule->_pad)); +} + +/* + * We verify rules coming from untrusted ioctls/storage. + */ +static bool valid_rule(struct squota_rule *rule) +{ + struct squota_rule_name *other; + struct squota_rule_name *name; + int i; + int j; + + /* invalid op */ + if (rule->op > SQ_OP__NR) + return false; + + if (rule->rule_flags & SQ_RF__UNKNOWN) + return false; + + for (i = 0; i < ARRAY_SIZE(rule->names); i++) { + name = &rule->names[i]; + + /* unknown name flags */ + if (name->flags & SQ_NF__UNKNOWN) + return false; + + if ((name->flags & SQ_NF_SELECT)) { + /* can only select sources that are inode attributes */ + if (!ns_is_attr(name->source)) + return false; + + for (j = 0; j < ARRAY_SIZE(rule->names); j++) { + if (i == j) + continue; + other = &rule->names[j]; + + /* can't select different values of same attr */ + if ((other->flags & SQ_NF_SELECT) && + name->source == other->source && + name->val != other->val) { + return false; + } + } + } + } + + return true; +} + +static int rule_val_to_rule(struct squota_rule *rule, struct scoutfs_quota_rule_val *rv, + int bytes) +{ + if (bytes != sizeof(struct scoutfs_quota_rule_val)) + return -EIO; + + rule->limit = le64_to_cpu(rv->limit); + rule->prio = rv->prio; + rule->op = rv->op; + rule->rule_flags = rv->rule_flags; + rule->names[0].val = le64_to_cpu(rv->name_val[0]); + rule->names[0].source = rv->name_source[0]; + rule->names[0].flags = rv->name_flags[0]; + rule->names[1].val = le64_to_cpu(rv->name_val[1]); + rule->names[1].source = rv->name_source[1]; + rule->names[1].flags = rv->name_flags[1]; + rule->names[2].val = le64_to_cpu(rv->name_val[2]); + rule->names[2].source = rv->name_source[2]; + rule->names[2].flags = rv->name_flags[2]; + + if (!valid_rule(rule)) + return -EIO; + + return 0; +} + +static int irule_to_rule(struct squota_rule *rule, struct scoutfs_ioctl_quota_rule *irule) +{ + rule->limit = irule->limit; + rule->prio = irule->prio; + rule->op = irule->op; + rule->rule_flags = irule->rule_flags; + rule->names[0].val = irule->name_val[0]; + rule->names[0].source = irule->name_source[0]; + rule->names[0].flags = irule->name_flags[0]; + rule->names[1].val = irule->name_val[1]; + rule->names[1].source = irule->name_source[1]; + rule->names[1].flags = irule->name_flags[1]; + rule->names[2].val = irule->name_val[2]; + rule->names[2].source = irule->name_source[2]; + rule->names[2].flags = irule->name_flags[2]; + + if (!valid_rule(rule)) + return -EINVAL; + + return 0; +} + +static void init_rule_key(struct scoutfs_key *key, u64 hash, u64 coll_nr) +{ + *key = (struct scoutfs_key) { + .sk_zone = SCOUTFS_QUOTA_ZONE, + .sk_type = SCOUTFS_QUOTA_RULE_TYPE, + .skqr_hash = cpu_to_le64(hash), + .skqr_coll_nr = cpu_to_le64(coll_nr), + }; +} + +static void rule_to_key(struct scoutfs_key *key, struct squota_rule *rule) +{ + struct scoutfs_quota_rule_val rv; + + rule_to_rule_val(&rv, rule); + init_rule_key(key, scoutfs_hash64(&rv, sizeof(rv)), 0); +} + +/* + * Callers specifically want to increase keys by increasing the + * collision nr, not just incing the key. + */ +static void inc_coll_nr(struct scoutfs_key *key) +{ + le64_add_cpu(&key->skqr_coll_nr, 1); + if (key->skqr_coll_nr == 0) + le64_add_cpu(&key->skqr_hash, 1); +} + +/* + * Rules have a defined sort order that determines matching priority + * when multiple rules match an input. + */ +static int cmp_rules(struct squota_rule *a, struct squota_rule *b) +{ + return scoutfs_cmp(a->prio, b->prio) ?: + scoutfs_cmp(a->names[0].val, b->names[0].val) ?: + scoutfs_cmp(a->names[0].source, b->names[0].source) ?: + scoutfs_cmp(a->names[0].flags, b->names[0].flags) ?: + scoutfs_cmp(a->names[1].val, b->names[1].val) ?: + scoutfs_cmp(a->names[1].source, b->names[1].source) ?: + scoutfs_cmp(a->names[1].flags, b->names[1].flags) ?: + scoutfs_cmp(a->names[2].val, b->names[2].val) ?: + scoutfs_cmp(a->names[2].source, b->names[2].source) ?: + scoutfs_cmp(a->names[2].flags, b->names[2].flags) ?: + scoutfs_cmp(a->op, b->op) ?: + scoutfs_cmp(a->limit, b->limit) ?: + scoutfs_cmp(a->rule_flags, b->rule_flags); +} + +static struct squota_rule *name_to_rule(struct squota_rule_name *name) +{ + return container_of(name, struct squota_rule, names[name->i]); +} + +static bool unlinked_rule(struct squota_rule *rule) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(rule->names); i++) { + if (!RB_EMPTY_NODE(&rule->names[i].node)) + return false; + } + + return true; +} + +static void free_ruleset(struct squota_ruleset *rs) +{ + struct squota_rule_name *name; + struct squota_rule_name *name_; + struct squota_rule *rule; + int i; + + if (!IS_ERR_OR_NULL(rs)) { + for (i = 0; i < ARRAY_SIZE(rs->roots); i++) { + rbtree_postorder_for_each_entry_safe(name, name_, &rs->roots[i], node) { + RB_CLEAR_NODE(&name->node); + + rule = name_to_rule(name); + if (unlinked_rule(rule)) + kfree(rule); + } + } + + for (i = 0; i < ARRAY_SIZE(rs->defaults); i++) + kfree(rs->defaults[i]); + + kfree(rs); + } +} + +static void free_ruleset_rcu(struct rcu_head *rcu) +{ + struct squota_ruleset *rs = container_of(rcu, struct squota_ruleset, rcu); + + free_ruleset(rs); +} + +static bool empty_ruleset(struct squota_ruleset *rs) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(rs->roots); i++) { + if (!RB_EMPTY_ROOT(&rs->roots[i])) + return false; + } + for (i = 0; i < ARRAY_SIZE(rs->defaults); i++) { + if (rs->defaults[i]) + return false; + } + + return true; +} + +/* + * Walk a rule tree for a given matching attr. Each tree only contains + * names which select on the tree's attr so we only have to compare each + * name's value, not its flags or source. + * + * The tree allows multiple names with a given val. The first match is + * found and callers can iterate through all matches with _next. + */ +static struct squota_rule_name *walk_rule_tree(struct rb_root *root, u64 val, + struct squota_rule_name *ins) +{ + struct rb_node **node = &root->rb_node; + struct rb_node *parent = NULL; + struct squota_rule_name *found = NULL; + struct squota_rule_name *name; + int cmp; + + while (*node) { + parent = *node; + name = container_of(*node, struct squota_rule_name, node); + + cmp = scoutfs_cmp(name->val, val); + if (cmp < 0) { + node = &(*node)->rb_left; + } else if (cmp > 0) { + node = &(*node)->rb_right; + } else { + found = name; + node = &(*node)->rb_left; + } + } + + if (ins) { + rb_link_node(&ins->node, parent, node); + rb_insert_color(&ins->node, root); + } + + return found; +} + +/* + * Return the next name in the ruleset attr tree that matches the val. + * All the nodes match this attribute, so we only have to compare the + * val. + */ +static struct squota_rule_name *next_val_name(struct squota_rule_name *name) +{ + struct squota_rule_name *next; + struct rb_node *node; + + if (!name || RB_EMPTY_NODE(&name->node)) + return NULL; + + node = rb_next(&name->node); + if (node) { + next = container_of(node, struct squota_rule_name, node); + if (next->val == name->val) + return next; + } + + return NULL; +} + +static bool ruleset_is_busy(struct squota_info *qtinf) +{ + bool busy; + + rcu_read_lock(); + busy = rcu_dereference(qtinf->ruleset) == ERR_PTR(-EBUSY); + rcu_read_unlock(); + + return busy; +} + +/* + * The caller found that we didn't have a valid ruleset and wants us to + * read in a new ruleset. + * + * We get exclusive access to the rules by marking the ruleset pointer + * busy, possibly waiting for someone else to finish if they beat us to + * it. If we get exclusive access then we walk all the rule items and + * build up a rule set and publish it for use. + */ +static int read_ruleset(struct super_block *sb, struct squota_info *qtinf) +{ + struct scoutfs_lock *lock = NULL; + struct squota_ruleset *rs = NULL; + struct scoutfs_quota_rule_val rv; + struct squota_rule *rule = NULL; + struct squota_rule_name *name; + struct scoutfs_key key; + struct scoutfs_key end; + bool reading = false; + int ret; + int i; + + ret = scoutfs_lock_quota(sb, SCOUTFS_LOCK_READ, 0, &lock); + if (ret < 0) + goto out; + + spin_lock(&qtinf->lock); + rs = rcu_dereference_protected(qtinf->ruleset, lockdep_is_held(&qtinf->lock)); + if (rs == ERR_PTR(-EINVAL)) { + rs = ERR_PTR(-EBUSY); + rcu_assign_pointer(qtinf->ruleset, rs); + reading = true; + } + spin_unlock(&qtinf->lock); + + if (!reading) { + wait_event(qtinf->waitq, !ruleset_is_busy(qtinf)); + ret = 0; + goto out; + } + + rs = kzalloc(sizeof(struct squota_ruleset), GFP_NOFS); + if (!rs) { + ret = -ENOMEM; + goto out; + } + + for (i = 0; i < ARRAY_SIZE(rs->roots); i++) + rs->roots[i] = RB_ROOT; + + init_rule_key(&key, 0, 0); + init_rule_key(&end, U64_MAX, U64_MAX); + + for (;;) { + if (!rule) { + rule = kmalloc(sizeof(struct squota_rule), GFP_NOFS); + if (!rule) { + ret = -ENOMEM; + goto out; + } + } + + ret = scoutfs_item_next(sb, &key, &end, &rv, sizeof(rv), lock); + if (ret < 0) { + if (ret == -ENOENT) + ret = 0; + goto out; + } + + ret = rule_val_to_rule(rule, &rv, ret); + if (ret < 0) + goto out; + + /* insert rule into attr tree if any of its names select */ + for (i = 0; i < ARRAY_SIZE(rule->names); i++) { + name = &rule->names[i]; + name->i = i; + + if (name->flags & SQ_NF_SELECT) { + walk_rule_tree(&rs->roots[ns_to_attr(name->source)], + name->val, name); + } else { + RB_CLEAR_NODE(&name->node); + } + } + + + if (!unlinked_rule(rule)) + rule = NULL; + + /* remember highest priority unlinked (default) rule */ + if (rule && + (!rs->defaults[rule->op] || cmp_rules(rule, rs->defaults[rule->op]) > 0)) { + rs->defaults[rule->op] = rule; + rule = NULL; + } + + inc_coll_nr(&key); + } + +out: + if (reading) { + if (ret == 0 && empty_ruleset(rs)) { + free_ruleset(rs); + rs = ERR_PTR(-ENOENT); + } + + if (ret < 0) { + free_ruleset(rs); + rs = ERR_PTR(-EINVAL); + } + + spin_lock(&qtinf->lock); + rcu_assign_pointer(qtinf->ruleset, rs); + spin_unlock(&qtinf->lock); + wake_up(&qtinf->waitq); + } + + scoutfs_unlock(sb, lock, SCOUTFS_LOCK_READ); + + kfree(rule); + + return ret; +} + +/* + * A rule matches input when the ops match and all of the rule's key + * name selectors match the input -- non-selecting key names always + * match. + */ +static bool rule_matches(struct squota_input *inp, struct squota_rule *rule) +{ + struct squota_rule_name *name; + int i; + + if (inp->op != rule->op) + return false; + + for (i = 0; i < ARRAY_SIZE(rule->names); i++) { + name = &rule->names[i]; + + if ((name->flags & SQ_NF_SELECT) && + (inp->attrs[ns_to_attr(name->source)] != name->val)) + return false; + } + + return true; +} + +struct squota_totl_check { + u64 totl[3]; + u64 limit; + u8 rule_flags; +}; + +/* + * Check the rules against the caller's inputs. We start with the + * highest priority default rule for the operation then search all the + * rules that select for any of the input's attrs and use the highest + * priority match. + * + * If we find a matching rule then we give the caller the totl xattr + * name and limit to check. + */ +static bool check_rules(struct squota_ruleset *rs, struct squota_input *inp, + struct squota_totl_check *tc) +{ + struct squota_rule_name *name; + struct squota_rule *match; + struct squota_rule *rule; + int i; + + if (WARN_ON_ONCE(!rcu_read_lock_held())) + return false; + + match = rs->defaults[inp->op]; + + for (i = 0; i < SQ_NS__NR_SELECT; i++) { + name = walk_rule_tree(&rs->roots[i], inp->attrs[i], NULL); + while (name) { + rule = name_to_rule(name); + if (rule_matches(inp, rule) && (!match || cmp_rules(rule, match) > 0)) + match = rule; + name = next_val_name(name); + } + } + + if (match) { + for (i = 0; i < ARRAY_SIZE(match->names); i++) { + name = &match->names[i]; + + if (ns_is_attr(name->source)) + tc->totl[i] = inp->attrs[ns_to_attr(name->source)]; + else + tc->totl[i] = name->val; /* LITERAL is only non-attr source */ + } + + tc->limit = match->limit; + tc->rule_flags = match->rule_flags; + return true; + } + + return false; +} + +static int check_totl_cb(struct scoutfs_key *key, void *val, unsigned int val_len, void *cb_arg) +{ + struct scoutfs_xattr_totl_val *tval = val; + struct squota_totl_check *tc = cb_arg; + u64 use; + + if (val_len != sizeof(struct scoutfs_xattr_totl_val)) + return -EIO; + + if (tc->rule_flags & SQ_RF_TOTL_COUNT) + use = le64_to_cpu(tval->count); + else + use = le64_to_cpu(tval->total); + + return use >= tc->limit ? -EDQUOT : 0; +} + +/* + * Check that operations can be performed on the given inode. The rules + * are protected by cluster locking and re-read any time the lock is + * revoked. The xattr totl items are read from the weak item cache and + * can be a little out of date. Check results are also cached so we can + * rely on those while the current persistent items would produce a + * different result. + */ +static int check_inputs(struct super_block *sb, struct squota_input *inp) +{ + DECLARE_QUOTA_INFO(sb, qtinf); + struct squota_ruleset *rs = NULL; + struct scoutfs_key range_start; + struct scoutfs_key range_end; + struct scoutfs_key key; + struct squota_totl_check tc; + bool found; + int ret; + + rcu_read_lock(); + + /* quick fast path check when there are no quota rules */ + rs = rcu_dereference(qtinf->ruleset); + if (rs == ERR_PTR(-ENOENT)) { + rcu_read_unlock(); + ret = 0; + goto out; + } + + /* see if we have a cached check result */ + if (get_cached_check(qtinf, inp, &ret)) { + rcu_read_unlock(); + goto out; + } + + /* get the current ruleset, blocking to lock+read if we need to read items */ + while ((rs = rcu_dereference(qtinf->ruleset)), + (rs == ERR_PTR(-EINVAL) || rs == ERR_PTR(-EBUSY))) { + rcu_read_unlock(); + + ret = read_ruleset(sb, qtinf); + if (ret < 0) + goto out; + + rcu_read_lock(); + } + + /* see if we have a matching rule for our inputs */ + if (!IS_ERR(rs)) + found = check_rules(rs, inp, &tc); + else + found = NULL; + + rcu_read_unlock(); + + /* check if the totl limit was exceeded if we found a rule */ + if (found) { + scoutfs_totl_set_range(&range_start, &range_end); + scoutfs_xattr_init_totl_key(&key, tc.totl); + + ret = scoutfs_wkic_iterate(sb, &key, &key, &range_start, &range_end, + check_totl_cb, &tc); + + trace_scoutfs_quota_totl_check(sb, inp, &key, tc.limit, ret); + } else { + ret = 0; + } + + if (ret == 0 || ret == -EDQUOT) + insert_cached_check(qtinf, inp, ret); +out: + trace_scoutfs_quota_check(sb, (long)rs, inp, ret); + return ret; +} + +static void init_inp(struct squota_input *inp, u64 proj, u32 uid, u32 gid, u8 op) +{ + /* zero full size for hash table memcmp */ + memset(inp, 0, sizeof(struct squota_input)); + + inp->attrs[ns_to_attr(SQ_NS_PROJ)] = proj; + inp->attrs[ns_to_attr(SQ_NS_UID)] = uid; + inp->attrs[ns_to_attr(SQ_NS_GID)] = gid; + inp->op = op; +} + +/* + * The [ug]id initialization here mirrors init_inode_owner() but that + * takes a live inode struct and our cluster lock and transaction + * layering makes that awkward. + */ +int scoutfs_quota_check_inode(struct super_block *sb, struct inode *dir) +{ + struct squota_input inp; + + if (quota_unsupported(sb)) + return 0; + + BUILD_BUG_ON(max(sizeof(uid_t), sizeof(gid_t)) > sizeof(u32)); + + init_inp(&inp, scoutfs_inode_get_proj(dir), from_kuid(&init_user_ns, current_fsuid()), + (dir->i_mode & S_ISGID) ? i_gid_read(dir) : + from_kgid(&init_user_ns, current_fsgid()), + SQ_OP_INODE); + + return check_inputs(sb, &inp); +} + +int scoutfs_quota_check_data(struct super_block *sb, struct inode *inode) +{ + struct squota_input inp; + + if (quota_unsupported(sb)) + return 0; + + init_inp(&inp, scoutfs_inode_get_proj(inode), i_uid_read(inode), i_gid_read(inode), + SQ_OP_DATA); + + return check_inputs(sb, &inp); +} + +/* + * Read rules from the iterator position into the caller's irules + * buffer. We set the iterator to point past the last irules we return + * so that it can be used to continue iteration. + */ +int scoutfs_quota_get_rules(struct super_block *sb, u64 *iterator, + struct scoutfs_ioctl_quota_rule *irules, int nr) +{ + DECLARE_QUOTA_INFO(sb, qtinf); + struct scoutfs_quota_rule_val rv; + struct scoutfs_lock *lock = NULL; + struct squota_rule rule; + struct scoutfs_key key; + struct scoutfs_key end; + int copied = 0; + int ret = 0; + + if ((ret = quota_unsupported(sb))) + return ret; + + if (nr == 0) + goto out; + + ret = scoutfs_lock_quota(sb, SCOUTFS_LOCK_READ, 0, &lock); + if (ret < 0) + goto out; + + down_read(&qtinf->rwsem); + + init_rule_key(&key, iterator[0], iterator[1]); + init_rule_key(&end, U64_MAX, U64_MAX); + + while (copied < nr) { + ret = scoutfs_item_next(sb, &key, &end, &rv, sizeof(rv), lock); + if (ret < 0) { + if (ret == -ENOENT) + ret = 0; + break; + } + + ret = rule_val_to_rule(&rule, &rv, ret); + if (ret < 0) + break; + + rule_to_irule(&irules[copied], &rule); + copied++; + + inc_coll_nr(&key); + iterator[0] = le64_to_cpu(key.skqr_hash); + iterator[1] = le64_to_cpu(key.skqr_coll_nr); + } + + up_read(&qtinf->rwsem); + scoutfs_unlock(sb, lock, SCOUTFS_LOCK_READ); +out: + return ret ?: copied; +} + +/* + * Search through rule items with the search hash value looking for a + * match. The return key is set to either the rule we found or the next + * unused collision nr. Returns 0 if found, -ENOENT if not, and -errno + * for errors. + */ +static int find_rule(struct super_block *sb, struct squota_rule *rule, struct scoutfs_key *key_ret, + struct scoutfs_lock *lock) +{ + struct scoutfs_quota_rule_val rv; + struct squota_rule found; + struct scoutfs_key key; + struct scoutfs_key end; + int ret; + + rule_to_key(&key, rule); + end = key; + end.skqr_coll_nr = cpu_to_le64(U64_MAX); + + for (;;) { + ret = scoutfs_item_next(sb, &key, &end, &rv, sizeof(rv), lock); + if (ret < 0) + break; + + ret = rule_val_to_rule(&found, &rv, ret); + if (ret) + break; + + if (cmp_rules(&found, rule) == 0) { + ret = 0; + break; + } + + inc_coll_nr(&key); + } + + *key_ret = key; + return ret; +} + +/* + * Modify a rule. This only operates on the persistent items. It holds + * a write cluster lock so it invalidates all other rules used by other + * nodes and also marks the local rules invalid. The next enforcement + * everywhere will re-read and process the full rule set. All this + * makes rule set modification expensive but it should be + * correspondingly rare. + */ +int scoutfs_quota_mod_rule(struct super_block *sb, bool is_add, + struct scoutfs_ioctl_quota_rule *irule) +{ + DECLARE_QUOTA_INFO(sb, qtinf); + struct scoutfs_quota_rule_val rv; + struct scoutfs_lock *lock = NULL; + struct squota_rule rule; + struct scoutfs_key key; + int ret; + + if ((ret = quota_unsupported(sb))) + return ret; + + ret = irule_to_rule(&rule, irule); + if (ret < 0) + goto out; + + ret = scoutfs_lock_quota(sb, SCOUTFS_LOCK_WRITE, 0, &lock); + if (ret < 0) + goto out; + + down_write(&qtinf->rwsem); + + if (is_add) { + ret = find_rule(sb, &rule, &key, lock); + if (ret == -ENOENT) + ret = 0; + else if (ret == 0) + ret = -EEXIST; + if (ret < 0) + goto unlock; + + rule_to_rule_val(&rv, &rule); + ret = scoutfs_item_create(sb, &key, &rv, sizeof(rv), lock); + if (ret < 0) + goto unlock; + + } else { + ret = find_rule(sb, &rule, &key, lock) ?: + scoutfs_item_delete(sb, &key, lock); + if (ret < 0) + goto unlock; + } + + scoutfs_quota_invalidate(sb); + ret = 0; + +unlock: + up_write(&qtinf->rwsem); + scoutfs_unlock(sb, lock, SCOUTFS_LOCK_WRITE); + +out: + if (is_add) + trace_scoutfs_quota_add_rule(sb, &rule, ret); + else + trace_scoutfs_quota_del_rule(sb, &rule, ret); + + return ret; +} + +void scoutfs_quota_get_lock_range(struct scoutfs_key *start, struct scoutfs_key *end) +{ + scoutfs_key_set_zeros(start); + start->sk_zone = SCOUTFS_QUOTA_ZONE; + + scoutfs_key_set_ones(end); + end->sk_zone = SCOUTFS_QUOTA_ZONE; +} + +/* + * This is called during cluster lock invalidation to indicate that the + * ruleset is no longer protected by cluster locking and might have been + * modified. We mark the ruleset invalid and free it once all readers + * drain. The next check will acquire the cluster lock and read the + * rules. Because this is called during invalidation this is serialized + * with write holders of cluster locks so we can never see -EBUSY here. + */ +void scoutfs_quota_invalidate(struct super_block *sb) +{ + DECLARE_QUOTA_INFO(sb, qtinf); + struct squota_ruleset *rs; + + if (quota_unsupported(sb)) + return; + + rcu_read_lock(); + + spin_lock(&qtinf->lock); + rs = rcu_dereference_protected(qtinf->ruleset, lockdep_is_held(&qtinf->lock)); + if (rs != ERR_PTR(-EINVAL)) + rcu_assign_pointer(qtinf->ruleset, ERR_PTR(-EINVAL)); + spin_unlock(&qtinf->lock); + + /* cluster locking should have prevented this */ + BUG_ON(rs == ERR_PTR(-EBUSY)); + + if (!IS_ERR(rs)) + call_rcu(&rs->rcu, free_ruleset_rcu); + + rcu_read_unlock(); + + shrink_all_cached_checks(qtinf); +} + +static ssize_t quota_drop_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) +{ + return 0; +} + +static ssize_t quota_drop_write(struct file *file, const char __user *buf, size_t size, + loff_t *ppos) +{ + struct squota_info *qtinf = file_inode(file)->i_private; + + shrink_all_cached_checks(qtinf); + + return size; +} + +static const struct file_operations quota_drop_fops = { + .read = quota_drop_read, + .write = quota_drop_write, +}; + +int scoutfs_quota_setup(struct super_block *sb) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + struct squota_info *qtinf = NULL; + int ret; + + if (quota_unsupported(sb)) + return 0; + + qtinf = kzalloc(sizeof(struct squota_info), GFP_KERNEL); + if (!qtinf) { + ret = -ENOMEM; + goto out; + } + + ret = rhashtable_init(&qtinf->check_ht, &check_ht_params); + if (ret < 0) { + kfree(qtinf); + goto out; + } + + qtinf->drop_dentry = debugfs_create_file("drop_quota_check_cache", S_IFREG|S_IRUSR, + sbi->debug_root, qtinf, "a_drop_fops); + if (!qtinf->drop_dentry) { + rhashtable_destroy(&qtinf->check_ht); + kfree(qtinf); + return -ENOMEM; + } + + qtinf->sb = sb; + RCU_INIT_POINTER(qtinf->ruleset, ERR_PTR(-EINVAL)); + atomic64_set(&qtinf->nr_checks, 0); + init_rwsem(&qtinf->rwsem); + spin_lock_init(&qtinf->lock); + init_waitqueue_head(&qtinf->waitq); + + KC_INIT_SHRINKER_FUNCS(&qtinf->shrinker, count_cached_checks, scan_cached_checks); + KC_REGISTER_SHRINKER(&qtinf->shrinker); + + sbi->squota_info = qtinf; + + ret = 0; +out: + return ret; +} + +static void free_cached_check(void *ptr, void *arg) +{ + struct squota_check *chk = ptr; + + kfree(chk); +} + +void scoutfs_quota_destroy(struct super_block *sb) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + DECLARE_QUOTA_INFO(sb, qtinf); + struct squota_ruleset *rs; + + if (qtinf) { + debugfs_remove(qtinf->drop_dentry); + KC_UNREGISTER_SHRINKER(&qtinf->shrinker); + + spin_lock(&qtinf->lock); + rs = rcu_dereference_protected(qtinf->ruleset, lockdep_is_held(&qtinf->lock)); + spin_unlock(&qtinf->lock); + if (!IS_ERR(rs)) + free_ruleset(rs); + + rhashtable_free_and_destroy(&qtinf->check_ht, free_cached_check, NULL); + + kfree(qtinf); + sbi->squota_info = NULL; + } +} diff --git a/kmod/src/quota.h b/kmod/src/quota.h new file mode 100644 index 00000000..324d8a36 --- /dev/null +++ b/kmod/src/quota.h @@ -0,0 +1,48 @@ +#ifndef _SCOUTFS_QUOTA_H_ +#define _SCOUTFS_QUOTA_H_ + +#include "ioctl.h" + +/* + * Each rule's name can be in the ruleset's rbtree associated with the + * source attr that it selects. This lets checks only test rules that + * the inputs could match. The 'i' field indicates which name is in the + * tree so we can find the containing rule. + * + * This is mostly private to quota.c but we expose it for tracing. + */ +struct squota_rule { + u64 limit; + u8 prio; + u8 op; + u8 rule_flags; + struct squota_rule_name { + struct rb_node node; + u64 val; + u8 source; + u8 flags; + u8 i; + } names[3]; +}; + +/* private to quota.c, only here for tracing */ +struct squota_input { + u64 attrs[SQ_NS__NR_SELECT]; + u8 op; +}; + +int scoutfs_quota_check_inode(struct super_block *sb, struct inode *dir); +int scoutfs_quota_check_data(struct super_block *sb, struct inode *inode); + +int scoutfs_quota_get_rules(struct super_block *sb, u64 *iterator, + struct scoutfs_ioctl_quota_rule *irules, int nr); +int scoutfs_quota_mod_rule(struct super_block *sb, bool is_add, + struct scoutfs_ioctl_quota_rule *irule); + +void scoutfs_quota_get_lock_range(struct scoutfs_key *start, struct scoutfs_key *end); +void scoutfs_quota_invalidate(struct super_block *sb); + +int scoutfs_quota_setup(struct super_block *sb); +void scoutfs_quota_destroy(struct super_block *sb); + +#endif diff --git a/kmod/src/scoutfs_trace.h b/kmod/src/scoutfs_trace.h index 0f30c513..3fd4821f 100644 --- a/kmod/src/scoutfs_trace.h +++ b/kmod/src/scoutfs_trace.h @@ -37,6 +37,10 @@ #include "net.h" #include "data.h" #include "ext.h" +#include "quota.h" + +#include "trace/quota.h" +#include "trace/wkic.h" struct lock_info; diff --git a/kmod/src/super.c b/kmod/src/super.c index 434aace6..0086d7fb 100644 --- a/kmod/src/super.c +++ b/kmod/src/super.c @@ -49,6 +49,8 @@ #include "volopt.h" #include "fence.h" #include "xattr.h" +#include "wkic.h" +#include "quota.h" #include "scoutfs_trace.h" static struct dentry *scoutfs_debugfs_root; @@ -194,7 +196,9 @@ static void scoutfs_put_super(struct super_block *sb) scoutfs_shutdown_trans(sb); scoutfs_volopt_destroy(sb); scoutfs_client_destroy(sb); + scoutfs_quota_destroy(sb); scoutfs_inode_destroy(sb); + scoutfs_wkic_destroy(sb); scoutfs_item_destroy(sb); scoutfs_forest_destroy(sb); scoutfs_data_destroy(sb); @@ -544,7 +548,9 @@ static int scoutfs_fill_super(struct super_block *sb, void *data, int silent) scoutfs_block_setup(sb) ?: scoutfs_forest_setup(sb) ?: scoutfs_item_setup(sb) ?: + scoutfs_wkic_setup(sb) ?: scoutfs_inode_setup(sb) ?: + scoutfs_quota_setup(sb) ?: scoutfs_data_setup(sb) ?: scoutfs_setup_trans(sb) ?: scoutfs_omap_setup(sb) ?: diff --git a/kmod/src/super.h b/kmod/src/super.h index 14ff626a..03c6a6ea 100644 --- a/kmod/src/super.h +++ b/kmod/src/super.h @@ -30,6 +30,8 @@ struct recov_info; struct omap_info; struct volopt_info; struct fence_info; +struct wkic_info; +struct squota_info; struct scoutfs_sb_info { struct super_block *sb; @@ -55,6 +57,8 @@ struct scoutfs_sb_info { struct omap_info *omap_info; struct volopt_info *volopt_info; struct item_cache_info *item_cache_info; + struct wkic_info *wkic_info; + struct squota_info *squota_info; struct fence_info *fence_info; /* tracks tasks waiting for data extents */ @@ -156,4 +160,17 @@ int scoutfs_write_super(struct super_block *sb, /* to keep this out of the ioctl.h public interface definition */ long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg); +/* + * Returns 0 when supported, non-zero -errno when unsupported. + */ +static inline int scoutfs_fmt_vers_unsupported(struct super_block *sb, u64 vers) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + + if (sbi && (sbi->fmt_vers < vers)) + return -EOPNOTSUPP; + else + return 0; +} + #endif diff --git a/kmod/src/totl.c b/kmod/src/totl.c new file mode 100644 index 00000000..cfa9b31a --- /dev/null +++ b/kmod/src/totl.c @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 Versity Software, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License v2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + */ +#include +#include + +#include "format.h" +#include "forest.h" +#include "totl.h" + +void scoutfs_totl_set_range(struct scoutfs_key *start, struct scoutfs_key *end) +{ + scoutfs_key_set_zeros(start); + start->sk_zone = SCOUTFS_XATTR_TOTL_ZONE; + scoutfs_key_set_ones(end); + end->sk_zone = SCOUTFS_XATTR_TOTL_ZONE; +} + +void scoutfs_totl_merge_init(struct scoutfs_totl_merging *merg) +{ + memset(merg, 0, sizeof(struct scoutfs_totl_merging)); +} + +void scoutfs_totl_merge_contribute(struct scoutfs_totl_merging *merg, + u64 seq, u8 flags, void *val, int val_len, int fic) +{ + struct scoutfs_xattr_totl_val *tval = val; + + if (fic & FIC_FS_ROOT) { + merg->fs_seq = seq; + merg->fs_total = le64_to_cpu(tval->total); + merg->fs_count = le64_to_cpu(tval->count); + } else if (fic & FIC_FINALIZED) { + merg->fin_seq = seq; + merg->fin_total += le64_to_cpu(tval->total); + merg->fin_count += le64_to_cpu(tval->count); + } else { + merg->log_seq = seq; + merg->log_total += le64_to_cpu(tval->total); + merg->log_count += le64_to_cpu(tval->count); + } +} + +/* + * .totl. item merging has to be careful because the log btree merging + * code can write partial results to the fs_root. This means that a + * reader can see both cases where new finalized logs should be applied + * to the old fs items and where old finalized logs have already been + * applied to the partially merged fs items. Currently active logged + * items are always applied on top of all cases. + * + * These cases are differentiated with a combination of sequence numbers + * in items, the count of contributing xattrs, and a flag + * differentiating finalized and active logged items. This lets us + * recognize all cases, including when finalized logs were merged and + * deleted the fs item. + */ +void scoutfs_totl_merge_resolve(struct scoutfs_totl_merging *merg, __u64 *total, __u64 *count) +{ + *total = 0; + *count = 0; + + /* start with the fs item if we have it */ + if (merg->fs_seq != 0) { + *total = merg->fs_total; + *count = merg->fs_count; + } + + /* apply finalized logs if they're newer or creating */ + if (((merg->fs_seq != 0) && (merg->fin_seq > merg->fs_seq)) || + ((merg->fs_seq == 0) && (merg->fin_count > 0))) { + *total += merg->fin_total; + *count += merg->fin_count; + } + + /* always apply active logs which must be newer than fs and finalized */ + if (merg->log_seq > 0) { + *total += merg->log_total; + *count += merg->log_count; + } +} diff --git a/kmod/src/totl.h b/kmod/src/totl.h new file mode 100644 index 00000000..52de1fd6 --- /dev/null +++ b/kmod/src/totl.h @@ -0,0 +1,24 @@ +#ifndef _SCOUTFS_TOTL_H_ +#define _SCOUTFS_TOTL_H_ + +#include "key.h" + +struct scoutfs_totl_merging { + u64 fs_seq; + u64 fs_total; + u64 fs_count; + u64 fin_seq; + u64 fin_total; + s64 fin_count; + u64 log_seq; + u64 log_total; + s64 log_count; +}; + +void scoutfs_totl_set_range(struct scoutfs_key *start, struct scoutfs_key *end); +void scoutfs_totl_merge_init(struct scoutfs_totl_merging *merg); +void scoutfs_totl_merge_contribute(struct scoutfs_totl_merging *merg, + u64 seq, u8 flags, void *val, int val_len, int fic); +void scoutfs_totl_merge_resolve(struct scoutfs_totl_merging *merg, __u64 *total, __u64 *count); + +#endif diff --git a/kmod/src/trace/quota.h b/kmod/src/trace/quota.h new file mode 100644 index 00000000..983b318c --- /dev/null +++ b/kmod/src/trace/quota.h @@ -0,0 +1,143 @@ + +/* + * Tracing squota_input + */ +#define SQI_FMT "[%u %llu %llu %llu]" + +#define SQI_ARGS(i) \ + (i)->op, (i)->attrs[0], (i)->attrs[1], (i)->attrs[2] + +#define SQI_FIELDS(pref) \ + __array(__u64, pref##_attrs, SQ_NS__NR_SELECT) \ + __field(__u8, pref##_op) + +#define SQI_ASSIGN(pref, i) \ + __entry->pref##_attrs[0] = (i)->attrs[0]; \ + __entry->pref##_attrs[1] = (i)->attrs[1]; \ + __entry->pref##_attrs[2] = (i)->attrs[2]; \ + __entry->pref##_op = (i)->op; + +#define SQI_ENTRY_ARGS(pref) \ + __entry->pref##_op, __entry->pref##_attrs[0], \ + __entry->pref##_attrs[1], __entry->pref##_attrs[2] + +/* + * Tracing squota_rule + */ +#define SQR_FMT "[%u %llu,%u,%x %llu,%u,%x %llu,%u,%x %u %llu]" + +#define SQR_ARGS(r) \ + (r)->prio, \ + (r)->name_val[0], (r)->name_source[0], (r)->name_flags[0], \ + (r)->name_val[1], (r)->name_source[1], (r)->name_flags[1], \ + (r)->name_val[2], (r)->name_source[2], (r)->name_flags[2], \ + (r)->op, (r)->limit \ + +#define SQR_FIELDS(pref) \ + __array(__u64, pref##_name_val, 3) \ + __field(__u64, pref##_limit) \ + __array(__u8, pref##_name_source, 3) \ + __array(__u8, pref##_name_flags, 3) \ + __field(__u8, pref##_prio) \ + __field(__u8, pref##_op) + +#define SQR_ASSIGN(pref, r) \ + __entry->pref##_name_val[0] = (r)->names[0].val; \ + __entry->pref##_name_val[1] = (r)->names[1].val; \ + __entry->pref##_name_val[2] = (r)->names[2].val; \ + __entry->pref##_limit = (r)->limit; \ + __entry->pref##_name_source[0] = (r)->names[0].source; \ + __entry->pref##_name_source[1] = (r)->names[1].source; \ + __entry->pref##_name_source[2] = (r)->names[2].source; \ + __entry->pref##_name_flags[0] = (r)->names[0].flags; \ + __entry->pref##_name_flags[1] = (r)->names[1].flags; \ + __entry->pref##_name_flags[2] = (r)->names[2].flags; \ + __entry->pref##_prio = (r)->prio; \ + __entry->pref##_op = (r)->op; + +#define SQR_ENTRY_ARGS(pref) \ + __entry->pref##_prio, __entry->pref##_name_val[0], \ + __entry->pref##_name_source[0], __entry->pref##_name_flags[0], \ + __entry->pref##_name_val[1], __entry->pref##_name_source[1], \ + __entry->pref##_name_flags[1], __entry->pref##_name_val[2], \ + __entry->pref##_name_source[2], __entry->pref##_name_flags[2], \ + __entry->pref##_op, __entry->pref##_limit + +TRACE_EVENT(scoutfs_quota_check, + TP_PROTO(struct super_block *sb, long rs_ptr, struct squota_input *inp, int ret), + + TP_ARGS(sb, rs_ptr, inp, ret), + + TP_STRUCT__entry( + SCSB_TRACE_FIELDS + __field(long, rs_ptr) + SQI_FIELDS(i) + __field(int, ret) + ), + + TP_fast_assign( + SCSB_TRACE_ASSIGN(sb); + __entry->rs_ptr = rs_ptr; + SQI_ASSIGN(i, inp); + __entry->ret = ret; + ), + + TP_printk(SCSBF" rs_ptr %ld ret %d inp "SQI_FMT, + SCSB_TRACE_ARGS, __entry->rs_ptr, __entry->ret, SQI_ENTRY_ARGS(i)) +); + +DECLARE_EVENT_CLASS(scoutfs_quota_rule_op_class, + TP_PROTO(struct super_block *sb, struct squota_rule *rule, int ret), + + TP_ARGS(sb, rule, ret), + + TP_STRUCT__entry( + SCSB_TRACE_FIELDS + SQR_FIELDS(r) + __field(int, ret) + ), + + TP_fast_assign( + SCSB_TRACE_ASSIGN(sb); + SQR_ASSIGN(r, rule); + __entry->ret = ret; + ), + + TP_printk(SCSBF" "SQR_FMT" ret %d", + SCSB_TRACE_ARGS, SQR_ENTRY_ARGS(r), __entry->ret) +); +DEFINE_EVENT(scoutfs_quota_rule_op_class, scoutfs_quota_add_rule, + TP_PROTO(struct super_block *sb, struct squota_rule *rule, int ret), + TP_ARGS(sb, rule, ret) +); +DEFINE_EVENT(scoutfs_quota_rule_op_class, scoutfs_quota_del_rule, + TP_PROTO(struct super_block *sb, struct squota_rule *rule, int ret), + TP_ARGS(sb, rule, ret) +); + +TRACE_EVENT(scoutfs_quota_totl_check, + TP_PROTO(struct super_block *sb, struct squota_input *inp, struct scoutfs_key *key, + u64 limit, int ret), + + TP_ARGS(sb, inp, key, limit, ret), + + TP_STRUCT__entry( + SCSB_TRACE_FIELDS + SQI_FIELDS(i) + sk_trace_define(k) + __field(__u64, limit) + __field(int, ret) + ), + + TP_fast_assign( + SCSB_TRACE_ASSIGN(sb); + SQI_ASSIGN(i, inp); + sk_trace_assign(k, key); + __entry->limit = limit; + __entry->ret = ret; + ), + + TP_printk(SCSBF" inp "SQI_FMT" key "SK_FMT" limit %llu ret %d", + SCSB_TRACE_ARGS, SQI_ENTRY_ARGS(i), sk_trace_args(k), __entry->limit, + __entry->ret) +); diff --git a/kmod/src/trace/wkic.h b/kmod/src/trace/wkic.h new file mode 100644 index 00000000..ca7efdcd --- /dev/null +++ b/kmod/src/trace/wkic.h @@ -0,0 +1,112 @@ + +DECLARE_EVENT_CLASS(scoutfs_wkic_wpage_class, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + + TP_ARGS(sb, ptr, which, n0l, n1l, start, end), + + TP_STRUCT__entry( + SCSB_TRACE_FIELDS + __field(void *, ptr) + __field(int, which) + __field(bool, n0l) + __field(bool, n1l) + sk_trace_define(start) + sk_trace_define(end) + ), + + TP_fast_assign( + SCSB_TRACE_ASSIGN(sb); + __entry->ptr = ptr; + __entry->which = which; + __entry->n0l = n0l; + __entry->n1l = n1l; + sk_trace_assign(start, start); + sk_trace_assign(end, end); + __entry->which = which; + ), + + TP_printk(SCSBF" ptr %p wh %d nl %u,%u start "SK_FMT " end "SK_FMT, SCSB_TRACE_ARGS, + __entry->ptr, __entry->which, __entry->n0l, __entry->n1l, + sk_trace_args(start), sk_trace_args(end)) +); + +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_alloced, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_freeing, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_found, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_trimmed, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_erased, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_inserting, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_inserted, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_shrinking, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_dropping, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_replaying, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); +DEFINE_EVENT(scoutfs_wkic_wpage_class, scoutfs_wkic_wpage_filled, + TP_PROTO(struct super_block *sb, void *ptr, int which, bool n0l, bool n1l, + struct scoutfs_key *start, struct scoutfs_key *end), + TP_ARGS(sb, ptr, which, n0l, n1l, start, end) +); + +TRACE_EVENT(scoutfs_wkic_read_items, + TP_PROTO(struct super_block *sb, struct scoutfs_key *key, struct scoutfs_key *start, + struct scoutfs_key *end), + + TP_ARGS(sb, key, start, end), + + TP_STRUCT__entry( + SCSB_TRACE_FIELDS + sk_trace_define(key) + sk_trace_define(start) + sk_trace_define(end) + ), + + TP_fast_assign( + SCSB_TRACE_ASSIGN(sb); + sk_trace_assign(key, start); + sk_trace_assign(start, start); + sk_trace_assign(end, end); + ), + + TP_printk(SCSBF" key "SK_FMT" start "SK_FMT " end "SK_FMT, SCSB_TRACE_ARGS, + sk_trace_args(key), sk_trace_args(start), sk_trace_args(end)) +); diff --git a/kmod/src/wkic.c b/kmod/src/wkic.c new file mode 100644 index 00000000..2c37f341 --- /dev/null +++ b/kmod/src/wkic.c @@ -0,0 +1,1155 @@ +/* + * Copyright (C) 2023 Versity Software, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License v2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "super.h" +#include "format.h" +#include "client.h" +#include "block.h" +#include "forest.h" +#include "totl.h" +#include "counters.h" +#include "util.h" +#include "scoutfs_trace.h" +#include "wkic.h" + +/* + * This weaker item cache differs from the core item cache in item.c: + * + * - It is not protected by cluster lock consistency. Reads do not + * flush currently dirty items and will not see them. Writes don't + * invalidate reads which can still be cached and returned after a + * write. + * + * - It is not used to write items. The items read here are not also + * written through the same cache. The only insertions into this cache + * come from bulk reads of persistent items. + * + * - Items are only evicted based on the passage of time or from memory + * pressure. + * + * These differences give us significant degrees of freedom to implement + * a cache that is simpler and creates less read contention than the + * core item cache which must provide stronger cache coherency. + * + * Items are stored in pages which are indexed by an rbtree. Once pages + * are visible to readers by being inserted into the rbtree they will + * not change. The key ranges of pages in the trees are not allowed to + * overlap. Overlapping insertions are resolved by removing the + * existing overlap, dropping the insertion, or trimming the insertion + * depending on the age and overlapping range of the pages. + * + * Readers are fully concurrent and create no store contention by only + * acquiring an RCU read lock. + * + * Readers are protected from traversing modifications made to the tree + * by actually having two trees. Writers grab an exclusive lock, update + * an idle tree while readers are busy with the other tree, swap the + * trees, and then replay the changes so that both trees are kept in + * sync. The writer uses RCU grace periods to ensure that readers have + * finished with each tree before they start modifying it. + * + * The passage of time is the only read consistency mechanic so we have + * to drop old pages regularly to have a chance to read the new version + * of cached items. The exclusive writer makes it trivial to keep a + * list of pages by age for both expiring and for shrinking in response + * to memory pressure. + */ + +/* + * A cached item can be returned for twice this delay after it is + * written to persistence. We can use a root for _LIFETIME after the + * write, and then we can insert a cached page at the end of that for + * another _LIFETIME. + */ +#define WKIC_CACHE_LIFETIME_MS (5 * MSEC_PER_SEC) + +struct wkic_info { + /* concurrent reader fast path */ + struct rb_root wpage_roots[2]; + unsigned long bits; + + /* exclusive writer */ + struct mutex update_mutex ____cacheline_aligned_in_smp; + struct list_head shrink_list; + atomic64_t shrink_count; + + /* block reading slow path */ + struct mutex roots_mutex; + struct scoutfs_net_roots roots; + u64 roots_read_seq; + ktime_t roots_expire; + + /* misc glue */ + struct super_block *sb; + KC_DEFINE_SHRINKER(shrinker); + struct dentry *drop_dentry; +}; + +/* + * Determines which root readers use during their rcu read lock. Tree + * updates flip this bit during their work to make sure readers don't + * see tree modifications while they're reading. + */ +#define WINF_BIT_READING 0 + +/* + * Pages are read-only once they're inserted. They're only removed if + * they expire or are reclaimed by shrinking. + * + * There's some false sharing here as the writer modifies page nodes + * that are near the nodes that the readers are traversing. + */ +struct wkic_page { + /* concurrent readers */ + struct rb_node nodes[2]; + struct scoutfs_key start; + struct scoutfs_key end; + struct rb_root item_root; + ktime_t expiration; + + /* serialized writers */ + struct list_head head; + struct rb_node *ins_parent; + struct rb_node **ins_node; + u64 read_seq; +}; + +#define trace_wpage(sb, event, which, wpage) \ +do { \ + __typeof__(wpage) _wpage = (wpage); \ + \ + trace_scoutfs_wkic_wpage_##event(sb, _wpage, which, \ + !RB_EMPTY_NODE(&_wpage->nodes[0]), \ + !RB_EMPTY_NODE(&_wpage->nodes[1]), \ + &_wpage->start, &_wpage->end); \ +} while (0) + +static struct wkic_page *wpage_container(struct rb_node *node, unsigned int which) +{ + return container_of(node, struct wkic_page, nodes[which]); +} + +static struct wkic_page *next_wpage(struct wkic_page *wpage, unsigned int which) +{ + return wpage_container(!wpage ? NULL : rb_next(&wpage->nodes[which]), which); +} + +/* + * Iterate over pages in the tree starting with a current page, allowing the for + * body to remove the wpage. + */ +#define for_each_wpage_safe(wpage, tmp, which) \ + for (; wpage && ((tmp = next_wpage(wpage, which)), 1); wpage = tmp) + +static bool wpage_expired(struct wkic_page *wpage, ktime_t kt) +{ + return ktime_before(wpage->expiration, kt); +} + +struct wkic_item { + struct rb_node node; + struct scoutfs_key key; + u64 seq; + unsigned int val_len; + u8 flags; + u8 val[0] __aligned(ARCH_KMALLOC_MINALIGN); /* totls have native structs */ +}; + +static struct wkic_item *witem_container(struct rb_node *node) +{ + return !node ? NULL : container_of(node, struct wkic_item, node); +} + +static struct wkic_item *first_witem(struct rb_root *root) +{ + return witem_container(rb_first(root)); +} + +static struct wkic_item *last_witem(struct rb_root *root) +{ + return witem_container(rb_last(root)); +} + +static struct wkic_item *next_witem(struct wkic_item *witem) +{ + return witem_container(!witem ? NULL : rb_next(&witem->node)); +} + +static struct wkic_item *prev_witem(struct wkic_item *witem) +{ + return witem_container(!witem ? NULL : rb_prev(&witem->node)); +} + +static struct wkic_page *alloc_wpage(struct super_block *sb, struct scoutfs_key *start, + struct scoutfs_key *end, u64 read_seq) +{ + struct wkic_page *wpage; + + wpage = (void *)__get_free_page(GFP_NOFS); + if (wpage) { + RB_CLEAR_NODE(&wpage->nodes[0]); + RB_CLEAR_NODE(&wpage->nodes[1]); + wpage->start = *start; + wpage->end = *end; + wpage->item_root = RB_ROOT; + wpage->expiration = ktime_set(0, 0); /* later set as added to expire list */ + INIT_LIST_HEAD(&wpage->head); + wpage->read_seq = read_seq; + wpage->ins_parent = NULL; + wpage->ins_node = NULL; + + trace_wpage(sb, alloced, -1, wpage); + } + + return wpage; +} + +static void free_wpage(struct super_block *sb, struct wkic_page *wpage) +{ + if (wpage) { + trace_wpage(sb, freeing, -1, wpage); + + BUG_ON(!RB_EMPTY_NODE(&wpage->nodes[0])); + BUG_ON(!RB_EMPTY_NODE(&wpage->nodes[1])); + BUG_ON(!list_empty(&wpage->head)); + + free_page((long)wpage); + } +} + +static struct wkic_page *find_wpage(struct super_block *sb, struct rb_root *root, + unsigned int which, struct scoutfs_key *start, + struct scoutfs_key *end, struct rb_node **parent_ret, + struct rb_node ***node_ret) +{ + struct rb_node **node = &root->rb_node; + struct rb_node *parent = NULL; + struct wkic_page *wpage = NULL; + int cmp; + + while (*node) { + parent = *node; + wpage = wpage_container(*node, which); + + cmp = scoutfs_key_compare_ranges(start, end, &wpage->start, &wpage->end); + if (cmp < 0) { + node = &(*node)->rb_left; + } else if (cmp > 0) { + node = &(*node)->rb_right; + } else { + trace_wpage(sb, found, which, wpage); + return wpage; + } + } + + if (parent_ret) { + *parent_ret = parent; + *node_ret = node; + } + + return NULL; +} + +static void trim_wpage(struct super_block *sb, struct wkic_page *wpage, + bool from_start, struct scoutfs_key *edge) +{ + struct wkic_item *witem; + struct wkic_item *tmp; + int cmp; + + witem = from_start ? first_witem(&wpage->item_root) : last_witem(&wpage->item_root); + while (witem) { + cmp = scoutfs_key_compare(&witem->key, edge); + + if ((from_start && cmp >= 0) || (!from_start && cmp <= 0)) + break; + + tmp = from_start ? next_witem(witem) : prev_witem(witem); + rb_erase(&witem->node, &wpage->item_root); + witem = tmp; + } + + trace_wpage(sb, trimmed, -1, wpage); +} + +static void erase_wpage(struct super_block *sb, struct rb_root *root, unsigned int which, + struct wkic_page *wpage) +{ + BUG_ON(RB_EMPTY_NODE(&wpage->nodes[which])); + + rb_erase(&wpage->nodes[which], root); + RB_CLEAR_NODE(&wpage->nodes[which]); + trace_wpage(sb, erased, which, wpage); +} + +/* + * Try to insert a page into a tree. We can't insert pages into the + * tree that overlap and once pages are inserted they can't be changed. + * If an existing page intersects with our insertion then we have to + * look at the two pages to find out which to drop or trim. + * + * As this modifies the tree it logs the pages on the caller's log list. + * If this doesn't insert the page then it returns false so the caller + * can free it. + */ +static bool try_insert_wpage(struct super_block *sb, struct wkic_info *winf, struct rb_root *root, + unsigned int which, ktime_t now, struct wkic_page *ins, + struct list_head *log_list) +{ + struct wkic_page *wpar; + struct wkic_page *olap; + struct rb_node *parent; + struct rb_node **node; + int is_os; + int ie_oe; + + trace_wpage(sb, inserting, which, ins); + + /* check for overlaps with insertion, there can be many */ + while ((olap = find_wpage(sb, root, which, &ins->start, &ins->end, &parent, &node))) { + + if (wpage_expired(olap, now) || ins->read_seq > olap->read_seq) { + /* erase overlap if it's expired or older than insertion */ + erase_wpage(sb, root, which, olap); + list_move_tail(&olap->head, log_list); + + } else { + is_os = scoutfs_key_compare(&ins->start, &olap->start); + ie_oe = scoutfs_key_compare(&ins->end, &olap->end); + + if (is_os >= 0 && ie_oe <= 0) { + /* drop insertion when entirely within newer overlap */ + ins = NULL; + break; + } + + if (ie_oe > 0) { + /* + * trim start to cache exposed insertion + * end. This also catches the case + * where the newer overlap is within the + * insertion, we want to bias later + * keys. + */ + ins->start = olap->end; + scoutfs_key_inc(&ins->start); + trim_wpage(sb, ins, true, &ins->start); + } else { + /* otherwise trim end to cache exposed start */ + ins->end = olap->start; + scoutfs_key_dec(&ins->end); + trim_wpage(sb, ins, false, &ins->end); + } + } + } + + if (ins) { + rb_link_node(&ins->nodes[which], parent, node); + rb_insert_color(&ins->nodes[which], root); + list_add_tail(&ins->head, log_list); + + trace_wpage(sb, inserted, which, ins); + + if (parent == NULL) { + ins->ins_parent = NULL; + ins->ins_node = &winf->wpage_roots[!which].rb_node; + } else { + wpar = wpage_container(parent, which); + + ins->ins_parent = &wpar->nodes[!which]; + if (node == &parent->rb_left) + ins->ins_node = &ins->ins_parent->rb_left; + else + ins->ins_node = &ins->ins_parent->rb_right; + } + } + + return ins != NULL; +} + +static unsigned which_reading(struct wkic_info *winf) +{ + unsigned int which; + + which = !!test_bit(WINF_BIT_READING, &winf->bits); + smp_rmb(); /* bit read before any tree access */ + return which; +} + +/* + * Set the reading bit so readers start using it. + */ +static void set_reading(struct wkic_info *winf, unsigned int which) +{ + smp_wmb(); /* tree updates visible before bit changes */ + if (which) + set_bit(WINF_BIT_READING, &winf->bits); + else + clear_bit(WINF_BIT_READING, &winf->bits); +} + +/* + * Verify that the two trees identically index the same set of pages. + */ +__always_unused +static void verify_trees(struct wkic_info *winf) +{ + struct rb_node *zero; + struct rb_node *one; + + zero = rb_first(&winf->wpage_roots[0]); + one = rb_first(&winf->wpage_roots[1]); + for (;;) { + BUG_ON((zero == NULL && one != NULL) || (zero && one != zero + 1)); + if (!zero) + break; + zero = rb_next(zero); + one = rb_next(one); + } +} + +/* + * Make an update to the page rbtrees. As we start the two trees are in + * sync. We make our changes to the non-reading tree, flip the reading + * bit, and then replay the changes in the other tree. + * + * Updates are serialized and high latency. They block on a mutex and + * then wait for two RCU grace periods to make sure each non-reading + * tree is idle. This keeps the writer simple with a constrained update + * mechanism that doesn't have any async processing. Writers are + * already either expensive insertion of multiple pages read from block + * IO or are responding to pressure from the shrinker. It's worth + * slowing them down to make reads fully concurrent with no store + * contention (almost, there's some false sharing in the pages). + * + * shrink_nr controls how we'll shrink. We'll always try and remove + * expired pages but we'll also push into un-expired pages when + * shrink_nr is non-zero and it wasn't satisfied by shrinking the + * initial set of expired pages. + * + * If the drop range is specified then we drop any pages that intersect + * with the range and which were not read from the current roots. + * + * Returns true if we were able to acquire the lock and made changes, + * which is always the case unless the caller asked us to acquire the + * update mutex with trylock. + */ +static bool update_trees(struct super_block *sb, struct wkic_info *winf, + struct list_head *ins_list, unsigned long shrink_nr, + struct scoutfs_key *drop_start, struct scoutfs_key *drop_end, + bool trylock) +{ + struct wkic_page *wpage; + struct wkic_page *tmp; + struct rb_root *root; + unsigned int which; + LIST_HEAD(log_list); + ktime_t expiration; + ktime_t now; + + if (trylock) { + if (!mutex_trylock(&winf->update_mutex)) + return false; + } else { + mutex_lock(&winf->update_mutex); + } + + synchronize_rcu(); + which = !which_reading(winf); + root = &winf->wpage_roots[which]; + now = ktime_get_raw(); + expiration = ktime_add_ms(now, WKIC_CACHE_LIFETIME_MS); + + /* erase expired pages, possibly satisfying nr by shrinking before expiration */ + list_for_each_entry_safe(wpage, tmp, &winf->shrink_list, head) { + if (shrink_nr == 0 && !wpage_expired(wpage, now)) + break; + + trace_wpage(sb, shrinking, which, wpage); + erase_wpage(sb, root, which, wpage); + list_move_tail(&wpage->head, &log_list); + atomic64_dec(&winf->shrink_count); + + if (shrink_nr > 0) + shrink_nr--; + } + + /* drop pages in the drop range outside the current read_seq */ + if (drop_start) + wpage = find_wpage(sb, root, which, drop_start, drop_end, NULL, NULL); + else + wpage = NULL; + for_each_wpage_safe(wpage, tmp, which) { + if (scoutfs_key_compare_ranges(&wpage->start, &wpage->end, drop_start, drop_end)) + break; + trace_wpage(sb, dropping, which, wpage); + erase_wpage(sb, root, which, wpage); + list_move_tail(&wpage->head, &log_list); + atomic64_dec(&winf->shrink_count); + } + + /* try to insert new pages, rarely freed if expired or overlap with newer */ + list_for_each_entry_safe(wpage, tmp, ins_list, head) { + list_del_init(&wpage->head); + BUG_ON(!RB_EMPTY_NODE(&wpage->nodes[0])); + BUG_ON(!RB_EMPTY_NODE(&wpage->nodes[1])); + + wpage->expiration = expiration; + + if (!try_insert_wpage(sb, winf, root, which, now, wpage, &log_list)) + free_wpage(sb, wpage); + } + + /* flip reading and wait to drain */ + set_reading(winf, which); + synchronize_rcu(); + which = !which; + root = &winf->wpage_roots[which]; + + /* bring trees in sync by replaying changes to logged pages */ + list_for_each_entry_safe(wpage, tmp, &log_list, head) { + trace_wpage(sb, replaying, which, wpage); + if (RB_EMPTY_NODE(&wpage->nodes[which])) { + rb_link_node(&wpage->nodes[which], wpage->ins_parent, wpage->ins_node); + rb_insert_color(&wpage->nodes[which], root); + wpage->ins_parent = NULL; + wpage->ins_node = NULL; + list_move_tail(&wpage->head, &winf->shrink_list); + atomic64_inc(&winf->shrink_count); + } else { + list_del_init(&wpage->head); + erase_wpage(sb, root, which, wpage); + free_wpage(sb, wpage); + } + } + + /* and finally let readers see stable tree */ + set_reading(winf, which); + + mutex_unlock(&winf->update_mutex); + return true; +} + +static unsigned long wkic_shrink_count(struct shrinker *shrink, struct shrink_control *sc) +{ + struct wkic_info *winf = KC_SHRINKER_CONTAINER_OF(shrink, struct wkic_info); + + return shrinker_min_long(atomic64_read(&winf->shrink_count)); +} + +static unsigned long wkic_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) +{ + struct wkic_info *winf = KC_SHRINKER_CONTAINER_OF(shrink, struct wkic_info); + struct super_block *sb = winf->sb; + unsigned long freed = 0; + unsigned long before; + unsigned long after; + LIST_HEAD(empty_list); + + if (sc->nr_to_scan > 0) { + before = wkic_shrink_count(shrink, sc); + update_trees(sb, winf, &empty_list, sc->nr_to_scan, NULL, NULL, true); + after = wkic_shrink_count(shrink, sc); + if (before > after) + freed = before - after; + } + + return freed; +} + +/* + * Search for and return the item with the given key. The caller can + * also ask us to set a pointer to the next item after the search key or + * to give them the final parent and node pointer for insertion. + */ +static struct wkic_item *find_witem(struct rb_root *root, struct scoutfs_key *key, + struct wkic_item **next, struct rb_node **parent_ret, + struct rb_node ***node_ret) +{ + struct rb_node **node = &root->rb_node; + struct rb_node *parent = NULL; + struct wkic_item *witem; + int cmp; + + if (next) + *next = NULL; + + while (*node) { + parent = *node; + witem = container_of(*node, struct wkic_item, node); + + cmp = scoutfs_key_compare(key, &witem->key); + if (cmp < 0) { + if (next) + *next = witem; + node = &(*node)->rb_left; + } else if (cmp > 0) { + node = &(*node)->rb_right; + } else { + return witem; + } + } + + if (parent_ret) { + *parent_ret = parent; + *node_ret = node; + } + + return NULL; +} + +/* + * Add each item from the persistent btree blocks into our private + * rbtree. We have to keep deleted items so that they can override + * older items but they'll be dropped as we copy into the final cached + * pages. + * + * xattr totl items are special because we need visibility into multiple + * versions of the totals before we resolve them into a consistent + * total. We transform their value payload into the totl_merge struct + * as we read and convert it back into the raw form once we've read all + * the items and copy it into the cached pages. + */ +static int read_items_cb(struct super_block *sb, struct scoutfs_key *key, u64 seq, u8 flags, + void *val, int val_len, int fic, void *arg) +{ + const bool is_totl = key->sk_zone == SCOUTFS_XATTR_TOTL_ZONE; + struct scoutfs_totl_merging *merg; + struct rb_root *root = arg; + struct wkic_item *witem; + struct wkic_item *found; + struct rb_node *parent = NULL; + struct rb_node **node = NULL; + int ret; + + found = find_witem(root, key, NULL, &parent, &node); + if (found) { + if (is_totl) { + merg = (void *)found->val; + scoutfs_totl_merge_contribute(merg, seq, flags, val, val_len, fic); + ret = 0; + goto out; + } + + if (found->seq >= seq) { + ret = 0; + goto out; + } + + /* caller's item is newer, will replace and free found below */ + } + + if (is_totl) { + if (val_len != sizeof(struct scoutfs_xattr_totl_val)) { + ret = -EIO; + goto out; + } + val_len = sizeof(struct scoutfs_totl_merging); + } + + witem = kmalloc(offsetof(struct wkic_item, val[val_len]), GFP_NOFS); + if (!witem) { + ret = -ENOMEM; + goto out; + } + + witem->key = *key; + witem->seq = seq; + witem->val_len = val_len; + witem->flags = flags; + if (is_totl) { + merg = (void *)witem->val; + scoutfs_totl_merge_init(merg); + scoutfs_totl_merge_contribute(merg, seq, flags, val, val_len, fic); + } else if (val_len) { + memcpy(witem->val, val, val_len); + } + + if (found) { + rb_replace_node(&found->node, &witem->node, root); + kfree(found); + } else { + rb_link_node(&witem->node, parent, node); + rb_insert_color(&witem->node, root); + } + + ret = 0; +out: + return ret; +} + +static void fill_page_items(struct super_block *sb, struct wkic_page *wpage, struct rb_root *root) +{ + struct scoutfs_xattr_totl_val *tval; + struct scoutfs_totl_merging *merg; + struct wkic_item *pg_item; + struct wkic_item *witem; + struct wkic_item *tmp; + struct rb_node *parent; + struct rb_node **node; + unsigned int bytes; + u64 total; + u64 count; + + pg_item = (void*)wpage + ALIGN(sizeof(struct wkic_page), ARCH_KMALLOC_MINALIGN); + parent = NULL; + node = &wpage->item_root.rb_node; + + for (witem = first_witem(root); witem && ((tmp = next_witem(witem)), 1); witem = tmp) { + + /* free deleted items */ + if (witem->flags & SCOUTFS_ITEM_FLAG_DELETION) { + rb_erase(&witem->node, root); + kfree(witem); + continue; + } + + /* + * Transform the totl merge back into the raw totals, + * freeing it if it comes to zero. + */ + if (witem->key.sk_zone == SCOUTFS_XATTR_TOTL_ZONE && + witem->val_len == sizeof(struct scoutfs_totl_merging)) { + BUILD_BUG_ON(sizeof(struct scoutfs_xattr_totl_val) > + sizeof(struct scoutfs_totl_merging)); + merg = (void *)witem->val; + scoutfs_totl_merge_resolve(merg, &total, &count); + tval = (void *)witem->val; + tval->total = cpu_to_le64(total); + tval->count = cpu_to_le64(count); + witem->val_len = sizeof(struct scoutfs_xattr_totl_val); + + if (tval->total == 0 && tval->count == 0) { + rb_erase(&witem->node, root); + kfree(witem); + continue; + } + } + + bytes = ALIGN(offsetof(struct wkic_item, val[witem->val_len]), + ARCH_KMALLOC_MINALIGN); + if ((long)pg_item - (long)wpage + bytes > PAGE_SIZE) { + wpage->end = witem->key; + scoutfs_key_dec(&wpage->end); + break; + } + + /* add item to end of page */ + pg_item->key = witem->key; + pg_item->seq = witem->seq; + pg_item->val_len = witem->val_len; + pg_item->flags = witem->flags; + if (witem->val_len) + memcpy(pg_item->val, witem->val, witem->val_len); + + /* always inserting greatest item into page */ + rb_link_node(&pg_item->node, parent, node); + rb_insert_color(&pg_item->node, &wpage->item_root); + parent = &pg_item->node; + node = &pg_item->node.rb_right; + + pg_item = (void*)pg_item + bytes; + + rb_erase(&witem->node, root); + kfree(witem); + } + + trace_wpage(sb, filled, -1, wpage); +} + +static void free_item_tree(struct rb_root *root) +{ + struct wkic_item *witem; + struct wkic_item *tmp; + + rbtree_postorder_for_each_entry_safe(witem, tmp, root, node) + kfree(witem); +} + +static void free_page_list(struct super_block *sb, struct list_head *list) +{ + struct wkic_page *wpage; + struct wkic_page *tmp; + + list_for_each_entry_safe(wpage, tmp, list, head) { + list_del_init(&wpage->head); + free_wpage(sb, wpage); + } +} + +/* + * We tie exclusive sampling of the forest btree roots to an increasing + * read_seq number so that we can compare the age of the items in cached + * pages. Only one request to refresh the roots is in progress at a + * time. This is the slow path that's only used when the cache isn't + * populated and the roots aren't cached. The root request is fast + * enough, especially compared to the resulting item reading IO, that we + * don't mind hiding it behind a trivial mutex. + */ +static int get_roots(struct super_block *sb, struct wkic_info *winf, + struct scoutfs_net_roots *roots_ret, u64 *read_seq, bool force_new) +{ + struct scoutfs_net_roots roots; + int ret; + + mutex_lock(&winf->roots_mutex); + + if (force_new || ktime_before(winf->roots_expire, ktime_get_raw())) { + ret = scoutfs_client_get_roots(sb, &roots); + if (ret < 0) + goto out; + + winf->roots = roots; + winf->roots_read_seq++; + winf->roots_expire = ktime_add_ms(ktime_get_raw(), WKIC_CACHE_LIFETIME_MS); + } + + *roots_ret = winf->roots; + *read_seq = winf->roots_read_seq; + ret = 0; +out: + mutex_unlock(&winf->roots_mutex); + + return ret; +} + +static void invalidate_cached_roots(struct wkic_info *winf) +{ + mutex_lock(&winf->roots_mutex); + winf->roots_expire = ktime_sub_ns(ktime_get_raw(), WKIC_CACHE_LIFETIME_MS * NSEC_PER_MSEC); + mutex_unlock(&winf->roots_mutex); +} + +/* + * Populate the cache when a caller finds that their key isn't cached. + * This is the expensive slow path that waits for many dependent block + * IO reads. + * + * We read items from the forest of btrees but we control the version of + * the roots we read from so that we can order pages and insure that + * cached items strictly advance over time. We need to carefully handle + * reading from stale blocks -- we need to force getting new roots and + * still return a hard error if the block still look bad. + * + * As our callback sees items we merge them into a private rbtree. We + * copy the eventual set of read items into pages and return once + * they're inserted into the trees. + */ +static int insert_read_pages(struct super_block *sb, struct wkic_info *winf, + struct scoutfs_key *key, struct scoutfs_key *range_start, + struct scoutfs_key *range_end) +{ + struct scoutfs_net_roots roots; + struct rb_root root = RB_ROOT; + struct scoutfs_key pg_start; + DECLARE_SAVED_REFS(saved); + struct scoutfs_key start; + struct scoutfs_key end; + struct wkic_page *wpage; + LIST_HEAD(pages); + u64 read_seq; + int ret; + + ret = 0; +retry_stale: + ret = get_roots(sb, winf, &roots, &read_seq, ret == -ESTALE); + if (ret < 0) + goto out; + + start = *range_start; + end = *range_end; + ret = scoutfs_forest_read_items_roots(sb, &roots, key, range_start, &start, &end, + read_items_cb, &root); + trace_scoutfs_wkic_read_items(sb, key, &start, &end); + ret = scoutfs_block_check_stale(sb, ret, &saved, &roots.fs_root.ref, &roots.logs_root.ref); + if (ret < 0) { + if (ret == -ESTALE) + goto retry_stale; + goto out; + } + + /* pack all the read items into pages, can make empty range page */ + pg_start = start; + do { + wpage = alloc_wpage(sb, &pg_start, &end, read_seq); + if (!wpage) { + ret = -ENOMEM; + goto out; + } + list_add_tail(&wpage->head, &pages); + + fill_page_items(sb, wpage, &root); + + pg_start = wpage->end; + scoutfs_key_inc(&pg_start); + + } while (!RB_EMPTY_ROOT(&root)); + + update_trees(sb, winf, &pages, 0, NULL, NULL, false); + ret = 0; +out: + free_item_tree(&root); + free_page_list(sb, &pages); + + return ret; +} + +/* + * Search for the page that contains the starting key and call the + * callback on items up to and including the last key. Iteration will + * stop if the callback returns anything other than -EAGAIN. If there + * isn't a page containing the key then we return -ENODATA, expecting + * the caller to read pages that contain the key and try again. + * + * If the caller wants stable iteration then we use their stable_seq storage + * to ensure that all the items come from the same root. We return + * -ESTALE if items were read from different roots and might be an + * inconsistent set of items. + */ +static int iterate_page_items(struct super_block *sb, struct wkic_info *winf, + struct scoutfs_key *key, struct scoutfs_key *last, + ktime_t now, u64 *stable_seq, wkic_iter_cb_t cb, void *cb_arg) +{ + struct wkic_page *wpage; + struct wkic_item *witem; + struct wkic_item *next; + struct rb_root *root; + unsigned int which; + int ret; + + rcu_read_lock(); + which = which_reading(winf); + root = &winf->wpage_roots[which]; + + wpage = find_wpage(sb, root, which, key, key, NULL, NULL); + if (!wpage || wpage_expired(wpage, now)) { + ret = -ENODATA; + goto out; + } + + if (stable_seq) { + if (*stable_seq == 0) { + *stable_seq = wpage->read_seq; + } else if (wpage->read_seq != *stable_seq) { + ret = -ESTALE; + goto out; + } + } + + witem = find_witem(&wpage->item_root, key, &next, NULL, NULL) ?: next; + while (witem) { + if (scoutfs_key_compare(&witem->key, last) > 0) + break; + + ret = cb(&witem->key, witem->val, witem->val_len, cb_arg); + if (WARN_ON_ONCE(ret == -ENODATA)) + ret = -EINVAL; + if (ret != -EAGAIN) + goto out; + + witem = next_witem(witem); + } + + if (scoutfs_key_compare(&wpage->end, last) >= 0) { + /* done if the page covered the end of the range */ + ret = 0; + } else { + /* keep iterating into the next page */ + *key = wpage->end; + scoutfs_key_inc(key); + ret = -EAGAIN; + } +out: + rcu_read_unlock(); + return ret; +} + +/* + * Call the caller's callback on all cached items from and including the + * starting key until and including the last key. The callback can stop + * iterating by returning anything other than -EAGAIN which will be + * returned to the caller. This can return hard allocation and IO + * errors. + * + * If the read is stable then we make sure that all the items came from + * pages with the same read_seq which means that they were read from + * the same root and are a consistent set of items. + */ +static int iterate_and_read(struct super_block *sb, struct scoutfs_key *key, + struct scoutfs_key *last, struct scoutfs_key *range_start, + struct scoutfs_key *range_end, u64 *stable_seq, + wkic_iter_cb_t cb, void *cb_arg) +{ + struct wkic_info *winf = SCOUTFS_SB(sb)->wkic_info; + struct scoutfs_key pos = *key; + ktime_t now = ktime_get_raw(); + int ret; + + do { + ret = iterate_page_items(sb, winf, &pos, last, now, stable_seq, cb, cb_arg); + if (ret == -ENODATA) { + ret = insert_read_pages(sb, winf, &pos, range_start, range_end); + if (ret == 0) { + now = ktime_get_raw(); + ret = -EAGAIN; + } + } + } while (ret == -EAGAIN); + + return ret; +} + +int scoutfs_wkic_iterate(struct super_block *sb, struct scoutfs_key *key, struct scoutfs_key *last, + struct scoutfs_key *range_start, struct scoutfs_key *range_end, + wkic_iter_cb_t cb, void *cb_arg) +{ + return iterate_and_read(sb, key, last, range_start, range_end, NULL, cb, cb_arg); +} + +/* + * If we hit a stable inconsistency then we drop the pages in the range + * that are not from the current read_seq and tell the caller to + * retry. We can't retry ourselves because we can't claw back items + * that we gave to the callback before we found the inconsistency. + */ +int scoutfs_wkic_iterate_stable(struct super_block *sb, struct scoutfs_key *key, + struct scoutfs_key *last, struct scoutfs_key *range_start, + struct scoutfs_key *range_end, wkic_iter_cb_t cb, void *cb_arg) +{ + struct wkic_info *winf = SCOUTFS_SB(sb)->wkic_info; + LIST_HEAD(empty_list); + u64 stable_seq = 0; + int ret; + + ret = iterate_and_read(sb, key, last, range_start, range_end, &stable_seq, cb, cb_arg); + if (ret == -ESTALE) + update_trees(sb, winf, &empty_list, 0, key, last, false); + + return ret; +} + +static ssize_t wkic_drop_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) +{ + return 0; +} + +/* + * Drop all pages in the weak item cache. We don't strictly drain the + * cache size to zero but make as many attempts as would be needed to + * fully drain the cache given its size as the call was made. We drop + * the cached roots so that the next cache read will get the current + * version of the persistent structures. This is only a best effort for + * testing in reasonably controlled circumstances. + */ +static ssize_t wkic_drop_write(struct file *file, const char __user *buf, size_t size, loff_t *ppos) +{ + struct wkic_info *winf = file_inode(file)->i_private; + struct super_block *sb = winf->sb; + LIST_HEAD(empty_list); + u64 iter; + + invalidate_cached_roots(winf); + + iter = (atomic64_read(&winf->shrink_count) + 1023) >> 10; + while (iter--) + update_trees(sb, winf, &empty_list, 1024, NULL, NULL, false); + + return size; +} + +static const struct file_operations wkic_drop_fops = { + .read = wkic_drop_read, + .write = wkic_drop_write, +}; + +int scoutfs_wkic_setup(struct super_block *sb) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + struct wkic_info *winf; + + winf = kzalloc(sizeof(struct wkic_info), GFP_KERNEL); + if (!winf) + return -ENOMEM; + + winf->wpage_roots[0] = RB_ROOT; + winf->wpage_roots[1] = RB_ROOT; + mutex_init(&winf->update_mutex); + INIT_LIST_HEAD(&winf->shrink_list); + atomic64_set(&winf->shrink_count, 0); + mutex_init(&winf->roots_mutex); + winf->roots_read_seq = 1; /* 0 is unused */ + + invalidate_cached_roots(winf); + + winf->drop_dentry = debugfs_create_file("drop_weak_item_cache", S_IFREG|S_IRUSR, + sbi->debug_root, winf, &wkic_drop_fops); + if (!winf->drop_dentry) { + kfree(winf); + return -ENOMEM; + } + + winf->sb = sb; + KC_INIT_SHRINKER_FUNCS(&winf->shrinker, wkic_shrink_count, wkic_shrink_scan); + KC_REGISTER_SHRINKER(&winf->shrinker); + + sbi->wkic_info = winf; + return 0; +} + +/* + * Make sure that the two nodes in a page have the same pointers set. + * It's a weak check, but it's trivial and will catch wild divergence + * between the trees. + */ +static void bug_on_differing_pointers(struct rb_node *zero, struct rb_node *one) +{ + BUG_ON(!rb_parent(zero) != !rb_parent(one)); + BUG_ON(!zero->rb_left != !one->rb_left); + BUG_ON(!zero->rb_right != !one->rb_right); +} + +void scoutfs_wkic_destroy(struct super_block *sb) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + struct wkic_info *winf = sbi->wkic_info; + struct wkic_page *wpage; + struct wkic_page *tmp; + LIST_HEAD(list); + + if (winf) { + debugfs_remove(winf->drop_dentry); + KC_UNREGISTER_SHRINKER(&winf->shrinker); + + /* trees are in sync so tearing down one frees all pages */ + rbtree_postorder_for_each_entry_safe(wpage, tmp, &winf->wpage_roots[0], nodes[0]) { + bug_on_differing_pointers(&wpage->nodes[0], &wpage->nodes[1]); + RB_CLEAR_NODE(&wpage->nodes[0]); + RB_CLEAR_NODE(&wpage->nodes[1]); + list_del_init(&wpage->head); + free_wpage(sb, wpage); + } + + BUG_ON(!list_empty(&winf->shrink_list)); + + kfree(winf); + sbi->wkic_info = NULL; + } +} diff --git a/kmod/src/wkic.h b/kmod/src/wkic.h new file mode 100644 index 00000000..6914bac5 --- /dev/null +++ b/kmod/src/wkic.h @@ -0,0 +1,19 @@ +#ifndef _SCOUTFS_WKIC_H_ +#define _SCOUTFS_WKIC_H_ + +#include "format.h" + +typedef int (*wkic_iter_cb_t)(struct scoutfs_key *key, void *val, unsigned int val_len, + void *cb_arg); + +int scoutfs_wkic_iterate(struct super_block *sb, struct scoutfs_key *key, struct scoutfs_key *last, + struct scoutfs_key *range_start, struct scoutfs_key *range_end, + wkic_iter_cb_t cb, void *cb_arg); +int scoutfs_wkic_iterate_stable(struct super_block *sb, struct scoutfs_key *key, + struct scoutfs_key *last, struct scoutfs_key *range_start, + struct scoutfs_key *range_end, wkic_iter_cb_t cb, void *cb_arg); + +int scoutfs_wkic_setup(struct super_block *sb); +void scoutfs_wkic_destroy(struct super_block *sb); + +#endif diff --git a/kmod/src/xattr.c b/kmod/src/xattr.c index fa418b8c..a13f3d19 100644 --- a/kmod/src/xattr.c +++ b/kmod/src/xattr.c @@ -81,7 +81,20 @@ static void init_xattr_key(struct scoutfs_key *key, u64 ino, u32 name_hash, #define SCOUTFS_XATTR_PREFIX "scoutfs." #define SCOUTFS_XATTR_PREFIX_LEN (sizeof(SCOUTFS_XATTR_PREFIX) - 1) +/* + * We could have hidden the logic that needs this in a user-prefix + * specific .set handler, but I wanted to make sure that we always + * applied that logic from any call chains to _xattr_set. The + * additional strcmp isn't so expensive given all the rest of the work + * we're doing in here. + */ +static inline bool is_user(const char *name) +{ + return !strncmp(name, XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN); +} + #define HIDE_TAG "hide." +#define INDX_TAG "indx." #define SRCH_TAG "srch." #define TOTL_TAG "totl." #define TAG_LEN (sizeof(HIDE_TAG) - 1) @@ -103,6 +116,9 @@ int scoutfs_xattr_parse_tags(const char *name, unsigned int name_len, if (!strncmp(name, HIDE_TAG, TAG_LEN)) { if (++tgs->hide == 0) return -EINVAL; + } else if (!strncmp(name, INDX_TAG, TAG_LEN)) { + if (++tgs->indx == 0) + return -EINVAL; } else if (!strncmp(name, SRCH_TAG, TAG_LEN)) { if (++tgs->srch == 0) return -EINVAL; @@ -540,47 +556,57 @@ static int parse_totl_u64(const char *s, int len, u64 *res) } /* - * non-destructive relatively quick parse of the last 3 dotted u64s that - * make up the name of the xattr total. -EINVAL is returned if there - * are anything but 3 valid u64 encodings between single dots at the end - * of the name. + * non-destructive relatively quick parse of final dotted u64s in an + * xattr name. If the required number of values are found then we + * return the number of bytes in the name that are not the final dotted + * u64s with their dots. -EINVAL is returned if we didn't find the + * required number of values. */ -static int parse_totl_key(struct scoutfs_key *key, const char *name, int name_len) +static int parse_dotted_u64s(u64 *u64s, int nr, const char *name, int name_len) { - u64 tot_name[3]; int end = name_len; - int nr = 0; int len; int ret; int i; + int u; /* parse name elements in reserve order from end of xattr name string */ - for (i = name_len - 1; i >= 0 && nr < ARRAY_SIZE(tot_name); i--) { + for (u = nr - 1, i = name_len - 1; u >= 0 && i >= 0; i--) { if (name[i] != '.') continue; len = end - (i + 1); - ret = parse_totl_u64(&name[i + 1], len, &tot_name[nr]); + ret = parse_totl_u64(&name[i + 1], len, &u64s[u]); if (ret < 0) goto out; end = i; - nr++; + u--; } - if (nr == ARRAY_SIZE(tot_name)) { - /* swap to account for parsing in reverse */ - swap(tot_name[0], tot_name[2]); - scoutfs_xattr_init_totl_key(key, tot_name); - ret = 0; - } else { + if (u == -1) + ret = end; + else ret = -EINVAL; - } out: return ret; } +static int parse_totl_key(struct scoutfs_key *key, const char *name, int name_len) +{ + u64 u64s[3]; + int ret; + + ret = parse_dotted_u64s(u64s, ARRAY_SIZE(u64s), name, name_len); + if (ret >= 0) { + scoutfs_xattr_init_totl_key(key, u64s); + ret = 0; + } + + return ret; +} + static int apply_totl_delta(struct super_block *sb, struct scoutfs_key *key, struct scoutfs_xattr_totl_val *tval, struct scoutfs_lock *lock) { @@ -607,6 +633,72 @@ int scoutfs_xattr_combine_totl(void *dst, int dst_len, void *src, int src_len) return SCOUTFS_DELTA_COMBINED; } +void scoutfs_xattr_indx_get_range(struct scoutfs_key *start, struct scoutfs_key *end) +{ + scoutfs_key_set_zeros(start); + start->sk_zone = SCOUTFS_XATTR_INDX_ZONE; + scoutfs_key_set_ones(end); + end->sk_zone = SCOUTFS_XATTR_INDX_ZONE; +} + +/* + * .indx. keys are a bit funny because we're iterating over index keys + * by major:minor:inode:xattr_id. That doesn't map nicely to the + * comparison precedence of the key fields. We have to mess around a + * little bit to get the major into the most significant key bits and + * the low bits of xattr id into the least significant key bits. + */ +void scoutfs_xattr_init_indx_key(struct scoutfs_key *key, u8 major, u64 minor, u64 ino, u64 xid) +{ + scoutfs_key_set_zeros(key); + key->sk_zone = SCOUTFS_XATTR_INDX_ZONE; + + key->_sk_first = cpu_to_le64(((u64)major << 56) | (minor >> 8)); + key->_sk_second = cpu_to_le64((minor << 56) | (ino >> 8)); + key->_sk_third = cpu_to_le64((ino << 56) | (xid >> 8)); + key->_sk_fourth = xid & 0xff; +} + +void scoutfs_xattr_get_indx_key(struct scoutfs_key *key, u8 *major, u64 *minor, u64 *ino, u64 *xid) +{ + *major = le64_to_cpu(key->_sk_first) >> 56; + *minor = (le64_to_cpu(key->_sk_first) << 8) | (le64_to_cpu(key->_sk_second) >> 56); + *ino = (le64_to_cpu(key->_sk_second) << 8) | (le64_to_cpu(key->_sk_third) >> 56); + *xid = (le64_to_cpu(key->_sk_third) << 8) | key->_sk_fourth; +} + +void scoutfs_xattr_set_indx_key_xid(struct scoutfs_key *key, u64 xid) +{ + u8 major; + u64 minor; + u64 ino; + u64 dummy; + + scoutfs_xattr_get_indx_key(key, &major, &minor, &ino, &dummy); + scoutfs_xattr_init_indx_key(key, major, minor, ino, xid); +} + +/* + * This initial parsing of the name doesn't yet have access to an xattr + * id to put in the key. That's added later as the existing xattr is + * found or a new xattr's id is allocated. + */ +static int parse_indx_key(struct scoutfs_key *key, const char *name, int name_len, u64 ino) +{ + u64 u64s[2]; + int ret; + + ret = parse_dotted_u64s(u64s, ARRAY_SIZE(u64s), name, name_len); + if (ret < 0) + return ret; + + if (u64s[0] > U8_MAX) + return -EINVAL; + + scoutfs_xattr_init_indx_key(key, u64s[0], u64s[1], ino, 0); + return 0; +} + /* * The confusing swiss army knife of creating, modifying, and deleting * xattrs. @@ -627,7 +719,7 @@ int scoutfs_xattr_combine_totl(void *dst, int dst_len, void *src, int src_len) int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_len, const void *value, size_t size, int flags, const struct scoutfs_xattr_prefix_tags *tgs, - struct scoutfs_lock *lck, struct scoutfs_lock *totl_lock, + struct scoutfs_lock *lck, struct scoutfs_lock *tag_lock, struct list_head *ind_locks) { struct scoutfs_inode_info *si = SCOUTFS_I(inode); @@ -635,10 +727,11 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ const u64 ino = scoutfs_ino(inode); struct scoutfs_xattr_totl_val tval = {0,}; struct scoutfs_xattr *xat = NULL; - struct scoutfs_key totl_key; + struct scoutfs_key tag_key; struct scoutfs_key key; bool undo_srch = false; bool undo_totl = false; + bool undo_indx = false; u8 found_parts; unsigned int xat_bytes_totl; unsigned int xat_bytes; @@ -651,7 +744,8 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ trace_scoutfs_xattr_set(sb, name_len, value, size, flags); - if (WARN_ON_ONCE(tgs->totl && !totl_lock)) + if (WARN_ON_ONCE(tgs->totl && tgs->indx) || + WARN_ON_ONCE((tgs->totl | tgs->indx) && !tag_lock)) return -EINVAL; /* mirror the syscall's errors for large names and values */ @@ -664,10 +758,22 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ (flags & ~(XATTR_CREATE | XATTR_REPLACE))) return -EINVAL; - if ((tgs->hide | tgs->srch | tgs->totl) && !capable(CAP_SYS_ADMIN)) + if ((tgs->hide | tgs->indx | tgs->srch | tgs->totl) && !capable(CAP_SYS_ADMIN)) return -EPERM; - if (tgs->totl && ((ret = parse_totl_key(&totl_key, name, name_len)) != 0)) + if (tgs->totl && ((ret = parse_totl_key(&tag_key, name, name_len)) != 0)) + return ret; + + if (tgs->indx && + (ret = scoutfs_fmt_vers_unsupported(sb, SCOUTFS_FORMAT_VERSION_FEAT_INDX_TAG))) + return ret; + + if (tgs->indx && ((ret = parse_indx_key(&tag_key, name, name_len, ino)) != 0)) + return ret; + + /* retention blocks user. xattr modification, all else allowed */ + ret = scoutfs_inode_check_retention(inode); + if (ret < 0 && is_user(name)) return ret; /* allocate enough to always read an existing xattr's totl */ @@ -708,6 +814,12 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ /* found fields in key will also be used */ found_parts = ret >= 0 ? xattr_nr_parts(xat) : 0; + /* use existing xattr's id or allocate new when creating */ + if (found_parts) + id = le64_to_cpu(key.skx_id); + else if (value) + id = si->next_xattr_id++; + if (found_parts && tgs->totl) { /* parse old totl value before we clobber xat buf */ val_len = ret - offsetof(struct scoutfs_xattr, name[xat->name_len]); @@ -718,12 +830,25 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ le64_add_cpu(&tval.total, -total); } + /* + * indx xattrs don't have a value. After returning an error for + * non-zero val length or short circuiting modifying with the + * same 0 length, all we're left with is creating or deleting + * the xattr. + */ + if (tgs->indx) { + if (size != 0) { + ret = -EINVAL; + goto out; + } + if (found_parts && value) { + ret = 0; + goto out; + } + } + /* prepare the xattr header, name, and start of value in first item */ if (value) { - if (found_parts) - id = le64_to_cpu(key.skx_id); - else - id = si->next_xattr_id++; xat->name_len = name_len; xat->val_len = cpu_to_le16(size); memset(xat->__pad, 0, sizeof(xat->__pad)); @@ -741,9 +866,18 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ le64_add_cpu(&tval.total, total); } + if (tgs->indx) { + scoutfs_xattr_set_indx_key_xid(&tag_key, id); + if (value) + ret = scoutfs_item_create_force(sb, &tag_key, NULL, 0, tag_lock, NULL); + else + ret = scoutfs_item_delete_force(sb, &tag_key, tag_lock, NULL); + if (ret < 0) + goto out; + undo_indx = true; + } + if (tgs->srch && !(found_parts && value)) { - if (found_parts) - id = le64_to_cpu(key.skx_id); hash = scoutfs_hash64(name, name_len); ret = scoutfs_forest_srch_add(sb, hash, ino, id); if (ret < 0) @@ -752,7 +886,7 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ } if (tgs->totl) { - ret = apply_totl_delta(sb, &totl_key, &tval, totl_lock); + ret = apply_totl_delta(sb, &tag_key, &tval, tag_lock); if (ret < 0) goto out; undo_totl = true; @@ -777,6 +911,13 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ ret = 0; out: + if (ret < 0 && undo_indx) { + if (value) + err = scoutfs_item_delete_force(sb, &tag_key, tag_lock, NULL); + else + err = scoutfs_item_create_force(sb, &tag_key, NULL, 0, tag_lock, NULL); + BUG_ON(err); /* inconsistent */ + } if (ret < 0 && undo_srch) { err = scoutfs_forest_srch_add(sb, hash, ino, id); BUG_ON(err); @@ -785,7 +926,7 @@ out: /* _delta() on dirty items shouldn't fail */ tval.total = cpu_to_le64(-le64_to_cpu(tval.total)); tval.count = cpu_to_le64(-le64_to_cpu(tval.count)); - err = apply_totl_delta(sb, &totl_key, &tval, totl_lock); + err = apply_totl_delta(sb, &tag_key, &tval, tag_lock); BUG_ON(err); } @@ -801,7 +942,7 @@ static int scoutfs_xattr_set(struct dentry *dentry, const char *name, const void struct inode *inode = dentry->d_inode; struct super_block *sb = inode->i_sb; struct scoutfs_xattr_prefix_tags tgs; - struct scoutfs_lock *totl_lock = NULL; + struct scoutfs_lock *tag_lock = NULL; struct scoutfs_lock *lck = NULL; size_t name_len = strlen(name); LIST_HEAD(ind_locks); @@ -816,8 +957,11 @@ static int scoutfs_xattr_set(struct dentry *dentry, const char *name, const void if (ret) goto unlock; - if (tgs.totl) { - ret = scoutfs_lock_xattr_totl(sb, SCOUTFS_LOCK_WRITE_ONLY, 0, &totl_lock); + if (tgs.totl || tgs.indx) { + if (tgs.totl) + ret = scoutfs_lock_xattr_totl(sb, SCOUTFS_LOCK_WRITE_ONLY, 0, &tag_lock); + else + ret = scoutfs_lock_xattr_indx(sb, SCOUTFS_LOCK_WRITE_ONLY, 0, &tag_lock); if (ret) goto unlock; } @@ -836,7 +980,7 @@ retry: goto release; ret = scoutfs_xattr_set_locked(dentry->d_inode, name, name_len, value, size, flags, &tgs, - lck, totl_lock, &ind_locks); + lck, tag_lock, &ind_locks); if (ret == 0) scoutfs_update_inode_item(inode, lck, &ind_locks); @@ -845,7 +989,7 @@ release: scoutfs_inode_index_unlock(sb, &ind_locks); unlock: scoutfs_unlock(sb, lck, SCOUTFS_LOCK_WRITE); - scoutfs_unlock(sb, totl_lock, SCOUTFS_LOCK_WRITE_ONLY); + scoutfs_unlock(sb, tag_lock, SCOUTFS_LOCK_WRITE_ONLY); return ret; } @@ -1055,14 +1199,15 @@ int scoutfs_xattr_drop(struct super_block *sb, u64 ino, { struct scoutfs_xattr_prefix_tags tgs; struct scoutfs_xattr *xat = NULL; - struct scoutfs_lock *totl_lock = NULL; + struct scoutfs_lock *tag_lock = NULL; struct scoutfs_xattr_totl_val tval; - struct scoutfs_key totl_key; + struct scoutfs_key tag_key; struct scoutfs_key last; struct scoutfs_key key; bool release = false; unsigned int bytes; unsigned int val_len; + u8 locked_zone = 0; void *value; u64 total; u64 hash; @@ -1108,16 +1253,32 @@ int scoutfs_xattr_drop(struct super_block *sb, u64 ino, goto out; } - ret = parse_totl_key(&totl_key, xat->name, xat->name_len) ?: + ret = parse_totl_key(&tag_key, xat->name, xat->name_len) ?: parse_totl_u64(value, val_len, &total); if (ret < 0) break; } - if (tgs.totl && totl_lock == NULL) { - ret = scoutfs_lock_xattr_totl(sb, SCOUTFS_LOCK_WRITE_ONLY, 0, &totl_lock); + if (tgs.indx) { + ret = parse_indx_key(&tag_key, xat->name, xat->name_len, ino); + if (ret < 0) + goto out; + } + + if ((tgs.totl || tgs.indx) && locked_zone != tag_key.sk_zone) { + if (tag_lock) { + scoutfs_unlock(sb, tag_lock, SCOUTFS_LOCK_WRITE_ONLY); + tag_lock = NULL; + } + if (tgs.totl) + ret = scoutfs_lock_xattr_totl(sb, SCOUTFS_LOCK_WRITE_ONLY, 0, + &tag_lock); + else + ret = scoutfs_lock_xattr_indx(sb, SCOUTFS_LOCK_WRITE_ONLY, 0, + &tag_lock); if (ret < 0) break; + locked_zone = tag_key.sk_zone; } ret = scoutfs_hold_trans(sb, false); @@ -1140,7 +1301,13 @@ int scoutfs_xattr_drop(struct super_block *sb, u64 ino, if (tgs.totl) { tval.total = cpu_to_le64(-total); tval.count = cpu_to_le64(-1LL); - ret = apply_totl_delta(sb, &totl_key, &tval, totl_lock); + ret = apply_totl_delta(sb, &tag_key, &tval, tag_lock); + if (ret < 0) + break; + } + + if (tgs.indx) { + ret = scoutfs_item_delete_force(sb, &tag_key, tag_lock, NULL); if (ret < 0) break; } @@ -1153,7 +1320,7 @@ int scoutfs_xattr_drop(struct super_block *sb, u64 ino, if (release) scoutfs_release_trans(sb); - scoutfs_unlock(sb, totl_lock, SCOUTFS_LOCK_WRITE_ONLY); + scoutfs_unlock(sb, tag_lock, SCOUTFS_LOCK_WRITE_ONLY); kfree(xat); out: return ret; diff --git a/kmod/src/xattr.h b/kmod/src/xattr.h index 1cb14566..8785aeda 100644 --- a/kmod/src/xattr.h +++ b/kmod/src/xattr.h @@ -3,6 +3,7 @@ struct scoutfs_xattr_prefix_tags { unsigned long hide:1, + indx:1, srch:1, totl:1; }; @@ -30,4 +31,9 @@ int scoutfs_xattr_parse_tags(const char *name, unsigned int name_len, void scoutfs_xattr_init_totl_key(struct scoutfs_key *key, u64 *name); int scoutfs_xattr_combine_totl(void *dst, int dst_len, void *src, int src_len); +void scoutfs_xattr_indx_get_range(struct scoutfs_key *start, struct scoutfs_key *end); +void scoutfs_xattr_init_indx_key(struct scoutfs_key *key, u8 major, u64 minor, u64 ino, u64 xid); +void scoutfs_xattr_get_indx_key(struct scoutfs_key *key, u8 *major, u64 *minor, u64 *ino, u64 *xid); +void scoutfs_xattr_set_indx_key_xid(struct scoutfs_key *key, u64 xid); + #endif diff --git a/tests/.gitignore b/tests/.gitignore index 1982cd68..b19b962a 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -9,3 +9,4 @@ src/find_xattrs src/stage_tmpfile src/create_xattr_loop src/o_tmpfile_umask +src/o_tmpfile_linkat diff --git a/tests/Makefile b/tests/Makefile index 9de59268..4c61a0b3 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -12,7 +12,8 @@ BIN := src/createmany \ src/find_xattrs \ src/create_xattr_loop \ src/fragmented_data_extents \ - src/o_tmpfile_umask + src/o_tmpfile_umask \ + src/o_tmpfile_linkat DEPS := $(wildcard src/*.d) diff --git a/tests/README.md b/tests/README.md index 68f3b157..4acbdb50 100644 --- a/tests/README.md +++ b/tests/README.md @@ -113,6 +113,7 @@ used during the test. | T\_EX\_META\_DEV | scratch meta bdev | -f | /dev/vdd | | T\_EX\_DATA\_DEV | scratch meta bdev | -e | /dev/vdc | | T\_M[0-9] | mount paths | mounted per run | /mnt/test.[0-9]/ | +| T\_MODULE | built kernel module | created per run | ../kmod/src/..ko | | T\_NR\_MOUNTS | number of mounts | -n | 3 | | T\_O[0-9] | mount options | created per run | -o server\_addr= | | T\_QUORUM | quorum count | -q | 2 | diff --git a/tests/funcs/filter.sh b/tests/funcs/filter.sh index 64f1f7c9..dd840109 100644 --- a/tests/funcs/filter.sh +++ b/tests/funcs/filter.sh @@ -147,6 +147,10 @@ t_filter_dmesg() # ignore systemd-journal rotating re="$re|systemd-journald.*" + # format vers back/compat tries bad mounts + re="$re|scoutfs .* error.*outside of supported version.*" + re="$re|scoutfs .* error.*could not get .*super.*" + egrep -v "($re)" | \ ignore_harmless_unwind_kasan_stack_oob } diff --git a/tests/funcs/fs.sh b/tests/funcs/fs.sh index 080b5c5f..78a8420b 100644 --- a/tests/funcs/fs.sh +++ b/tests/funcs/fs.sh @@ -29,13 +29,12 @@ t_mount_rid() } # -# Output the "f.$fsid.r.$rid" identifier string for the given mount -# number, 0 is used by default if none is specified. +# Output the "f.$fsid.r.$rid" identifier string for the given path +# in a mounted scoutfs volume. # -t_ident() +t_ident_from_mnt() { - local nr="${1:-0}" - local mnt="$(eval echo \$T_M$nr)" + local mnt="$1" local fsid local rid @@ -45,6 +44,38 @@ t_ident() echo "f.${fsid:0:6}.r.${rid:0:6}" } +# +# Output the "f.$fsid.r.$rid" identifier string for the given mount +# number, 0 is used by default if none is specified. +# +t_ident() +{ + local nr="${1:-0}" + local mnt="$(eval echo \$T_M$nr)" + + t_ident_from_mnt "$mnt" +} + +# +# Output the sysfs path for a path in a mounted fs. +# +t_sysfs_path_from_ident() +{ + local ident="$1" + + echo "/sys/fs/scoutfs/$ident" +} + +# +# Output the sysfs path for a path in a mounted fs. +# +t_sysfs_path_from_mnt() +{ + local mnt="$1" + + t_sysfs_path_from_ident $(t_ident_from_mnt $mnt) +} + # # Output the mount's sysfs path, defaulting to mount 0 if none is # specified. @@ -53,7 +84,7 @@ t_sysfs_path() { local nr="$1" - echo "/sys/fs/scoutfs/$(t_ident $nr)" + t_sysfs_path_from_ident $(t_ident $nr) } # diff --git a/tests/golden/format-version-forward-back b/tests/golden/format-version-forward-back new file mode 100644 index 00000000..cc75bfcf --- /dev/null +++ b/tests/golden/format-version-forward-back @@ -0,0 +1,4 @@ +== ensuring utils and module for old versions +== unmounting test fs and removing test module +== testing combinations of old and new format versions +== restoring test module and mount diff --git a/tests/golden/projects b/tests/golden/projects new file mode 100644 index 00000000..4cb6b5ce --- /dev/null +++ b/tests/golden/projects @@ -0,0 +1,24 @@ +== default new files don't have project +0 +== set new project on files and dirs +8675309 +8675309 +== non-root can see id +8675309 +== can use IDs around long width limits +2147483647 +2147483648 +4294967295 +9223372036854775807 +9223372036854775808 +18446744073709551615 +== created files and dirs inherit project id +8675309 +8675309 +== inheritance continues +8675309 +== clearing project id stops inheritance +0 +0 +== o_tmpfile creations inherit dir +8675309 diff --git a/tests/golden/quota b/tests/golden/quota new file mode 100644 index 00000000..2ab746c5 --- /dev/null +++ b/tests/golden/quota @@ -0,0 +1,40 @@ +== prepare dir with write perm for test ids +== test assumes starting with no rules, empty list +== add rule + 7 13,L,- 15,L,- 17,L,- I 33 - +== list is empty again after delete +== can change limits without deleting + 1 1,L,- 1,L,- 1,L,- I 100 - + 1 1,L,- 1,L,- 1,L,- I 101 - + 1 1,L,- 1,L,- 1,L,- I 99 - +== wipe and restore rules in bulk + 7 15,L,- 0,L,- 0,L,- I 33 - + 7 14,L,- 0,L,- 0,L,- I 33 - + 7 13,L,- 0,L,- 0,L,- I 33 - + 7 12,L,- 0,L,- 0,L,- I 33 - + 7 11,L,- 0,L,- 0,L,- I 33 - + 7 10,L,- 0,L,- 0,L,- I 33 - + 7 15,L,- 0,L,- 0,L,- I 33 - + 7 14,L,- 0,L,- 0,L,- I 33 - + 7 13,L,- 0,L,- 0,L,- I 33 - + 7 12,L,- 0,L,- 0,L,- I 33 - + 7 11,L,- 0,L,- 0,L,- I 33 - + 7 10,L,- 0,L,- 0,L,- I 33 - +== default rule prevents file creation +touch: cannot touch '/mnt/test/test/quota/dir/file': Disk quota exceeded +== decreasing totl allows file creation again +== attr selecting rules prevent creation +touch: cannot touch '/mnt/test/test/quota/dir/file': Disk quota exceeded +touch: cannot touch '/mnt/test/test/quota/dir/file': Disk quota exceeded +== multi attr selecting doesn't prevent partial +touch: cannot touch '/mnt/test/test/quota/dir/file': Disk quota exceeded +== op differentiates +== higher priority rule applies +touch: cannot touch '/mnt/test/test/quota/dir/file': Disk quota exceeded +== data rules with total and count prevent write and fallocate +dd: error writing '/mnt/test/test/quota/dir/file': Disk quota exceeded +fallocate: fallocate failed: Disk quota exceeded +dd: error writing '/mnt/test/test/quota/dir/file': Disk quota exceeded +fallocate: fallocate failed: Disk quota exceeded +== added rules work after bulk restore +touch: cannot touch '/mnt/test/test/quota/dir/file': Disk quota exceeded diff --git a/tests/golden/retention-basic b/tests/golden/retention-basic new file mode 100644 index 00000000..05d2c495 --- /dev/null +++ b/tests/golden/retention-basic @@ -0,0 +1,28 @@ +== setting retention on dir fails +attr_x ioctl failed on '/mnt/test/test/retention-basic': Invalid argument (22) +scoutfs: set-attr-x failed: Invalid argument (22) +== set retention +== get-attr-x shows retention +1 +== unpriv can't clear retention +attr_x ioctl failed on '/mnt/test/test/retention-basic/file-1': Operation not permitted (1) +scoutfs: set-attr-x failed: Operation not permitted (1) +== can set hidden scoutfs xattr in retention +== setting user. xattr fails in retention +setfattr: /mnt/test/test/retention-basic/file-1: Operation not permitted +== file deletion fails in retention +rm: cannot remove '/mnt/test/test/retention-basic/file-1': Operation not permitted +== file rename fails in retention +mv: cannot move '/mnt/test/test/retention-basic/file-1' to '/mnt/test/test/retention-basic/file-2': Operation not permitted +== file write fails in retention +date: write error: Operation not permitted +== file truncate fails in retention +truncate: failed to truncate '/mnt/test/test/retention-basic/file-1' at 0 bytes: Operation not permitted +== setattr fails in retention +touch: setting times of '/mnt/test/test/retention-basic/file-1': Operation not permitted +== clear retention +== file write +== file rename +== setattr +== xattr deletion +== cleanup diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 2890f58a..d2aa3246 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -73,6 +73,7 @@ $(basename $0) options: -t | Enabled trace events that match the given glob argument. | Multiple options enable multiple globbed events. -T | Multiply the original trace buffer size by nr during the run. + -V | Set mkfs device format version. -X | xfstests git repo. Used by tests/xfstests.sh. -x | xfstests git branch to checkout and track. -y | xfstests ./check additional args @@ -176,6 +177,11 @@ while true; do T_TRACE_MULT="$2" shift ;; + -V) + test -n "$2" || die "-V must have a format version argument" + T_MKFS_FORMAT_VERSION="-V $2" + shift + ;; -X) test -n "$2" || die "-X requires xfstests git repo dir argument" T_XFSTESTS_REPO="$2" @@ -344,7 +350,7 @@ if [ -n "$T_MKFS" ]; then done msg "making new filesystem with $T_QUORUM quorum members" - cmd scoutfs mkfs -f $quo $T_DATA_ALLOC_ZONE_BLOCKS \ + cmd scoutfs mkfs -f $quo $T_DATA_ALLOC_ZONE_BLOCKS $T_MKFS_FORMAT_VERSION \ "$T_META_DEVICE" "$T_DATA_DEVICE" fi @@ -352,7 +358,8 @@ if [ -n "$T_INSMOD" ]; then msg "removing and reinserting scoutfs module" test -e /sys/module/scoutfs && cmd rmmod scoutfs cmd modprobe libcrc32c - cmd insmod "$T_KMOD/src/scoutfs.ko" + T_MODULE="$T_KMOD/src/scoutfs.ko" + cmd insmod "$T_MODULE" fi if [ -n "$T_TRACE_MULT" ]; then diff --git a/tests/sequence b/tests/sequence index fc656e2c..1d099b85 100644 --- a/tests/sequence +++ b/tests/sequence @@ -12,12 +12,16 @@ data-prealloc.sh setattr_more.sh offline-extent-waiting.sh move-blocks.sh +projects.sh large-fragmented-free.sh +format-version-forward-back.sh enospc.sh srch-safe-merge-pos.sh srch-basic-functionality.sh simple-xattr-unit.sh +retention-basic.sh totl-xattr-tag.sh +quota.sh lock-refleak.sh lock-shrink-consistency.sh lock-pr-cw-conflict.sh diff --git a/tests/src/o_tmpfile_linkat.c b/tests/src/o_tmpfile_linkat.c new file mode 100644 index 00000000..1b5d7f70 --- /dev/null +++ b/tests/src/o_tmpfile_linkat.c @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 Versity Software, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License v2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void linkat_tmpfile(char *dir, char *lpath) +{ + char proc_self[PATH_MAX]; + int ret; + int fd; + + fd = open(dir, O_RDWR | O_TMPFILE, 0777); + if (fd < 0) { + perror("open(O_TMPFILE)"); + exit(1); + } + + snprintf(proc_self, sizeof(proc_self), "/proc/self/fd/%d", fd); + + ret = linkat(AT_FDCWD, proc_self, AT_FDCWD, lpath, AT_SYMLINK_FOLLOW); + if (ret < 0) { + perror("linkat"); + exit(1); + } + + close(fd); +} + +/* + * Use O_TMPFILE and linkat to create a new visible file, used to test + * the O_TMPFILE creation path by inspecting the created file. + */ +int main(int argc, char **argv) +{ + char *lpath; + char *dir; + + if (argc < 3) { + printf("%s \n", argv[0]); + return 1; + } + + dir = argv[1]; + lpath = argv[2]; + + linkat_tmpfile(dir, lpath); + + return 0; +} diff --git a/tests/tests/format-version-forward-back.sh b/tests/tests/format-version-forward-back.sh new file mode 100644 index 00000000..403d0aaf --- /dev/null +++ b/tests/tests/format-version-forward-back.sh @@ -0,0 +1,179 @@ +# +# Test our basic ability to work with different format versions. +# +# The current code being tested has a range of supported format +# versions. For each of the older supported format versions we have a +# git hash of the commit before the next greater version was introduced. +# We build versions of the scoutfs utility and kernel module for the +# last commit in tree that had a lesser supported version as its max +# supported version. We use those binaries to test forward and back +# compat as new and old code works with a persistent volume with a given +# format version. +# + +mount_has_format_version() +{ + local mnt="$1" + local vers="$2" + local sysfs_fmt_vers="$(t_sysfs_path_from_mnt $SCR)/format_version" + + test "$(cat $sysfs_fmt_vers)" == "$vers" +} + +SCR="/mnt/scoutfs.scratch" + +MIN=$(modinfo $T_MODULE | awk '($1 == "scoutfs_format_version_min:"){print $2}') +MAX=$(modinfo $T_MODULE | awk '($1 == "scoutfs_format_version_max:"){print $2}') + +echo "min: $MIN max: $MAX" > "$T_TMP.log" + +test "$MIN" -gt 0 -a "$MAX" -gt 0 -a "$MIN" -le "$MAX" || \ + t_fail "parsed bad versions, min: $MIN max: $MAX" + +test "$MIN" == "$MAX" && \ + t_skip "only one supported format version: $MIN" + +# prepare dir and wipe any weird old partial state +builds="$T_RESULTS/format_version_builds" +mkdir -p "$builds" + +echo "== ensuring utils and module for old versions" +declare -A commits +commits[1]=c3c4b080 +for vers in $(seq $MIN $((MAX - 1))); do + dir="$builds/$vers" + platform=$(uname -rp) + buildmark="$dir/buildmark" + commit="${commits[$vers]}" + + test -n "$commit" || \ + t_fail "no commit for vers $vers" + + # have our files for this version + test "$(cat $buildmark 2>&1)" == "$platform" && \ + continue + + # build as one big sequence of commands that can return failure + ( + set -o pipefail + + rm -rf $dir && + mkdir -p $dir/building && + cd "$T_TESTS/.." && + git archive --format=tar "$commit" | tar -C "$dir/building" -xf - && + cd - && + find $dir && + make -C "$dir/building" && + mv $dir/building/utils/src/scoutfs $dir && + mv $dir/building/kmod/src/scoutfs.ko $dir && + rm -rf $dir/building && + echo "$platform" > $buildmark && + find $dir && + cat $buildmark + ) >> "$T_TMP.log" 2>&1 || t_fail "version $vers build failed" +done + +echo "== unmounting test fs and removing test module" +t_quiet t_umount_all +t_quiet rmmod scoutfs + +echo "== testing combinations of old and new format versions" +mkdir -p "$SCR" +for vers in $(seq $MIN $((MAX - 1))); do + old_scoutfs="$builds/$vers/scoutfs" + old_module="$builds/$vers/scoutfs.ko" + + echo "mkfs $vers" >> "$T_TMP.log" + t_quiet $old_scoutfs mkfs -f -Q 0,127.0.0.1,53000 "$T_EX_META_DEV" "$T_EX_DATA_DEV" \ + || t_fail "mkfs $vers failed" + + echo "mount $vers with $vers" >> "$T_TMP.log" + t_quiet insmod $old_module + t_quiet mount -t scoutfs -o metadev_path=$T_EX_META_DEV,quorum_slot_nr=0 \ + "$T_EX_DATA_DEV" "$SCR" + t_quiet mount_has_format_version "$SCR" "$vers" + + echo "creating files in $vers" >> "$T_TMP.log" + t_quiet touch "$SCR/file-"{1,2,3} + stat "$SCR"/file-* > "$T_TMP.stat" || \ + t_fail "stat in $vers failed" + + echo "remounting $vers fs with $MAX" >> "$T_TMP.log" + t_quiet umount "$SCR" + rmmod scoutfs + insmod "$T_MODULE" + t_quiet mount -t scoutfs -o metadev_path=$T_EX_META_DEV,quorum_slot_nr=0 \ + "$T_EX_DATA_DEV" "$SCR" + t_quiet mount_has_format_version "$SCR" "$vers" + + echo "verifying stat in $vers with $MAX" >> "$T_TMP.log" + diff -u "$T_TMP.stat" <(stat "$SCR"/file-*) + + echo "keep/update/del existing, create new in $vers" >> "$T_TMP.log" + t_quiet touch "$SCR/file-2" + t_quiet rm -f "$SCR/file-3" + t_quiet touch "$SCR/file-4" + stat "$SCR"/file-* > "$T_TMP.stat" || \ + t_fail "stat in $vers failed" + + echo "remounting $vers fs with $vers" >> "$T_TMP.log" + t_quiet umount "$SCR" + rmmod scoutfs + insmod "$old_module" + t_quiet mount -t scoutfs -o metadev_path=$T_EX_META_DEV,quorum_slot_nr=0 \ + "$T_EX_DATA_DEV" "$SCR" + t_quiet mount_has_format_version "$SCR" "$vers" + + echo "verifying stat in $vers with $vers" >> "$T_TMP.log" + diff -u "$T_TMP.stat" <(stat "$SCR"/file-*) + + echo "changing format vers to $MAX" >> "$T_TMP.log" + t_quiet umount "$SCR" + rmmod scoutfs + t_quiet scoutfs change-format-version -F -V $MAX $T_EX_META_DEV "$T_EX_DATA_DEV" + + echo "mount fs $MAX with old $vers should fail" >> "$T_TMP.log" + insmod "$old_module" + mount -t scoutfs -o metadev_path=$T_EX_META_DEV,quorum_slot_nr=0 \ + "$T_EX_DATA_DEV" "$SCR" >> "$T_TMP.log" 2>&1 + if [ "$?" == "0" ]; then + umount "$SCR" + t_fail "old code ver $vers able to mount new ver $MAX" + fi + + echo "remounting $MAX fs with $MAX" >> "$T_TMP.log" + rmmod scoutfs + insmod "$T_MODULE" + t_quiet mount -t scoutfs -o metadev_path=$T_EX_META_DEV,quorum_slot_nr=0 \ + "$T_EX_DATA_DEV" "$SCR" + t_quiet mount_has_format_version "$SCR" "$MAX" + + echo "verifying stat in $MAX with $MAX" >> "$T_TMP.log" + diff -u "$T_TMP.stat" <(stat "$SCR"/file-*) + + echo "keep/update/del existing, create new in $MAX" >> "$T_TMP.log" + t_quiet touch "$SCR/file-2" + t_quiet rm -f "$SCR/file-4" + t_quiet touch "$SCR/file-5" + stat "$SCR"/file-* > "$T_TMP.stat" || \ + t_fail "stat in $MAX failed" + + echo "remounting $MAX fs with $MAX again" >> "$T_TMP.log" + t_quiet umount "$SCR" + t_quiet mount -t scoutfs -o metadev_path=$T_EX_META_DEV,quorum_slot_nr=0 \ + "$T_EX_DATA_DEV" "$SCR" + t_quiet mount_has_format_version "$SCR" "$MAX" + + echo "verifying stat in $MAX with $MAX again" >> "$T_TMP.log" + diff -u "$T_TMP.stat" <(stat "$SCR"/file-*) + + echo "done with old vers $vers" >> "$T_TMP.log" + t_quiet umount "$SCR" + rmmod scoutfs +done + +echo "== restoring test module and mount" +insmod "$T_MODULE" +t_mount_all + +t_pass diff --git a/tests/tests/projects.sh b/tests/tests/projects.sh new file mode 100644 index 00000000..105f3e6a --- /dev/null +++ b/tests/tests/projects.sh @@ -0,0 +1,52 @@ + +# notable id to recognize in output +ID=8675309 + +echo "== default new files don't have project" +touch "$T_D0/file" +scoutfs get-attr-x -p "$T_D0/file" + +echo "== set new project on files and dirs" +mkdir "$T_D0/dir" +scoutfs set-attr-x -p $ID "$T_D0/file" +scoutfs set-attr-x -p $ID "$T_D0/dir" +scoutfs get-attr-x -p "$T_D0/file" +scoutfs get-attr-x -p "$T_D0/dir" + +echo "== non-root can see id" +chmod 644 "$T_D0/file" +setpriv --ruid=12345 --euid=12345 scoutfs get-attr-x -p "$T_D0/file" + +echo "== can use IDs around long width limits" +touch "$T_D0/ids" +for id in 0x7FFFFFFF 0x80000000 0xFFFFFFFF \ + 0x7FFFFFFFFFFFFFFF 0x8000000000000000 0xFFFFFFFFFFFFFFFF; do + scoutfs set-attr-x -p $id "$T_D0/ids" + scoutfs get-attr-x -p "$T_D0/ids" +done + +echo "== created files and dirs inherit project id" +touch "$T_D0/dir/file" +mkdir "$T_D0/dir/sub" +scoutfs get-attr-x -p "$T_D0/dir/file" +scoutfs get-attr-x -p "$T_D0/dir/sub" + +echo "== inheritance continues" +mkdir "$T_D0/dir/sub/more" +scoutfs get-attr-x -p "$T_D0/dir/sub/more" + +# .. just inherits 0 :) +echo "== clearing project id stops inheritance" +scoutfs set-attr-x -p 0 "$T_D0/dir" +touch "$T_D0/dir/another-file" +mkdir "$T_D0/dir/another-sub" +scoutfs get-attr-x -p "$T_D0/dir/another-file" +scoutfs get-attr-x -p "$T_D0/dir/another-sub" + +echo "== o_tmpfile creations inherit dir" +scoutfs set-attr-x -p $ID "$T_D0/dir" +o_tmpfile_linkat "$T_D0/dir" "$T_D0/dir/tmpfile" +scoutfs get-attr-x -p "$T_D0/dir/tmpfile" + + +t_pass diff --git a/tests/tests/quota.sh b/tests/tests/quota.sh new file mode 100644 index 00000000..7def9d67 --- /dev/null +++ b/tests/tests/quota.sh @@ -0,0 +1,150 @@ + +TEST_UID=22222 +TEST_GID=44444 + +# sys_setreuid() set fs[uid] to e[ug]id +SET_UID="--ruid=$TEST_UID --euid=$TEST_UID" +SET_GID="--rgid=$TEST_GID --egid=$TEST_GID --clear-groups" + +FILE="$T_D0/dir/file" + +sync_and_drop() +{ + sync + echo 1 > $(t_debugfs_path)/drop_weak_item_cache + echo 1 > $(t_debugfs_path)/drop_quota_check_cache +} + +reset_all() +{ + rm -f "$FILE" + scoutfs quota-wipe -p "$T_M0" + getfattr --absolute-names -d -m - "$T_D0" | \ + grep "^scoutfs.totl." | \ + cut -d '=' -f 1 | \ + xargs -n 1 -I'{}' setfattr -x '{}' "$T_D0" +} + +echo "== prepare dir with write perm for test ids" +mkdir "$T_D0/dir" +chown --quiet $TEST_UID "$T_D0/dir" +chgrp --quiet $TEST_GID "$T_D0/dir" + +echo "== test assumes starting with no rules, empty list" +scoutfs quota-list -p "$T_M0" + +echo "== add rule" +scoutfs quota-add -p "$T_M0" -r "7 13,L,- 15,L,- 17,L,- I 33 -" +scoutfs quota-list -p "$T_M0" + +echo "== list is empty again after delete" +scoutfs quota-del -p "$T_M0" -r "7 13,L,- 15,L,- 17,L,- I 33 -" +scoutfs quota-list -p "$T_M0" + +echo "== can change limits without deleting" +scoutfs quota-add -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- I 100 -" +scoutfs quota-list -p "$T_M0" +scoutfs quota-add -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- I 101 -" +scoutfs quota-del -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- I 100 -" +scoutfs quota-list -p "$T_M0" +scoutfs quota-add -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- I 99 -" +scoutfs quota-del -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- I 101 -" +scoutfs quota-list -p "$T_M0" +scoutfs quota-del -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- I 99 -" +reset_all + +echo "== wipe and restore rules in bulk" +for a in $(seq 10 15); do + scoutfs quota-add -p "$T_M0" -r "7 $a,L,- 0,L,- 0,L,- I 33 -" +done +scoutfs quota-list -p "$T_M0" +scoutfs quota-list -p "$T_M0" > "$T_TMP.list" +scoutfs quota-wipe -p "$T_M0" +scoutfs quota-list -p "$T_M0" +scoutfs quota-restore -p "$T_M0" < "$T_TMP.list" +scoutfs quota-list -p "$T_M0" +reset_all + +echo "== default rule prevents file creation" +scoutfs quota-add -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- I 1 -" +setfattr -n scoutfs.totl.test.1.1.1 -v 2 "$T_D0" +sync_and_drop +setpriv $SET_UID touch "$FILE" 2>&1 | t_filter_fs + +echo "== decreasing totl allows file creation again" +setfattr -x scoutfs.totl.test.1.1.1 "$T_D0" +sync_and_drop +setpriv $SET_UID touch "$FILE" +reset_all + +echo "== attr selecting rules prevent creation" +scoutfs quota-add -p "$T_M0" -r "1 $TEST_UID,U,S 1,L,- 1,L,- I 1 -" +scoutfs quota-add -p "$T_M0" -r "1 $TEST_GID,G,S 1,L,- 1,L,- I 1 -" +setfattr -n scoutfs.totl.test.$TEST_UID.1.1 -v 2 "$T_D0" +setfattr -n scoutfs.totl.test.$TEST_GID.1.1 -v 2 "$T_D0" +sync_and_drop +setpriv $SET_UID touch "$FILE" 2>&1 | t_filter_fs +setpriv $SET_GID touch "$FILE" 2>&1 | t_filter_fs +reset_all + +echo "== multi attr selecting doesn't prevent partial" +scoutfs quota-add -p "$T_M0" -r "1 $TEST_UID,U,S $TEST_GID,G,S 1,L,- I 1 -" +setfattr -n scoutfs.totl.test.$TEST_UID.$TEST_GID.1 -v 2 "$T_D0" +sync_and_drop +setpriv $SET_UID touch "$FILE" +rm -f "$FILE" +setpriv $SET_GID touch "$FILE" +rm -f "$FILE" +setpriv $SET_UID $SET_GID touch "$FILE" 2>&1 | t_filter_fs +reset_all + +echo "== op differentiates" +# inode ops succeed in presence of data rule +scoutfs quota-add -p "$T_M0" -r "1 $TEST_UID,U,S 1,L,- 1,L,- D 1 -" +setfattr -n scoutfs.totl.test.$TEST_UID.1.1 -v 2 "$T_D0" +sync_and_drop +setpriv $SET_UID touch "$FILE" 2>&1 | t_filter_fs +reset_all +# data ops succeed in presence of inode rule +touch "$FILE" +chown --quiet $TEST_UID "$FILE" +scoutfs quota-add -p "$T_M0" -r "1 $TEST_UID,U,S 1,L,- 1,L,- I 1 -" +setfattr -n scoutfs.totl.test.$TEST_UID.1.1 -v 2 "$T_D0" +sync_and_drop +setpriv $SET_UID fallocate -l 4096 "$FILE" 2>&1 | t_filter_fs +reset_all + +echo "== higher priority rule applies" +scoutfs quota-add -p "$T_M0" -r "1 $TEST_UID,U,S 1,L,- 1,L,- I 1000 -" +scoutfs quota-add -p "$T_M0" -r "2 $TEST_UID,U,S 1,L,- 1,L,- I 1 -" +setfattr -n scoutfs.totl.test.$TEST_UID.1.1 -v 2 "$T_D0" +sync_and_drop +setpriv $SET_UID touch "$FILE" 2>&1 | t_filter_fs +reset_all + +echo "== data rules with total and count prevent write and fallocate" +touch "$FILE" +scoutfs quota-add -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- D 1 -" +setfattr -n scoutfs.totl.test.1.1.1 -v 2 "$T_D0" +sync_and_drop +dd if=/dev/zero of="$FILE" bs=4096 count=1 conv=notrunc status=none 2>&1 | t_filter_fs +fallocate -l 4096 "$FILE" 2>&1 | t_filter_fs +scoutfs quota-del -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- D 1 -" +scoutfs quota-add -p "$T_M0" -r "1 1,L,- 1,L,- 1,L,- D 0 C" +sync_and_drop +dd if=/dev/zero of="$FILE" bs=4096 count=1 conv=notrunc status=none 2>&1 | t_filter_fs +fallocate -l 4096 "$FILE" 2>&1 | t_filter_fs +reset_all + +echo "== added rules work after bulk restore" +seq -f " 1 %.0f,U,S 1,L,- 1,L,- I 1 -" 9000050000 -1 9000000000 > "$T_TMP.lots" +scoutfs quota-restore -p "$T_M0" < "$T_TMP.lots" +scoutfs quota-list -p "$T_M0" > "$T_TMP.list" +diff -u "$T_TMP.lots" "$T_TMP.list" +scoutfs quota-add -p "$T_M0" -r "1 $TEST_UID,U,S 1,L,- 1,L,- I 1 -" +setfattr -n scoutfs.totl.test.$TEST_UID.1.1 -v 2 "$T_D0" +sync_and_drop +setpriv $SET_UID touch "$FILE" 2>&1 | t_filter_fs +reset_all + +t_pass diff --git a/tests/tests/retention-basic.sh b/tests/tests/retention-basic.sh new file mode 100644 index 00000000..60a5b544 --- /dev/null +++ b/tests/tests/retention-basic.sh @@ -0,0 +1,57 @@ +t_require_commands scoutfs touch rm setfattr + +touch "$T_D0/file-1" + +echo "== setting retention on dir fails" +scoutfs set-attr-x -t 1 "$T_D0" 2>&1 | t_filter_fs + +echo "== set retention" +scoutfs set-attr-x -t 1 "$T_D0/file-1" + +echo "== get-attr-x shows retention" +scoutfs get-attr-x -t "$T_D0/file-1" + +echo "== unpriv can't clear retention" +setpriv --ruid=12345 --euid=12345 scoutfs set-attr-x -t 0 "$T_D0/file-1" 2>&1 | t_filter_fs + +echo "== can set hidden scoutfs xattr in retention" +setfattr -n scoutfs.hide.srch.retention_test -v val "$T_D0/file-1" + +echo "== setting user. xattr fails in retention" +setfattr -n user.retention_test -v val "$T_D0/file-1" 2>&1 | t_filter_fs + +echo "== file deletion fails in retention" +rm -f "$T_D0/file-1" 2>&1 | t_filter_fs + +echo "== file rename fails in retention" +mv $T_D0/file-1 $T_D0/file-2 2>&1 | t_filter_fs + +echo "== file write fails in retention" +date >> $T_D0/file-1 + +echo "== file truncate fails in retention" +truncate -s 0 $T_D0/file-1 2>&1 | t_filter_fs + +echo "== setattr fails in retention" +touch $T_D0/file-1 2>&1 | t_filter_fs + +echo "== clear retention" +scoutfs set-attr-x -t 0 "$T_D0/file-1" + +echo "== file write" +date >> $T_D0/file-1 + +echo "== file rename" +mv $T_D0/file-1 $T_D0/file-2 +mv $T_D0/file-2 $T_D0/file-1 + +echo "== setattr" +touch $T_D0/file-1 2>&1 | t_filter_fs + +echo "== xattr deletion" +setfattr -x scoutfs.hide.srch.retention_test "$T_D0/file-1" + +echo "== cleanup" +rm -f "$T_D0/file-1" + +t_pass diff --git a/tests/tests/totl-xattr-tag.sh b/tests/tests/totl-xattr-tag.sh index dd2d90b7..6a133860 100644 --- a/tests/tests/totl-xattr-tag.sh +++ b/tests/tests/totl-xattr-tag.sh @@ -3,6 +3,7 @@ t_require_commands touch rm setfattr scoutfs find_xattrs read_xattr_totals() { sync + echo 1 > $(t_debugfs_path)/drop_weak_item_cache scoutfs read-xattr-totals -p "$T_M0" } @@ -112,7 +113,6 @@ for phase in create update remove; do echo "$k.0.0 = ${totals[$k]}, ${counts[$k]}" done ) | grep -v "= 0, 0$" | sort -n >> $T_TMP.check_arr - sync read_xattr_totals | sort -n >> $T_TMP.check_read diff -u $T_TMP.check_arr $T_TMP.check_read || \ diff --git a/utils/man/scoutfs.5 b/utils/man/scoutfs.5 index fb56d19d..f57d8fe8 100644 --- a/utils/man/scoutfs.5 +++ b/utils/man/scoutfs.5 @@ -270,6 +270,21 @@ metadata that is bound to a specific volume and should not be transferred with the file by tools that read extended attributes, like .BR tar(1) . .TP +.B .indx. +Attributes with the .indx. tag dd the inode containing the attribute to +a filesystem-wide index. The name of the extended attribute must end +with strings representing two values separated by dots. The first value +is an unsigned 8bit value and the second is an unsigned 64bit value. +These attributes can only be modified with root privileges and the +attributes can not have a value. +.sp +The inodes in the index are stored in increasing sort order of the +values, with the first u8 value being most significant. Inodes can be +at many positions as tracked by many extended attributes, and their +position follows the creation, renaming, or deletion of the attributes. +The index can be read with the read-xattr-index command which uses the +underlying READ_XATTR_INDEX ioctl. +.TP .B .srch. Attributes with the .srch. tag are indexed so that they can be found by the @@ -295,6 +310,36 @@ with the ioctl. .RE +.SH FILE RETENTION MODE +A file can be set to retention mode by setting the +.IB RETENTION +attribute with the +.IB SET_ATTR_X +ioctl. This flag can only be set on regular files and requires root +permission (the +.IB CAP_SYS_ADMIN +capability). +.sp +Once in retention mode all modifications of the file will fail. The +only exceptions are that system extended attributes (all those without +the "user." prefix) may be modified. The retention bit may be cleared +with sufficient priveledges to remove the retention restrictions on +other modifications. +.RE + +.SH PROJECT IDs +All inodes have a project ID attribute that can be set via the +SET_ATTR_X ioctl and displayed with the GET_ATTR_X ioctl. Project IDs +are an unsigned 64bit value and the value of 0 is reserved to indicate +that no project ID is assigned. If a project ID is set on a directory +then all inodes created with it as the initial parent inheret that ID, +for all file types. This includes files initially unlinked from the +namespace when created with O_TMPFILE. Project IDs are only +automatically inherited from the parent dir on initial creation. +They're not changed as directory entry linkes to the inode are created +or renamed. +.RE + .SH FORMAT VERSION The format version defines the layout and use of structures stored on devices and passed over the network. The version is incremented for @@ -373,6 +418,19 @@ The version that a mount is using is shown in the file in the mount's sysfs directory, typically .I /sys/fs/scoutfs/f.FSID.r.RID/ .RE +.sp +The defined format versions are: +.RS +.TP +.sp +.B 1 +Initial format version. +.TP +.B 2 +Added retention mode by setting the retention attribute. Added the +project ID inode attribute. Added quota rules and enforcement. Added +the .indx. extended attribute tag. +.RE .SH CORRUPTION DETECTION A diff --git a/utils/man/scoutfs.8 b/utils/man/scoutfs.8 index 60566c97..d105d87b 100644 --- a/utils/man/scoutfs.8 +++ b/utils/man/scoutfs.8 @@ -209,6 +209,16 @@ A path within a ScoutFS filesystem. .RE .PD +.TP +.BI "get-attr-x FILE" +.sp +Display ScoutFS-specific attributes from a file. If no options are +given than all the attributes that the command supports will be +displayed. If attributes are specified with options then only those +attributes are displayed. If only one attribute is specified then it +will not have a label prefix in the display output. The --help option +will list the attributes that the command supports. The file system may +support a different set of attributes. .TP .BI "get-referring-entries [-p|--path PATH] INO" .sp @@ -506,6 +516,15 @@ A path within a ScoutFS filesystem. .RE .PD +.TP +.BI "set-attr-x FILE" +.sp +Set ScoutFS-specific attributes on a file. Only the attributes that are +spcified by options will be set. The --help option will list the +attributes that the command understands. The file system may support a +different set of attributes. +.PD + .TP .BI "setattr FILE [-d, --data-version=VERSION [-s, --size=SIZE [-o, --offline]]] [-t, --ctime=TIMESPEC]" .sp diff --git a/utils/src/attr_x.c b/utils/src/attr_x.c new file mode 100644 index 00000000..9674624e --- /dev/null +++ b/utils/src/attr_x.c @@ -0,0 +1,304 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sparse.h" +#include "util.h" +#include "format.h" +#include "ioctl.h" +#include "parse.h" +#include "cmd.h" + +struct attr_x_args { + bool set; + char *filename; + struct scoutfs_ioctl_inode_attr_x iax; +}; + +#define pr(iax, name, label, fmt, args...) \ +do { \ + if ((iax->x_mask & SCOUTFS_IOC_IAX_##name)) { \ + if (__builtin_popcount(iax->x_mask) > 1) \ + printf(label ": " fmt "\n", ##args); \ + else \ + printf(fmt "\n", ##args); \ + } \ +} while (0) + +#define prb(iax, name, label) \ + pr(iax, name, label, "%u", !!((iax)->bits & SCOUTFS_IOC_IAX_B_##name)) + +static int do_attr_x(struct attr_x_args *args) +{ + struct scoutfs_ioctl_inode_attr_x *iax = &args->iax; + int fd = -1; + int ret; + int op; + + if (args->set) { + /* nothing to do if not setting */ + if (iax->x_mask == 0) + return 0; + op = SCOUTFS_IOC_SET_ATTR_X; + } else { + /* get all known if none specified */ + if (iax->x_mask == 0) + iax->x_mask = ~SCOUTFS_IOC_IAX__UNKNOWN; + op = SCOUTFS_IOC_GET_ATTR_X; + } + + fd = open(args->filename, O_RDONLY); + if (fd < 0) { + ret = -errno; + fprintf(stderr, "failed to open '%s': %s (%d)\n", + args->filename, strerror(errno), errno); + goto out; + } + + ret = ioctl(fd, op, iax); + if (ret < 0) { + ret = -errno; + fprintf(stderr, "attr_x ioctl failed on '%s': " + "%s (%d)\n", args->filename, strerror(errno), errno); + goto out; + } + + if (!args->set) { + pr(iax, META_SEQ, "meta_seq", "%llu", iax->meta_seq); + pr(iax, DATA_SEQ, "data_seq", "%llu", iax->data_seq); + pr(iax, DATA_VERSION, "data_version", "%llu", iax->data_version); + pr(iax, ONLINE_BLOCKS, "online_blocks", "%llu", iax->online_blocks); + pr(iax, OFFLINE_BLOCKS, "offline_blocks", "%llu", iax->offline_blocks); + pr(iax, CTIME, "ctime", "%llu.%u", iax->ctime_sec, iax->ctime_nsec); + pr(iax, CRTIME, "crtime", "%llu.%u", iax->crtime_sec, iax->crtime_nsec); + pr(iax, SIZE, "size", "%llu", iax->size); + prb(iax, RETENTION, "retention"); + pr(iax, PROJECT_ID, "project_id", "%llu", iax->project_id); + } + + ret = 0; +out: + if (fd >= 0) + close(fd); + return ret; +} + +/* + * This is called for both get and set. The get calls won't have + * arguments and are only setting the mask. The set calls parse the + * value to set. We could have defaults by making set option arguments + * optional, like setting the current time for timestamps, but that + * hasn't been needed. + * + * Option value parsing places no constraints on the attributes or + * values themselves once parsed. This lets us use the set command to + * test the kernel's testing for invalid attribute combinations and + * values. + */ +static int parse_opt(int key, char *arg, struct argp_state *state) +{ + struct attr_x_args *args = state->input; + struct timespec ts; + int ret; + u64 x; + + switch (key) { + case 'm': + args->iax.x_mask |= SCOUTFS_IOC_IAX_META_SEQ; + if (arg) { + ret = parse_u64(arg, &args->iax.meta_seq); + if (ret) + return ret; + } + break; + case 'd': + args->iax.x_mask |= SCOUTFS_IOC_IAX_DATA_SEQ; + if (arg) { + ret = parse_u64(arg, &args->iax.data_seq); + if (ret) + return ret; + } + break; + case 'v': + args->iax.x_mask |= SCOUTFS_IOC_IAX_DATA_VERSION; + if (arg) { + ret = parse_u64(arg, &args->iax.data_version); + if (ret) + return ret; + if (args->iax.data_version == 0) + argp_error(state, "data version must not be 0"); + } + break; + case 'n': + args->iax.x_mask |= SCOUTFS_IOC_IAX_ONLINE_BLOCKS; + if (arg) { + ret = parse_u64(arg, &args->iax.online_blocks); + if (ret) + return ret; + } + break; + case 'f': + args->iax.x_mask |= SCOUTFS_IOC_IAX_OFFLINE_BLOCKS; + if (arg) { + ret = parse_u64(arg, &args->iax.offline_blocks); + if (ret) + return ret; + } + break; + case 'c': + args->iax.x_mask |= SCOUTFS_IOC_IAX_CTIME; + if (arg) { + ret = parse_timespec(arg, &ts); + if (ret) + return ret; + args->iax.ctime_sec = ts.tv_sec; + args->iax.ctime_nsec = ts.tv_nsec; + } + break; + case 'r': + args->iax.x_mask |= SCOUTFS_IOC_IAX_CRTIME; + if (arg) { + ret = parse_timespec(arg, &ts); + if (ret) + return ret; + args->iax.crtime_sec = ts.tv_sec; + args->iax.crtime_nsec = ts.tv_nsec; + } + break; + case 's': + args->iax.x_mask |= SCOUTFS_IOC_IAX_SIZE; + if (arg) { + ret = parse_u64(arg, &args->iax.size); + if (ret) + return ret; + } + break; + case 't': + args->iax.x_mask |= SCOUTFS_IOC_IAX_RETENTION; + if (arg) { + ret = parse_u64(arg, &x); + if (ret) + return ret; + if (x) + args->iax.bits |= SCOUTFS_IOC_IAX_B_RETENTION; + } + break; + case 'p': + args->iax.x_mask |= SCOUTFS_IOC_IAX_PROJECT_ID; + if (arg) { + ret = parse_u64(arg, &args->iax.project_id); + if (ret) + return ret; + } + break; + case ARGP_KEY_ARG: + if (!args->filename) + args->filename = strdup_or_error(state, arg); + else + argp_error(state, "more than one argument given"); + break; + case ARGP_KEY_FINI: + if (!args->filename) + argp_error(state, "no filename given"); + break; + default: + break; + } + + return 0; +} + +/* + * The get options are derived from these by copying the struct and + * modifying fields. + */ +static struct argp_option set_options[] = { + { "meta_seq", 'm', "SEQ", 0, "Inode Metadata change index sequence number"}, + { "data_seq", 'd', "SEQ", 0, "File Data change index sequence number"}, + { "data_version", 'v', "VERSION", 0, "File Data contents version"}, + { "online_blocks", 'n', "COUNT", 0, "Online data block count"}, + { "offline_blocks", 'f', "COUNT", 0, "Offline data block count"}, + { "ctime", 'c', "SECS.NSECS", 0, "Inode change time (posix ctime)"}, + { "crtime", 'r', "SECS.NSECS", 0, "ScoutFS creation time"}, + { "size", 's', "SIZE", 0, "Inode i_size field"}, + { "retention", 't', "0|1", 0, "Retention flag"}, + { "project_id", 'p', "PROJECT_ID", 0, "Project ID"}, + { NULL } +}; + +static struct argp get_argp = { + NULL, /* dynamically built */ + parse_opt, + "FILE", + "get extensible file attributes" +}; + +static int get_attr_x_cmd(int argc, char **argv) +{ + struct attr_x_args args = {0,}; + int ret; + + ret = argp_parse(&get_argp, argc, argv, 0, NULL, &args); + if (ret) + return ret; + + return do_attr_x(&args); +} + +/* + * The set options match the get arguments but don't take argument + * values to set. + */ +static void build_get_options(void) +{ + struct argp_option **opts = (struct argp_option **)&get_argp.options; + int i; + + *opts = calloc(array_size(set_options), sizeof(set_options[0])); + assert(*opts); + + memcpy(*opts, set_options, array_size(set_options) * sizeof(set_options[0])); + + for (i = 0; i < array_size(set_options) - 1; i++) + (*opts)[i].arg = NULL; +} + +static void __attribute__((constructor)) get_ctor(void) +{ + build_get_options(); + + cmd_register_argp("get-attr-x", &get_argp, GROUP_AGENT, get_attr_x_cmd); +} + +static struct argp set_argp = { + set_options, + parse_opt, + "FILE", + "Set extensible file attributes" +}; + +static int set_attr_x_cmd(int argc, char **argv) +{ + struct attr_x_args args = {.set = true,}; + int ret; + + ret = argp_parse(&set_argp, argc, argv, 0, NULL, &args); + if (ret) + return ret; + + return do_attr_x(&args); +} + +static void __attribute__((constructor)) set_ctor(void) +{ + cmd_register_argp("set-attr-x", &set_argp, GROUP_AGENT, set_attr_x_cmd); +} diff --git a/utils/src/key.h b/utils/src/key.h index 7e3ad20d..38b6672c 100644 --- a/utils/src/key.h +++ b/utils/src/key.h @@ -141,4 +141,13 @@ static inline void scoutfs_key_dec(struct scoutfs_key *key) key->sk_zone--; } +static inline void scoutfs_xattr_get_indx_key(struct scoutfs_key *key, u8 *major, u64 *minor, + u64 *ino, u64 *xid) +{ + *major = le64_to_cpu(key->_sk_first) >> 56; + *minor = (le64_to_cpu(key->_sk_first) << 8) | (le64_to_cpu(key->_sk_second) >> 56); + *ino = (le64_to_cpu(key->_sk_second) << 8) | (le64_to_cpu(key->_sk_third) >> 56); + *xid = (le64_to_cpu(key->_sk_third) << 8) | key->_sk_fourth; +} + #endif diff --git a/utils/src/mkfs.c b/utils/src/mkfs.c index 211302aa..7c89aff0 100644 --- a/utils/src/mkfs.c +++ b/utils/src/mkfs.c @@ -276,7 +276,7 @@ static int do_mkfs(struct mkfs_args *args) inode.mtime.nsec = inode.atime.nsec; inode.crtime.sec = inode.atime.sec; inode.crtime.nsec = inode.atime.nsec; - btree_append_item(bt, &key, &inode, sizeof(inode)); + btree_append_item(bt, &key, &inode, scoutfs_inode_vers_bytes(args->fmt_vers)); ret = write_block(meta_fd, SCOUTFS_BLOCK_MAGIC_BTREE, fsid, 1, blkno, SCOUTFS_BLOCK_LG_SHIFT, &bt->hdr); diff --git a/utils/src/print.c b/utils/src/print.c index b977da46..fb8d60d1 100644 --- a/utils/src/print.c +++ b/utils/src/print.c @@ -49,7 +49,7 @@ static void print_inode(struct scoutfs_key *key, void *val, int val_len) { struct scoutfs_inode *inode = val; - printf(" inode: ino %llu size %llu version %llu nlink %u\n" + printf(" inode: ino %llu size %llu version %llu proj %llu nlink %u\n" " uid %u gid %u mode 0%o rdev 0x%x flags 0x%x\n" " next_readdir_pos %llu meta_seq %llu data_seq %llu data_version %llu\n" " atime %llu.%08u ctime %llu.%08u\n" @@ -57,6 +57,7 @@ static void print_inode(struct scoutfs_key *key, void *val, int val_len) le64_to_cpu(key->ski_ino), le64_to_cpu(inode->size), le64_to_cpu(inode->version), + le64_to_cpu(inode->proj), le32_to_cpu(inode->nlink), le32_to_cpu(inode->uid), le32_to_cpu(inode->gid), le32_to_cpu(inode->mode), le32_to_cpu(inode->rdev), @@ -79,6 +80,24 @@ static void print_orphan(struct scoutfs_key *key, void *val, int val_len) } +#define SQR_FMT "[%u %llu,%u,%x %llu,%u,%x %llu,%u,%x %u %llu %x]" + +#define SQR_ARGS(r) \ + (r)->prio, \ + le64_to_cpu((r)->name_val[0]), (r)->name_source[0], (r)->name_flags[0], \ + le64_to_cpu((r)->name_val[1]), (r)->name_source[1], (r)->name_flags[1], \ + le64_to_cpu((r)->name_val[2]), (r)->name_source[2], (r)->name_flags[2], \ + (r)->op, le64_to_cpu((r)->limit), (r)->rule_flags + +static void print_quota(struct scoutfs_key *key, void *val, int val_len) +{ + struct scoutfs_quota_rule_val *rv = val; + + printf(" quota rule: hash 0x%016llx coll_nr %llu\n" + " "SQR_FMT"\n", + le64_to_cpu(key->skqr_hash), le64_to_cpu(key->skqr_coll_nr), SQR_ARGS(rv)); +} + static void print_xattr_totl(struct scoutfs_key *key, void *val, int val_len) { struct scoutfs_xattr_totl_val *tval = val; @@ -89,6 +108,17 @@ static void print_xattr_totl(struct scoutfs_key *key, void *val, int val_len) le64_to_cpu(tval->count)); } +static void print_xattr_indx(struct scoutfs_key *key, void *val, int val_len) +{ + u64 minor; + u64 ino; + u64 xid; + u8 major; + + scoutfs_xattr_get_indx_key(key, &major, &minor, &ino, &xid); + printf(" xattr indx: major %u minor %llu ino %llu xid %llu", major, minor, ino, xid); +} + static u8 *global_printable_name(u8 *name, int name_len) { static u8 name_buf[SCOUTFS_NAME_LEN + 1]; @@ -177,9 +207,15 @@ static print_func_t find_printer(u8 zone, u8 type) return print_orphan; } + if (zone == SCOUTFS_QUOTA_ZONE) + return print_quota; + if (zone == SCOUTFS_XATTR_TOTL_ZONE) return print_xattr_totl; + if (zone == SCOUTFS_XATTR_INDX_ZONE) + return print_xattr_indx; + if (zone == SCOUTFS_FS_ZONE) { switch(type) { case SCOUTFS_INODE_TYPE: return print_inode; diff --git a/utils/src/quota.c b/utils/src/quota.c new file mode 100644 index 00000000..37d69f79 --- /dev/null +++ b/utils/src/quota.c @@ -0,0 +1,547 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sparse.h" +#include "parse.h" +#include "util.h" +#include "format.h" +#include "ioctl.h" +#include "cmd.h" +#include "util.h" +#include "key.h" + +static char opc[] = { + [SQ_OP_DATA] = 'D', + [SQ_OP_INODE] = 'I', +}; + +static char nsc[] = { + [SQ_NS_LITERAL] = 'L', + [SQ_NS_PROJ] = 'P', + [SQ_NS_UID] = 'U', + [SQ_NS_GID] = 'G', +}; + +static void printf_rule(struct scoutfs_ioctl_quota_rule *irule) +{ + int i; + + /* priority: [0-9]+ */ + printf("%3u ", irule->prio); + + /* totl name: ([0-9]+,[LPUG-]+,[S-]+){3} */ + for (i = 0; i < array_size(irule->name_val); i++) { + + printf("%llu,%c,%c ", + irule->name_val[i], + nsc[irule->name_source[i]], + (irule->name_flags[i] & SQ_NF_SELECT) ? 'S' : '-'); + } + + /* op: [ID], limit: [0-9]+, flags [C-] */ + printf("%c %llu %c\n", + opc[irule->op], irule->limit, (irule->rule_flags & SQ_RF_TOTL_COUNT) ? 'C' : '-'); +} + +static int parse_rule(struct scoutfs_ioctl_quota_rule *irule, char *str) +{ + char ns[3]; + char nf[3]; + char rf; + char op; + int ret; + int i; + int j; + + memset(irule, 0, sizeof(struct scoutfs_ioctl_quota_rule)); + + ret = sscanf(str, " %hhu %llu,%c,%c %llu,%c,%c %llu,%c,%c %c %llu %c", + &irule->prio, &irule->name_val[0], &ns[0], &nf[0], &irule->name_val[1], + &ns[1], &nf[1], &irule->name_val[2], &ns[2], &nf[2], &op, &irule->limit, + &rf); + if (ret != 13) { + printf("invalid rule, missing fields: %s\n", str); + ret = -EINVAL; + goto out; + } + + for (i = 0; i < array_size(irule->name_val); i++) { + irule->name_source[i] = SQ_NS__NR; + + for (j = 0; j < array_size(nsc); j++) { + if (ns[i] == nsc[j]) { + irule->name_source[i] = j; + break; + } + } + + if (irule->name_source[i] == SQ_NS__NR) { + printf("invalid name source '%c' in name #%u in rule:\n\t%s\n", + ns[i], i + 1, str); + ret = -EINVAL; + goto out; + } + + irule->name_flags[i] = nf[i] == '-' ? 0 : + nf[i] == 'S' ? SQ_NF_SELECT : + SQ_NF__UNKNOWN; + if (irule->name_flags[i] == SQ_NF__UNKNOWN) { + printf("invalid name flags '%c' in name #%u in rule:\n\t%s\n", + nf[i], i + 1, str); + ret = -EINVAL; + goto out; + } + } + + irule->op = SQ_NS__NR; + for (i = 0; i < array_size(opc); i++) { + if (op == opc[i]) { + irule->op = i; + break; + } + } + + if (irule->op == SQ_NS__NR) { + printf("invalid op '%c' in rule:\n\t%s\n", op, str); + ret = -EINVAL; + goto out; + } + + irule->rule_flags = rf == '-' ? 0 : rf == 'C' ? SQ_RF_TOTL_COUNT : SQ_RF__UNKNOWN; + if (irule->rule_flags == SQ_RF__UNKNOWN) { + printf("invalid rule flags '%c' in rule:\n\t%s\n", rf, str); + ret = -EINVAL; + goto out; + } + + ret = 0; +out: + return ret; +} + +/* ---------------------------------------------- */ + +struct mod_args { + char *path; + char *rule_str; + bool is_add; +}; + +static int do_mod(struct mod_args *args) +{ + struct scoutfs_ioctl_quota_rule irule; + unsigned int cmd; + int fd = -1; + int ret; + + memset(&irule, 0, sizeof(irule)); + + ret = parse_rule(&irule, args->rule_str); + if (ret < 0) + goto out; + + fd = get_path(args->path, O_RDONLY); + if (fd < 0) + return fd; + + cmd = args->is_add ? SCOUTFS_IOC_ADD_QUOTA_RULE : SCOUTFS_IOC_DEL_QUOTA_RULE; + ret = ioctl(fd, cmd, &irule); + if (ret < 0) { + ret = -errno; + fprintf(stderr, "MOD_QUOTA_RULE ioctl failed: %s (%d)\n", + strerror(errno), errno); + goto out; + } + + ret = 0; +out: + if (fd >= 0) + close(fd); + + return ret; +} + +static int parse_mod_opt(int key, char *arg, struct argp_state *state) +{ + struct mod_args *args = state->input; + + switch (key) { + case 'p': + args->path = strdup_or_error(state, arg); + break; + case 'r': + args->rule_str = strdup_or_error(state, arg); + break; + case ARGP_KEY_FINI: + if (!args->path) + argp_error(state, "must provide file path"); + if (!args->rule_str) + argp_error(state, "must provide rule string"); + break; + default: + break; + } + + return 0; +} + +static struct argp_option add_options[] = { + { "path", 'p', "PATH", 0, "Path to ScoutFS filesystem"}, + { "rule", 'r', "RULE_STRING", 0, "Rule string"}, + { NULL } +}; + +static struct argp add_argp = { + add_options, + parse_mod_opt, + "", + "Add quota rule" +}; + +static int add_cmd(int argc, char **argv) +{ + struct mod_args args = { .is_add = true, }; + int ret; + + ret = argp_parse(&add_argp, argc, argv, 0, NULL, &args); + if (ret) + return ret; + + return do_mod(&args); +} + +static void __attribute__((constructor)) add_ctor(void) +{ + cmd_register_argp("quota-add", &add_argp, GROUP_CORE, add_cmd); +} + +static struct argp_option del_options[] = { + { "path", 'p', "PATH", 0, "Path to ScoutFS filesystem"}, + { "rule", 'r', "RULE_STRING", 0, "Rule string"}, + { NULL } +}; + +static struct argp del_argp = { + del_options, + parse_mod_opt, + "", + "Delete quota rule" +}; + +static int del_cmd(int argc, char **argv) +{ + struct mod_args args = { .is_add = false }; + int ret; + + ret = argp_parse(&del_argp, argc, argv, 0, NULL, &args); + if (ret) + return ret; + + return do_mod(&args); +} + +static void __attribute__((constructor)) del_ctor(void) +{ + cmd_register_argp("quota-del", &del_argp, GROUP_CORE, del_cmd); +} + +/* ---------------------------------------------- */ + +struct bulk_args { + char *path; + bool unsorted; +}; + +typedef int (*bulk_in_fn)(int fd, struct scoutfs_ioctl_quota_rule *irules, size_t nr, + void *in_args); +typedef int (*bulk_out_fn)(int fd, struct scoutfs_ioctl_quota_rule *irule, void *out_args); + +static int cmp_irules(const struct scoutfs_ioctl_quota_rule *a, + const struct scoutfs_ioctl_quota_rule *b) +{ + return scoutfs_cmp(a->prio, b->prio) ?: + scoutfs_cmp(a->name_val[0], b->name_val[0]) ?: + scoutfs_cmp(a->name_source[0], b->name_source[0]) ?: + scoutfs_cmp(a->name_flags[0], b->name_flags[0]) ?: + scoutfs_cmp(a->name_val[1], b->name_val[1]) ?: + scoutfs_cmp(a->name_source[1], b->name_source[1]) ?: + scoutfs_cmp(a->name_flags[1], b->name_flags[1]) ?: + scoutfs_cmp(a->name_val[2], b->name_val[2]) ?: + scoutfs_cmp(a->name_source[2], b->name_source[2]) ?: + scoutfs_cmp(a->name_flags[2], b->name_flags[2]) ?: + scoutfs_cmp(a->op, b->op) ?: + scoutfs_cmp(a->limit, b->limit) ?: + scoutfs_cmp(a->rule_flags, b->rule_flags); +} + +static int compar_irules(const void *a, const void *b) +{ + return -cmp_irules(a, b); +} + +static int do_bulk(struct bulk_args *args, bulk_in_fn in_fn, void *in_args, + bulk_out_fn out_fn, void *out_args) +{ + struct scoutfs_ioctl_quota_rule *irules = NULL; + size_t alloced = 0; + size_t nr = 0; + size_t batch; + size_t i; + int fd = -1; + int ret; + + fd = get_path(args->path, O_RDONLY); + if (fd < 0) + return fd; + + for (;;) { + if (nr == alloced) { + alloced += 1024; + irules = realloc(irules, alloced * sizeof(irules[0])); + if (!irules) { + ret = -errno; + fprintf(stderr, "memory allocation failed: %s (%d)\n", + strerror(errno), errno); + goto out; + } + } + + ret = in_fn(fd, &irules[nr], alloced - nr, in_args); + if (ret == 0) + break; + if (ret < 0) + goto out; + + batch = ret; + + if (args->unsorted) { + for (i = 0; i < batch; i++) { + ret = out_fn(fd, &irules[nr + i], out_args); + if (ret < 0) + goto out; + } + } else { + nr += batch; + } + } + + if (!args->unsorted) { + qsort(irules, nr, sizeof(irules[0]), compar_irules); + + for (i = 0; i < nr; i++) { + ret = out_fn(fd, &irules[i], out_args); + if (ret < 0) + goto out; + } + } + + ret = 0; +out: + if (fd >= 0) + close(fd); + if (irules) + free(irules); + + return ret; +} + +/* ---------------------------------------------- */ + +/* maintain iterator in gqr between calls */ +static int get_ioctl_in_fn(int fd, struct scoutfs_ioctl_quota_rule *irules, size_t nr, + void *in_args) +{ + struct scoutfs_ioctl_get_quota_rules *gqr = in_args; + int ret; + + gqr->rules_ptr = (intptr_t)irules; + gqr->rules_nr = nr; + + ret = ioctl(fd, SCOUTFS_IOC_GET_QUOTA_RULES, gqr); + if (ret < 0) { + ret = -errno; + fprintf(stderr, "GET_QUOTA_RULES ioctl failed: %s (%d)\n", + strerror(errno), errno); + } + + return ret; +} + +static int parse_stdin_in_fn(int fd, struct scoutfs_ioctl_quota_rule *irules, size_t nr, + void *in_args) +{ + char *line = NULL; + size_t size; + int ret; + + ret = getline(&line, &size, stdin); + if (ret < 0) { + if (errno == ENOENT) + return 0; + + ret = -errno; + fprintf(stderr, "error reading rules: %s (%d)\n", + strerror(errno), errno); + return ret; + } + + ret = parse_rule(&irules[0], line); + if (ret == 0) + ret = 1; + + free(line); + + return ret; +} + +struct mod_ioctl_args { + unsigned int cmd; + char *which; +}; + +static int mod_ioctl_out_fn(int fd, struct scoutfs_ioctl_quota_rule *irule, void *out_args) +{ + struct mod_ioctl_args *args = out_args; + int ret; + + ret = ioctl(fd, args->cmd, irule); + if (ret < 0) { + ret = -errno; + printf("Failed to %s following rule:\n ", args->which); + printf_rule(irule); + fprintf(stderr, "Error: %s (%d)\n", strerror(-ret), -ret); + } + + return ret; +} + +static int print_out_fn(int fd, struct scoutfs_ioctl_quota_rule *irule, void *out_args) +{ + printf_rule(irule); + return 0; +} + +/* ---------------------------------------------- */ + +static int parse_bulk_opt(int key, char *arg, struct argp_state *state) +{ + struct bulk_args *args = state->input; + + switch (key) { + case 'p': + args->path = strdup_or_error(state, arg); + break; + case 'U': + args->unsorted = true; + break; + case ARGP_KEY_FINI: + if (!args->path) + argp_error(state, "must provide file path"); + break; + default: + break; + } + + return 0; +} + +static struct argp_option bulk_options[] = { + { "path", 'p', "PATH", 0, "Path to ScoutFS filesystem"}, + { "unsorted", 'U', NULL, 0, "Process rules in unsorted filesystem storage order"}, + { NULL } +}; + +static struct argp list_argp = { + bulk_options, + parse_bulk_opt, + "", + "List quota rules" +}; + +static int list_cmd(int argc, char **argv) +{ + struct scoutfs_ioctl_get_quota_rules gqr = {{0,}}; + struct bulk_args args = {NULL}; + int ret; + + ret = argp_parse(&list_argp, argc, argv, 0, NULL, &args); + if (ret) + return ret; + + return do_bulk(&args, get_ioctl_in_fn, &gqr, print_out_fn, NULL); +} + +static void __attribute__((constructor)) list_ctor(void) +{ + cmd_register_argp("quota-list", &list_argp, GROUP_CORE, list_cmd); +} + +/* ---------------------------------------------- */ + +static struct argp wipe_argp = { + bulk_options, + parse_bulk_opt, + "", + "Delete all quota rules" +}; + +static int wipe_cmd(int argc, char **argv) +{ + struct bulk_args args = {NULL}; + struct scoutfs_ioctl_get_quota_rules gqr = {{0,}}; + struct mod_ioctl_args out_args = { + .cmd = SCOUTFS_IOC_DEL_QUOTA_RULE, + .which = "delete", + }; + int ret; + + ret = argp_parse(&wipe_argp, argc, argv, 0, NULL, &args); + if (ret) + return ret; + + return do_bulk(&args, get_ioctl_in_fn, &gqr, mod_ioctl_out_fn, &out_args); +} + +static void __attribute__((constructor)) wipe_ctor(void) +{ + cmd_register_argp("quota-wipe", &wipe_argp, GROUP_CORE, wipe_cmd); +} + +/* ---------------------------------------------- */ + +static struct argp restore_argp = { + bulk_options, + parse_bulk_opt, + "", + "Restore quota rules from list output on stdin" +}; + +static int restore_cmd(int argc, char **argv) +{ + struct bulk_args args = {NULL}; + struct mod_ioctl_args out_args = { + .cmd = SCOUTFS_IOC_ADD_QUOTA_RULE, + .which = "add", + }; + int ret; + + ret = argp_parse(&restore_argp, argc, argv, 0, NULL, &args); + if (ret) + return ret; + + return do_bulk(&args, parse_stdin_in_fn, NULL, mod_ioctl_out_fn, &out_args); +} + +static void __attribute__((constructor)) restore_ctor(void) +{ + cmd_register_argp("quota-restore", &restore_argp, GROUP_CORE, restore_cmd); +} diff --git a/utils/src/read_xattr_index.c b/utils/src/read_xattr_index.c new file mode 100644 index 00000000..7f378558 --- /dev/null +++ b/utils/src/read_xattr_index.c @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sparse.h" +#include "parse.h" +#include "util.h" +#include "format.h" +#include "ioctl.h" +#include "cmd.h" +#include "cmp.h" + +#define ENTF "%u.%llu.%llu" +#define ENTA(e) (e)->major, (e)->minor, (e)->ino + +struct xattr_args { + char *path; + char *first_entry; + char *last_entry; +}; + +static int compare_entries(struct scoutfs_ioctl_xattr_index_entry *a, + struct scoutfs_ioctl_xattr_index_entry *b) +{ + return scoutfs_cmp(a->major, b->major) ?: scoutfs_cmp(a->minor, b->minor) ?: + scoutfs_cmp(a->ino, b->ino); +} + +static int parse_entry(struct scoutfs_ioctl_xattr_index_entry *ent, char *str) +{ + int major; + int ret; + + ret = sscanf(str, "%i.%lli.%lli", &major, &ent->minor, &ent->ino); + if (ret != 3) { + fprintf(stderr, "bad index position entry argument '%s', it must be " + "in the form \"a.b.ino\" where each value can be prefixed by " + "'0' for octal or '0x' for hex\n", str); + return -EINVAL; + } + + if (major < 0 || major > UCHAR_MAX) { + fprintf(stderr, "initial major index position '%d' must be between 0 and 255, " + "inclusive.\n", major); + return -EINVAL; + } + + ent->major = major; + + return 0; +} + +#define NR_ENTRIES 1024 + +static int do_read_xattr_index(struct xattr_args *args) +{ + struct scoutfs_ioctl_read_xattr_index rxi; + struct scoutfs_ioctl_xattr_index_entry *ents; + struct scoutfs_ioctl_xattr_index_entry *ent; + int fd = -1; + int ret; + int i; + + ents = calloc(NR_ENTRIES, sizeof(struct scoutfs_ioctl_xattr_index_entry)); + if (!ents) { + fprintf(stderr, "xattr index entry allocation failed\n"); + ret = -ENOMEM; + goto out; + } + + fd = get_path(args->path, O_RDONLY); + if (fd < 0) + return fd; + + memset(&rxi, 0, sizeof(rxi)); + memset(&rxi.last, 0xff, sizeof(rxi.last)); + rxi.entries_ptr = (unsigned long)ents; + rxi.entries_nr = NR_ENTRIES; + + ret = 0; + if (args->first_entry) + ret = parse_entry(&rxi.first, args->first_entry); + if (args->last_entry) + ret = parse_entry(&rxi.last, args->last_entry); + if (ret < 0) + goto out; + + if (compare_entries(&rxi.first, &rxi.last) > 0) { + fprintf(stderr, "first index position "ENTF" must be less than last index position "ENTF"\n", + ENTA(&rxi.first), ENTA(&rxi.last)); + ret = -EINVAL; + goto out; + } + + for (;;) { + ret = ioctl(fd, SCOUTFS_IOC_READ_XATTR_INDEX, &rxi); + if (ret == 0) + break; + if (ret < 0) { + ret = -errno; + fprintf(stderr, "read_xattr_index ioctl failed: " + "%s (%d)\n", strerror(errno), errno); + goto out; + } + + for (i = 0; i < ret; i++) { + ent = &ents[i]; + printf("%u.%llu = %llu\n", + ent->major, ent->minor, ent->ino); + } + + rxi.first = *ent; + + if ((++rxi.first.ino == 0 && ++rxi.first.minor == 0 && ++rxi.first.major == 0) || + compare_entries(&rxi.first, &rxi.last) > 0) + break; + } + + ret = 0; +out: + if (fd >= 0) + close(fd); + free(ents); + + return ret; +}; + +static int parse_opt(int key, char *arg, struct argp_state *state) +{ + struct xattr_args *args = state->input; + + switch (key) { + case 'p': + args->path = strdup_or_error(state, arg); + break; + case ARGP_KEY_ARG: + if (!args->first_entry) + args->first_entry = strdup_or_error(state, arg); + else if (!args->last_entry) + args->last_entry = strdup_or_error(state, arg); + else + argp_error(state, "more than two entry arguments given"); + break; + default: + break; + } + + return 0; +} + +static struct argp_option options[] = { + { "path", 'p', "PATH", 0, "Path to ScoutFS filesystem"}, + { NULL } +}; + +static struct argp argp = { + options, + parse_opt, + "FIRST-ENTRY LAST-ENTRY", + "Search and print inode numbers indexed by their .indx. xattrs" +}; + +static int read_xattr_index_cmd(int argc, char **argv) +{ + + struct xattr_args xattr_args = {NULL}; + int ret; + + ret = argp_parse(&argp, argc, argv, 0, NULL, &xattr_args); + if (ret) + return ret; + + return do_read_xattr_index(&xattr_args); +} + +static void __attribute__((constructor)) read_xattr_index_ctor(void) +{ + cmd_register_argp("read-xattr-index", &argp, GROUP_INFO, read_xattr_index_cmd); +}