Linux CIFS filesystem development
 help / color / mirror / Atom feed
* [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes
@ 2026-02-18 18:00 Bharath SM
  2026-02-19 11:05 ` David Howells
  2026-02-27 10:25 ` Shyam Prasad N
  0 siblings, 2 replies; 7+ messages in thread
From: Bharath SM @ 2026-02-18 18:00 UTC (permalink / raw)
  To: David Howells, Shyam Prasad N, Shyam Prasad, Steve French, CIFS,
	Paulo Alcantara, Enzo Matsumiya, Henrique Carvalho, Bharath S M

[-- Attachment #1: Type: text/plain, Size: 5260 bytes --]

We are noticing a data corruption issue in kernels based on stable
6.6.y. Especially, when a synchronous writes retried after a
connection reset.

Based on investigation so far, it looks like we are having issue in
the following code path:
When SMB3 encryption is enabled, partial-page buffered writes hit the
synchronous write path in cifs_write_end() when the folio is not
uptodate (!folio_test_uptodate(folio)), it calls cifs_write() directly
with the kmap()'d page cache buffer, bypassing the async writeback
path.
cifs_write() calls SMB2_write(), which places the write payload in
rq_iov[1], pointing directly at the page cache buffer. When
smb3_init_transform_rq() builds the encryption request, it shares
rq_iov by pointer (new->rq_iov = old->rq_iov), and crypt_message()
encrypts in-place via aead_request_set_crypt(req, sg, sg, ...). This
destroys the original page cache data. If the write gets -EAGAIN after
encryption (e.g., connection reset), cifs_write() re-sends the
now-ciphertext buffer as if it were plaintext, resulting in
double-encrypted garbage on the server. The server accepts it and
returns success.
Please let me know if you have seen this issue in the past, your
comments on the analysis and probable fixes.

Repro steps: Attached repro.zip with repro scripts and instructions:
1) Mount with SMB3 encryption enabled
2) Perform buffered writes in a loop (e.g., echo "known_pattern" >> file)
3) Kill the TCP connection during writes (ss -K dport 445) to force
retryable errors
4) Read the file back and compare against expected content

Issue can occur when all below conditions met in buffered writes:
1) SMB2 encryption is active
2) Sync write path: Writes reached SMB2_write via cifs_write
3) Retryable network error for writes: When EAGAIN or ECONABORTED
returned from  cifs_send_recv().

Here is a the sequence of operations leading to issue:
write(2) syscall
 └─ cifs_write_end()                          [file.c]
     └─ cifs_write()                          [file.c]
         │  iov[1].iov_base = write_data      ← page cache pointer enters iov[1]
         │
         └─ server->ops->sync_write()         [file.c]
             └─ smb2_sync_write()             [smb2ops.c:]
                 └─ SMB2_write()              [smb2pdu.c:]
                     │  rqst.rq_iov = iov     ← rqst points to iov[]
(with page cache in [1])
                     │  rqst.rq_nvec = n_vec+1  ← BUG: payload in
rq_iov, not rq_iter
                     │
                     └─ cifs_send_recv()      [transport.c:1305]
                         └─ compound_send_recv()  [transport.c:1071]
                             │
                             └─ smb_send_rqst()   [transport.c:427]
                                 │  if (flags & CIFS_TRANSFORM_REQ)  ←
YES for SMB3 encryption
                                 │
                                 └─ server->ops->init_transform_rq()
[smb2ops.c:~4398]
                                 │   = smb3_init_transform_rq()
                                 │     new->rq_iov = old->rq_iov     ←
SHARES pointer (not copied!)
                                 │     size =
iov_iter_count(old->rq_iter) = 0  ← empty, no copy
                                 │
                                 └─ __smb_send_rqst()  [transport.c:272]
                                     │  → crypt_message()  [smb2ops.c:~4280]
                                     │     → smb2_get_aead_req()
[smb2ops.c:~4196]
                                     │        sg = scatterwalk from rq_iov[0..n]
                                     │
aead_request_set_crypt(req, sg, sg, ...)
                                     │
   ^^^  ^^^
                                     │
src=dst → IN-PLACE encrypt
                                     │
                                     │   iov[1] (= page cache) is now
AES ciphertext
                                     │
                                     └─ kernel_sendmsg() / sock_sendmsg()
                                         → sends encrypted data on wire

         ← rc = -EAGAIN (connection dropped)

         is_replayable_error(rc) == true or cifs_write while loop detects EAGAIN
         goto replay_again                    ← loops back with corrupted iov[1]
             └─ SMB2_write() re-sends...
                 └─ smb3_init_transform_rq()  ← encrypts ciphertext AGAIN
                     └─ crypt_message()       ← double-encrypted garbage
                         └─ server writes it to disk  ←  CORRUPTION



Modifying SMB2_write function by adding payload to rq_iter seems to
help here. Need to further test.
With below fix, when rq_iter size > 0 code in smb3_init_transform_rq
allocates fresh pages, copies the data via copy_page_from_iter(), and
encrypts the copy instead of the original.
Please let me know your comments.


 rqst.rq_iov = iov;
-rqst.rq_nvec = n_vec + 1;
+rqst.rq_nvec = 1;
+iov_iter_kvec(&rqst.rq_iter, ITER_SOURCE, &iov[1], n_vec,
+              io_parms->length);
+rqst.rq_iter_size = io_parms->length;

[-- Attachment #2: repro.zip --]
[-- Type: application/x-zip-compressed, Size: 2484 bytes --]

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes
  2026-02-18 18:00 [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes Bharath SM
@ 2026-02-19 11:05 ` David Howells
  2026-02-19 11:14   ` Bharath SM
  2026-02-27 10:25 ` Shyam Prasad N
  1 sibling, 1 reply; 7+ messages in thread
From: David Howells @ 2026-02-19 11:05 UTC (permalink / raw)
  To: Bharath SM
  Cc: dhowells, Shyam Prasad N, Shyam Prasad, Steve French, CIFS,
	Paulo Alcantara, Enzo Matsumiya, Henrique Carvalho, Bharath S M

Bharath SM <bharathsm.hsk@gmail.com> wrote:

> We are noticing a data corruption issue in kernels based on stable
> 6.6.y. Especially, when a synchronous writes retried after a
> connection reset.
> ...
> When SMB3 encryption is enabled, partial-page buffered writes hit the
> synchronous write path in cifs_write_end()

This is pre-netfslib, right?

David


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes
  2026-02-19 11:05 ` David Howells
@ 2026-02-19 11:14   ` Bharath SM
  0 siblings, 0 replies; 7+ messages in thread
From: Bharath SM @ 2026-02-19 11:14 UTC (permalink / raw)
  To: David Howells
  Cc: Shyam Prasad N, Shyam Prasad, Steve French, CIFS, Paulo Alcantara,
	Enzo Matsumiya, Henrique Carvalho, Bharath S M

On Thu, Feb 19, 2026 at 3:05 AM David Howells <dhowells@redhat.com> wrote:
>
> Bharath SM <bharathsm.hsk@gmail.com> wrote:
>
> > We are noticing a data corruption issue in kernels based on stable
> > 6.6.y. Especially, when a synchronous writes retried after a
> > connection reset.
> > ...
> > When SMB3 encryption is enabled, partial-page buffered writes hit the
> > synchronous write path in cifs_write_end()
>
> This is pre-netfslib, right?
Yes,  its v6.6 stable kernel which has folio changes but not netfs.
netfs cut happened in the ~6.10 kernel for SMB clients.

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes
  2026-02-18 18:00 [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes Bharath SM
  2026-02-19 11:05 ` David Howells
@ 2026-02-27 10:25 ` Shyam Prasad N
  2026-03-03 18:03   ` Bharath SM
  1 sibling, 1 reply; 7+ messages in thread
From: Shyam Prasad N @ 2026-02-27 10:25 UTC (permalink / raw)
  To: Bharath SM
  Cc: David Howells, Shyam Prasad N, Shyam Prasad, Steve French, CIFS,
	Paulo Alcantara, Enzo Matsumiya, Henrique Carvalho, Bharath S M

On Wed, Feb 18, 2026 at 11:30 PM Bharath SM <bharathsm.hsk@gmail.com> wrote:
>
> We are noticing a data corruption issue in kernels based on stable
> 6.6.y. Especially, when a synchronous writes retried after a
> connection reset.
>
> Based on investigation so far, it looks like we are having issue in
> the following code path:
> When SMB3 encryption is enabled, partial-page buffered writes hit the
> synchronous write path in cifs_write_end() when the folio is not
> uptodate (!folio_test_uptodate(folio)), it calls cifs_write() directly
> with the kmap()'d page cache buffer, bypassing the async writeback
> path.
> cifs_write() calls SMB2_write(), which places the write payload in
> rq_iov[1], pointing directly at the page cache buffer. When
> smb3_init_transform_rq() builds the encryption request, it shares
> rq_iov by pointer (new->rq_iov = old->rq_iov), and crypt_message()
> encrypts in-place via aead_request_set_crypt(req, sg, sg, ...). This
> destroys the original page cache data. If the write gets -EAGAIN after
> encryption (e.g., connection reset), cifs_write() re-sends the
> now-ciphertext buffer as if it were plaintext, resulting in
> double-encrypted garbage on the server. The server accepts it and
> returns success.
> Please let me know if you have seen this issue in the past, your
> comments on the analysis and probable fixes.
>
> Repro steps: Attached repro.zip with repro scripts and instructions:
> 1) Mount with SMB3 encryption enabled
> 2) Perform buffered writes in a loop (e.g., echo "known_pattern" >> file)
> 3) Kill the TCP connection during writes (ss -K dport 445) to force
> retryable errors
> 4) Read the file back and compare against expected content
>
Looking at the callers, it looks like simple_fallocate_range*
functions that make use of sync writes are also susceptible to this
issue.

> Issue can occur when all below conditions met in buffered writes:
> 1) SMB2 encryption is active
> 2) Sync write path: Writes reached SMB2_write via cifs_write
> 3) Retryable network error for writes: When EAGAIN or ECONABORTED
> returned from  cifs_send_recv().
>
> Here is a the sequence of operations leading to issue:
> write(2) syscall
>  └─ cifs_write_end()                          [file.c]
>      └─ cifs_write()                          [file.c]
>          │  iov[1].iov_base = write_data      ← page cache pointer enters iov[1]
>          │
>          └─ server->ops->sync_write()         [file.c]
>              └─ smb2_sync_write()             [smb2ops.c:]
>                  └─ SMB2_write()              [smb2pdu.c:]
>                      │  rqst.rq_iov = iov     ← rqst points to iov[]
> (with page cache in [1])
>                      │  rqst.rq_nvec = n_vec+1  ← BUG: payload in
> rq_iov, not rq_iter
>                      │
>                      └─ cifs_send_recv()      [transport.c:1305]
>                          └─ compound_send_recv()  [transport.c:1071]
>                              │
>                              └─ smb_send_rqst()   [transport.c:427]
>                                  │  if (flags & CIFS_TRANSFORM_REQ)  ←
> YES for SMB3 encryption
>                                  │
>                                  └─ server->ops->init_transform_rq()
> [smb2ops.c:~4398]
>                                  │   = smb3_init_transform_rq()
>                                  │     new->rq_iov = old->rq_iov     ←
> SHARES pointer (not copied!)
>                                  │     size =
> iov_iter_count(old->rq_iter) = 0  ← empty, no copy
>                                  │
>                                  └─ __smb_send_rqst()  [transport.c:272]
>                                      │  → crypt_message()  [smb2ops.c:~4280]
>                                      │     → smb2_get_aead_req()
> [smb2ops.c:~4196]
>                                      │        sg = scatterwalk from rq_iov[0..n]
>                                      │
> aead_request_set_crypt(req, sg, sg, ...)
>                                      │
>    ^^^  ^^^
>                                      │
> src=dst → IN-PLACE encrypt
>                                      │
>                                      │   iov[1] (= page cache) is now
> AES ciphertext
>                                      │
>                                      └─ kernel_sendmsg() / sock_sendmsg()
>                                          → sends encrypted data on wire
>
>          ← rc = -EAGAIN (connection dropped)
>
>          is_replayable_error(rc) == true or cifs_write while loop detects EAGAIN
>          goto replay_again                    ← loops back with corrupted iov[1]
>              └─ SMB2_write() re-sends...
>                  └─ smb3_init_transform_rq()  ← encrypts ciphertext AGAIN
>                      └─ crypt_message()       ← double-encrypted garbage
>                          └─ server writes it to disk  ←  CORRUPTION
>
>
>
> Modifying SMB2_write function by adding payload to rq_iter seems to
> help here. Need to further test.
> With below fix, when rq_iter size > 0 code in smb3_init_transform_rq
> allocates fresh pages, copies the data via copy_page_from_iter(), and
> encrypts the copy instead of the original.
> Please let me know your comments.
>
>
>  rqst.rq_iov = iov;
> -rqst.rq_nvec = n_vec + 1;
> +rqst.rq_nvec = 1;
> +iov_iter_kvec(&rqst.rq_iter, ITER_SOURCE, &iov[1], n_vec,
> +              io_parms->length);
> +rqst.rq_iter_size = io_parms->length;

Another option is to initialize iov_iter_xarray with rqst.rq_buffer,
similar to what smb3_init_transform_rq does. But this should work too.
Changes look good to me. Please submit a formal patch.

-- 
Regards,
Shyam

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes
  2026-02-27 10:25 ` Shyam Prasad N
@ 2026-03-03 18:03   ` Bharath SM
  2026-03-04  8:09     ` Shyam Prasad N
  0 siblings, 1 reply; 7+ messages in thread
From: Bharath SM @ 2026-03-03 18:03 UTC (permalink / raw)
  To: Shyam Prasad N, David Howells, Steve French, CIFS,
	Paulo Alcantara, Enzo Matsumiya, Henrique Carvalho
  Cc: Shyam Prasad N, Shyam Prasad, Bharath S M, Greg KH

[-- Attachment #1: Type: text/plain, Size: 7183 bytes --]

On Fri, Feb 27, 2026 at 2:25 AM Shyam Prasad N <nspmangalore@gmail.com> wrote:
>
> On Wed, Feb 18, 2026 at 11:30 PM Bharath SM <bharathsm.hsk@gmail.com> wrote:
> >
> > We are noticing a data corruption issue in kernels based on stable
> > 6.6.y. Especially, when a synchronous writes retried after a
> > connection reset.
> >
> > Based on investigation so far, it looks like we are having issue in
> > the following code path:
> > When SMB3 encryption is enabled, partial-page buffered writes hit the
> > synchronous write path in cifs_write_end() when the folio is not
> > uptodate (!folio_test_uptodate(folio)), it calls cifs_write() directly
> > with the kmap()'d page cache buffer, bypassing the async writeback
> > path.
> > cifs_write() calls SMB2_write(), which places the write payload in
> > rq_iov[1], pointing directly at the page cache buffer. When
> > smb3_init_transform_rq() builds the encryption request, it shares
> > rq_iov by pointer (new->rq_iov = old->rq_iov), and crypt_message()
> > encrypts in-place via aead_request_set_crypt(req, sg, sg, ...). This
> > destroys the original page cache data. If the write gets -EAGAIN after
> > encryption (e.g., connection reset), cifs_write() re-sends the
> > now-ciphertext buffer as if it were plaintext, resulting in
> > double-encrypted garbage on the server. The server accepts it and
> > returns success.
> > Please let me know if you have seen this issue in the past, your
> > comments on the analysis and probable fixes.
> >
> > Repro steps: Attached repro.zip with repro scripts and instructions:
> > 1) Mount with SMB3 encryption enabled
> > 2) Perform buffered writes in a loop (e.g., echo "known_pattern" >> file)
> > 3) Kill the TCP connection during writes (ss -K dport 445) to force
> > retryable errors
> > 4) Read the file back and compare against expected content
> >
> Looking at the callers, it looks like simple_fallocate_range*
> functions that make use of sync writes are also susceptible to this
> issue.
>
> > Issue can occur when all below conditions met in buffered writes:
> > 1) SMB2 encryption is active
> > 2) Sync write path: Writes reached SMB2_write via cifs_write
> > 3) Retryable network error for writes: When EAGAIN or ECONABORTED
> > returned from  cifs_send_recv().
> >
> > Here is a the sequence of operations leading to issue:
> > write(2) syscall
> >  └─ cifs_write_end()                          [file.c]
> >      └─ cifs_write()                          [file.c]
> >          │  iov[1].iov_base = write_data      ← page cache pointer enters iov[1]
> >          │
> >          └─ server->ops->sync_write()         [file.c]
> >              └─ smb2_sync_write()             [smb2ops.c:]
> >                  └─ SMB2_write()              [smb2pdu.c:]
> >                      │  rqst.rq_iov = iov     ← rqst points to iov[]
> > (with page cache in [1])
> >                      │  rqst.rq_nvec = n_vec+1  ← BUG: payload in
> > rq_iov, not rq_iter
> >                      │
> >                      └─ cifs_send_recv()      [transport.c:1305]
> >                          └─ compound_send_recv()  [transport.c:1071]
> >                              │
> >                              └─ smb_send_rqst()   [transport.c:427]
> >                                  │  if (flags & CIFS_TRANSFORM_REQ)  ←
> > YES for SMB3 encryption
> >                                  │
> >                                  └─ server->ops->init_transform_rq()
> > [smb2ops.c:~4398]
> >                                  │   = smb3_init_transform_rq()
> >                                  │     new->rq_iov = old->rq_iov     ←
> > SHARES pointer (not copied!)
> >                                  │     size =
> > iov_iter_count(old->rq_iter) = 0  ← empty, no copy
> >                                  │
> >                                  └─ __smb_send_rqst()  [transport.c:272]
> >                                      │  → crypt_message()  [smb2ops.c:~4280]
> >                                      │     → smb2_get_aead_req()
> > [smb2ops.c:~4196]
> >                                      │        sg = scatterwalk from rq_iov[0..n]
> >                                      │
> > aead_request_set_crypt(req, sg, sg, ...)
> >                                      │
> >    ^^^  ^^^
> >                                      │
> > src=dst → IN-PLACE encrypt
> >                                      │
> >                                      │   iov[1] (= page cache) is now
> > AES ciphertext
> >                                      │
> >                                      └─ kernel_sendmsg() / sock_sendmsg()
> >                                          → sends encrypted data on wire
> >
> >          ← rc = -EAGAIN (connection dropped)
> >
> >          is_replayable_error(rc) == true or cifs_write while loop detects EAGAIN
> >          goto replay_again                    ← loops back with corrupted iov[1]
> >              └─ SMB2_write() re-sends...
> >                  └─ smb3_init_transform_rq()  ← encrypts ciphertext AGAIN
> >                      └─ crypt_message()       ← double-encrypted garbage
> >                          └─ server writes it to disk  ←  CORRUPTION
> >
> >
> >
> > Modifying SMB2_write function by adding payload to rq_iter seems to
> > help here. Need to further test.
> > With below fix, when rq_iter size > 0 code in smb3_init_transform_rq
> > allocates fresh pages, copies the data via copy_page_from_iter(), and
> > encrypts the copy instead of the original.
> > Please let me know your comments.
> >
> >
> >  rqst.rq_iov = iov;
> > -rqst.rq_nvec = n_vec + 1;
> > +rqst.rq_nvec = 1;
> > +iov_iter_kvec(&rqst.rq_iter, ITER_SOURCE, &iov[1], n_vec,
> > +              io_parms->length);
> > +rqst.rq_iter_size = io_parms->length;
>
> Another option is to initialize iov_iter_xarray with rqst.rq_buffer,
> similar to what smb3_init_transform_rq does. But this should work too.
> Changes look good to me. Please submit a formal patch.

Thank you, Attached the patch please review. Also created the minimal
repro script with network disconnects.

Further investigation on this issue indicates that the issue is not
specific to the 6.6 Kernel; instead,
the issue can happen in kernels <6.10 including 6.1, 6.6 and 5.15 and beyond.

On older kernels ~5.15 with deferred closes may reduce exposure
because deferred handles might keep help pages
in memory and which may help writes to avoid sync_write path.
But commit 262b73ef442e (smb3 client: fix open hardlink on deferred
close file error, backported till 6.1) appears to
increase the likelihood of taking the sync_write path because we close
deferred handles aggressively in some cases,
which makes the existing encryption write corruption bug easier to
trigger. The commit does not introduce the bug;
It increases trigger frequency for writes taking sync_write path.

I will send out this patch to the stable mailing list separately.

[-- Attachment #2: 0001-smb-client-fix-page-cache-corruption-from-in-place-e.patch --]
[-- Type: application/octet-stream, Size: 2333 bytes --]

From d9df05cb5eddaf160a2947f4510728095f62e873 Mon Sep 17 00:00:00 2001
From: Bharath SM <bharathsm@microsoft.com>
Date: Tue, 3 Mar 2026 14:05:30 +0000
Subject: [PATCH] smb: client: fix page cache corruption from in-place
 encryption in SMB2_write

SMB2_write() passes data kvecs inline in rq_iov by setting
rqst.rq_nvec = n_vec + 1. When SMB3 encryption is negotiated,
smb3_init_transform_rq() -> crypt_message() encrypts data in the
kvec buffers in-place.

For synchronous writes through cifs_write(), the kvec buffers point
directly into the page cache via kmap(). In-place encryption overwrites
the page cache with ciphertext. If the send fails with a replayable
error such as -EAGAIN (e.g., from a connection reset), SMB2_write()
retries the write using the same iov[1] buffer. Since iov[1] now
contains ciphertext from the first attempt, the retry encrypts and
sends ciphertext-as-data to the server, resulting in data corruption.

The corruption is most likely to be observed when connections are
unstable, as reconnects trigger write retries that re-send the
already-encrypted page cache data.

The sync path can be reached during partial-page O_WRONLY writes when
the page is not in cache (common for append workloads with repeated
open/write/close patterns).

The async write path (smb2_async_writev) is not affected because it
passes data via rqst.rq_iter, which the encryption layer handles
without modifying the source buffers.

Fix by setting rq_nvec = 1 (header only) and moving data kvecs into
rq_iter via iov_iter_kvec().

Signed-off-by: Bharath SM <bharathsm@microsoft.com>
Reviewed-by: Shyam Prasad N <sprasad@microsoft.com>
Cc: stable@vger.kernel.org #v6.1~v6.9
---
 fs/smb/client/smb2pdu.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/fs/smb/client/smb2pdu.c b/fs/smb/client/smb2pdu.c
index a8890ae21714..a88a19dec494 100644
--- a/fs/smb/client/smb2pdu.c
+++ b/fs/smb/client/smb2pdu.c
@@ -5072,7 +5072,11 @@ SMB2_write(const unsigned int xid, struct cifs_io_parms *io_parms,
 
 	memset(&rqst, 0, sizeof(struct smb_rqst));
 	rqst.rq_iov = iov;
-	rqst.rq_nvec = n_vec + 1;
+	rqst.rq_nvec = 1;
+	iov_iter_kvec(&rqst.rq_iter, ITER_SOURCE, &iov[1], n_vec,
+		      io_parms->length);
+	rqst.rq_iter_size = io_parms->length;
+
 
 	if (retries)
 		smb2_set_replay(server, &rqst);
-- 
2.45.4


[-- Attachment #3: repro_corruption.sh --]
[-- Type: text/x-sh, Size: 4390 bytes --]

#!/bin/bash
#
# Reproducer for page cache corruption caused by in-place encryption
# in SMB2_write().
#
# SMB2_write() passes page cache data inline in rq_iov (rq_nvec = n_vec + 1).
# When SMB3 encryption is active, crypt_message() encrypts these buffers
# in-place, overwriting page cache with ciphertext. On replay after a
# replayable send failure, the ciphertext is re-sent as plaintext.
# The server stores ciphertext as file content. No error to userspace.
#
# The sync write path (cifs_write_end -> cifs_write -> SMB2_write) is taken
# when O_WRONLY partial writes hit a non-uptodate page at a mid-file offset.
#
# Strategy: 10 concurrent O_WRONLY mid-file writers + periodic ss -K to
# kill the socket mid-encrypt. Typically reproduces in 1-3 cycles.
#
# Usage: sudo ./test_smb2_write_encrypt_corruption.sh /mnt/smbshare/testdir
#


set -u

TESTDIR="${1:-.}"
NFILES=10
ERRORS=0

cleanup() {
    kill -9 $PIDS 2>/dev/null
    wait 2>/dev/null
}
trap cleanup EXIT

echo "=== SMB2_write() encryption page cache corruption reproducer ==="
echo "Dir:    $TESTDIR"
echo "Kernel: $(uname -r)"
echo "Start:  $(date)"
echo ""

mkdir -p "$TESTDIR"

# Create files filled with a known letter: f1='A', f2='B', ..., f10='J'
echo "Creating test files..."
for f in $(seq 1 $NFILES); do
    BYTE=$((0x40 + f))  # 0x41='A', 0x42='B', ..., 0x4a='J'
    python3 -c "open('$TESTDIR/f${f}.dat','wb').write(bytes([$BYTE])*65536)"
done
sync; sleep 1
echo "Done."

# Writer: O_WRONLY mid-file partial writes (triggers sync path)
# - O_WRONLY: write_begin() skips reading the page -> page stays not-uptodate
# - Mid-file: write_begin()'s EOF zero-fill shortcut does not apply
# - 200 bytes: partial page, page never marked uptodate
# -> cifs_write_end() takes sync path: kmap -> cifs_write -> SMB2_write
writer() {
    local fname=$1 byte=$2
    while true; do
        python3 -c "
import os, random
try:
    fd = os.open('$fname', os.O_WRONLY)
    off = random.randint(100, 60000)
    os.lseek(fd, off, os.SEEK_SET)
    os.write(fd, bytes([$byte]) * 200)
    os.close(fd)
except:
    pass
" 2>/dev/null
    done
}

echo "Starting $NFILES writers..."
PIDS=""
for f in $(seq 1 $NFILES); do
    BYTE=$((0x40 + f))
    writer "$TESTDIR/f${f}.dat" $BYTE &
    PIDS="$PIDS $!"
done

echo "Running disruption cycles..."
echo ""
for cycle in $(seq 1 15); do
    # Warm up: let writers run normally
    sleep 3

    # Cold cache: evict pages so next writes hit non-uptodate pages
    echo 3 > /proc/sys/vm/drop_caches 2>/dev/null

    # Let writes accumulate on cold cache (all go through sync path)
    sleep 2

    # Kill socket: if a write is between crypt_message() and TCP send,
    # iov[1] (page cache) is already encrypted in-place. The send fails,
    # replay_again re-sends ciphertext as plaintext to the server.
    # we can also replace ss -K with tcpkill or similar to target the socket more precisely, but ss -K is simpler and works well enough.
    ss -K dport = 445 > /dev/null 2>&1
    echo "  [cycle $cycle] socket killed at $(date +%T)"

    # Wait for reconnect, then re-read from server (not local cache)
    sleep 4
    echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
    sleep 1

    # Verify: every byte should match expected value
    CYCLE_ERRORS=0
    for f in $(seq 1 $NFILES); do
        BYTE=$((0x40 + f))
        python3 -c "
import sys
try:
    data = open('$TESTDIR/f${f}.dat','rb').read()
except OSError as e:
    print(f'  SKIP f${f}.dat: {e}')
    sys.exit(0)
for i,b in enumerate(data):
    if b != $BYTE:
        print(f'  CORRUPTION f${f}.dat offset=0x{i:x} exp=0x{$BYTE:02x}({chr($BYTE)}) got=0x{b:02x}')
        sys.exit(1)
"
        if [ $? -ne 0 ]; then
            CYCLE_ERRORS=$((CYCLE_ERRORS + 1))
            ERRORS=$((ERRORS + 1))
        fi
    done
    echo "  [cycle $cycle] verified ($CYCLE_ERRORS errors)"
done

echo ""
echo "Stopping writers..."
kill $PIDS 2>/dev/null
wait 2>/dev/null

echo ""
echo "=== Results ==="
echo "Errors: $ERRORS"
echo "End:    $(date)"

if [ $ERRORS -gt 0 ]; then
    echo "FAIL: In-place encryption corrupted page cache data."
    echo "Fix: pass data via rq_iter instead of inline in rq_iov."
else
    echo "PASS: No corruption detected. Try running again."
fi

rm -f "$TESTDIR"/f*.dat

^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes
  2026-03-03 18:03   ` Bharath SM
@ 2026-03-04  8:09     ` Shyam Prasad N
  2026-03-09 10:39       ` Bharath SM
  0 siblings, 1 reply; 7+ messages in thread
From: Shyam Prasad N @ 2026-03-04  8:09 UTC (permalink / raw)
  To: Bharath SM
  Cc: David Howells, Steve French, CIFS, Paulo Alcantara,
	Enzo Matsumiya, Henrique Carvalho, Shyam Prasad N, Shyam Prasad,
	Bharath S M, Greg KH

On Tue, Mar 3, 2026 at 11:34 PM Bharath SM <bharathsm.hsk@gmail.com> wrote:
>
> On Fri, Feb 27, 2026 at 2:25 AM Shyam Prasad N <nspmangalore@gmail.com> wrote:
> >
> > On Wed, Feb 18, 2026 at 11:30 PM Bharath SM <bharathsm.hsk@gmail.com> wrote:
> > >
> > > We are noticing a data corruption issue in kernels based on stable
> > > 6.6.y. Especially, when a synchronous writes retried after a
> > > connection reset.
> > >
> > > Based on investigation so far, it looks like we are having issue in
> > > the following code path:
> > > When SMB3 encryption is enabled, partial-page buffered writes hit the
> > > synchronous write path in cifs_write_end() when the folio is not
> > > uptodate (!folio_test_uptodate(folio)), it calls cifs_write() directly
> > > with the kmap()'d page cache buffer, bypassing the async writeback
> > > path.
> > > cifs_write() calls SMB2_write(), which places the write payload in
> > > rq_iov[1], pointing directly at the page cache buffer. When
> > > smb3_init_transform_rq() builds the encryption request, it shares
> > > rq_iov by pointer (new->rq_iov = old->rq_iov), and crypt_message()
> > > encrypts in-place via aead_request_set_crypt(req, sg, sg, ...). This
> > > destroys the original page cache data. If the write gets -EAGAIN after
> > > encryption (e.g., connection reset), cifs_write() re-sends the
> > > now-ciphertext buffer as if it were plaintext, resulting in
> > > double-encrypted garbage on the server. The server accepts it and
> > > returns success.
> > > Please let me know if you have seen this issue in the past, your
> > > comments on the analysis and probable fixes.
> > >
> > > Repro steps: Attached repro.zip with repro scripts and instructions:
> > > 1) Mount with SMB3 encryption enabled
> > > 2) Perform buffered writes in a loop (e.g., echo "known_pattern" >> file)
> > > 3) Kill the TCP connection during writes (ss -K dport 445) to force
> > > retryable errors
> > > 4) Read the file back and compare against expected content
> > >
> > Looking at the callers, it looks like simple_fallocate_range*
> > functions that make use of sync writes are also susceptible to this
> > issue.
> >
> > > Issue can occur when all below conditions met in buffered writes:
> > > 1) SMB2 encryption is active
> > > 2) Sync write path: Writes reached SMB2_write via cifs_write
> > > 3) Retryable network error for writes: When EAGAIN or ECONABORTED
> > > returned from  cifs_send_recv().
> > >
> > > Here is a the sequence of operations leading to issue:
> > > write(2) syscall
> > >  └─ cifs_write_end()                          [file.c]
> > >      └─ cifs_write()                          [file.c]
> > >          │  iov[1].iov_base = write_data      ← page cache pointer enters iov[1]
> > >          │
> > >          └─ server->ops->sync_write()         [file.c]
> > >              └─ smb2_sync_write()             [smb2ops.c:]
> > >                  └─ SMB2_write()              [smb2pdu.c:]
> > >                      │  rqst.rq_iov = iov     ← rqst points to iov[]
> > > (with page cache in [1])
> > >                      │  rqst.rq_nvec = n_vec+1  ← BUG: payload in
> > > rq_iov, not rq_iter
> > >                      │
> > >                      └─ cifs_send_recv()      [transport.c:1305]
> > >                          └─ compound_send_recv()  [transport.c:1071]
> > >                              │
> > >                              └─ smb_send_rqst()   [transport.c:427]
> > >                                  │  if (flags & CIFS_TRANSFORM_REQ)  ←
> > > YES for SMB3 encryption
> > >                                  │
> > >                                  └─ server->ops->init_transform_rq()
> > > [smb2ops.c:~4398]
> > >                                  │   = smb3_init_transform_rq()
> > >                                  │     new->rq_iov = old->rq_iov     ←
> > > SHARES pointer (not copied!)
> > >                                  │     size =
> > > iov_iter_count(old->rq_iter) = 0  ← empty, no copy
> > >                                  │
> > >                                  └─ __smb_send_rqst()  [transport.c:272]
> > >                                      │  → crypt_message()  [smb2ops.c:~4280]
> > >                                      │     → smb2_get_aead_req()
> > > [smb2ops.c:~4196]
> > >                                      │        sg = scatterwalk from rq_iov[0..n]
> > >                                      │
> > > aead_request_set_crypt(req, sg, sg, ...)
> > >                                      │
> > >    ^^^  ^^^
> > >                                      │
> > > src=dst → IN-PLACE encrypt
> > >                                      │
> > >                                      │   iov[1] (= page cache) is now
> > > AES ciphertext
> > >                                      │
> > >                                      └─ kernel_sendmsg() / sock_sendmsg()
> > >                                          → sends encrypted data on wire
> > >
> > >          ← rc = -EAGAIN (connection dropped)
> > >
> > >          is_replayable_error(rc) == true or cifs_write while loop detects EAGAIN
> > >          goto replay_again                    ← loops back with corrupted iov[1]
> > >              └─ SMB2_write() re-sends...
> > >                  └─ smb3_init_transform_rq()  ← encrypts ciphertext AGAIN
> > >                      └─ crypt_message()       ← double-encrypted garbage
> > >                          └─ server writes it to disk  ←  CORRUPTION
> > >
> > >
> > >
> > > Modifying SMB2_write function by adding payload to rq_iter seems to
> > > help here. Need to further test.
> > > With below fix, when rq_iter size > 0 code in smb3_init_transform_rq
> > > allocates fresh pages, copies the data via copy_page_from_iter(), and
> > > encrypts the copy instead of the original.
> > > Please let me know your comments.
> > >
> > >
> > >  rqst.rq_iov = iov;
> > > -rqst.rq_nvec = n_vec + 1;
> > > +rqst.rq_nvec = 1;
> > > +iov_iter_kvec(&rqst.rq_iter, ITER_SOURCE, &iov[1], n_vec,
> > > +              io_parms->length);
> > > +rqst.rq_iter_size = io_parms->length;
> >
> > Another option is to initialize iov_iter_xarray with rqst.rq_buffer,
> > similar to what smb3_init_transform_rq does. But this should work too.
> > Changes look good to me. Please submit a formal patch.
>
> Thank you, Attached the patch please review. Also created the minimal
> repro script with network disconnects.
>
> Further investigation on this issue indicates that the issue is not
> specific to the 6.6 Kernel; instead,
> the issue can happen in kernels <6.10 including 6.1, 6.6 and 5.15 and beyond.
>
> On older kernels ~5.15 with deferred closes may reduce exposure
> because deferred handles might keep help pages
> in memory and which may help writes to avoid sync_write path.
> But commit 262b73ef442e (smb3 client: fix open hardlink on deferred
> close file error, backported till 6.1) appears to
> increase the likelihood of taking the sync_write path because we close
> deferred handles aggressively in some cases,
> which makes the existing encryption write corruption bug easier to
> trigger. The commit does not introduce the bug;
> It increases trigger frequency for writes taking sync_write path.
>
> I will send out this patch to the stable mailing list separately.

Looks good to me. My RB is already added

-- 
Regards,
Shyam

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes
  2026-03-04  8:09     ` Shyam Prasad N
@ 2026-03-09 10:39       ` Bharath SM
  0 siblings, 0 replies; 7+ messages in thread
From: Bharath SM @ 2026-03-09 10:39 UTC (permalink / raw)
  To: Shyam Prasad N
  Cc: David Howells, Steve French, CIFS, Paulo Alcantara,
	Enzo Matsumiya, Henrique Carvalho, Shyam Prasad N, Shyam Prasad,
	Bharath S M, Greg KH

Functions in mainline kernel, such as cifs_sfu_make_node() and
smb3_create_mf_symlink() use SMB2_write() to send data and can be
affected by an in-place encryption corruption bug on retry. Submitted
a slightly modified patch titled 'smb: client: fix in-place encryption
corruption in SMB2_write()' to mainline.
Please take a look.

On Wed, Mar 4, 2026 at 12:09 AM Shyam Prasad N <nspmangalore@gmail.com> wrote:
>
> On Tue, Mar 3, 2026 at 11:34 PM Bharath SM <bharathsm.hsk@gmail.com> wrote:
> >
> > On Fri, Feb 27, 2026 at 2:25 AM Shyam Prasad N <nspmangalore@gmail.com> wrote:
> > >
> > > On Wed, Feb 18, 2026 at 11:30 PM Bharath SM <bharathsm.hsk@gmail.com> wrote:
> > > >
> > > > We are noticing a data corruption issue in kernels based on stable
> > > > 6.6.y. Especially, when a synchronous writes retried after a
> > > > connection reset.
> > > >
> > > > Based on investigation so far, it looks like we are having issue in
> > > > the following code path:
> > > > When SMB3 encryption is enabled, partial-page buffered writes hit the
> > > > synchronous write path in cifs_write_end() when the folio is not
> > > > uptodate (!folio_test_uptodate(folio)), it calls cifs_write() directly
> > > > with the kmap()'d page cache buffer, bypassing the async writeback
> > > > path.
> > > > cifs_write() calls SMB2_write(), which places the write payload in
> > > > rq_iov[1], pointing directly at the page cache buffer. When
> > > > smb3_init_transform_rq() builds the encryption request, it shares
> > > > rq_iov by pointer (new->rq_iov = old->rq_iov), and crypt_message()
> > > > encrypts in-place via aead_request_set_crypt(req, sg, sg, ...). This
> > > > destroys the original page cache data. If the write gets -EAGAIN after
> > > > encryption (e.g., connection reset), cifs_write() re-sends the
> > > > now-ciphertext buffer as if it were plaintext, resulting in
> > > > double-encrypted garbage on the server. The server accepts it and
> > > > returns success.
> > > > Please let me know if you have seen this issue in the past, your
> > > > comments on the analysis and probable fixes.
> > > >
> > > > Repro steps: Attached repro.zip with repro scripts and instructions:
> > > > 1) Mount with SMB3 encryption enabled
> > > > 2) Perform buffered writes in a loop (e.g., echo "known_pattern" >> file)
> > > > 3) Kill the TCP connection during writes (ss -K dport 445) to force
> > > > retryable errors
> > > > 4) Read the file back and compare against expected content
> > > >
> > > Looking at the callers, it looks like simple_fallocate_range*
> > > functions that make use of sync writes are also susceptible to this
> > > issue.
> > >
> > > > Issue can occur when all below conditions met in buffered writes:
> > > > 1) SMB2 encryption is active
> > > > 2) Sync write path: Writes reached SMB2_write via cifs_write
> > > > 3) Retryable network error for writes: When EAGAIN or ECONABORTED
> > > > returned from  cifs_send_recv().
> > > >
> > > > Here is a the sequence of operations leading to issue:
> > > > write(2) syscall
> > > >  └─ cifs_write_end()                          [file.c]
> > > >      └─ cifs_write()                          [file.c]
> > > >          │  iov[1].iov_base = write_data      ← page cache pointer enters iov[1]
> > > >          │
> > > >          └─ server->ops->sync_write()         [file.c]
> > > >              └─ smb2_sync_write()             [smb2ops.c:]
> > > >                  └─ SMB2_write()              [smb2pdu.c:]
> > > >                      │  rqst.rq_iov = iov     ← rqst points to iov[]
> > > > (with page cache in [1])
> > > >                      │  rqst.rq_nvec = n_vec+1  ← BUG: payload in
> > > > rq_iov, not rq_iter
> > > >                      │
> > > >                      └─ cifs_send_recv()      [transport.c:1305]
> > > >                          └─ compound_send_recv()  [transport.c:1071]
> > > >                              │
> > > >                              └─ smb_send_rqst()   [transport.c:427]
> > > >                                  │  if (flags & CIFS_TRANSFORM_REQ)  ←
> > > > YES for SMB3 encryption
> > > >                                  │
> > > >                                  └─ server->ops->init_transform_rq()
> > > > [smb2ops.c:~4398]
> > > >                                  │   = smb3_init_transform_rq()
> > > >                                  │     new->rq_iov = old->rq_iov     ←
> > > > SHARES pointer (not copied!)
> > > >                                  │     size =
> > > > iov_iter_count(old->rq_iter) = 0  ← empty, no copy
> > > >                                  │
> > > >                                  └─ __smb_send_rqst()  [transport.c:272]
> > > >                                      │  → crypt_message()  [smb2ops.c:~4280]
> > > >                                      │     → smb2_get_aead_req()
> > > > [smb2ops.c:~4196]
> > > >                                      │        sg = scatterwalk from rq_iov[0..n]
> > > >                                      │
> > > > aead_request_set_crypt(req, sg, sg, ...)
> > > >                                      │
> > > >    ^^^  ^^^
> > > >                                      │
> > > > src=dst → IN-PLACE encrypt
> > > >                                      │
> > > >                                      │   iov[1] (= page cache) is now
> > > > AES ciphertext
> > > >                                      │
> > > >                                      └─ kernel_sendmsg() / sock_sendmsg()
> > > >                                          → sends encrypted data on wire
> > > >
> > > >          ← rc = -EAGAIN (connection dropped)
> > > >
> > > >          is_replayable_error(rc) == true or cifs_write while loop detects EAGAIN
> > > >          goto replay_again                    ← loops back with corrupted iov[1]
> > > >              └─ SMB2_write() re-sends...
> > > >                  └─ smb3_init_transform_rq()  ← encrypts ciphertext AGAIN
> > > >                      └─ crypt_message()       ← double-encrypted garbage
> > > >                          └─ server writes it to disk  ←  CORRUPTION
> > > >
> > > >
> > > >
> > > > Modifying SMB2_write function by adding payload to rq_iter seems to
> > > > help here. Need to further test.
> > > > With below fix, when rq_iter size > 0 code in smb3_init_transform_rq
> > > > allocates fresh pages, copies the data via copy_page_from_iter(), and
> > > > encrypts the copy instead of the original.
> > > > Please let me know your comments.
> > > >
> > > >
> > > >  rqst.rq_iov = iov;
> > > > -rqst.rq_nvec = n_vec + 1;
> > > > +rqst.rq_nvec = 1;
> > > > +iov_iter_kvec(&rqst.rq_iter, ITER_SOURCE, &iov[1], n_vec,
> > > > +              io_parms->length);
> > > > +rqst.rq_iter_size = io_parms->length;
> > >
> > > Another option is to initialize iov_iter_xarray with rqst.rq_buffer,
> > > similar to what smb3_init_transform_rq does. But this should work too.
> > > Changes look good to me. Please submit a formal patch.
> >
> > Thank you, Attached the patch please review. Also created the minimal
> > repro script with network disconnects.
> >
> > Further investigation on this issue indicates that the issue is not
> > specific to the 6.6 Kernel; instead,
> > the issue can happen in kernels <6.10 including 6.1, 6.6 and 5.15 and beyond.
> >
> > On older kernels ~5.15 with deferred closes may reduce exposure
> > because deferred handles might keep help pages
> > in memory and which may help writes to avoid sync_write path.
> > But commit 262b73ef442e (smb3 client: fix open hardlink on deferred
> > close file error, backported till 6.1) appears to
> > increase the likelihood of taking the sync_write path because we close
> > deferred handles aggressively in some cases,
> > which makes the existing encryption write corruption bug easier to
> > trigger. The commit does not introduce the bug;
> > It increases trigger frequency for writes taking sync_write path.
> >
> > I will send out this patch to the stable mailing list separately.
>
> Looks good to me. My RB is already added
>
> --
> Regards,
> Shyam

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-03-09 10:39 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-18 18:00 [BUG] [~6.6 Kernel] Corruption when retrying encrypted sync writes Bharath SM
2026-02-19 11:05 ` David Howells
2026-02-19 11:14   ` Bharath SM
2026-02-27 10:25 ` Shyam Prasad N
2026-03-03 18:03   ` Bharath SM
2026-03-04  8:09     ` Shyam Prasad N
2026-03-09 10:39       ` Bharath SM

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox