<feed xmlns='http://www.w3.org/2005/Atom'>
<title>kernel/git/stable/linux.git/fs/ceph/caps.c, branch master</title>
<subtitle>Linux kernel stable tree</subtitle>
<id>https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/atom?h=master</id>
<link rel='self' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/'/>
<updated>2026-08-26T17:57:29+00:00</updated>
<entry>
<title>ceph: force a cap message when a deferred revoke can't be acked immediately</title>
<updated>2026-08-26T17:57:29+00:00</updated>
<author>
<name>Max Kellermann</name>
<email>max.kellermann@ionos.com</email>
</author>
<published>2026-08-18T18:40:05+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=8fdf946445732c2bcd685abc8bd0e509d2ebc158'/>
<id>urn:sha1:8fdf946445732c2bcd685abc8bd0e509d2ebc158</id>
<content type='text'>
When the MDS revokes capabilities, handle_cap_grant() normally
guarantees a response by setting `CHECK_CAPS_FLUSH_FORCE` (see
commit 31634d7597d8 ("ceph: force sending a cap update msg back to MDS
for revoke op")), so ceph_check_caps() sends a cap message even if the
client would otherwise decide it has nothing to do.  That guarantee is
skipped whenever the revoke has to be deferred (via revoke_wait):
revoking Fb while dirty data is still buffered (writeback is queued
first) or revoking Fc while pages are cached (async invalidation is
queued first).

In those cases, the ack is left to the deferred completion
(ceph_put_wrbuffer_cap_refs() after writeback, or the invalidate
worker after invalidation); both of which call ceph_check_caps(ci,0)
i.e.  without `CHECK_CAPS_FLUSH_FORCE`.  Nothing gets sent under one
of the following conditions:

- the inode is retaining caps because the file was used recently
  (file_wanted != 0; retain |= CEPH_CAP_ANY)

- the revoked cap is still used because the page was re-cached (e.g. a
  file being re-read)

- the MDS has meanwhile re-granted, so `issued==implemented` and the
  client sees nothing being revoked

The client then never emits the cap message which the MDS is waiting
for.  The MDS blocks on the revoke indefinitely and logs, for minutes
or hours:

  client.NNN isn't responding to mclientcaps(revoke), ino 0x... pending
  pAsxLsXsxFsxcrwb issued pAsxLsXsxFsxcrwb, sent 964.899182 seconds ago

The client-side state at that point shows the full cap set still
issued, nothing in the revoking/flushing sets.  Thus nothing gets
sent.

This patch fixes it by remembering that a forced response is expected.
When a revoke is deferred, set `CEPH_I_FLUSH_FORCE` on the inode.
ceph_check_caps() replays it as `CHECK_CAPS_FLUSH_FORCE`, so whichever
path re-checks the inode next (the writeback/invalidate completion,
the delayed worker, or any other caller) is guaranteed to send a cap
message to the MDS.  __prep_cap() clears the flag once a message is
actually built.

This is the deferred-path counterpart of the existing
`CHECK_CAPS_FLUSH_FORCE` handling; a normal (non-deferred) revoke
still forces the response inline as before.

Cc: stable@vger.kernel.org
Fixes: 31634d7597d8 ("ceph: force sending a cap update msg back to MDS for revoke op")
Fixes: 257e6172ab36 ("ceph: don't let check_caps skip sending responses for revoke msgs")
Signed-off-by: Max Kellermann &lt;max.kellermann@ionos.com&gt;
Reviewed-by: Alex Markuze &lt;amarkuze@redhat.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: fix UAF in __kick_flushing_caps() on cf entry freed during unlock</title>
<updated>2026-08-26T17:57:28+00:00</updated>
<author>
<name>Xiubo Li</name>
<email>xiubo.li@clyso.com</email>
</author>
<published>2026-07-14T08:13:43+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=7af4c4f01305b0935adf6d4301b1ec407025485d'/>
<id>urn:sha1:7af4c4f01305b0935adf6d4301b1ec407025485d</id>
<content type='text'>
list_for_each_entry() iterates ci-&gt;i_cap_flush_list but drops
i_ceph_lock to send cap messages.  During the unlock window,
handle_cap_flush_ack() can acquire i_ceph_lock, detach cf entries
with tid &lt;= flush_tid from the list, release i_ceph_lock, and free
them via ceph_free_cap_flush() outside any lock.  When the original
thread reacquires i_ceph_lock and the for-loop macro advances via
cf = list_next_entry(cf, i_list), it dereferences cf-&gt;i_list.next
on freed memory.

The race timeline:

  __kick_flushing_caps()              handle_cap_flush_ack()
  -----------------------             -----------------------
  holds i_ceph_lock        &lt;---
  iterates to cf (tid=10)
  prepares FLUSH message
  drops i_ceph_lock        &lt;---
  __send_cap() ── FLUSH(tid=10)
	                              MDS sends FLUSH_ACK(tid=10)
                           ---&gt;       acquires i_ceph_lock
                                      cf-&gt;tid(10) &lt;= flush_tid(10),
                                      detaches cf from i_cap_flush_list
                                      drops i_ceph_lock
                                      ceph_free_cap_flush(cf) &lt;- frees it!
  acquires i_ceph_lock     &lt;---
  for-loop advances:
    cf = list_next_entry(cf, i_list)
      -- UAF on freed cf-&gt;i_list.next

The cf was just sent by __kick_flushing_caps itself via __send_cap().
The MDS may respond with FLUSH_ACK quickly enough that
handle_cap_flush_ack() frees cf before __kick_flushing_caps can
finish the iteration.

Fix by converting to a manual while loop: save the next pointer
under i_ceph_lock before dropping it, then use the saved pointer
after reacquiring, so the potentially-freed cf is never accessed again.

Cc: stable@vger.kernel.org
Signed-off-by: Xiubo Li &lt;xiubo.li@clyso.com&gt;
Reviewed-by: Viacheslav Dubeyko &lt;slava@dubeyko.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: pass inode pointer around instead of reloading it</title>
<updated>2026-08-26T17:57:28+00:00</updated>
<author>
<name>Max Kellermann</name>
<email>max.kellermann@ionos.com</email>
</author>
<published>2026-07-06T07:38:12+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=0cb176595797466da1792aaff1124b74d9df6e81'/>
<id>urn:sha1:0cb176595797466da1792aaff1124b74d9df6e81</id>
<content type='text'>
All these functions already have a ceph_inode_info pointer, so let's
use that instead of letting every function reload it from RAM
(i.e. `ceph_cap.ci`).  This eliminates several memory accesses.

Signed-off-by: Max Kellermann &lt;max.kellermann@ionos.com&gt;
Reviewed-by: Alex Markuze &lt;amarkuze@redhat.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: mark cap remove with RB_CLEAR_NODE() instead of setting ci=NULL</title>
<updated>2026-08-26T17:57:28+00:00</updated>
<author>
<name>Max Kellermann</name>
<email>max.kellermann@ionos.com</email>
</author>
<published>2026-07-06T07:38:11+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=af05588c9700de133aad9f8ba623ad0469e174fe'/>
<id>urn:sha1:af05588c9700de133aad9f8ba623ad0469e174fe</id>
<content type='text'>
__ceph_remove_cap() erases the ceph_cap object from the RB tree, thus
it seems natural to use RB_CLEAR_NODE() / RB_EMPTY_NODE() for the
removal check.

Signed-off-by: Max Kellermann &lt;max.kellermann@ionos.com&gt;
Reviewed-by: Alex Markuze &lt;amarkuze@redhat.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: add helper function ceph_cap_is_removed()</title>
<updated>2026-08-26T17:57:27+00:00</updated>
<author>
<name>Max Kellermann</name>
<email>max.kellermann@ionos.com</email>
</author>
<published>2026-07-06T07:38:10+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=8619a36ff55ac8723bc449332460368f9a090a77'/>
<id>urn:sha1:8619a36ff55ac8723bc449332460368f9a090a77</id>
<content type='text'>
Having it as a wrapper allows replacing the implementation, which the
next patch will do.

Signed-off-by: Max Kellermann &lt;max.kellermann@ionos.com&gt;
Reviewed-by: Alex Markuze &lt;amarkuze@redhat.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: make __ceph_remove_cap() static</title>
<updated>2026-08-26T17:57:27+00:00</updated>
<author>
<name>Max Kellermann</name>
<email>max.kellermann@ionos.com</email>
</author>
<published>2026-07-06T07:38:09+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=6cd69ea0f04c481b7b104c32824546e7df1806a5'/>
<id>urn:sha1:6cd69ea0f04c481b7b104c32824546e7df1806a5</id>
<content type='text'>
It's only used from within caps.c.

Signed-off-by: Max Kellermann &lt;max.kellermann@ionos.com&gt;
Reviewed-by: Alex Markuze &lt;amarkuze@redhat.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: fix use-after-dereference of NULL ci in __ceph_remove_cap()</title>
<updated>2026-08-26T17:57:27+00:00</updated>
<author>
<name>Xiubo Li</name>
<email>xiubo.li@clyso.com</email>
</author>
<published>2026-07-14T06:20:37+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=a354d7eaa1a57f1532c8072a424cc2d339a73cc0'/>
<id>urn:sha1:a354d7eaa1a57f1532c8072a424cc2d339a73cc0</id>
<content type='text'>
The NULL check for "ci" in __ceph_remove_cap() was dead code because
ci was dereferenced via &amp;ci-&gt;netfs.inode before the check, and
cap-&gt;session was dereferenced via session-&gt;s_mdsc-&gt;fsc-&gt;client even
earlier.  On a double-remove, both cap-&gt;ci and cap-&gt;session are set
to NULL by the first call, so the second call would crash before
ever reaching the guard.

Move ci, session, cl, and inode initializations after the NULL check
so that the early-return actually works.

Signed-off-by: Xiubo Li &lt;xiubo.li@clyso.com&gt;
Reviewed-by: Viacheslav Dubeyko &lt;slava@dubeyko.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: use GFP_NOFS for cap flush allocation in writeback path</title>
<updated>2026-08-26T17:57:26+00:00</updated>
<author>
<name>Xiubo Li</name>
<email>xiubo.li@clyso.com</email>
</author>
<published>2026-07-23T05:47:42+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=9be23efacbac35ebd6ff1512cb22e69c25e4861d'/>
<id>urn:sha1:9be23efacbac35ebd6ff1512cb22e69c25e4861d</id>
<content type='text'>
ceph_alloc_cap_flush() is called from ceph_writepages_start() inside
the writeback layer, where other allocations in the same path
(ceph_osdc_alloc_request, ceph_osdc_alloc_messages) already use
GFP_NOFS.  A GFP_KERNEL allocation here can trigger direct reclaim
that recursively enters the filesystem writeback path:

  ceph_writepages_start()                       // inode A writeback
    ceph_alloc_cap_flush()
      kmem_cache_alloc(..., GFP_KERNEL)
        [direct reclaim]
          try_to_free_pages()
            shrink_slab()
              super_cache_scan()
                prune_icache_sb()
                  inode_lru_isolate()
                    iput() -&gt; evict(inode_B)
                      [inode_B has dirty pages]
                      filemap_flush()
                        ceph_writepages_start()  // re-enters writeback
                          ceph_alloc_cap_flush()
                            -&gt; RECURSION / STACK OVERFLOW

All 11 callers of ceph_alloc_cap_flush() are in write or writeback
contexts: writepages (x2), write_iter, fallocate, copy_file_range,
setxattr, setattr, and page_mkwrite.

Signed-off-by: Xiubo Li &lt;xiubo.li@clyso.com&gt;
Reviewed-by: Viacheslav Dubeyko &lt;slava@dubeyko.com&gt;
Reviewed-by: Alex Markuze &lt;amarkuze@redhat.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: skip __touch_cap() most of the time</title>
<updated>2026-08-26T17:57:26+00:00</updated>
<author>
<name>Max Kellermann</name>
<email>max.kellermann@ionos.com</email>
</author>
<published>2026-07-06T14:59:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=e33752c8510076f3ef63198ee2bce15ed1ae14d9'/>
<id>urn:sha1:e33752c8510076f3ef63198ee2bce15ed1ae14d9</id>
<content type='text'>
__touch_cap() moves one capability to the end of the LRU list; this
list is sorted by access time for just one thing: ceph_trim_caps().
That function is supposed to discard the least-recently used
capabilities.

__touch_cap() is called extremely often - several times for every
system call, but ceph_trim_caps() is only called rarely.

__touch_cap() causes considerable lock contention on
`ceph_mds_session.s_cap_lock`; this is a /proc/lock_stat I captured on
one of our web servers for 5 minutes:

      class name    con-bounces    contentions   waittime-min   waittime-max waittime-total   waittime-avg    acq-bounces   acquisitions   holdtime-min   holdtime-max holdtime-total   holdtime-avg

  &amp;s-&gt;s_cap_lock:     336304046      341686597           0.04        4905.76   418498578.76           1.22      892783632     1957814739           0.04         959.40   355752146.24           0.18
  --------------
  &amp;s-&gt;s_cap_lock      339379730          [&lt;00000000a2197200&gt;] __ceph_caps_issued_mask+0x1bc/0x240
  &amp;s-&gt;s_cap_lock        1268054          [&lt;00000000c96a24b7&gt;] ceph_add_cap+0x234/0x3e0
  &amp;s-&gt;s_cap_lock        1021360          [&lt;00000000aa76f996&gt;] ceph_add_cap+0x108/0x3e0
  &amp;s-&gt;s_cap_lock          16042          [&lt;0000000099463548&gt;] __ceph_remove_cap+0x1f4/0x270
  --------------
  &amp;s-&gt;s_cap_lock      338509619          [&lt;00000000a2197200&gt;] __ceph_caps_issued_mask+0x1bc/0x240
  &amp;s-&gt;s_cap_lock        1937864          [&lt;00000000c96a24b7&gt;] ceph_add_cap+0x234/0x3e0
  &amp;s-&gt;s_cap_lock        1203451          [&lt;00000000aa76f996&gt;] ceph_add_cap+0x108/0x3e0
  &amp;s-&gt;s_cap_lock            202          [&lt;00000000888f212a&gt;] __ceph_remove_cap+0x7c/0x270

In this /proc/lock_stat output, __touch_cap() is inlined in
__ceph_caps_issued_mask().  It is responsible for 99% of all
contentions.

Since __touch_cap() is called so often, it is acceptable to just skip
most calls.  The most busy capabilities will still gravitate towards
the end of the linked list, and if not, it doesn't hurt as much as the
lock contention.  This is still good enough for ceph_trim_caps().

This patch adds a static variable that gets incremented with each
call, and 255 out of 256 calls will just be skipped.  I didn't bother
to make the increment atomic or use READ_ONCE because I don't think
that makes a practical difference for this use case.

Another /proc/lock_stat for 5 minutes with this patch (__touch_cap()
is no longer inlined probably because it contains a static variable):

      class name    con-bounces    contentions   waittime-min   waittime-max waittime-total   waittime-avg    acq-bounces   acquisitions   holdtime-min   holdtime-max holdtime-total   holdtime-avg

  &amp;s-&gt;s_cap_lock:       1043711        1065182           0.04         502.72      737472.88           0.69       10522578       25069948           0.04         796.44    11053669.64           0.44
  --------------
  &amp;s-&gt;s_cap_lock        1043074          [&lt;00000000f4367d73&gt;] __touch_cap.isra.0+0x50/0xa8
  &amp;s-&gt;s_cap_lock          12147          [&lt;0000000096f45706&gt;] ceph_add_cap+0x234/0x3e0
  &amp;s-&gt;s_cap_lock           9472          [&lt;0000000038a23e0f&gt;] ceph_add_cap+0x108/0x3e0
  &amp;s-&gt;s_cap_lock            471          [&lt;00000000e2eba934&gt;] __ceph_remove_cap+0x1f4/0x270
  --------------
  &amp;s-&gt;s_cap_lock         978499          [&lt;00000000f4367d73&gt;] __touch_cap.isra.0+0x50/0xa8
  &amp;s-&gt;s_cap_lock          57794          [&lt;0000000038a23e0f&gt;] ceph_add_cap+0x108/0x3e0
  &amp;s-&gt;s_cap_lock          27226          [&lt;0000000096f45706&gt;] ceph_add_cap+0x234/0x3e0
  &amp;s-&gt;s_cap_lock           1581          [&lt;00000000e2eba934&gt;] __ceph_remove_cap+0x1f4/0x270

__touch_cap() is still responsible for 91% of all contentions, but the
number of contentions has been reduced by a factor of 320 and the
total wait time by a factor of 567.

Signed-off-by: Max Kellermann &lt;max.kellermann@ionos.com&gt;
Reviewed-by: Alex Markuze &lt;amarkuze@redhat.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
<entry>
<title>ceph: fix hanging __ceph_get_caps() with stale mds_wanted</title>
<updated>2026-07-23T18:29:42+00:00</updated>
<author>
<name>Max Kellermann</name>
<email>max.kellermann@ionos.com</email>
</author>
<published>2026-07-06T15:06:59+00:00</published>
<link rel='alternate' type='text/html' href='https://git.landau.one/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=50958bb928bad3bdba9e5d1b7ff4bbadcf6951e6'/>
<id>urn:sha1:50958bb928bad3bdba9e5d1b7ff4bbadcf6951e6</id>
<content type='text'>
A reader can hang forever in __ceph_get_caps() when the client no
longer holds `FILE_RD`, but local cap state still says that the
capability is already wanted (via `mds_wanted`).

One way to trigger this is through MDS cap revocation.  If another
client performs a conflicting operation, the MDS can revoke `FILE_RD`
from the reader; the next read then has to reacquire `FILE_RD`.  If
the cap update that should request `FILE_RD` never reaches the MDS
after `cap-&gt;mds_wanted` was raised, the reader is left holding only
non-file caps while local `mds_wanted` still includes the file read
caps.

In that state, try_get_cap_refs() sees `need &lt;= mds_wanted` and
returns 0, so __ceph_get_caps() just waits on `i_cap_wq`.  If the cap
update that was supposed to request `FILE_RD never reaches the MDS
after `cap-&gt;mds_wanted was` raised, no further request is sent and the
waiter can sleep indefinitely until unrelated cap traffic happens to
wake it up.

The ordering issue is that `cap-&gt;mds_wanted` is updated in
__prep_cap() before the `CEPH_MSG_CLIENT_CAPS message` is actually
queued for send.  That makes one field serve two different meanings at
once: what this client wants, and what the client believes the MDS
already knows it wants.

A proper fix would be to split those states and track whether a cap
update is actually in flight or has been observed by the MDS.
However, simply moving the `cap-&gt;mds_wanted assignment` later would
not be sufficient: queueing the message in the messenger does not
guarantee that the MDS processed that specific wanted set, and
reconnect or message loss can still invalidate that assumption.
Fixing that properly would require a larger rework of the cap state
machine.

To allow simpler backports to stable kernels, this patch implements a
simpler workaround:

- stop waiting forever in __ceph_get_caps(); after a bounded wait,
  fall back to the renew path

- make ceph_renew_caps() issue a synchronous `OPEN` request whenever
  the inode still does not actually hold the wanted caps, instead of
  only calling ceph_check_caps()

The extra issued-vs-wanted check in ceph_renew_caps() is necessary
because the previous test only checked whether the inode still had any
real caps at all.  That is not enough after revocation: the client can
still hold something like `pLs` and yet be missing `FILE_RD`
completely.  In that case, falling back to ceph_check_caps() is not
sufficient, because it still trusts `cap-&gt;mds_wanted` and may resend
nothing.  By requiring `(issued &amp; wanted) == wanted` before taking the
asynchronous path, the code only uses ceph_check_caps() when the
`wanted caps` are already actually issued.  Otherwise, it sends the
synchronous `OPEN` renew.

This preserves the existing asynchronous fast path when the wanted
caps are already issued, avoids changing cap-state semantics, and
fixes the hang by guaranteeing that a stalled waiter eventually
retries through a path that does not rely on the stale `mds_wanted`
state.

[ idryomov: move CEPH_GET_CAPS_WAIT_TIMEOUT from libceph.h to
  mds_client.h, formatting ]

Cc: stable@vger.kernel.org
Fixes: 0a454bdd501a ("ceph: reorganize __send_cap for less spinlock abuse")
Signed-off-by: Max Kellermann &lt;max.kellermann@ionos.com&gt;
Reviewed-by: Alex Markuze &lt;amarkuze@redhat.com&gt;
Signed-off-by: Ilya Dryomov &lt;idryomov@gmail.com&gt;
</content>
</entry>
</feed>
