[PATCH] ceph: don't unregister an MDS session before removing its caps

From: Max Kellermann

Date: Tue Aug 18 2026 - 12:52:10 EST


handle_session() removed the session from mdsc->sessions[] at the very
top of `CEPH_SESSION_CLOSE` handling, before taking `s_mutex`.
Between session unregistration and remove_session_caps(), the MDS rank
has no registered session while the old session still owns all caps it
was granted.

Any concurrent filesystem operation may walk into that and the next
__do_request() call registers a new session for this rank. Once it is
open and the MDS issues caps, ceph_fill_inode() calls
ceph_add_cap(), which looks caps up by rank, not
by session identity, finding old caps linked to the old session.

The list_move_tail() call then moves the cap object to the new
session, which is already a bad thing to do. Since it doesn't
decrement `old_session->s_nr_caps`, this will quickly run into a BUG()
instead of crashing:

kernel BUG at fs/ceph/mds_client.c:1959!
Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
[...]
Workqueue: ceph-msgr ceph_con_workfn
pstate: 20400009 (nzCv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--)
pc : remove_session_caps+0x2bc/0x2d8
lr : remove_session_caps+0x74/0x2d8
[...]
Call trace:
remove_session_caps+0x2bc/0x2d8 (P)
mds_dispatch+0xf48/0x1b60
ceph_con_process_message+0x74/0xa0
ceph_con_v1_try_read+0x3a0/0x1510
ceph_con_workfn+0x260/0x460
process_one_work+0x168/0x3b8
worker_thread+0x1bc/0x3a0
kthread+0x118/0x1e0
ret_from_fork+0x10/0x20

That's BUG_ON(session->s_nr_caps > 0).

I was able to reproduce this reliably by delaying the close and
starting I/O during the delay.

This patch keeps the session registered with
`CEPH_MDS_SESSION_CLOSED`. New requests will be put on the
`s_waiting` list where they will be resumed on the new session.

Signed-off-by: Max Kellermann <max.kellermann@xxxxxxxxx>
---
fs/ceph/caps.c | 9 +++++++-
fs/ceph/mds_client.c | 53 +++++++++++++++++++++++++++++++++++++++-----
2 files changed, 56 insertions(+), 6 deletions(-)

diff --git a/fs/ceph/caps.c b/fs/ceph/caps.c
index d7283fb54cec..ad9609162c4a 100644
--- a/fs/ceph/caps.c
+++ b/fs/ceph/caps.c
@@ -4195,7 +4195,14 @@ static void handle_cap_export(struct inode *inode, struct ceph_mds_caps *ex,
}
new_cap = ceph_get_cap(mdsc, NULL);
} else {
- WARN_ON(1);
+ /*
+ * -EAGAIN means the target rank's session is
+ * currently being closed; set target=-1, which drops
+ * the exported cap - the MDS will reissue it for the
+ * new session
+ */
+ WARN_ON(tsession != ERR_PTR(-EAGAIN));
+
tsession = NULL;
target = -1;
mutex_lock(&session->s_mutex);
diff --git a/fs/ceph/mds_client.c b/fs/ceph/mds_client.c
index 3c692ad02c85..6c7328f9ce94 100644
--- a/fs/ceph/mds_client.c
+++ b/fs/ceph/mds_client.c
@@ -1750,6 +1750,16 @@ __open_export_target_session(struct ceph_mds_client *mdsc, int target)
if (IS_ERR(session))
return session;
}
+ if (session->s_state == CEPH_MDS_SESSION_CLOSED) {
+ /*
+ * handle_session() is currently closing this session;
+ * it stays registered until its caps are gone. Do
+ * not return it to our caller because we don't want
+ * it to attach new caps to it.
+ */
+ ceph_put_mds_session(session);
+ return ERR_PTR(-EAGAIN);
+ }
if (session->s_state == CEPH_MDS_SESSION_NEW ||
session->s_state == CEPH_MDS_SESSION_CLOSING) {
ret = __open_session(mdsc, session);
@@ -4492,8 +4502,20 @@ static void handle_session(struct ceph_mds_session *session,
ceph_metric_bind_session(mdsc, session);
}
if (op == CEPH_SESSION_CLOSE) {
+ /*
+ * Pin the session for the rest of this function. The
+ * __unregister_session() call is deferred until after
+ * remove_session_caps() below, or else other
+ * processes may find caps still assigned to this
+ * session while working with a new session object.
+ */
ceph_get_mds_session(session);
- __unregister_session(mdsc, session);
+
+ if (session->s_state == CEPH_MDS_SESSION_RECONNECTING)
+ pr_info_client(cl, "mds%d reconnect denied\n",
+ session->s_mds);
+
+ session->s_state = CEPH_MDS_SESSION_CLOSED;
}
/* FIXME: this ttl calculation is generous */
session->s_ttl = jiffies + HZ*mdsc->mdsmap->m_session_autoclose;
@@ -4551,12 +4573,24 @@ static void handle_session(struct ceph_mds_session *session,
break;

case CEPH_SESSION_CLOSE:
- if (session->s_state == CEPH_MDS_SESSION_RECONNECTING)
- pr_info_client(cl, "mds%d reconnect denied\n",
- session->s_mds);
- session->s_state = CEPH_MDS_SESSION_CLOSED;
cleanup_session_requests(mdsc, session);
remove_session_caps(session);
+
+ /*
+ * Now that all caps are removed, it is safe release
+ * the MDS rank and allow other processes to create a
+ * new session object.
+ *
+ * A concurrent ceph_mdsc_close_sessions() or
+ * check_new_map() may have unregistered the session
+ * already, so check __verify_registered_session()
+ * first.
+ */
+ mutex_lock(&mdsc->mutex);
+ if (!__verify_registered_session(mdsc, session))
+ __unregister_session(mdsc, session);
+ mutex_unlock(&mdsc->mutex);
+
wake = 2; /* for good measure */
wake_up_all(&mdsc->session_close_wq);
break;
@@ -5909,6 +5943,15 @@ static void check_new_map(struct ceph_mds_client *mdsc,
* reconnection request in up:reconnect state.
*/
s = __ceph_lookup_mds_session(mdsc, i);
+ if (s && s->s_state == CEPH_MDS_SESSION_CLOSED) {
+ /*
+ * handle_session() has not finished removing
+ * this session yet, so it cannot do a
+ * reconnect
+ */
+ ceph_put_mds_session(s);
+ s = NULL;
+ }
if (likely(!s)) {
s = __open_export_target_session(mdsc, i);
if (IS_ERR(s)) {
--
2.47.3