[PATCH 00/35] cifs: Fix SMB rmdir() and unlink() against Windows SMB servers

Pali Rohár posted 35 patches 1 month ago
fs/smb/client/cifsglob.h     |  11 +-
fs/smb/client/cifspdu.h      |   5 +
fs/smb/client/cifsproto.h    |   6 +-
fs/smb/client/cifssmb.c      | 228 +++++++++++++----------
fs/smb/client/inode.c        | 346 ++++++++++++++++-------------------
fs/smb/client/netmisc.c      |   2 +-
fs/smb/client/reparse.c      |  75 +++-----
fs/smb/client/smb1ops.c      |  84 +++++++--
fs/smb/client/smb2glob.h     |   1 +
fs/smb/client/smb2inode.c    | 264 +++++++++++++++++++++++---
fs/smb/client/smb2maperror.c |   2 +-
fs/smb/client/smb2ops.c      |  24 +++
fs/smb/client/smb2pdu.c      |  51 +++++-
fs/smb/client/smb2proto.h    |   6 +
14 files changed, 704 insertions(+), 401 deletions(-)
[PATCH 00/35] cifs: Fix SMB rmdir() and unlink() against Windows SMB servers
Posted by Pali Rohár 1 month ago
This patch series improves Linux rmdir() and unlink() syscalls called on
SMB mounts exported from Windows SMB servers which do not implement
POSIX semantics of the file and directory removal.

This patch series should have no impact and no function change when
communicating with the POSIX SMB servers, as they should implement
proper rmdir and unlink logic.

When issuing remove path command against non-POSIX / Windows SMB server,
it let the directory entry which is being removed in the directory until
all users / clients close all handles / references to that path.

POSIX requires from rmdir() and unlink() syscalls that after successful
call, the requested path / directory entry is released and allows to
create a new file or directory with that name. This is currently not
working against non-POSIX / Windows SMB servers.

To workaround this problem fix and improve existing cifs silly rename
code and extend it also to SMB2 and SMB3 dialects when communicating
with Windows SMB servers. Silly rename is applied only when it is
necessary (when some other client has opened file or directory).
If no other client has the file / dir open then silly rename is not
used.

With this patch series, after successful rmdir() or unlink() call from
Linux userspace application, the path is released, it is not visible in
the followup readdir() call, also stat() properly returns ENOENT and it
is possible to call creat() or mkdir() on the same path to create new
node. Silly rename is used to ensure that path is released.

Before this change, the original path was visible in the readdir() call
and create() or mkdir() was failing with EEXIST, even rmdir() or
unlink() on that path returned success.


Test cases when "path" is opened on Windows server by other client, so
path cannot be removed.

1.
  unlink("path");
  mkdir("path");

  before: unlink returns success but mkdir returns EEXIST.
  after: unlink returns success, mkdir creates new directory and the
  orignal one is silly renamed.

2.
  unlink("path")
  open("path") without O_CREAT
  mkdir("path")

  before: unlink returns success, open returns ENOENT and mkdir returns
  EEXIST.
  after: unlink returns success, open returns ENOENT, mkdir creates
  new directory and the original path is silly renamed.

3.
  unlink("path")
  stat("path")
  mkdir("path")

  before: unlink returns success, stat returns ENOENT and mkdir returns
  EEXIST.
  after: unlink returns success, stat returns ENOENT, mkdir creates new
  directory and the original path is silly renamed.

4.
  unlink("path")
  stat("path")
  open("path") with O_CREAT | O_EXCL

  before: unlink returns success, stat returns ENOENT and open returns
  EEXIST.
  after: unlink returns success, stat returns ENOENT, open creates new
  file and the original path is silly renamed.

5.
  unlink("path")
  readdir("parent_of_path")

  before: unlink returns success but readdir returns entry corresponding
  to path.
  after: unlink returns success, original path is silly renamed and
  readdir returns the entry under new silly name.


Test case when "tmp_path" on the Windows server was created (or opened)
as a exclusive temporary file with the DELETE_PENDING flag set:

Before:

  stat("tmp_path") - returns ENOENT
  mkdir("tmp_path") - returns EEXIST
  open("tmp_path") without O_CREAT - returns ENOENT
  open("tmp_path") with O_CREAT | O_EXCL - return EEXIST
  unlink("tmp_path") - returns ENOENT
  readdir(parent_of_tmp_path) - returns the "tmp_path" entry

After:

  stat("tmp_path") - success
  mkdir("tmp_path") - returns EEXIST
  open("tmp_path") without O_CREAT - returns EBUSY
  open("tmp_path") with O_CREAT | O_EXCL - return EEXIST
  unlink("tmp_path") - returns EBUSY
  readdir(parent_of_tmp_path) - returns the "tmp_path" entry

The difference is in stat(), open() and unlink() syscalls. Now Linux
applications can stat such files, so file attributes are now visible in
"ls -l -a" output, but trying to open / modify / delete them fails with
EBUSY instead of ENOENT.


Pali Rohár (35):
  cifs: Fix and improve cifs_is_path_accessible() function
  cifs: Allow fallback code in smb_set_file_info() also for directories
  cifs: Add fallback code path for cifs_mkdir_setinfo()
  cifs: Remove code for querying FILE_INFO_STANDARD via
    CIFSSMBQPathInfo()
  cifs: Remove CIFSSMBSetPathInfoFB() fallback function
  cifs: Remove cifs_backup_query_path_info() and replace it by
    cifs_query_path_info()
  cifs: Change translation of STATUS_DELETE_PENDING to -EBUSY
  cifs: Improve SMB2+ stat() to work also for paths in DELETE_PENDING
    state
  cifs: Improve SMB1 stat() to work also for paths in DELETE_PENDING
    state
  cifs: Improve detect_directory_symlink_target() function
  cifs: Fix random name construction for cifs_rename_pending_delete()
  cifs: Fix DELETE comments in cifs_rename_pending_delete()
  cifs: Avoid dynamic memory allocation of FILE_BASIC_INFO buffer in
    cifs_rename_pending_delete()
  cifs: Extend CIFSSMBRenameOpenFile() function for overwrite parameter
  cifs: Do not try to overwrite existing sillyname in
    cifs_rename_pending_delete()
  cifs: Add comments for DeletePending assignments in open functions
  cifs: Use NT_STATUS_DELETE_PENDING for filling fi.DeletePending in
    cifs_query_path_info()
  cifs: Do not set NumberOfLinks to 1 from open or query calls
  cifs: Fix cifs_rename_pending_delete() for files with more hardlinks
  cifs: Fix permission logic in cifs_rename_pending_delete()
  cifs: Propagate error code from CIFSSMBSetFileInfo() to
    cifs_rename_pending_delete()
  cifs: Improve cifs_rename_pending_delete() to work without the
    PASSTHRU support
  cifs: Fix SMBLegacyOpen() function
  cifs: Add a new callback set_file_disp() for setting file disposition
    (delete pending state)
  cifs: Add a new callback rename_opened_file() for renaming an opened
    file
  cifs: Add SMB2+ support into cifs_rename_pending_delete() function.
  cifs: Move SMB1 usage of CIFSPOSIXDelFile() from inode.c to cifssmb.c
  cifs: Fix smb2_unlink() to fail on directory
  cifs: Fix smb2_rmdir() on reparse point
  cifs: Simplify SMB2_OP_DELETE API usage
  cifs: Deduplicate smb2_unlink() and smb2_rmdir() into one common
    function
  cifs: Use cifs_rename_pending_delete() fallback also for rmdir()
  cifs: Add a new open flag CREATE_OPTION_EXCLUSIVE to open with deny
    all shared reservation
  cifs: Use CREATE_OPTION_EXCLUSIVE when opening file/dir for SMB2+
    non-POSIX unlink/rmdir
  cifs: Use CREATE_OPTION_EXCLUSIVE when doing SMB1 rmdir on NT server

 fs/smb/client/cifsglob.h     |  11 +-
 fs/smb/client/cifspdu.h      |   5 +
 fs/smb/client/cifsproto.h    |   6 +-
 fs/smb/client/cifssmb.c      | 228 +++++++++++++----------
 fs/smb/client/inode.c        | 346 ++++++++++++++++-------------------
 fs/smb/client/netmisc.c      |   2 +-
 fs/smb/client/reparse.c      |  75 +++-----
 fs/smb/client/smb1ops.c      |  84 +++++++--
 fs/smb/client/smb2glob.h     |   1 +
 fs/smb/client/smb2inode.c    | 264 +++++++++++++++++++++++---
 fs/smb/client/smb2maperror.c |   2 +-
 fs/smb/client/smb2ops.c      |  24 +++
 fs/smb/client/smb2pdu.c      |  51 +++++-
 fs/smb/client/smb2proto.h    |   6 +
 14 files changed, 704 insertions(+), 401 deletions(-)

-- 
2.20.1

Re: [PATCH 00/35] cifs: Fix SMB rmdir() and unlink() against Windows SMB servers
Posted by Stefan Metzmacher 1 month ago
Hi Pali,

> This patch series improves Linux rmdir() and unlink() syscalls called on
> SMB mounts exported from Windows SMB servers which do not implement
> POSIX semantics of the file and directory removal.
> 
> This patch series should have no impact and no function change when
> communicating with the POSIX SMB servers, as they should implement
> proper rmdir and unlink logic.

Please note that even servers implementing posix/unix extensions,
may also have windows clients connected operating on the same files/directories.
And in that case even posix clients will see the windows behaviour
of DELETE_PENDING for set disposition or on rename
NT_STATUS_ACCESS_DENIED or NT_STATUS_DIRECTORY_NOT_EMPTY.

> When issuing remove path command against non-POSIX / Windows SMB server,
> it let the directory entry which is being removed in the directory until
> all users / clients close all handles / references to that path.
> 
> POSIX requires from rmdir() and unlink() syscalls that after successful
> call, the requested path / directory entry is released and allows to
> create a new file or directory with that name. This is currently not
> working against non-POSIX / Windows SMB servers.
> 
> To workaround this problem fix and improve existing cifs silly rename
> code and extend it also to SMB2 and SMB3 dialects when communicating
> with Windows SMB servers. Silly rename is applied only when it is
> necessary (when some other client has opened file or directory).
> If no other client has the file / dir open then silly rename is not
> used.

If I 'git grep -i silly fs/smb/client' there's no hit, can you
please explain what code do you mean with silly rename?

Thanks!
metze
Re: [PATCH 00/35] cifs: Fix SMB rmdir() and unlink() against Windows SMB servers
Posted by Pali Rohár 1 month ago
Hello!

On Monday 01 September 2025 09:55:45 Stefan Metzmacher wrote:
> Hi Pali,
> 
> > This patch series improves Linux rmdir() and unlink() syscalls called on
> > SMB mounts exported from Windows SMB servers which do not implement
> > POSIX semantics of the file and directory removal.
> > 
> > This patch series should have no impact and no function change when
> > communicating with the POSIX SMB servers, as they should implement
> > proper rmdir and unlink logic.
> 
> Please note that even servers implementing posix/unix extensions,
> may also have windows clients connected operating on the same files/directories.
> And in that case even posix clients will see the windows behaviour
> of DELETE_PENDING for set disposition or on rename
> NT_STATUS_ACCESS_DENIED or NT_STATUS_DIRECTORY_NOT_EMPTY.

Ok. So does it mean that the issue described here applies also for POSIX
SMB server?

If yes, then I would propose to first fix the problem with
Windows/non-POSIX SMB server and then with others. So it is not too big.

> > When issuing remove path command against non-POSIX / Windows SMB server,
> > it let the directory entry which is being removed in the directory until
> > all users / clients close all handles / references to that path.
> > 
> > POSIX requires from rmdir() and unlink() syscalls that after successful
> > call, the requested path / directory entry is released and allows to
> > create a new file or directory with that name. This is currently not
> > working against non-POSIX / Windows SMB servers.
> > 
> > To workaround this problem fix and improve existing cifs silly rename
> > code and extend it also to SMB2 and SMB3 dialects when communicating
> > with Windows SMB servers. Silly rename is applied only when it is
> > necessary (when some other client has opened file or directory).
> > If no other client has the file / dir open then silly rename is not
> > used.
> 
> If I 'git grep -i silly fs/smb/client' there's no hit, can you
> please explain what code do you mean with silly rename?

Currently (without this patch series) it is CIFSSMBRenameOpenFile()
function when called with NULL as 3rd argument.

Cleanup is done in PATCH 11/35, where are more details.

Originally the "Silly rename" is the term used by NFS client, when it
does rename instead of unlink when the path is in use.
I reused this term.


So for SMB this "silly rename" means:
- open path with DELETE access and get its handle
- rename path (via opened handle) to some unique (auto generated) name
- set delete pending state on the path (via opened handle)
- close handle

(plus some stuff around to remove READ_ONLY attr which may disallow to
open path with DELETE ACCESS)

So above silly rename means that the original path is not occupied
anymore (thanks to rename) and the original file / dir is removed after
all clients / users release handles (thanks to set delete pending).

It is clear now clear? Or do you need to explain some other steps?
Sometimes some parts are too obvious for me and I unintentionally omit
description for something which is important. And seems that this is
such case. So it is my mistake, I should have explain it better.
Re: [PATCH 00/35] cifs: Fix SMB rmdir() and unlink() against Windows SMB servers
Posted by Stefan Metzmacher 1 month ago
Hi Pali,

>>> This patch series improves Linux rmdir() and unlink() syscalls called on
>>> SMB mounts exported from Windows SMB servers which do not implement
>>> POSIX semantics of the file and directory removal.
>>>
>>> This patch series should have no impact and no function change when
>>> communicating with the POSIX SMB servers, as they should implement
>>> proper rmdir and unlink logic.
>>
>> Please note that even servers implementing posix/unix extensions,
>> may also have windows clients connected operating on the same files/directories.
>> And in that case even posix clients will see the windows behaviour
>> of DELETE_PENDING for set disposition or on rename
>> NT_STATUS_ACCESS_DENIED or NT_STATUS_DIRECTORY_NOT_EMPTY.
> 
> Ok. So does it mean that the issue described here applies also for POSIX
> SMB server?

I guess so.

> If yes, then I would propose to first fix the problem with
> Windows/non-POSIX SMB server and then with others. So it is not too big.

That's up to Steve. But isn't it just a matter of removing the
if statement that checks for posix?

>>> When issuing remove path command against non-POSIX / Windows SMB server,
>>> it let the directory entry which is being removed in the directory until
>>> all users / clients close all handles / references to that path.
>>>
>>> POSIX requires from rmdir() and unlink() syscalls that after successful
>>> call, the requested path / directory entry is released and allows to
>>> create a new file or directory with that name. This is currently not
>>> working against non-POSIX / Windows SMB servers.
>>>
>>> To workaround this problem fix and improve existing cifs silly rename
>>> code and extend it also to SMB2 and SMB3 dialects when communicating
>>> with Windows SMB servers. Silly rename is applied only when it is
>>> necessary (when some other client has opened file or directory).
>>> If no other client has the file / dir open then silly rename is not
>>> used.
>>
>> If I 'git grep -i silly fs/smb/client' there's no hit, can you
>> please explain what code do you mean with silly rename?
> 
> Currently (without this patch series) it is CIFSSMBRenameOpenFile()
> function when called with NULL as 3rd argument.
> 
> Cleanup is done in PATCH 11/35, where are more details.
> 
> Originally the "Silly rename" is the term used by NFS client, when it
> does rename instead of unlink when the path is in use.
> I reused this term.
> 
> 
> So for SMB this "silly rename" means:
> - open path with DELETE access and get its handle
> - rename path (via opened handle) to some unique (auto generated) name
> - set delete pending state on the path (via opened handle)
> - close handle
> 
> (plus some stuff around to remove READ_ONLY attr which may disallow to
> open path with DELETE ACCESS)
> 
> So above silly rename means that the original path is not occupied
> anymore (thanks to rename) and the original file / dir is removed after
> all clients / users release handles (thanks to set delete pending).
> 
> It is clear now clear? Or do you need to explain some other steps?
> Sometimes some parts are too obvious for me and I unintentionally omit
> description for something which is important. And seems that this is
> such case. So it is my mistake, I should have explain it better.

I think I understand what it tries to do, thanks for explaining.

I was just wondering why the rename on a busy handle would work
while delete won't work. I'd guess the chances are high that both fail.

Do you have network captures showing the old and new behavior
that's often easier to understand than looking at patches alone.

metze
Re: [PATCH 00/35] cifs: Fix SMB rmdir() and unlink() against Windows SMB servers
Posted by Pali Rohár 1 month ago
Hello!

On Tuesday 02 September 2025 17:17:14 Stefan Metzmacher wrote:
> Hi Pali,
> 
> > > > This patch series improves Linux rmdir() and unlink() syscalls called on
> > > > SMB mounts exported from Windows SMB servers which do not implement
> > > > POSIX semantics of the file and directory removal.
> > > > 
> > > > This patch series should have no impact and no function change when
> > > > communicating with the POSIX SMB servers, as they should implement
> > > > proper rmdir and unlink logic.
> > > 
> > > Please note that even servers implementing posix/unix extensions,
> > > may also have windows clients connected operating on the same files/directories.
> > > And in that case even posix clients will see the windows behaviour
> > > of DELETE_PENDING for set disposition or on rename
> > > NT_STATUS_ACCESS_DENIED or NT_STATUS_DIRECTORY_NOT_EMPTY.
> > 
> > Ok. So does it mean that the issue described here applies also for POSIX
> > SMB server?
> 
> I guess so.
> 
> > If yes, then I would propose to first fix the problem with
> > Windows/non-POSIX SMB server and then with others. So it is not too big.
> 
> That's up to Steve. But isn't it just a matter of removing the
> if statement that checks for posix?

I can modify that if statement, no problem. I just did not wanted to
touch POSIX code path as my focus is on the Windows code path.
I do not know all those case which POSIX paths against POSIX server may
trigger, so it was safer for me to let POSIX as is.

> > > > When issuing remove path command against non-POSIX / Windows SMB server,
> > > > it let the directory entry which is being removed in the directory until
> > > > all users / clients close all handles / references to that path.
> > > > 
> > > > POSIX requires from rmdir() and unlink() syscalls that after successful
> > > > call, the requested path / directory entry is released and allows to
> > > > create a new file or directory with that name. This is currently not
> > > > working against non-POSIX / Windows SMB servers.
> > > > 
> > > > To workaround this problem fix and improve existing cifs silly rename
> > > > code and extend it also to SMB2 and SMB3 dialects when communicating
> > > > with Windows SMB servers. Silly rename is applied only when it is
> > > > necessary (when some other client has opened file or directory).
> > > > If no other client has the file / dir open then silly rename is not
> > > > used.
> > > 
> > > If I 'git grep -i silly fs/smb/client' there's no hit, can you
> > > please explain what code do you mean with silly rename?
> > 
> > Currently (without this patch series) it is CIFSSMBRenameOpenFile()
> > function when called with NULL as 3rd argument.
> > 
> > Cleanup is done in PATCH 11/35, where are more details.
> > 
> > Originally the "Silly rename" is the term used by NFS client, when it
> > does rename instead of unlink when the path is in use.
> > I reused this term.
> > 
> > 
> > So for SMB this "silly rename" means:
> > - open path with DELETE access and get its handle
> > - rename path (via opened handle) to some unique (auto generated) name
> > - set delete pending state on the path (via opened handle)
> > - close handle
> > 
> > (plus some stuff around to remove READ_ONLY attr which may disallow to
> > open path with DELETE ACCESS)
> > 
> > So above silly rename means that the original path is not occupied
> > anymore (thanks to rename) and the original file / dir is removed after
> > all clients / users release handles (thanks to set delete pending).
> > 
> > It is clear now clear? Or do you need to explain some other steps?
> > Sometimes some parts are too obvious for me and I unintentionally omit
> > description for something which is important. And seems that this is
> > such case. So it is my mistake, I should have explain it better.
> 
> I think I understand what it tries to do, thanks for explaining.
> 
> I was just wondering why the rename on a busy handle would work
> while delete won't work. I'd guess the chances are high that both fail.

Both "set delete pending" and "rename" operations are working (if open
pass). Just "set delete pending" does not unlink file/dir immediately
but rather wait until path is not busy anymore. "rename" on the other
hand is executed immediately.

So we can rename the in-use/busy file on Windows server, but we cannot
remove it immediately. We can only "schedule" its removal on the Windows
server. So combination of "rename" + "schedule removal" is what are
patches doing.

In case open fails (e.g. due to conflicting DENY shared reservations or
because path is already in delete pending state) then obviously it is
not possible to continue with either rename or set delete pending
operation, both then fails.

> Do you have network captures showing the old and new behavior
> that's often easier to understand than looking at patches alone.
> 
> metze

I do not have them right now, but I can run test scenario and capture
them, this is not problem. Test case is pretty straightforward.
Re: [PATCH 00/35] cifs: Fix SMB rmdir() and unlink() against Windows SMB servers
Posted by Pali Rohár 3 weeks, 5 days ago
On Tuesday 02 September 2025 18:30:53 Pali Rohár wrote:
> On Tuesday 02 September 2025 17:17:14 Stefan Metzmacher wrote:
> > Do you have network captures showing the old and new behavior
> > that's often easier to understand than looking at patches alone.
> > 
> > metze
> 
> I do not have them right now, but I can run test scenario and capture
> them, this is not problem. Test case is pretty straightforward.

In attachments are network traces of both the old and new behaviors.
In the old behavior is visible that after calling "rm object", the
object is still in the followup "ls" output and calling "mkdir object"
is failing, also "stat object" is failing.
In the new behavior is visible that "rm object" is using exclusive
removal, which fails, and then fallback to rename+deletepending which
success. After that in the followup "ls" output the object entry is not
there, there is only renamed ".smb<num>", and "mkdir object" pass and
creates new directory "object".