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 } };