[PATCH v2 1/2] ceph: handle a NULL oldest snap context in writeback
From: Xiubo Li via B4 Relay
Date: Sat Aug 29 2026 - 08:28:12 EST
From: Xiubo Li <xiubo.li@xxxxxxxxx>
get_oldest_context() returns NULL when the inode has no dirty capsnaps
and no head writeback references. That is a legal return value that
context_is_writeable_or_written() handles explicitly, but
ceph_find_incompatible() and write_folio_nounlock() dereference the
returned pointer without checking:
BUG: kernel NULL pointer dereference, address: 0000000000000008
#PF: supervisor read access in kernel mode
...
RIP: 0010:ceph_find_incompatible+0x75/0x1d0 [ceph]
Skip the sequence comparison when there is no oldest context: with no
dirty accounting there is nothing for the folio's snap context to
conflict with and the folio is writable. In write_folio_nounlock()
also fill the writeback ctl with the inode's current size and truncate
state in that case, since get_oldest_context() only fills it on the
capsnap and head paths and the uninitialized ctl would otherwise be
used for the EOF check and the OSD request.
Fixes: 80e755fedebc ("ceph: allow writeback of snapped pages older than 'oldest' snapc")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Xiubo Li <xiubo.li@xxxxxxxxx>
---
fs/ceph/addr.c | 19 +++++++++++++++++--
fs/ceph/caps.c | 13 ++++++++++---
2 files changed, 27 insertions(+), 5 deletions(-)
diff --git a/fs/ceph/addr.c b/fs/ceph/addr.c
index 534a7a377b2f..38db91459440 100644
--- a/fs/ceph/addr.c
+++ b/fs/ceph/addr.c
@@ -769,7 +769,7 @@ static int write_folio_nounlock(struct folio *folio,
return 0;
}
oldest = get_oldest_context(inode, &ceph_wbc, snapc);
- if (snapc->seq > oldest->seq) {
+ if (oldest && snapc->seq > oldest->seq) {
doutc(cl, "%llx.%llx folio %p snapc %p not writeable - noop\n",
ceph_vinop(inode), folio, snapc);
/* we should only noop if called by kswapd */
@@ -780,6 +780,21 @@ static int write_folio_nounlock(struct folio *folio,
}
ceph_put_snap_context(oldest);
+ if (!oldest) {
+ /*
+ * No dirty capsnap and no head writeback refs: there is
+ * nothing to conflict with and the folio is writable.
+ * Fill in the ctl as for the head context, since
+ * get_oldest_context() only does so on the capsnap and
+ * head paths.
+ */
+ ceph_wbc.i_size = i_size_read(inode);
+ ceph_wbc.truncate_size = ci->i_truncate_size;
+ ceph_wbc.truncate_seq = ci->i_truncate_seq;
+ ceph_wbc.size_stable = false;
+ ceph_wbc.head_snapc = true;
+ }
+
/* is this a partial page at end of file? */
if (page_off >= ceph_wbc.i_size) {
doutc(cl, "%llx.%llx folio at %lu beyond eof %llu\n",
@@ -1869,7 +1884,7 @@ ceph_find_incompatible(struct folio *folio)
* context! is it writeable now?
*/
oldest = get_oldest_context(inode, NULL, NULL);
- if (snapc->seq > oldest->seq) {
+ if (oldest && snapc->seq > oldest->seq) {
/* not writeable -- return it for the caller to deal with */
ceph_put_snap_context(oldest);
doutc(cl, " %llx.%llx folio %p snapc %p not current or oldest\n",
diff --git a/fs/ceph/caps.c b/fs/ceph/caps.c
index 1847badddd87..d07a80674a17 100644
--- a/fs/ceph/caps.c
+++ b/fs/ceph/caps.c
@@ -3447,10 +3447,17 @@ void ceph_put_wrbuffer_cap_refs(struct ceph_inode_info *ci, int nr,
if (!capsnap) {
/*
- * The capsnap should already be removed when removing
- * auth cap in the case of a forced unmount.
+ * The cap_snap may be gone even though the auth cap
+ * is present: a session loss removes all cap_snaps
+ * (ceph_purge_inode_cap()), but the dirty folios
+ * referencing their contexts can survive in the
+ * page cache. Once the session reconnects and the
+ * inode gets a new auth cap, those folios write
+ * back and no longer find a matching cap_snap
+ * here. There is nothing left to account for.
*/
- WARN_ON_ONCE(ci->i_auth_cap);
+ doutc(cl, "%p %llx.%llx snapc %p no cap_snap\n",
+ inode, ceph_vinop(inode), snapc);
goto unlock;
}
--
2.53.0