From 7d4db05445578bd714e10a2af2d796764bab9802 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 11 May 2022 14:06:02 -0700 Subject: [PATCH 01/28] Add scoutfs_item_lookup_smaller_zero Add a lookup variant that returns an error if the item value is larger than the caller's value buffer size and which zeros the rest of the caller's buffer if the returned value is smaller. Signed-off-by: Zach Brown --- kmod/src/item.c | 33 ++++++++++++++++++++++++++++++--- kmod/src/item.h | 2 ++ 2 files changed, 32 insertions(+), 3 deletions(-) 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); From 6931cb7b0e4423361efa4c26de9dd8888d4a6d3e Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Fri, 14 Jun 2024 15:54:12 -0700 Subject: [PATCH 02/28] Add scoutfs_inode_[gs]et_flags Add functions for getting and setting our private inode flags. Signed-off-by: Zach Brown --- kmod/src/inode.c | 26 ++++++++++++++++++++++++++ kmod/src/inode.h | 2 ++ 2 files changed, 28 insertions(+) diff --git a/kmod/src/inode.c b/kmod/src/inode.c index db76f798..11c9264e 100644 --- a/kmod/src/inode.c +++ b/kmod/src/inode.c @@ -686,6 +686,32 @@ 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); +} + static int scoutfs_iget_test(struct inode *inode, void *arg) { struct scoutfs_inode_info *si = SCOUTFS_I(inode); diff --git a/kmod/src/inode.h b/kmod/src/inode.h index dd8e33f3..b7d347a3 100644 --- a/kmod/src/inode.h +++ b/kmod/src/inode.h @@ -120,6 +120,8 @@ 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); int scoutfs_complete_truncate(struct inode *inode, struct scoutfs_lock *lock); int scoutfs_inode_refresh(struct inode *inode, struct scoutfs_lock *lock); From 90cfaf17d11d349ce0a1da3331a0cc2983da848e Mon Sep 17 00:00:00 2001 From: "Bryant G. Duffy-Ly" Date: Fri, 7 Jan 2022 08:03:43 -0600 Subject: [PATCH 03/28] Initial support for different inode sizes We're about to increase the inode size and increment the format version. Inode reading and writing has to handle different valid inode sizes as allowed by the format version. This is the initial skeletal work that later patches which really increase the inode size will further refine to add the specific known sizes and format versions. Signed-off-by: Bryant G. Duffy-Ly [zab@versity.com: reworded description, reworked to use _within] Signed-off-by: Zach Brown --- kmod/src/format.h | 21 +++++++++++++++++ kmod/src/inode.c | 59 +++++++++++++++++++++++++++++++++++------------ utils/src/mkfs.c | 2 +- 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/kmod/src/format.h b/kmod/src/format.h index da1683dd..2824c3cb 100644 --- a/kmod/src/format.h +++ b/kmod/src/format.h @@ -861,6 +861,27 @@ struct scoutfs_inode { struct scoutfs_timespec crtime; }; +/* + * 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) +{ + 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); +} + #define SCOUTFS_INO_FLAG_TRUNCATE 0x1 #define SCOUTFS_ROOT_INO 1 diff --git a/kmod/src/inode.c b/kmod/src/inode.c index 11c9264e..fdc473c2 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); @@ -298,6 +298,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 +351,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; @@ -821,7 +839,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; @@ -880,15 +898,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; @@ -1090,9 +1111,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; @@ -1101,15 +1124,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); @@ -1477,10 +1502,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); @@ -1511,14 +1538,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: @@ -1772,7 +1801,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; 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); From 9ba4271c2642dde70d1db32ee9041822d3539d2f Mon Sep 17 00:00:00 2001 From: "Bryant G. Duffy-Ly" Date: Fri, 7 Jan 2022 10:31:46 -0600 Subject: [PATCH 04/28] Add new max format version of 2 We're about to add new format structures so increment the max version to 2. Future commits will add the features before we release version 2 in the wild. Signed-off-by: Zach Brown --- kmod/src/format.h | 2 +- utils/man/scoutfs.5 | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/kmod/src/format.h b/kmod/src/format.h index 2824c3cb..46f2cb85 100644 --- a/kmod/src/format.h +++ b/kmod/src/format.h @@ -8,7 +8,7 @@ */ #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) /* statfs(2) f_type */ diff --git a/utils/man/scoutfs.5 b/utils/man/scoutfs.5 index fb56d19d..650145ec 100644 --- a/utils/man/scoutfs.5 +++ b/utils/man/scoutfs.5 @@ -373,6 +373,16 @@ 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 +.RE .SH CORRUPTION DETECTION A From 361491846de49b966b6812cfdf0b7de656194b76 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Thu, 27 Jun 2024 13:53:53 -0700 Subject: [PATCH 05/28] Add scoutfs_fmt_vers_unsupported() Add a little inline helper to test whether the mounted format version supports a feature or not, returning an errno that callers can use when they can return a shared expected error. Signed-off-by: Zach Brown --- kmod/src/super.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/kmod/src/super.h b/kmod/src/super.h index 14ff626a..ff572ef5 100644 --- a/kmod/src/super.h +++ b/kmod/src/super.h @@ -156,4 +156,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 From 0521bd0e6bd9e77296e407d04a66a0f0f347dd77 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 26 Jun 2024 13:36:22 -0700 Subject: [PATCH 06/28] Make offline extent creation use one transaction Initially setattr_more followed the general pattern where extent manipulation might require multiple transactions if there are lots of extent items to work with. The scoutfs_data_init_offline_extent() function that creates an offline extent handled transactions itself. But in this case the call only supports adding a single offline extent. It will always use a small fixed amount of metadata and could be combined with other metadata changes in one atomic transaction. This changes scoutfs_data_init_offline_extent() to have the caller handle transactions, inode updates, etc. This lets the caller perform all the restore changes in one transaction. This interface change will then be used as we add another caller that adds a single offline extent in the same way. Signed-off-by: Zach Brown --- kmod/src/data.c | 25 +++---------------------- kmod/src/ioctl.c | 16 ++++++++-------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/kmod/src/data.c b/kmod/src/data.c index 54a1857b..1e48b6c3 100644 --- a/kmod/src/data.c +++ b/kmod/src/data.c @@ -1155,9 +1155,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 +1171,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 +1183,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; } diff --git a/kmod/src/ioctl.c b/kmod/src/ioctl.c index 6bf70403..885ad330 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -669,19 +669,18 @@ static long scoutfs_ioc_setattr_more(struct file *file, unsigned long arg) goto unlock; } - /* create offline extents in potentially many transactions */ + /* 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, true); + if (ret) + goto unlock; + if (sm.flags & SCOUTFS_IOC_SETATTR_MORE_OFFLINE) { ret = scoutfs_data_init_offline_extent(inode, sm.i_size, lock); if (ret) - goto unlock; + goto release; } - /* 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) @@ -694,6 +693,7 @@ static long scoutfs_ioc_setattr_more(struct file *file, unsigned long arg) scoutfs_update_inode_item(inode, lock, &ind_locks); ret = 0; +release: scoutfs_release_trans(sb); unlock: scoutfs_inode_index_unlock(sb, &ind_locks); From 6a99ca9ede06a5764ef66876d0923a8ed0cf0c73 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Thu, 27 Jun 2024 15:48:34 -0700 Subject: [PATCH 07/28] Add attr_x core and ioctls The existing stat_more and setattr_more interfaces aren't extensible. This solves that problem by adding attribute interfaces which specify the specific fields to work with. We're about to add a few more inode fields and it makes sense to add them to this extensible structure rather than adding more ioctls or relatively clumsy xattrs. This is modeled loosely on the upstream kernel's statx support. The ioctl entry points call core functions so that we can also implement the existing stat_more and setattr_more interfaces in terms of these new attr_x functions. Signed-off-by: Zach Brown --- kmod/src/Makefile | 1 + kmod/src/attr_x.c | 218 ++++++++++++++++++++++++++++++++++++++++++++++ kmod/src/attr_x.h | 11 +++ kmod/src/ioctl.c | 67 ++++++++++++++ kmod/src/ioctl.h | 54 ++++++++++++ 5 files changed, 351 insertions(+) create mode 100644 kmod/src/attr_x.c create mode 100644 kmod/src/attr_x.h diff --git a/kmod/src/Makefile b/kmod/src/Makefile index 2666e6df..dde66039 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 \ diff --git a/kmod/src/attr_x.c b/kmod/src/attr_x.c new file mode 100644 index 00000000..75c62b32 --- /dev/null +++ b/kmod/src/attr_x.c @@ -0,0 +1,218 @@ +/* + * 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; + + 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)); + + 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; + + 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; + } + + /* 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; + } + + 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/ioctl.c b/kmod/src/ioctl.c index 885ad330..74abcf1d 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -42,6 +42,7 @@ #include "alloc.h" #include "server.h" #include "counters.h" +#include "attr_x.h" #include "scoutfs_trace.h" /* @@ -1504,6 +1505,68 @@ 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; +} + long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { @@ -1541,6 +1604,10 @@ 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); } return -ENOTTY; diff --git a/kmod/src/ioctl.h b/kmod/src/ioctl.h index 7be22dbb..d62f0283 100644 --- a/kmod/src/ioctl.h +++ b/kmod/src/ioctl.h @@ -673,4 +673,58 @@ 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; +}; + +/* + * 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) + +/* + * 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. + */ +#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__UNKNOWN (U64_MAX << 8) + +#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) + #endif From de304628ea24a879a916e4c6ec15e226e4db61b1 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Tue, 18 Jun 2024 15:14:30 -0700 Subject: [PATCH 08/28] Add attr_x commands and documentation to utils Signed-off-by: Zach Brown --- utils/man/scoutfs.8 | 19 +++ utils/src/attr_x.c | 281 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 utils/src/attr_x.c 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..00cb94b3 --- /dev/null +++ b/utils/src/attr_x.c @@ -0,0 +1,281 @@ +#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); + } + + 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; + + 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 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"}, + { 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); +} From 270726a6ea54921a64e05425d1a040c4f717d8b1 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 26 Jun 2024 12:48:32 -0700 Subject: [PATCH 09/28] Implement stat_more and setattr_more with attr_x Now that we have the attr_x calls we can implement stat_more with get_attr_x and setattr_more with set_attr_x. The conversion of stat_more fixes a surprising consistency bug. stat_more wasn't acquiring a cluster lock for the inode nore refreshing it so it could have returned stale data if modifications were made in another mount. Signed-off-by: Zach Brown --- kmod/src/ioctl.c | 129 ++++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 70 deletions(-) diff --git a/kmod/src/ioctl.c b/kmod/src/ioctl.c index 74abcf1d..8cda4594 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -546,20 +546,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) @@ -616,24 +637,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; @@ -644,65 +660,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; - } - - /* 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, true); - if (ret) - goto unlock; - - if (sm.flags & SCOUTFS_IOC_SETATTR_MORE_OFFLINE) { - ret = scoutfs_data_init_offline_extent(inode, sm.i_size, lock); - if (ret) - goto release; - } - - 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; - -release: - 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; } From 5ccdf3c9f097d00a8d008a23ea6bd2708daf4768 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Thu, 3 Feb 2022 09:26:01 -0800 Subject: [PATCH 10/28] Add T_MODULE for tests Signed-off-by: Zach Brown --- tests/README.md | 1 + tests/run-tests.sh | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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/run-tests.sh b/tests/run-tests.sh index 2890f58a..c91cea7b 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -352,7 +352,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 From a23877b1506b2fc679006a05d134083e5ecd0afa Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Thu, 3 Feb 2022 14:05:32 -0800 Subject: [PATCH 11/28] Add fs test functions for mounted paths We have some fs functions which return info based on the test mount nr as the test has setup. This refactors those a bit to also provide some of the info when the caller has a path in a given mount. This will let tests work with scratch mounts a little more easily. Signed-off-by: Zach Brown Signed-off-by: Bryant G. Duffy-Ly --- tests/funcs/fs.sh | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) 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) } # From 5a53e7144d47638bd0d452bb7fe52143c877d7a7 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Thu, 3 Feb 2022 09:06:34 -0800 Subject: [PATCH 12/28] Add format-version back/forward compat test Signed-off-by: Zach Brown Signed-off-by: Bryant G. Duffy-Ly --- tests/funcs/filter.sh | 4 + tests/golden/format-version-forward-back | 4 + tests/sequence | 1 + tests/tests/format-version-forward-back.sh | 179 +++++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 tests/golden/format-version-forward-back create mode 100644 tests/tests/format-version-forward-back.sh 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/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/sequence b/tests/sequence index fc656e2c..c47d41aa 100644 --- a/tests/sequence +++ b/tests/sequence @@ -13,6 +13,7 @@ setattr_more.sh offline-extent-waiting.sh move-blocks.sh large-fragmented-free.sh +format-version-forward-back.sh enospc.sh srch-safe-merge-pos.sh srch-basic-functionality.sh 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 From fb5331a1d9bcd912d0332f420693e9e39ca344d0 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Mon, 17 Jun 2024 14:20:45 -0700 Subject: [PATCH 13/28] Add inode retention bit Add a bit to the private scoutfs inode flags which indicates that the inode is in retention mode. The bit is visible through the _attr_x interface. It can only be set on regular files and when set it prevents modification to all but non-user xattrs. It can be cleared by root. Signed-off-by: Zach Brown --- kmod/src/attr_x.c | 26 ++++++++++++++++++++++++++ kmod/src/data.c | 9 +++++++++ kmod/src/dir.c | 8 ++++++++ kmod/src/file.c | 8 ++++++++ kmod/src/format.h | 5 ++++- kmod/src/inode.c | 15 +++++++++++++++ kmod/src/inode.h | 2 ++ kmod/src/ioctl.h | 23 ++++++++++++++++++++++- kmod/src/xattr.c | 17 +++++++++++++++++ utils/man/scoutfs.5 | 18 ++++++++++++++++++ utils/src/attr_x.c | 13 +++++++++++++ 11 files changed, 142 insertions(+), 2 deletions(-) diff --git a/kmod/src/attr_x.c b/kmod/src/attr_x.c index 75c62b32..56718197 100644 --- a/kmod/src/attr_x.c +++ b/kmod/src/attr_x.c @@ -29,6 +29,10 @@ static int validate_attr_x_input(struct super_block *sb, struct scoutfs_ioctl_in (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; + return 0; } @@ -100,6 +104,13 @@ int scoutfs_get_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *i 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); + } ret = size; unlock: @@ -131,6 +142,10 @@ static bool valid_attr_changes(struct inode *inode, struct scoutfs_ioctl_inode_a (!(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; } @@ -170,6 +185,12 @@ int scoutfs_set_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *i 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; @@ -204,6 +225,11 @@ int scoutfs_set_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *i 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); + } scoutfs_update_inode_item(inode, lock, &ind_locks); ret = 0; diff --git a/kmod/src/data.c b/kmod/src/data.c index 1e48b6c3..7d14c55a 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; @@ -1254,6 +1260,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..e382c78d 100644 --- a/kmod/src/dir.c +++ b/kmod/src/dir.c @@ -926,6 +926,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 +1636,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..3b6a2d12 100644 --- a/kmod/src/file.c +++ b/kmod/src/file.c @@ -108,6 +108,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; @@ -216,6 +220,10 @@ 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; diff --git a/kmod/src/format.h b/kmod/src/format.h index 46f2cb85..88abb822 100644 --- a/kmod/src/format.h +++ b/kmod/src/format.h @@ -11,6 +11,8 @@ #define SCOUTFS_FORMAT_VERSION_MAX 2 #define SCOUTFS_FORMAT_VERSION_MAX_STR __stringify(SCOUTFS_FORMAT_VERSION_MAX) +#define SCOUTFS_FORMAT_VERSION_FEAT_RETENTION 2 + /* statfs(2) f_type */ #define SCOUTFS_SUPER_MAGIC 0x554f4353 /* "SCOU" */ @@ -882,7 +884,8 @@ static inline int scoutfs_inode_valid_vers_bytes(__u64 fmt_vers, int bytes) return bytes == sizeof(struct scoutfs_inode); } -#define SCOUTFS_INO_FLAG_TRUNCATE 0x1 +#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 fdc473c2..4bd1492b 100644 --- a/kmod/src/inode.c +++ b/kmod/src/inode.c @@ -504,6 +504,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); @@ -2190,6 +2194,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 b7d347a3..6ab7d220 100644 --- a/kmod/src/inode.h +++ b/kmod/src/inode.h @@ -124,6 +124,8 @@ u32 scoutfs_inode_get_flags(struct inode *inode); void scoutfs_inode_set_flags(struct inode *inode, u32 and, u32 or); 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.h b/kmod/src/ioctl.h index d62f0283..fbb04217 100644 --- a/kmod/src/ioctl.h +++ b/kmod/src/ioctl.h @@ -686,6 +686,7 @@ struct scoutfs_ioctl_inode_attr_x { __u32 crtime_nsec; __u64 crtime_sec; __u64 size; + __u64 bits; }; /* @@ -701,6 +702,13 @@ struct scoutfs_ioctl_inode_attr_x { #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 @@ -710,6 +718,14 @@ struct scoutfs_ioctl_inode_attr_x { * 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) @@ -719,7 +735,12 @@ struct scoutfs_ioctl_inode_attr_x { #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__UNKNOWN (U64_MAX << 8) +#define SCOUTFS_IOC_IAX_RETENTION (1ULL << 8) + +/* 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 << 9) #define SCOUTFS_IOC_GET_ATTR_X \ _IOW(SCOUTFS_IOCTL_MAGIC, 18, struct scoutfs_ioctl_inode_attr_x) diff --git a/kmod/src/xattr.c b/kmod/src/xattr.c index fa418b8c..2f33b60a 100644 --- a/kmod/src/xattr.c +++ b/kmod/src/xattr.c @@ -81,6 +81,18 @@ 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 SRCH_TAG "srch." #define TOTL_TAG "totl." @@ -670,6 +682,11 @@ int scoutfs_xattr_set_locked(struct inode *inode, const char *name, size_t name_ if (tgs->totl && ((ret = parse_totl_key(&totl_key, name, name_len)) != 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 */ xat_bytes_totl = first_item_bytes(name_len, max_t(size_t, size, SCOUTFS_XATTR_MAX_TOTL_U64)); diff --git a/utils/man/scoutfs.5 b/utils/man/scoutfs.5 index 650145ec..839c8cd9 100644 --- a/utils/man/scoutfs.5 +++ b/utils/man/scoutfs.5 @@ -295,6 +295,23 @@ 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 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 @@ -382,6 +399,7 @@ The defined format versions are: Initial format version. .TP .B 2 +Added retention mode by setting the retention attribute. .RE .SH CORRUPTION DETECTION diff --git a/utils/src/attr_x.c b/utils/src/attr_x.c index 00cb94b3..979ac517 100644 --- a/utils/src/attr_x.c +++ b/utils/src/attr_x.c @@ -81,6 +81,7 @@ static int do_attr_x(struct attr_x_args *args) 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"); } ret = 0; @@ -107,6 +108,7 @@ 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': @@ -179,6 +181,16 @@ static int parse_opt(int key, char *arg, struct argp_state *state) 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 ARGP_KEY_ARG: if (!args->filename) args->filename = strdup_or_error(state, arg); @@ -209,6 +221,7 @@ static struct argp_option set_options[] = { { "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"}, { NULL } }; From 460f3ce503bdef043ff6d64b7778442092c75cdf Mon Sep 17 00:00:00 2001 From: "Bryant G. Duffy-Ly" Date: Mon, 24 Jan 2022 14:48:14 -0600 Subject: [PATCH 14/28] Add unit tests for retention Signed-off-by: Bryant G. Duffy-Ly [zab@versity.com: refactored for retention, added test cases] Signed-off-by: Zach Brown --- tests/golden/retention-basic | 28 +++++++++++++++++ tests/sequence | 1 + tests/tests/retention-basic.sh | 57 ++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 tests/golden/retention-basic create mode 100644 tests/tests/retention-basic.sh 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/sequence b/tests/sequence index c47d41aa..448f0473 100644 --- a/tests/sequence +++ b/tests/sequence @@ -18,6 +18,7 @@ enospc.sh srch-safe-merge-pos.sh srch-basic-functionality.sh simple-xattr-unit.sh +retention-basic.sh totl-xattr-tag.sh lock-refleak.sh lock-shrink-consistency.sh 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 From 3a51ca369b2178308a3ddf3cc513641eb8477e21 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Tue, 8 Aug 2023 09:13:34 -0700 Subject: [PATCH 15/28] Add the weak item cache Add the weak item cache that is used for reads that can handle results being a little behind. This gives us a lot more freedom to implement the cache that biases concurrent reads. Signed-off-by: Zach Brown --- kmod/src/Makefile | 1 + kmod/src/scoutfs_trace.h | 2 + kmod/src/super.c | 3 + kmod/src/super.h | 2 + kmod/src/trace/wkic.h | 112 ++++ kmod/src/wkic.c | 1155 ++++++++++++++++++++++++++++++++++++++ kmod/src/wkic.h | 19 + 7 files changed, 1294 insertions(+) create mode 100644 kmod/src/trace/wkic.h create mode 100644 kmod/src/wkic.c create mode 100644 kmod/src/wkic.h diff --git a/kmod/src/Makefile b/kmod/src/Makefile index dde66039..a2447278 100644 --- a/kmod/src/Makefile +++ b/kmod/src/Makefile @@ -47,6 +47,7 @@ scoutfs-y += \ triggers.o \ tseq.o \ volopt.o \ + wkic.o \ xattr.o # diff --git a/kmod/src/scoutfs_trace.h b/kmod/src/scoutfs_trace.h index 0f30c513..da75b03b 100644 --- a/kmod/src/scoutfs_trace.h +++ b/kmod/src/scoutfs_trace.h @@ -38,6 +38,8 @@ #include "data.h" #include "ext.h" +#include "trace/wkic.h" + struct lock_info; #define STE_FMT "[%llu %llu %llu 0x%x]" diff --git a/kmod/src/super.c b/kmod/src/super.c index 434aace6..f38af5ce 100644 --- a/kmod/src/super.c +++ b/kmod/src/super.c @@ -49,6 +49,7 @@ #include "volopt.h" #include "fence.h" #include "xattr.h" +#include "wkic.h" #include "scoutfs_trace.h" static struct dentry *scoutfs_debugfs_root; @@ -195,6 +196,7 @@ static void scoutfs_put_super(struct super_block *sb) scoutfs_volopt_destroy(sb); scoutfs_client_destroy(sb); scoutfs_inode_destroy(sb); + scoutfs_wkic_destroy(sb); scoutfs_item_destroy(sb); scoutfs_forest_destroy(sb); scoutfs_data_destroy(sb); @@ -544,6 +546,7 @@ 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_data_setup(sb) ?: scoutfs_setup_trans(sb) ?: diff --git a/kmod/src/super.h b/kmod/src/super.h index ff572ef5..bdbc1b81 100644 --- a/kmod/src/super.h +++ b/kmod/src/super.h @@ -30,6 +30,7 @@ struct recov_info; struct omap_info; struct volopt_info; struct fence_info; +struct wkic_info; struct scoutfs_sb_info { struct super_block *sb; @@ -55,6 +56,7 @@ 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 fence_info *fence_info; /* tracks tasks waiting for data extents */ 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 From 5f156b7a36082610f6915145b089e6f6f45b742c Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Tue, 15 Aug 2023 16:15:11 -0700 Subject: [PATCH 16/28] Add scoutfs_forest_read_items_roots Add a forest item reading interface that lets the caller specify the net roots instead of always getting them from a network request. Signed-off-by: Zach Brown --- kmod/src/forest.c | 38 ++++++++++++++++++++++++-------------- kmod/src/forest.h | 4 ++++ 2 files changed, 28 insertions(+), 14 deletions(-) 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); From ee9e8c3e1ad3a32e2e0b3524507d9ce47b21bfaa Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 16 Aug 2023 12:26:59 -0700 Subject: [PATCH 17/28] Extract .totl. item merging into own functions The _READ_XATTR_TOTALS ioctl had manual code for merging the .totl. total and value while reading fs items. We're going to want to do this in another reader so let's put these in their own funcions that clearly isolate the logic of merging the fs items into a coherent result. We can get rid of some of the totl_read_ counters that tracked which items we were merging. They weren't adding much value and conflated the reading ioctl interface with the merging logic. Signed-off-by: Zach Brown --- kmod/src/Makefile | 1 + kmod/src/counters.h | 3 -- kmod/src/ioctl.c | 73 +++--------------------------------- kmod/src/lock.c | 6 +-- kmod/src/totl.c | 90 +++++++++++++++++++++++++++++++++++++++++++++ kmod/src/totl.h | 24 ++++++++++++ 6 files changed, 122 insertions(+), 75 deletions(-) create mode 100644 kmod/src/totl.c create mode 100644 kmod/src/totl.h diff --git a/kmod/src/Makefile b/kmod/src/Makefile index a2447278..ca1b657e 100644 --- a/kmod/src/Makefile +++ b/kmod/src/Makefile @@ -43,6 +43,7 @@ scoutfs-y += \ srch.o \ super.o \ sysfs.o \ + totl.o \ trans.o \ triggers.o \ tseq.o \ 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/ioctl.c b/kmod/src/ioctl.c index 8cda4594..8906580b 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -43,6 +43,7 @@ #include "server.h" #include "counters.h" #include "attr_x.h" +#include "totl.h" #include "scoutfs_trace.h" /* @@ -1028,15 +1029,7 @@ out: 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 scoutfs_totl_merging merg; }; static int cmp_xt_entry_name(const struct xattr_total_entry *a, @@ -1058,7 +1051,6 @@ static int cmp_xt_entry_name(const struct xattr_total_entry *a, 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) { - struct scoutfs_xattr_totl_val *tval = val; struct xattr_total_entry *ent; struct xattr_total_entry rd; struct rb_root *root = arg; @@ -1095,24 +1087,13 @@ static int read_xattr_total_item(struct super_block *sb, struct scoutfs_key *key return -ENOMEM; memcpy(&ent->xt.name, &rd.xt.name, sizeof(ent->xt.name)); + scoutfs_totl_merge_init(&ent->merg); 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_totl_merge_contribute(&ent->merg, seq, flags, val, val_len, fic); scoutfs_inc_counter(sb, totl_read_item); @@ -1152,30 +1133,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) { @@ -1263,27 +1220,7 @@ static long scoutfs_ioc_read_xattr_totals(struct file *file, unsigned long arg) 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); - } + scoutfs_totl_merge_resolve(&ent->merg, &ent->xt.total, &ent->xt.count); if (ent->xt.total != 0 || ent->xt.count != 0) { if (copy_to_user(uxt, &ent->xt, sizeof(ent->xt))) { diff --git a/kmod/src/lock.c b/kmod/src/lock.c index db4c384f..b3c6b957 100644 --- a/kmod/src/lock.c +++ b/kmod/src/lock.c @@ -36,6 +36,7 @@ #include "item.h" #include "omap.h" #include "util.h" +#include "totl.h" /* * scoutfs uses a lock service to manage item cache consistency between @@ -1244,10 +1245,7 @@ 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); } 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 From 9c45e8b7ef476c484f9f8a477ca582a9dc9d385a Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 16 Aug 2023 14:56:06 -0700 Subject: [PATCH 18/28] read_xattr_totls ioctl uses weak item cache Change the read_xattr_totls ioctl to use the weak item cache instead of manually reading and merging the fs items for the xattr totals on every call. Signed-off-by: Zach Brown --- kmod/src/ioctl.c | 216 ++++++++++++----------------------------------- 1 file changed, 55 insertions(+), 161 deletions(-) diff --git a/kmod/src/ioctl.c b/kmod/src/ioctl.c index 8906580b..c99580d8 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -44,6 +44,7 @@ #include "counters.h" #include "attr_x.h" #include "totl.h" +#include "wkic.h" #include "scoutfs_trace.h" /* @@ -1026,104 +1027,32 @@ out: return ret; } -struct xattr_total_entry { - struct rb_node node; - struct scoutfs_ioctl_xattr_total xt; - struct scoutfs_totl_merging merg; +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 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 read_xattr_total_iter_cb_args *cba = cb_arg; + struct scoutfs_xattr_totl_val *tval = val; + 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)); - scoutfs_totl_merge_init(&ent->merg); - - rb_link_node(&ent->node, parent, node); - rb_insert_color(&ent->node, root); - } - - scoutfs_totl_merge_contribute(&ent->merg, seq, flags, val, val_len, fic); - - 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; } /* @@ -1140,14 +1069,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)) { @@ -1160,6 +1088,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; @@ -1172,81 +1107,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; - - scoutfs_totl_merge_resolve(&ent->merg, &ent->xt.total, &ent->xt.count); - - 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) From 60ca950f42014fbfde91a47d97f1c25d804cdfa2 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Mon, 21 Aug 2023 12:50:48 -0700 Subject: [PATCH 19/28] Drop caches in totl test Now that the _READ_XATTR_TOTALS ioctl uses the weak item cache we have to drop caches before each attempt to read the xattrs that we just wrote and synced. Signed-off-by: Zach Brown --- tests/tests/totl-xattr-tag.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 || \ From 4a8240748e9b832ebb7bf98d6aac85300e250719 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 13 Sep 2023 09:30:14 -0700 Subject: [PATCH 20/28] Add project ID support Add support for project IDs. They're managed through the _attr_x interfaces and are inherited from the parent directory during creation. Signed-off-by: Zach Brown --- kmod/src/attr_x.c | 8 ++++++++ kmod/src/dir.c | 2 ++ kmod/src/format.h | 12 ++++++++++-- kmod/src/inode.c | 26 ++++++++++++++++++++++++++ kmod/src/inode.h | 4 ++++ kmod/src/ioctl.h | 4 +++- utils/man/scoutfs.5 | 16 +++++++++++++++- utils/src/attr_x.c | 10 ++++++++++ utils/src/print.c | 3 ++- 9 files changed, 80 insertions(+), 5 deletions(-) diff --git a/kmod/src/attr_x.c b/kmod/src/attr_x.c index 56718197..39e84a36 100644 --- a/kmod/src/attr_x.c +++ b/kmod/src/attr_x.c @@ -33,6 +33,10 @@ static int validate_attr_x_input(struct super_block *sb, struct scoutfs_ioctl_in (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; } @@ -111,6 +115,8 @@ int scoutfs_get_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *i 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: @@ -230,6 +236,8 @@ int scoutfs_set_attr_x(struct inode *inode, struct scoutfs_ioctl_inode_attr_x *i (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; diff --git a/kmod/src/dir.c b/kmod/src/dir.c index e382c78d..c8417bdf 100644 --- a/kmod/src/dir.c +++ b/kmod/src/dir.c @@ -672,6 +672,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) diff --git a/kmod/src/format.h b/kmod/src/format.h index 88abb822..7e283de8 100644 --- a/kmod/src/format.h +++ b/kmod/src/format.h @@ -12,6 +12,7 @@ #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 /* statfs(2) f_type */ #define SCOUTFS_SUPER_MAGIC 0x554f4353 /* "SCOU" */ @@ -861,8 +862,11 @@ struct scoutfs_inode { struct scoutfs_timespec ctime; struct scoutfs_timespec mtime; struct scoutfs_timespec crtime; + __le64 proj; }; +#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 @@ -872,7 +876,10 @@ struct scoutfs_inode { /* 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) { - return sizeof(struct scoutfs_inode); + 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 @@ -881,7 +888,8 @@ static inline int scoutfs_inode_vers_bytes(__u64 fmt_vers) */ static inline int scoutfs_inode_valid_vers_bytes(__u64 fmt_vers, int bytes) { - return bytes == sizeof(struct scoutfs_inode); + return (bytes == sizeof(struct scoutfs_inode) && fmt_vers == SCOUTFS_FORMAT_VERSION_MAX) || + (bytes == SCOUTFS_INODE_FMT_V1_BYTES); } #define SCOUTFS_INO_FLAG_TRUNCATE 0x1 diff --git a/kmod/src/inode.c b/kmod/src/inode.c index 4bd1492b..2203a8e9 100644 --- a/kmod/src/inode.c +++ b/kmod/src/inode.c @@ -278,6 +278,7 @@ static void load_inode(struct inode *inode, struct scoutfs_inode *cinode, int in 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 @@ -734,6 +735,29 @@ void scoutfs_inode_set_flags(struct inode *inode, u32 and, u32 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); @@ -879,6 +903,7 @@ static void store_inode(struct scoutfs_inode *cinode, struct inode *inode, int i 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); } /* @@ -1527,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); diff --git a/kmod/src/inode.h b/kmod/src/inode.h index 6ab7d220..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; @@ -122,6 +123,9 @@ 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); diff --git a/kmod/src/ioctl.h b/kmod/src/ioctl.h index fbb04217..9a6b9b30 100644 --- a/kmod/src/ioctl.h +++ b/kmod/src/ioctl.h @@ -687,6 +687,7 @@ struct scoutfs_ioctl_inode_attr_x { __u64 crtime_sec; __u64 size; __u64 bits; + __u64 project_id; }; /* @@ -736,11 +737,12 @@ struct scoutfs_ioctl_inode_attr_x { #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 << 9) +#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) diff --git a/utils/man/scoutfs.5 b/utils/man/scoutfs.5 index 839c8cd9..458d69a3 100644 --- a/utils/man/scoutfs.5 +++ b/utils/man/scoutfs.5 @@ -312,6 +312,19 @@ 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 @@ -399,7 +412,8 @@ The defined format versions are: Initial format version. .TP .B 2 -Added retention mode by setting the retention attribute. +Added retention mode by setting the retention attribute. Added the +project ID inode attribute. .RE .SH CORRUPTION DETECTION diff --git a/utils/src/attr_x.c b/utils/src/attr_x.c index 979ac517..9674624e 100644 --- a/utils/src/attr_x.c +++ b/utils/src/attr_x.c @@ -82,6 +82,7 @@ static int do_attr_x(struct attr_x_args *args) 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; @@ -191,6 +192,14 @@ static int parse_opt(int key, char *arg, struct argp_state *state) 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); @@ -222,6 +231,7 @@ static struct argp_option set_options[] = { { "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 } }; diff --git a/utils/src/print.c b/utils/src/print.c index b977da46..e3f81780 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), From 82c2d0b1d0354be18eddd163e12ea9143676b046 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Tue, 12 Dec 2023 14:43:46 -0800 Subject: [PATCH 21/28] Add o_tmpfile_linkat test binary Add a test binary that uses o_tmpfile and linkat to create a file in a given dir. We have something similar, but it's weirdly specific to a given test. This is a simpler building block that could be used by more tests. Signed-off-by: Zach Brown --- tests/.gitignore | 1 + tests/Makefile | 3 +- tests/src/o_tmpfile_linkat.c | 71 ++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/src/o_tmpfile_linkat.c 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/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; +} From 442980f1c9aedcb6840868bec11163ba838e20bb Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Tue, 12 Dec 2023 13:56:25 -0800 Subject: [PATCH 22/28] Add project ID tests Signed-off-by: Zach Brown --- tests/golden/projects | 24 +++++++++++++++++++ tests/sequence | 1 + tests/tests/projects.sh | 52 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 tests/golden/projects create mode 100644 tests/tests/projects.sh 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/sequence b/tests/sequence index 448f0473..317404c5 100644 --- a/tests/sequence +++ b/tests/sequence @@ -12,6 +12,7 @@ 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 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 From 38e6f11ee4e0f6fee4a3d5e8f113bfea930835ce Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 13 Sep 2023 09:30:23 -0700 Subject: [PATCH 23/28] Add quota support Signed-off-by: Zach Brown --- kmod/src/Makefile | 1 + kmod/src/data.c | 4 + kmod/src/dir.c | 5 + kmod/src/file.c | 9 + kmod/src/format.h | 37 ++ kmod/src/ioctl.c | 80 +++ kmod/src/ioctl.h | 69 +++ kmod/src/lock.c | 15 + kmod/src/lock.h | 2 + kmod/src/quota.c | 1261 ++++++++++++++++++++++++++++++++++++++ kmod/src/quota.h | 48 ++ kmod/src/scoutfs_trace.h | 2 + kmod/src/super.c | 3 + kmod/src/super.h | 2 + kmod/src/trace/quota.h | 143 +++++ 15 files changed, 1681 insertions(+) create mode 100644 kmod/src/quota.c create mode 100644 kmod/src/quota.h create mode 100644 kmod/src/trace/quota.h diff --git a/kmod/src/Makefile b/kmod/src/Makefile index ca1b657e..fa632aa1 100644 --- a/kmod/src/Makefile +++ b/kmod/src/Makefile @@ -35,6 +35,7 @@ scoutfs-y += \ options.o \ per_task.o \ quorum.o \ + quota.o \ recov.o \ scoutfs_trace.o \ server.o \ diff --git a/kmod/src/data.c b/kmod/src/data.c index 7d14c55a..dd10a06f 100644 --- a/kmod/src/data.c +++ b/kmod/src/data.c @@ -1110,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; diff --git a/kmod/src/dir.c b/kmod/src/dir.c index c8417bdf..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) diff --git a/kmod/src/file.c b/kmod/src/file.c index 3b6a2d12..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 /* @@ -126,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); @@ -228,6 +233,10 @@ retry: 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/format.h b/kmod/src/format.h index 7e283de8..47a3a7c9 100644 --- a/kmod/src/format.h +++ b/kmod/src/format.h @@ -13,6 +13,7 @@ #define SCOUTFS_FORMAT_VERSION_FEAT_RETENTION 2 #define SCOUTFS_FORMAT_VERSION_FEAT_PROJECT_ID 2 +#define SCOUTFS_FORMAT_VERSION_FEAT_QUOTA 2 /* statfs(2) f_type */ #define SCOUTFS_SUPER_MAGIC 0x554f4353 /* "SCOU" */ @@ -178,6 +179,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 @@ -588,6 +593,7 @@ 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_FS_ZONE 16 #define SCOUTFS_LOCK_ZONE 20 @@ -611,6 +617,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 @@ -664,6 +673,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)) diff --git a/kmod/src/ioctl.c b/kmod/src/ioctl.c index c99580d8..ceadf91f 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -45,6 +45,7 @@ #include "attr_x.h" #include "totl.h" #include "wkic.h" +#include "quota.h" #include "scoutfs_trace.h" /* @@ -1387,6 +1388,79 @@ out: 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); +} + long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { @@ -1428,6 +1502,12 @@ long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg) 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); } return -ENOTTY; diff --git a/kmod/src/ioctl.h b/kmod/src/ioctl.h index 9a6b9b30..69cf05c3 100644 --- a/kmod/src/ioctl.h +++ b/kmod/src/ioctl.h @@ -750,4 +750,73 @@ 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) + #endif diff --git a/kmod/src/lock.c b/kmod/src/lock.c index b3c6b957..2398f3d9 100644 --- a/kmod/src/lock.c +++ b/kmod/src/lock.c @@ -37,6 +37,7 @@ #include "omap.h" #include "util.h" #include "totl.h" +#include "quota.h" /* * scoutfs uses a lock service to manage item cache consistency between @@ -186,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: @@ -1250,6 +1254,17 @@ int scoutfs_lock_xattr_totl(struct super_block *sb, enum scoutfs_lock_mode mode, 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); +} + void scoutfs_unlock(struct super_block *sb, struct scoutfs_lock *lock, enum scoutfs_lock_mode mode) { DECLARE_LOCK_INFO(sb, linfo); diff --git a/kmod/src/lock.h b/kmod/src/lock.h index 4ba50bcd..1b49e534 100644 --- a/kmod/src/lock.h +++ b/kmod/src/lock.h @@ -86,6 +86,8 @@ 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_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 da75b03b..3fd4821f 100644 --- a/kmod/src/scoutfs_trace.h +++ b/kmod/src/scoutfs_trace.h @@ -37,7 +37,9 @@ #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 f38af5ce..0086d7fb 100644 --- a/kmod/src/super.c +++ b/kmod/src/super.c @@ -50,6 +50,7 @@ #include "fence.h" #include "xattr.h" #include "wkic.h" +#include "quota.h" #include "scoutfs_trace.h" static struct dentry *scoutfs_debugfs_root; @@ -195,6 +196,7 @@ 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); @@ -548,6 +550,7 @@ static int scoutfs_fill_super(struct super_block *sb, void *data, int silent) 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 bdbc1b81..03c6a6ea 100644 --- a/kmod/src/super.h +++ b/kmod/src/super.h @@ -31,6 +31,7 @@ struct omap_info; struct volopt_info; struct fence_info; struct wkic_info; +struct squota_info; struct scoutfs_sb_info { struct super_block *sb; @@ -57,6 +58,7 @@ struct scoutfs_sb_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 */ 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) +); From e0bb6ca481e9bbd8865665a500dd982b871efd15 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Fri, 29 Sep 2023 11:55:38 -0700 Subject: [PATCH 24/28] Add quota support to utils Add scoutfs cli commands for managing quotas and add its persistent structures to the print command. Signed-off-by: Zach Brown --- utils/man/scoutfs.5 | 2 +- utils/src/print.c | 21 ++ utils/src/quota.c | 547 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 569 insertions(+), 1 deletion(-) create mode 100644 utils/src/quota.c diff --git a/utils/man/scoutfs.5 b/utils/man/scoutfs.5 index 458d69a3..4cc6489d 100644 --- a/utils/man/scoutfs.5 +++ b/utils/man/scoutfs.5 @@ -413,7 +413,7 @@ Initial format version. .TP .B 2 Added retention mode by setting the retention attribute. Added the -project ID inode attribute. +project ID inode attribute. Added quota rules and enforcement. .RE .SH CORRUPTION DETECTION diff --git a/utils/src/print.c b/utils/src/print.c index e3f81780..2975b05d 100644 --- a/utils/src/print.c +++ b/utils/src/print.c @@ -80,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; @@ -178,6 +196,9 @@ 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; 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); +} From 6a17dc335f85832a10e119ae422672bea1615f05 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Thu, 30 Nov 2023 15:53:15 -0800 Subject: [PATCH 25/28] Add quota tests Signed-off-by: Zach Brown --- tests/golden/quota | 40 ++++++++++++ tests/sequence | 1 + tests/tests/quota.sh | 150 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 tests/golden/quota create mode 100644 tests/tests/quota.sh 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/sequence b/tests/sequence index 317404c5..1d099b85 100644 --- a/tests/sequence +++ b/tests/sequence @@ -21,6 +21,7 @@ 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/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 From 38c6d66ffc7de0e634cba93c66bd9f0fd4dd0973 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 6 Dec 2023 13:36:18 -0800 Subject: [PATCH 26/28] Add indx xattr tag support Add support for the indx xattr tag which lets xattrs determine the sort order of by their inode number in a global index. Signed-off-by: Zach Brown --- kmod/src/format.h | 2 + kmod/src/ioctl.c | 126 +++++++++++++++++++++++++ kmod/src/ioctl.h | 24 +++++ kmod/src/lock.c | 11 +++ kmod/src/lock.h | 2 + kmod/src/xattr.c | 234 +++++++++++++++++++++++++++++++++++++--------- kmod/src/xattr.h | 6 ++ 7 files changed, 363 insertions(+), 42 deletions(-) diff --git a/kmod/src/format.h b/kmod/src/format.h index 47a3a7c9..83e6015c 100644 --- a/kmod/src/format.h +++ b/kmod/src/format.h @@ -14,6 +14,7 @@ #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" */ @@ -595,6 +596,7 @@ struct scoutfs_log_merge_freeing { #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 */ diff --git a/kmod/src/ioctl.c b/kmod/src/ioctl.c index ceadf91f..cbf442bd 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -1461,6 +1461,130 @@ static long scoutfs_ioc_mod_quota_rule(struct file *file, unsigned long arg, boo 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) { @@ -1508,6 +1632,8 @@ long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg) 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 69cf05c3..9f79839e 100644 --- a/kmod/src/ioctl.h +++ b/kmod/src/ioctl.h @@ -819,4 +819,28 @@ struct scoutfs_ioctl_get_quota_rules { #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/lock.c b/kmod/src/lock.c index 2398f3d9..a6ae9a6e 100644 --- a/kmod/src/lock.c +++ b/kmod/src/lock.c @@ -1254,6 +1254,17 @@ int scoutfs_lock_xattr_totl(struct super_block *sb, enum scoutfs_lock_mode mode, 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) { diff --git a/kmod/src/lock.h b/kmod/src/lock.h index 1b49e534..07908d62 100644 --- a/kmod/src/lock.h +++ b/kmod/src/lock.h @@ -86,6 +86,8 @@ 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, diff --git a/kmod/src/xattr.c b/kmod/src/xattr.c index 2f33b60a..a13f3d19 100644 --- a/kmod/src/xattr.c +++ b/kmod/src/xattr.c @@ -94,6 +94,7 @@ static inline bool is_user(const char *name) } #define HIDE_TAG "hide." +#define INDX_TAG "indx." #define SRCH_TAG "srch." #define TOTL_TAG "totl." #define TAG_LEN (sizeof(HIDE_TAG) - 1) @@ -115,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; @@ -552,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) { @@ -619,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. @@ -639,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); @@ -647,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; @@ -663,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 */ @@ -676,10 +758,17 @@ 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 */ @@ -725,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]); @@ -735,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)); @@ -758,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) @@ -769,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; @@ -794,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); @@ -802,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); } @@ -818,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); @@ -833,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; } @@ -853,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); @@ -862,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; } @@ -1072,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; @@ -1125,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); @@ -1157,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; } @@ -1170,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 From 1bc83e9e2d5d4b2dd4133ec3660db0fb62d83e50 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Mon, 29 Jan 2024 13:29:55 -0800 Subject: [PATCH 27/28] Add indx xattr tag support to utils Signed-off-by: Zach Brown --- utils/man/scoutfs.5 | 18 +++- utils/src/key.h | 9 ++ utils/src/print.c | 14 +++ utils/src/read_xattr_index.c | 186 +++++++++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 utils/src/read_xattr_index.c diff --git a/utils/man/scoutfs.5 b/utils/man/scoutfs.5 index 4cc6489d..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 @@ -413,7 +428,8 @@ 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. +project ID inode attribute. Added quota rules and enforcement. Added +the .indx. extended attribute tag. .RE .SH CORRUPTION DETECTION 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/print.c b/utils/src/print.c index 2975b05d..fb8d60d1 100644 --- a/utils/src/print.c +++ b/utils/src/print.c @@ -108,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]; @@ -202,6 +213,9 @@ static print_func_t find_printer(u8 zone, u8 type) 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/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); +} From 8c0630298462431821fab06ec75cc4f4ccf7a069 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 26 Jun 2024 10:21:46 -0700 Subject: [PATCH 28/28] Let run-tests specify mkfs format version Add a run-tests -V option that passes through the -V option to mkfs so that runs can specify the format version that the primary volume will have. This doesn't affect the scratch file system versions. Signed-off-by: Zach Brown --- tests/run-tests.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/run-tests.sh b/tests/run-tests.sh index c91cea7b..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