Linux FSCRYPT development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Alex Markuze" <amarkuze@redhat.com>
Cc: idryomov@gmail.com, xiubo.li@clyso.com, slava@dubeyko.com,
	viro@zeniv.linux.org.uk, brauner@kernel.org, jack@suse.cz,
	linux-fscrypt@vger.kernel.org, ebiggers@kernel.org,
	tytso@mit.edu, jaegeuk@kernel.org
Subject: Re: [PATCH v5 4/5] ceph: record subvolume quarantine from MDS cap updates
Date: Tue, 20 Oct 2026 23:00:30 +0000	[thread overview]
Message-ID: <sashiko-outbox-179589@kernel.org> (raw)
In-Reply-To: <ff90b10c248b7f46707d0448b5c05cffe85a2ce8.1792527529.git.amarkuze@redhat.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] ceph: epoch self-invalidation in ceph_quarantine_reply_can_clear()
- [High] ceph: unhashed directory alias traversal in ceph_quarantine_walk_once()
- [High] ceph: pre-import cap sequence off-by-one in handle_cap_grant()
- [Medium] ceph: unconditional quarantine reset on non-reporting MDS sessions
--

commit 78e79694b7e226fb4224da6b24cf5abda7482a66
Author: Alex Markuze <amarkuze@redhat.com>

ceph: record subvolume quarantine from MDS cap updates

This commit decodes the MClientCaps v14 quarantine errno to maintain a
sticky, counted quarantine mark on inodes, and adds an epoch-based rcu
ancestry walk cache to track whether an inode is under a quarantined
subvolume.

> diff --git a/fs/ceph/caps.c b/fs/ceph/caps.c
> index b27bcdc24d29f..94f015112942d 100644
> --- a/fs/ceph/caps.c
> +++ b/fs/ceph/caps.c
> @@ -692,8 +692,9 @@ void change_auth_cap_ses(struct ceph_inode_info *ci,
>   * @fmode is the open file mode, if we are opening a file, otherwise
>   * it is < 0.  (This is so we can atomically add the cap and add an
>   * open file reference to it.)
> + * Returns false when the raw grant predates the recorded cap sequence.
>   */
> -void ceph_add_cap(struct inode *inode,
> +bool ceph_add_cap(struct inode *inode,
>  		  struct ceph_mds_session *session, u64 cap_id,
>  		  unsigned issued, unsigned wanted,
>  		  unsigned seq, unsigned mseq, u64 realmino, int flags,
> @@ -706,6 +707,7 @@ void ceph_add_cap(struct inode *inode,
>  	int mds = session->s_mds;
>  	int actual_wanted;
>  	u32 gen;
> +	bool cap_current = true;
>  
>  	lockdep_assert_held(&ci->i_ceph_lock);
>  
> @@ -737,6 +739,7 @@ void ceph_add_cap(struct inode *inode,
>  		atomic64_inc(&mdsc->metric.total_caps);
>  		spin_unlock(&session->s_cap_lock);
>  	} else {
> +		cap_current = ceph_seq_cmp(seq, cap->seq) >= 0;
>  		spin_lock(&session->s_cap_lock);
>  		list_move_tail(&cap->session_caps, &session->s_caps);
>  		spin_unlock(&session->s_cap_lock);
> @@ -823,6 +826,7 @@ void ceph_add_cap(struct inode *inode,
>  	cap->mseq = mseq;
>  	cap->cap_gen = gen;
>  	wake_up_all(&ci->i_cap_wq);
> +	return cap_current;
>  }

[ ... ]

> @@ -3757,6 +3769,45 @@ static int parse_inline_data(void **p, void *end,
>  	return -EIO;
>  }
>  
> +/*
> + * Record quarantine state carried by an MClientCaps oserrno.
> + * Caller holds i_ceph_lock. Returns true when waiters should be woken
> + * because quarantine changed or a pending reset was resolved.
> + *
> + * -EQUARANTINED refreshes the sticky flag and its reporting MDS rank.
> + * A current grant above PIN with errno 0 clears only that rank's mark.
> + * Another rank needs a successful metadata request for the current epoch.
> + * A PIN-only update may zero qtine_errno while leaving the flag set.
> + */
> +static bool ceph_apply_quarantine(struct ceph_inode_info *ci, int mds,
> +				  int newcaps, int oserrno)
> +{
> +	bool was, now, pending;
> +
> +	was = READ_ONCE(ci->i_qtine_errno) == -CEPH_EQUARANTINED ||
> +	      test_bit(CEPH_I_QUARANTINE_BIT, &ci->i_ceph_flags);
> +	pending = test_bit(CEPH_I_QUARANTINE_PENDING_BIT, &ci->i_ceph_flags);
> +
> +	if ((newcaps & ~CEPH_CAP_PIN) && was && oserrno == 0 &&
> +	    READ_ONCE(ci->i_qtine_mds) == mds)
> +		ceph_quarantine_clear(ci);
> +
> +	if (oserrno == -CEPH_EQUARANTINED) {
> +		ceph_quarantine_set(ci);
> +		WRITE_ONCE(ci->i_qtine_mds, mds);
> +	}
> +
> +	if (newcaps || was ||
> +	    test_bit(CEPH_I_QUARANTINE_BIT, &ci->i_ceph_flags))
> +		WRITE_ONCE(ci->i_qtine_errno, oserrno);
> +
> +	now = READ_ONCE(ci->i_qtine_errno) == -CEPH_EQUARANTINED ||
> +	      test_bit(CEPH_I_QUARANTINE_BIT, &ci->i_ceph_flags);
> +	return was != now ||
> +	       pending != test_bit(CEPH_I_QUARANTINE_PENDING_BIT,
> +				   &ci->i_ceph_flags);
> +}
> +
>  /*
>   * Handle a cap GRANT message from the MDS.  (Note that a GRANT may
>   * actually be a revocation if it specifies a smaller cap set.)
> @@ -3781,6 +3832,7 @@ static void handle_cap_grant(struct inode *inode,
>  	u64 max_size = le64_to_cpu(grant->max_size);
>  	unsigned char check_caps = 0;
>  	bool was_stale = cap->cap_gen < atomic_read(&session->s_cap_gen);
> +	bool quarantine_current = ceph_seq_cmp(seq, cap->seq) >= 0;
>  	bool wake = false;
>  	bool writeback = false;
>  	bool queue_trunc = false;
> @@ -4128,6 +4180,15 @@ static void handle_cap_grant(struct inode *inode,
>  			fill_inline = true;
>  	}
>  
> +	if (quarantine_current && extra_info->oserrno_valid &&
> +	    ceph_apply_quarantine(ci, session->s_mds, newcaps,
> +				  extra_info->oserrno)) {
> +		boutc(cl, "quarantine oserrno %d caps %s %llx.%llx\n",
> +		      extra_info->oserrno, ceph_cap_string(newcaps),
> +		      ceph_vinop(inode));
> +		wake = true;
> +	}

[Severity: High]
When an export target mds sends a cap message or reply just before cap
import, can ceph_seq_cmp(seq, cap->seq) >= 0 treat a pre-import sequence
number as current?

In fs/ceph/caps.c, when an export message arrives before the corresponding
import message, handle_cap_export() sets the target mds cap sequence to
t_issue_seq - 1 and merges the exported caps into cap->issued:

fs/ceph/caps.c:handle_cap_export() {
	if (tcap->cap_id == t_cap_id &&
	    ceph_seq_cmp(tcap->seq, t_issue_seq) < 0) {
		tcap->cap_id = t_cap_id;
		tcap->seq = t_issue_seq - 1;
		tcap->issue_seq = t_issue_seq - 1;
		tcap->issued |= issued;
		...
	}
	...
	} else if (tsession) {
		ceph_add_cap(inode, tsession, t_cap_id, issued, 0,
			     t_issue_seq - 1, t_mseq, (u64)-1, flag, &new_cap);
	}
}

Both ceph_add_cap() and handle_cap_grant() treat any message with
ceph_seq_cmp(seq, cap->seq) <= 0 (including seq == cap->seq, which is
t_issue_seq - 1) as a message sent before the cap import, and merge
cap->issued into issued or newcaps:

fs/ceph/caps.c:handle_cap_grant() {
	if (ceph_seq_cmp(seq, cap->seq) <= 0) {
		WARN_ON(cap != ci->i_auth_cap);
		WARN_ON(cap->cap_id != le64_to_cpu(grant->cap_id));
		seq = cap->seq;
		newcaps |= cap->issued;
	}
}

Because cap_current in ceph_add_cap() and quarantine_current in
handle_cap_grant() use ceph_seq_cmp(seq, cap->seq) >= 0, a pre-import
message with seq == t_issue_seq - 1 satisfies both <= 0 and >= 0. In
handle_cap_grant(), newcaps is inflated with cap->issued from the export
placeholder before ceph_apply_quarantine() checks (newcaps & ~CEPH_CAP_PIN),
and in ceph_add_cap(), cap_current returns true to ceph_fill_inode() for a
pre-import reply.

Should pre-import messages with seq == cap->seq (when the op is not
CEPH_CAP_OP_IMPORT) be excluded from updating or clearing quarantine state?

[ ... ]

> diff --git a/fs/ceph/inode.c b/fs/ceph/inode.c
> index 94f5215f8eed5..78e97cf2f9a41 100644
> --- a/fs/ceph/inode.c
> +++ b/fs/ceph/inode.c

[ ... ]

> +/*
> + * Parent walk with no inode references. Caller holds rcu_read_lock().
> + * Stops at the mount root, at an MDS mark, or at an ancestor whose
> + * cached answer is for this epoch. No hashed alias, or the depth
> + * limit, is not an answer: the caller must not cache it.
> + */
> +static int ceph_quarantine_walk_once(struct inode *inode, u64 epoch,
> +				     bool use_cache)
> +{
> +	struct inode *cur = inode;
> +	unsigned int depth = 0;
> +	bool under = false;
> +	bool done = false;
> +
> +	while (cur && depth++ < 256) {
> +		struct ceph_inode_info *ci = ceph_inode(cur);
> +		struct dentry *alias;
> +		struct dentry *parent;
> +		struct inode *next;
> +
> +		if (test_bit(CEPH_I_QUARANTINE_PENDING_BIT, &ci->i_ceph_flags))
> +			break;
> +		if (READ_ONCE(ci->i_qtine_errno) == -CEPH_EQUARANTINED ||
> +		    test_bit(CEPH_I_QUARANTINE_BIT, &ci->i_ceph_flags)) {
> +			under = true;
> +			done = true;
> +			break;
> +		}
> +
> +		if (use_cache && cur != inode &&
> +		    ceph_qtag_read(ci, epoch, &under)) {
> +			done = true;
> +			break;
> +		}
> +
> +		alias = d_find_alias_rcu(cur);
> +		if (!alias)
> +			break;
> +
> +		parent = READ_ONCE(alias->d_parent);
> +		/*
> +		 * The mount root is its own parent. A disconnected
> +		 * alias from d_obtain_alias(), or a secondary root from
> +		 * d_obtain_root(), has the same shape and is not a
> +		 * finished walk.
> +		 */
> +		if (!parent || parent == alias) {
> +			if (parent == alias && S_ISDIR(cur->i_mode) &&
> +			    alias == READ_ONCE(inode->i_sb->s_root))
> +				done = true;
> +			break;
> +		}
> +
> +		next = d_inode_rcu(parent);

[Severity: High]
If a directory dentry is unhashed while still referenced, could
ceph_quarantine_walk_once() follow a stale d_parent chain to s_root and
cache a clear result in i_qtag?

Looking at d_find_alias_rcu() in fs/dcache.c, it only checks !d_unhashed()
for non-directory inodes, and returns the first alias unconditionally when
S_ISDIR(inode->i_mode) is true:

fs/dcache.c:d_find_alias_rcu() {
	if (likely(!(inode_state_read(inode) & I_FREEING) && !hlist_empty(l))) {
		if (S_ISDIR(inode->i_mode)) {
			de = hlist_entry(l->first, struct dentry, d_alias);
		} else {
			hlist_for_each_entry(de, l, d_alias)
				if (!d_unhashed(de))
					break;
		}
	}
	return de;
}

If a directory dentry is unhashed via d_drop(), d_delete(), or
d_invalidate() (for example, after a remote rename into a quarantined
subvolume) while held open or referenced by a child dentry, alias remains on
cur->i_dentry with d_unhashed(alias) true and alias->d_parent still pointing
to the old parent directory.

Since ceph_quarantine_walk_once() does not check d_unhashed(alias) when
parent != alias, wouldn't the walk continue up the old parent chain to
s_root and publish a clear tag in ci->i_qtag?

[ ... ]

> +void ceph_quarantine_splice_start(struct super_block *sb)
> +{
> +	struct ceph_mds_client *mdsc = ceph_sb_to_mdsc(sb);
> +
> +	spin_lock(&mdsc->quarantine_lock);
> +	atomic_inc_return(&mdsc->quarantine_splices);
> +	atomic64_add_return(2, &mdsc->quarantine_epoch);
> +	spin_unlock(&mdsc->quarantine_lock);
> +}
> +
> +void ceph_quarantine_splice_finish(struct super_block *sb)
> +{
> +	struct ceph_mds_client *mdsc = ceph_sb_to_mdsc(sb);
> +
> +	spin_lock(&mdsc->quarantine_lock);
> +	atomic64_add_return(2, &mdsc->quarantine_epoch);
> +	atomic_dec_return(&mdsc->quarantine_splices);
> +	spin_unlock(&mdsc->quarantine_lock);
> +}
> +
> +static bool ceph_quarantine_reply_can_clear(struct ceph_inode_info *ci,
> +					    struct ceph_mds_session *session)
> +{
> +	struct ceph_mds_client *mdsc = ceph_sb_to_mdsc(ci->netfs.inode.i_sb);
> +	struct ceph_mds_request *req;
> +	u64 epoch;
> +
> +	if (READ_ONCE(ci->i_qtine_mds) == session->s_mds)
> +		return true;
> +	/* Different ranks have independent cap sequences. */
> +	req = ceph_current_mds_request(mdsc->fsc);
> +	epoch = (u64)atomic64_read_acquire(&mdsc->quarantine_epoch);
> +	return req && !(epoch & 1) && req->r_quarantine_epoch == epoch &&
> +		!atomic_read(&mdsc->quarantine_splices);
> +}

[ ... ]

> @@ -1407,7 +1866,7 @@ int ceph_fill_inode(struct inode *inode, struct folio *locked_folio,
>  	/* were we issued a capability? */
>  	if (info_caps) {
>  		if (!ceph_in_snap(inode)) {
> -			ceph_add_cap(inode, session,
> +			bool cap_current = ceph_add_cap(inode, session,
>  				     le64_to_cpu(info->cap.cap_id),
>  				     info_caps,
>  				     le32_to_cpu(info->cap.wanted),
> @@ -1416,6 +1875,14 @@ int ceph_fill_inode(struct inode *inode, struct folio *locked_folio,
>  				     le64_to_cpu(info->cap.realm),
>  				     info->cap.flags, &new_cap);
>  
> +			/*
> +			 * Caps above PIN mean quarantine has been lifted,
> +			 * including when the disable message was lost.
> +			 */
> +			if (cap_current && (info_caps & ~CEPH_CAP_PIN) &&
> +			    ceph_quarantine_reply_can_clear(ci, session))
> +				ceph_quarantine_clear(ci);

[Severity: High]
How can ceph_quarantine_reply_can_clear() succeed for req->r_target_inode or
later readdir entries when earlier steps in the same reply advance
mdsc->quarantine_epoch?

req->r_quarantine_epoch is sampled once in ceph_mdsc_create_request(), and
ceph_quarantine_reply_can_clear() requires req->r_quarantine_epoch == epoch
when session->s_mds != ci->i_qtine_mds. However, mdsc->quarantine_epoch is
also incremented by ceph_quarantine_clear(), ceph_quarantine_reset(), and
ceph_quarantine_splice_start()/finish():

1. In ceph_fill_trace(), ceph_fill_inode() is called on parent_dir before it
is called on req->r_target_inode:

fs/ceph/inode.c:ceph_fill_trace() {
	if (parent_dir) {
		ceph_inode_set_subvolume(parent_dir,
					 rinfo->diri.subvolume_id);
		err = ceph_fill_inode(parent_dir, NULL, &rinfo->diri,
				      rinfo->dirfrag, session, -1,
				      &req->r_caps_reservation);
		...
	}
	...
	if (rinfo->head->is_target) {
		in = req->r_target_inode;
		ceph_inode_set_subvolume(in, rinfo->targeti.subvolume_id);
		err = ceph_fill_inode(in, req->r_locked_folio, &rinfo->targeti,
				...);
	}
}

If filling parent_dir clears a quarantine mark on parent_dir,
ceph_quarantine_clear() advances mdsc->quarantine_epoch by 2, so
req->r_quarantine_epoch == epoch becomes false before ceph_fill_inode() runs
on req->r_target_inode.

2. In ceph_readdir_prepopulate(), each entry calls ceph_fill_inode() and,
for negative dentries, splice_dentry(), which calls
ceph_quarantine_splice_start() and ceph_quarantine_splice_finish(). On the
first entry that splices a dentry or clears a quarantine mark,
mdsc->quarantine_epoch advances, preventing ceph_quarantine_reply_can_clear()
from clearing quarantine on any subsequent entry in the readdir reply.

3. If a request is created before the old reporting session's cap is removed
or reset via __ceph_remove_cap(), remove_session_caps_cb(), or
reconnect_caps_cb(), ceph_quarantine_reset() advances mdsc->quarantine_epoch
after req->r_quarantine_epoch was recorded, so the reply cannot clear the
pending mark on another rank.

[ ... ]

> diff --git a/fs/ceph/mds_client.c b/fs/ceph/mds_client.c
> index d4ef8a355a480..bb7cf9384fd6d 100644
> --- a/fs/ceph/mds_client.c
> +++ b/fs/ceph/mds_client.c
> @@ -2041,10 +2041,12 @@ static int remove_session_caps_cb(struct inode *inode, int mds, void *arg)
>  	struct ceph_inode_info *ci = ceph_inode(inode);
>  	struct ceph_client *cl = ceph_inode_to_client(inode);
>  	bool invalidate = false;
> +	bool reset;
>  	struct ceph_cap *cap;
>  	int iputs = 0;
>  
>  	spin_lock(&ci->i_ceph_lock);
> +	reset = ceph_quarantine_reset(ci);
>  	cap = __get_cap_for_mds(ci, mds);
>  	if (cap) {
>  		boutc(cl, " removing cap %p, ci is %p, inode is %p\n",
> @@ -2054,7 +2056,7 @@ static int remove_session_caps_cb(struct inode *inode, int mds, void *arg)
>  	}
>  	spin_unlock(&ci->i_ceph_lock);
>  
> -	if (cap)
> +	if (cap || reset)
>  		wake_up_all(&ci->i_cap_wq);
>  	if (invalidate)
>  		ceph_queue_invalidate(inode);
> @@ -2860,6 +2862,7 @@ ceph_mdsc_create_request(struct ceph_mds_client *mdsc, int op, int mode)
>  
>  	mutex_init(&req->r_fill_mutex);
>  	req->r_mdsc = mdsc;
> +	req->r_quarantine_epoch = atomic64_read_acquire(&mdsc->quarantine_epoch);
>  	req->r_started = jiffies;
>  	req->r_start_latency = ktime_get();
>  	req->r_resend_mds = -1;
> @@ -5009,6 +5012,7 @@ static int reconnect_caps_cb(struct inode *inode, int mds, void *arg)
>  	struct ceph_path_info path_info = {0};
>  	int err;
>  	u64 snap_follows;
> +	bool reset = false;
>  
>  	dentry = d_find_primary(inode);
>  	if (dentry) {
> @@ -5023,9 +5027,16 @@ static int reconnect_caps_cb(struct inode *inode, int mds, void *arg)
>  	}
>  
>  	spin_lock(&ci->i_ceph_lock);
> +	/*
> +	 * Require a fresh MDS answer after reconnect. A PIN import may
> +	 * carry no quarantine errno, so it must not reopen cached snap caps.
> +	 */
> +	reset = ceph_quarantine_reset(ci);
>  	cap = __get_cap_for_mds(ci, mds);
>  	if (!cap) {
>  		spin_unlock(&ci->i_ceph_lock);
> +		if (reset)
> +			wake_up_all(&ci->i_cap_wq);
>  		err = 0;
>  		goto out_err;
>  	}

[Severity: Medium]
Should remove_session_caps_cb() and reconnect_caps_cb() check whether
READ_ONCE(ci->i_qtine_mds) == mds before calling ceph_quarantine_reset(ci)?

In __ceph_remove_cap(), ceph_quarantine_reset(ci) is gated on
READ_ONCE(ci->i_qtine_mds) == cap->mds so that only losing the reporting
rank's cap transitions the mark to pending. However,
remove_session_caps_cb() and reconnect_caps_cb() call
ceph_quarantine_reset(ci) unconditionally for every inode visited during a
session close or reconnect, even when the quarantine mark was reported by a
different mds rank whose session is still active.

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1792527529.git.amarkuze@redhat.com?part=4

  reply	other threads:[~2026-10-20 23:00 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-20 20:26 [PATCH v5 0/5] ceph: support subvolume quarantine Alex Markuze
2026-10-20 20:26 ` [PATCH v5 1/5] dcache: export d_find_alias_rcu() for modular filesystems Alex Markuze
2026-10-20 20:26 ` [PATCH v5 2/5] fscrypt: allow filesystems to disable symlink target caching Alex Markuze
2026-10-20 20:26 ` [PATCH v5 3/5] ceph: add subvolume quarantine wire definitions Alex Markuze
2026-10-20 20:26 ` [PATCH v5 4/5] ceph: record subvolume quarantine from MDS cap updates Alex Markuze
2026-10-20 23:00   ` sashiko-bot [this message]
2026-10-20 20:26 ` [PATCH v5 5/5] ceph: fail quarantined subvolume access with EACCES Alex Markuze
2026-10-20 23:37   ` sashiko-bot

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=sashiko-outbox-179589@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=amarkuze@redhat.com \
    --cc=brauner@kernel.org \
    --cc=ebiggers@kernel.org \
    --cc=idryomov@gmail.com \
    --cc=jack@suse.cz \
    --cc=jaegeuk@kernel.org \
    --cc=linux-fscrypt@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=slava@dubeyko.com \
    --cc=tytso@mit.edu \
    --cc=viro@zeniv.linux.org.uk \
    --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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox