CVE-2026-46066
Description
In the Linux kernel, the following vulnerability has been resolved:
ceph: fix num_ops off-by-one when crypto allocation fails
move_dirty_folio_in_page_array() may fail if the file is encrypted, the dirty folio is not the first in the batch, and it fails to allocate a bounce buffer to hold the ciphertext. When that happens, ceph_process_folio_batch() simply redirties the folio and flushes the current batch -- it can retry that folio in a future batch.
However, if this failed folio is not contiguous with the last folio that did make it into the batch, then ceph_process_folio_batch() has already incremented ceph_wbc->num_ops; because it doesn't follow through and add the discontiguous folio to the array, ceph_submit_write() -- which expects that ceph_wbc->num_ops accurately reflects the number of contiguous ranges (and therefore the required number of "write extent" ops) in the writeback -- will panic the kernel:
BUG_ON(ceph_wbc->op_idx + 1 != req->r_num_ops);
This issue can be reproduced on affected kernels by writing to fscrypt-enabled CephFS file(s) with a 4KiB-written/4KiB-skipped/repeat pattern (total filesize should not matter) and gradually increasing the system's memory pressure until a bounce buffer allocation fails.
Fix this crash by decrementing ceph_wbc->num_ops back to the correct value when move_dirty_folio_in_page_array() fails, but the folio already started counting a new (i.e. still-empty) extent.
The defect corrected by this patch has existed since 2022 (see first Fixes:), but another bug blocked multi-folio encrypted writeback until recently (see second Fixes:). The second commit made it into 6.18.16, 6.19.6, and 7.0-rc1, unmasking the panic in those versions. This patch therefore fixes a regression (panic) introduced by cac190c7674f.
Affected products
2- Range: >=6.18.16,<6.19.6
Patches
6ba12c1e57889ceph: fix num_ops off-by-one when crypto allocation fails
2 files changed · +8 −2
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 2090fc78529cb4..44553556ac742b 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1365,6 +1365,10 @@ void ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); break; -- cgit 1.3-korg
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 2090fc78529cb4..44553556ac742b 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1365,6 +1365,10 @@ void ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); break; -- cgit 1.3-korg
a0d9555bf9eaceph: fix num_ops off-by-one when crypto allocation fails
2 files changed · +8 −2
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 2090fc78529cb4..44553556ac742b 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1365,6 +1365,10 @@ void ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); break; -- cgit 1.3-korg
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 2090fc78529cb4..44553556ac742b 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1365,6 +1365,10 @@ void ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); break; -- cgit 1.3-korg
6200f41d6fcfceph: fix num_ops off-by-one when crypto allocation fails
2 files changed · +8 −2
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 390f122feeaaf5..3af6795cb3c15d 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1373,6 +1373,10 @@ int ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + rc = 0; folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); -- cgit 1.3-korg
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 390f122feeaaf5..3af6795cb3c15d 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1373,6 +1373,10 @@ int ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + rc = 0; folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); -- cgit 1.3-korg
a0d9555bf9eaceph: fix num_ops off-by-one when crypto allocation fails
2 files changed · +8 −2
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 2090fc78529cb4..44553556ac742b 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1365,6 +1365,10 @@ void ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); break; -- cgit 1.3-korg
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 2090fc78529cb4..44553556ac742b 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1365,6 +1365,10 @@ void ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); break; -- cgit 1.3-korg
ba12c1e57889ceph: fix num_ops off-by-one when crypto allocation fails
2 files changed · +8 −2
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 2090fc78529cb4..44553556ac742b 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1365,6 +1365,10 @@ void ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); break; -- cgit 1.3-korg
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 2090fc78529cb4..44553556ac742b 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1365,6 +1365,10 @@ void ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); break; -- cgit 1.3-korg
6200f41d6fcfceph: fix num_ops off-by-one when crypto allocation fails
2 files changed · +8 −2
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 390f122feeaaf5..3af6795cb3c15d 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1373,6 +1373,10 @@ int ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + rc = 0; folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); -- cgit 1.3-korg
fs/ceph/addr.c+4 −1 modifieddiff --git a/fs/ceph/addr.c b/fs/ceph/addr.c index 390f122feeaaf5..3af6795cb3c15d 100644 --- a/fs/ceph/addr.c +++ b/fs/ceph/addr.c @@ -1373,6 +1373,10 @@ int ceph_process_folio_batch(struct address_space *mapping, rc = move_dirty_folio_in_page_array(mapping, wbc, ceph_wbc, folio); if (rc) { + /* Did we just begin a new contiguous op? Nevermind! */ + if (ceph_wbc->len == 0) + ceph_wbc->num_ops--; + rc = 0; folio_redirty_for_writepage(wbc, folio); folio_unlock(folio); -- cgit 1.3-korg
Vulnerability mechanics
Root cause
"Off-by-one error in ceph_process_folio_batch() where ceph_wbc->num_ops is incremented for a new contiguous extent but not decremented when move_dirty_folio_in_page_array() subsequently fails, causing a mismatch with the actual number of write extent operations."
Attack vector
An attacker with write access to an fscrypt-enabled CephFS file can trigger this bug by writing a pattern of 4KiB-written/4KiB-skipped/repeat (any total filesize) while gradually increasing system memory pressure until a bounce buffer allocation for ciphertext fails [patch_id=2659994]. When move_dirty_folio_in_page_array() fails on a discontiguous folio that already caused num_ops to be incremented, the kernel panics via BUG_ON(ceph_wbc->op_idx + 1 != req->r_num_ops) in ceph_submit_write() [patch_id=2659994]. The precondition is that the file is encrypted (fscrypt) and the system is under memory pressure.
Affected code
The vulnerable function is ceph_process_folio_batch() in fs/ceph/addr.c [patch_id=2659994]. The bug occurs in the error path after move_dirty_folio_in_page_array() returns a non-zero status.
What the fix does
The patch adds a check in ceph_process_folio_batch() (in fs/ceph/addr.c) after move_dirty_folio_in_page_array() fails: if ceph_wbc->len is 0, meaning the folio had just started a new (still-empty) contiguous extent, it decrements ceph_wbc->num_ops to undo the premature increment [patch_id=2659994]. This ensures that num_ops accurately reflects the number of contiguous ranges that were actually added to the page array, preventing the BUG_ON panic in ceph_submit_write().
Preconditions
- configThe CephFS file must be encrypted (fscrypt-enabled).
- inputWrite pattern of 4KiB written / 4KiB skipped, repeated (any total filesize).
- configSystem must be under sufficient memory pressure that a bounce buffer (GFP_KERNEL) allocation for ciphertext fails.
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.