From: Al Viro <viro@zeniv.linux.org.uk>
To: Alex Markuze <amarkuze@redhat.com>
Cc: ceph-devel@vger.kernel.org, idryomov@gmail.com,
xiubo.li@clyso.com, slava@dubeyko.com, brauner@kernel.org,
jack@suse.cz, linux-fsdevel@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: Re: [PATCH v3 4/4] ceph: fail quarantined subvolume access with EACCES
Date: Sat, 17 Oct 2026 17:23:40 +0100 [thread overview]
Message-ID: <20261017162340.GI989762@ZenIV> (raw)
In-Reply-To: <e0ed00e6d17dc8c6603c7de42d4e7b48d483274b.1792241577.git.amarkuze@redhat.com>
> +/* A snapshot path must reach a head inode to encode its snapshot name. */
> +static bool ceph_snapshot_has_path(struct inode *inode)
> +{
> + struct dentry *alias = d_find_alias(inode);
> + struct dentry *cur;
> + unsigned int seq, depth = 0;
> + bool connected = false;
> +
> + if (!alias)
> + return false;
> +
> + rcu_read_lock();
> + seq = read_seqbegin(&rename_lock);
> + cur = alias;
> + while (cur && depth++ < 256) {
> + struct inode *in = d_inode_rcu(cur);
> + struct dentry *parent;
> +
> + if (!in)
> + break;
> + if (!ceph_in_snap(in)) {
> + connected = true;
> + break;
> + }
> + if (d_unhashed(cur) ||
> + (READ_ONCE(cur->d_flags) & DCACHE_DISCONNECTED))
> + break;
> + parent = READ_ONCE(cur->d_parent);
> + if (!parent || parent == cur)
^^^^^^^
Please, explain how could that possibly happen.
> + break;
> + cur = parent;
> + }
> + if (read_seqretry(&rename_lock, seq))
> + connected = false;
> + rcu_read_unlock();
> + dput(alias);
> + return connected;
> +}
> +
> +static int ceph_snapshot_reply_error(struct inode *inode,
> + struct ceph_mds_request *req, int mask)
> +{
> + struct ceph_inode_info *ci = ceph_inode(inode);
> + bool have_xattrs;
> +
> + if (!req->r_target_inode ||
> + ceph_ino(req->r_target_inode) != ceph_ino(inode) ||
> + ceph_snap(req->r_target_inode) != ceph_snap(inode))
> + return -EOPNOTSUPP;
> +
> + /* LOOKUPINO can authorize access without returning the requested blob. */
> + if ((mask & CEPH_CAP_XATTR_SHARED) &&
> + req->r_reply_info.targeti.xattr_len < 4) {
> + spin_lock(&ci->i_ceph_lock);
> + have_xattrs = ci->i_xattrs.version != 0;
> + spin_unlock(&ci->i_ceph_lock);
> + if (!have_xattrs)
> + return -EOPNOTSUPP;
> + }
> + if ((mask & CEPH_CAP_FILE_RD) &&
> + req->r_reply_info.targeti.inline_version == 0 &&
> + ceph_has_inline_data(ci) && i_size_read(inode) > 0)
> + return -EOPNOTSUPP;
> + return 0;
> +}
> +
> +static int ceph_snapshot_lookupino(struct inode *inode,
> + struct folio *locked_folio, int mask)
> +{
> + struct ceph_mds_client *mdsc = ceph_sb_to_mdsc(inode->i_sb);
> + struct ceph_mds_request *req;
> + int err;
> +
> + req = ceph_mdsc_create_request(mdsc, CEPH_MDS_OP_LOOKUPINO, USE_ANY_MDS);
> + if (IS_ERR(req))
> + return PTR_ERR(req);
> + /* No r_inode: an NFS alias may not encode a snapshot pathname. */
> + req->r_ino1 = ceph_vino(inode);
> + req->r_args.lookupino.snapid = cpu_to_le64(ceph_snap(inode));
> + req->r_args.lookupino.mask = cpu_to_le32(mask);
> + req->r_num_caps = 1;
> + req->r_locked_folio = locked_folio;
> + err = ceph_mdsc_do_request(mdsc, NULL, req);
> + if (!err)
> + err = ceph_snapshot_reply_error(inode, req, mask);
> + if (locked_folio && !err) {
> + u64 version = req->r_reply_info.targeti.inline_version;
> +
> + if (version == 0)
> + err = -EINVAL;
> + else if (version == CEPH_INLINE_NONE || version == 1)
> + err = -ENODATA;
> + else
> + err = req->r_reply_info.targeti.inline_len;
> + }
> + ceph_mdsc_put_request(req);
> + return err;
> +}
> +
> +int ceph_snapshot_read_auth(struct inode *inode)
> +{
> + /*
> + * Preserve a connected snapshot path: its head inode may have been
> + * deleted and no longer be cached by the MDS. Disconnected handles
> + * fall back to LOOKUPINO, with the exact inode and snapshot ID.
> + * The authorization is scoped to this operation, never cached.
> + */
> + return ceph_do_getattr(inode, CEPH_CAP_AUTH_SHARED, true);
> +}
> +
> /*
> * Verify that we have a lease on the given mask. If not,
> * do a getattr against an mds.
> @@ -3582,20 +3863,43 @@ int __ceph_do_getattr(struct inode *inode, struct folio *locked_folio,
> struct ceph_client *cl = fsc->client;
> struct ceph_mds_client *mdsc = fsc->mdsc;
> struct ceph_mds_request *req;
> + u64 qepoch;
> int mode;
> int err;
>
> if (ceph_snap(inode) == CEPH_SNAPDIR) {
> + struct inode *parent;
> +
> boutc(cl, "inode %p %llx.%llx SNAPDIR\n", inode,
> ceph_vinop(inode));
> - return 0;
> + /* The synthetic snapdir has the real directory's ino. */
> + parent = ceph_lookup_inode(inode->i_sb, ceph_ino(inode));
> + if (IS_ERR(parent))
> + return PTR_ERR(parent);
> + err = ceph_do_getattr(parent, mask, true);
> + iput(parent);
> + return err;
> }
> + if (ceph_in_snap(inode) && !ceph_snapshot_has_path(inode))
> + return ceph_snapshot_lookupino(inode, locked_folio, mask);
> +
> + /*
> + * Cached caps would satisfy getattr after the MDS has revoked
> + * access, and a short walk has no ancestor to consult. Ask the
> + * MDS so a lost quarantine-disable can clear the mark, and so
> + * an uncached parent chain is not treated as clear. Immutable snapshot
> + * caps cannot receive revocations and always require an MDS check.
> + */
> + if (ceph_in_snap(inode) ||
> + ceph_inode_quarantine_state(inode) != CEPH_Q_CLEAR)
> + force = true;
>
> boutc(cl, "inode %p %llx.%llx mask %s mode 0%o\n", inode,
> ceph_vinop(inode), ceph_cap_string(mask), inode->i_mode);
> if (!force && ceph_caps_issued_mask_metric(ceph_inode(inode), mask, 1))
> return 0;
>
> + qepoch = (u64)atomic64_read_acquire(&mdsc->quarantine_epoch);
> mode = ceph_try_to_choose_auth_mds(inode, mask);
> req = ceph_mdsc_create_request(mdsc, CEPH_MDS_OP_GETATTR, mode);
> if (IS_ERR(req))
> @@ -3606,6 +3910,15 @@ int __ceph_do_getattr(struct inode *inode, struct folio *locked_folio,
> req->r_args.getattr.mask = cpu_to_le32(mask);
> req->r_locked_folio = locked_folio;
> err = ceph_mdsc_do_request(mdsc, NULL, req);
> + if (ceph_in_snap(inode)) {
> + if (err == -EINVAL || err == -ESTALE ||
> + (!err && ceph_snapshot_reply_error(inode, req, mask))) {
> + ceph_mdsc_put_request(req);
> + return ceph_snapshot_lookupino(inode, locked_folio, mask);
> + }
> + }
> + if (!err && !ceph_in_snap(inode))
> + ceph_quarantine_remember_clear(inode, qepoch, req->r_cred);
> if (locked_folio && err == 0) {
> u64 inline_version = req->r_reply_info.targeti.inline_version;
> if (inline_version == 0) {
> @@ -3751,8 +4064,14 @@ int ceph_getattr(struct mnt_idmap *idmap, const struct path *path,
> return -ESTALE;
> }
>
> - /* Skip the getattr altogether if we're asked not to sync */
> - if ((flags & AT_STATX_SYNC_TYPE) != AT_STATX_DONT_SYNC) {
> + /*
> + * AT_STATX_DONT_SYNC would return the cached attrs. A
> + * quarantined inode, or one whose parents are not cached,
> + * still has to ask the MDS. Snapshot caps cannot receive revocations.
> + */
> + if (ceph_in_snap(inode) ||
> + ceph_inode_quarantine_state(inode) != CEPH_Q_CLEAR ||
> + (flags & AT_STATX_SYNC_TYPE) != AT_STATX_DONT_SYNC) {
> err = ceph_do_getattr(inode,
> statx_to_caps(request_mask, inode->i_mode),
> flags & AT_STATX_FORCE_SYNC);
> diff --git a/fs/ceph/ioctl.c b/fs/ceph/ioctl.c
> index 12cb50e0a166..d152cad10c23 100644
> --- a/fs/ceph/ioctl.c
> +++ b/fs/ceph/ioctl.c
> @@ -201,6 +201,10 @@ static long ceph_ioctl_get_dataloc(struct file *file, void __user *arg)
> if (copy_from_user(&dl, arg, sizeof(dl)))
> return -EFAULT;
>
> + r = ceph_do_getattr(inode, CEPH_STAT_CAP_LAYOUT, false);
> + if (r)
> + return r;
> +
> down_read(&osdc->lock);
> ceph_calc_file_object_mapping(&ci->i_layout, dl.file_offset, 1,
> &dl.object_no, &dl.object_offset, &xlen);
> @@ -401,12 +405,18 @@ long ceph_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
>
> case FS_IOC_GET_ENCRYPTION_POLICY:
> ret = vet_mds_for_fscrypt(file);
> + if (ret)
> + return ret;
> + ret = ceph_quarantine_revalidate(inode);
> if (ret)
> return ret;
> return fscrypt_ioctl_get_policy(file, (void __user *)arg);
>
> case FS_IOC_GET_ENCRYPTION_POLICY_EX:
> ret = vet_mds_for_fscrypt(file);
> + if (ret)
> + return ret;
> + ret = ceph_quarantine_revalidate(inode);
> if (ret)
> return ret;
> return fscrypt_ioctl_get_policy_ex(file, (void __user *)arg);
> @@ -429,6 +439,9 @@ long ceph_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
>
> case FS_IOC_GET_ENCRYPTION_NONCE:
> ret = vet_mds_for_fscrypt(file);
> + if (ret)
> + return ret;
> + ret = ceph_quarantine_revalidate(inode);
> if (ret)
> return ret;
> return fscrypt_ioctl_get_nonce(file, (void __user *)arg);
> diff --git a/fs/ceph/mds_client.h b/fs/ceph/mds_client.h
> index 816f5f2d32cc..67d4c6279e00 100644
> --- a/fs/ceph/mds_client.h
> +++ b/fs/ceph/mds_client.h
> @@ -39,7 +39,14 @@ enum ceph_feature_type {
> CEPHFS_FEATURE_MDS_AUTH_CAPS_CHECK,
> CEPHFS_FEATURE_SUBVOLUME_METRICS,
>
> - CEPHFS_FEATURE_MAX = CEPHFS_FEATURE_SUBVOLUME_METRICS,
> + /*
> + * Bit 23 is CEPHFS_FEATURE_BLOCKDIFF on the MDS and is not
> + * advertised here. Bit 24 is subvolume quarantine; it must stay
> + * aligned with the userspace client.
> + */
> + CEPHFS_FEATURE_QUARANTINE = 24,
> +
> + CEPHFS_FEATURE_MAX = CEPHFS_FEATURE_QUARANTINE,
> };
>
> #define CEPHFS_FEATURES_CLIENT_SUPPORTED { \
> @@ -57,6 +64,7 @@ enum ceph_feature_type {
> CEPHFS_FEATURE_HAS_OWNER_UIDGID, \
> CEPHFS_FEATURE_MDS_AUTH_CAPS_CHECK, \
> CEPHFS_FEATURE_SUBVOLUME_METRICS, \
> + CEPHFS_FEATURE_QUARANTINE, \
> }
>
> /*
> diff --git a/fs/ceph/super.h b/fs/ceph/super.h
> index 0264bd9d705f..540f31121f84 100644
> --- a/fs/ceph/super.h
> +++ b/fs/ceph/super.h
> @@ -1394,8 +1394,6 @@ int ceph_set_acl(struct mnt_idmap *idmap,
> struct dentry *dentry, struct posix_acl *acl, int type);
> int ceph_pre_init_acls(struct inode *dir, umode_t *mode,
> struct ceph_acl_sec_ctx *as_ctx);
> -void ceph_init_inode_acls(struct inode *inode,
> - struct ceph_acl_sec_ctx *as_ctx);
>
> static inline void ceph_forget_all_cached_acls(struct inode *inode)
> {
> @@ -1412,11 +1410,6 @@ static inline int ceph_pre_init_acls(struct inode *dir, umode_t *mode,
> {
> return 0;
> }
> -static inline void ceph_init_inode_acls(struct inode *inode,
> - struct ceph_acl_sec_ctx *as_ctx)
> -{
> -}
> -
> static inline void ceph_forget_all_cached_acls(struct inode *inode)
> {
> }
> @@ -1578,7 +1571,7 @@ static inline bool ceph_has_inline_data(const struct ceph_inode_info *ci)
> /* file.c */
> extern const struct file_operations ceph_file_fops;
>
> -extern int ceph_renew_caps(struct inode *inode, int fmode);
> +int ceph_renew_caps(struct inode *inode, int fmode, bool force);
> extern int ceph_open(struct inode *inode, struct file *file);
> extern int ceph_atomic_open(struct inode *dir, struct dentry *dentry,
> struct file *file, unsigned flags, umode_t mode);
> diff --git a/fs/ceph/xattr.c b/fs/ceph/xattr.c
> index 4cba6e7bffd3..7fd307fcbe5a 100644
> --- a/fs/ceph/xattr.c
> +++ b/fs/ceph/xattr.c
> @@ -997,6 +997,7 @@ ssize_t __ceph_getxattr(struct inode *inode, const char *name, void *value,
> struct ceph_vxattr *vxattr;
> int req_mask;
> ssize_t err;
> + bool ask_mds = false;
>
> if (strncmp(name, XATTR_CEPH_PREFIX, XATTR_CEPH_PREFIX_LEN))
> goto handle_non_vxattrs;
> @@ -1034,7 +1035,19 @@ ssize_t __ceph_getxattr(struct inode *inode, const char *name, void *value,
> ceph_vinop(inode), name, ci->i_xattrs.version,
> ci->i_xattrs.index_version);
>
> - if (ci->i_xattrs.version == 0 ||
> + /*
> + * A shared cap would answer from the cache. The state check may
> + * schedule, so drop i_ceph_lock first. A reply reading the blob
> + * it just installed must still use that cache.
> + */
> + if (!ceph_current_fill_trace_request()) {
> + spin_unlock(&ci->i_ceph_lock);
> + ask_mds = ceph_in_snap(inode) ||
> + ceph_inode_quarantine_state(inode) != CEPH_Q_CLEAR;
> + spin_lock(&ci->i_ceph_lock);
> + }
> +
> + if (ask_mds || ci->i_xattrs.version == 0 ||
> !((req_mask & CEPH_CAP_XATTR_SHARED) ||
> __ceph_caps_issued_mask_metric(ci, CEPH_CAP_XATTR_SHARED, 1))) {
> spin_unlock(&ci->i_ceph_lock);
> @@ -1089,6 +1102,7 @@ ssize_t ceph_listxattr(struct dentry *dentry, char *names, size_t size)
> struct ceph_client *cl = ceph_inode_to_client(inode);
> struct ceph_inode_info *ci = ceph_inode(inode);
> bool len_only = (size == 0);
> + bool ask_mds = false;
> u32 namelen;
> int err;
> struct ceph_journal_info __ji;
> @@ -1100,7 +1114,15 @@ ssize_t ceph_listxattr(struct dentry *dentry, char *names, size_t size)
> ceph_vinop(inode), ci->i_xattrs.version,
> ci->i_xattrs.index_version);
>
> - if (ci->i_xattrs.version == 0 ||
> + /* Same as __ceph_getxattr(): do not serve a shared cap hit. */
> + if (!ceph_current_fill_trace_request()) {
> + spin_unlock(&ci->i_ceph_lock);
> + ask_mds = ceph_in_snap(inode) ||
> + ceph_inode_quarantine_state(inode) != CEPH_Q_CLEAR;
> + spin_lock(&ci->i_ceph_lock);
> + }
> +
> + if (ask_mds || ci->i_xattrs.version == 0 ||
> !__ceph_caps_issued_mask_metric(ci, CEPH_CAP_XATTR_SHARED, 1)) {
> spin_unlock(&ci->i_ceph_lock);
> err = ceph_do_getattr(inode, CEPH_STAT_CAP_XATTR, true);
> @@ -1222,6 +1244,7 @@ int __ceph_setxattr(struct inode *inode, const char *name,
> int required_blob_size;
> bool check_realm = false;
> bool lock_snap_rwsem = false;
> + bool checked_quarantine = false;
>
> if (ceph_in_snap(inode))
> return -EROFS;
> @@ -1319,6 +1342,33 @@ int __ceph_setxattr(struct inode *inode, const char *name,
> goto retry;
> }
>
> + /*
> + * XATTR_EXCL applies the new blob with no MDS round trip.
> + * The state check may schedule, so drop i_ceph_lock first.
> + */
> + if (!checked_quarantine) {
> + enum ceph_qstate qstate;
> +
> + spin_unlock(&ci->i_ceph_lock);
> + qstate = ceph_inode_quarantine_write_state(inode);
> + if (qstate == CEPH_Q_UNDER &&
> + ceph_inode_quarantine_direct(inode)) {
> + ceph_buffer_put(old_blob);
> + if (lock_snap_rwsem)
> + up_read(&mdsc->snap_rwsem);
> + err = -EACCES;
> + goto out;
> + }
> + if (qstate != CEPH_Q_CLEAR) {
> + ceph_buffer_put(old_blob);
> + old_blob = NULL;
> + goto do_sync_unlocked;
> + }
> + checked_quarantine = true;
> + spin_lock(&ci->i_ceph_lock);
> + goto retry;
> + }
> +
> err = __set_xattr(ci, newname, name_len, newval, val_len,
> flags, value ? 1 : -1, &xattr);
>
> --
> 2.34.1
>
>
next prev parent reply other threads:[~2026-10-17 16:23 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 16:12 [PATCH v2 0/3] ceph: support subvolume quarantine Alex Markuze
2026-10-15 16:12 ` [PATCH v2 1/3] ceph: add subvolume quarantine wire definitions Alex Markuze
2026-10-15 16:12 ` [PATCH v2 2/3] ceph: record subvolume quarantine from MDS cap updates Alex Markuze
2026-10-17 4:51 ` kernel test robot
2026-10-15 16:12 ` [PATCH v2 3/3] ceph: fail quarantined subvolume access with EACCES Alex Markuze
2026-10-17 13:40 ` [PATCH v3 0/4] ceph: support subvolume quarantine Alex Markuze
2026-10-17 13:40 ` [PATCH v3 1/4] dcache: export d_find_alias_rcu() for modular filesystems Alex Markuze
2026-10-17 13:40 ` [PATCH v3 2/4] ceph: add subvolume quarantine wire definitions Alex Markuze
2026-10-17 13:40 ` [PATCH v3 3/4] ceph: record subvolume quarantine from MDS cap updates Alex Markuze
2026-10-17 13:40 ` [PATCH v3 4/4] ceph: fail quarantined subvolume access with EACCES Alex Markuze
2026-10-17 16:23 ` Al Viro [this message]
2026-10-18 10:38 ` Alex Markuze
-- strict thread matches above, loose matches on Subject: below --
2026-10-17 14:48 [PATCH v3 0/4] ceph: support subvolume quarantine Alex Markuze
2026-10-17 14:48 ` [PATCH v3 4/4] ceph: fail quarantined subvolume access with EACCES Alex Markuze
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261017162340.GI989762@ZenIV \
--to=viro@zeniv.linux.org.uk \
--cc=amarkuze@redhat.com \
--cc=brauner@kernel.org \
--cc=ceph-devel@vger.kernel.org \
--cc=idryomov@gmail.com \
--cc=jack@suse.cz \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=slava@dubeyko.com \
--cc=xiubo.li@clyso.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.