8

Editor FooEdit (let's call it) uses ReplaceFile() when saving to ensure that the save operation is effectively atomic, and that if anything goes wrong then the original file on disc is preserved. (The other important benefit of ReplaceFile() is continuity of file identity - creation date and other metadata.)

FooEdit also keeps open a handle to the file with a sharing mode of just FILE_SHARE_READ, so that other processes can open the file but can't write to it while it while FooEdit has it open for writing.

"Obviously", this handle has to be closed briefly while the ReplaceFile operation takes place, and this allows a race in which another process can potentially open the file with write access before FooEdit re-establishes it's FILE_SHARE_READ lock handle.

(If FooEdit doesn't close its FILE_SHARE_READ handle before calling ReplaceFile(), then ReplaceFile() fails with a sharing violation.)

I'd like to know what is the simplest way to resolve this race. The options seem to be either to find another way to lock the file that is compatible with ReplaceFile() (I don't see how this is possible) or to replicate all the behaviour of ReplaceFile(), but using an existing file handle to access the destination file rather than a path. I'm a bit stuck on how all of the operations of ReplaceFile() could be carried out atomically from user code (and reimplementing ReplaceFile() seems a bad idea anyway).

This must be a common problem, so probably there's an obvious solution that I've missed.

(This question seems related but has no answer: Transactionally write a file change on Windows.)


Here's a minimal verifiable example showing what I am trying to achieve (updated 13:18 30/9/2015 UTC). You must supply three file names as command line arguments, all on the same volume. The first must already exist.

I always get a sharing violation from ReplaceFile().

#include <Windows.h>
#include <stdio.h>
#include <assert.h>
int main(int argc, char *argv[])
{
  HANDLE lock;
  HANDLE temp;
  DWORD  bytes;

  if (argc != 4)
  {
    puts("First argument is the project file. Second argument is the temporary file.");
    puts("The third argument is the backup file.");
  }

  /* Open and lock the project file to make sure no one else can modify it */
  lock = CreateFile(argv[1], GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0);
  assert(lock != INVALID_HANDLE_VALUE);

  /* Save to the temporary file. */
  temp = CreateFile(argv[2], GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, CREATE_ALWAYS, 0, 0);
  assert(temp != INVALID_HANDLE_VALUE);
  WriteFile(temp, "test", 4, &bytes, NULL);
  /* Keep temp open so that another process can't modify the file. */

  if (!ReplaceFile(argv[1], argv[2], argv[3], 0, NULL, NULL))
  {
    if (GetLastError() == ERROR_SHARING_VIOLATION)
      puts("Sharing violation as I expected");
    else
      puts("Something went wrong");
  }
  else
    puts("ReplaceFile worked - not what I expected");

  /* If it worked the file referenced by temp would now be called argv[1]. */
  CloseHandle(lock);
  lock = temp;

  return EXIT_SUCCESS;
}

Thanks to Hans Passant, who provided some valuable clarifying thoughts in an answer now deleted. Here's what I discovered while following up his suggestions:

It seems ReplaceFile() allows lpReplacedFileName to be open FILE_SHARE_READ | FILE_SHARE_DELETE, but lpReplacementFileName can't be. (And this behaviour doesn't seem to depend on whether lpBackupFileName is supplied.) So it's perfectly possible to replace a file that another process has open even if that other process doesn't allow FILE_SHARE_WRITE, which was Hans' point.

But FooEdit is trying to ensure no other process can open the file with GENERIC_WRITE in the first place. To ensure in FooEdit that there's no race where another process can open the replacement file with GENERIC_WRITE, it seems that FooEdit has to keep hold continuously of a FILE_SHARE_READ | FILE_SHARE_DELETE handle to lpReplacementFileName, which then precludes use of ReplaceFile().

Community
  • 1
  • 1
Ian Goldby
  • 5,609
  • 1
  • 45
  • 81
  • A "traditional" safe save involves saving to a new file with a temporary name; once the write has succeeded, delete the old file and rename the new one. – Jonathan Potter Sep 30 '15 at 07:41
  • That's what ReplaceFile is for, but crucially ReplaceFile does the delete and rename as an atomic operation, and also preserves the original file's metadata. – Ian Goldby Sep 30 '15 at 07:42
  • Why do you need it to be atomic? The metadata (timestamps at least) can be preserved easily. – Jonathan Potter Sep 30 '15 at 07:56
  • 1
    @JonathanPotter Have a look at the documentation for ReplaceFile. It does quite a lot, and guarantees that new metadata added in future versions of the filing system are also preserved. Part of my question is that yes I could replicate ReplaceFile but this is almost certainly a very bad solution. – Ian Goldby Sep 30 '15 at 08:00
  • Do you control both processes that want to access the file, or are you trying to protect against an arbitrary process racing with your application? – theB Sep 30 '15 at 11:01
  • @theB The latter. It seems to be a virus checker (or perhaps an indexer). – Ian Goldby Sep 30 '15 at 11:04
  • 3
    ReplaceFile isn't atomic at the file system level anyway, if that's what you're relying on. – Jonathan Potter Sep 30 '15 at 20:05
  • @IanGoldby I [tried to follow](http://stackoverflow.com/questions/43578567/why-does-replacefile-fail-with-error-sharing-violation) the approach in the [deleted answer](http://stackoverflow.com/a/32862684/336527) below, but it didn't work for me. Did you have to do anything special to make it work? – max Apr 24 '17 at 01:55
  • @max Hans deleted his answer because, despite providing some useful information, it answered a different question to the one I asked (see my question). So I didn't "make it work" - it wasn't what I was trying to do. – Ian Goldby Apr 24 '17 at 09:41
  • Oh but I mean did you manage to at least avoid file sharing error when renaming files, when though they were open by other processes without FILE_SHARE_DELETE? – max Apr 24 '17 at 09:43
  • @max I'm still not sure what you are asking, but as David says in his answer, there is no robust solution to the race. My 'hacky' workaround is to retry `ReplaceFile()` up to 10 times, with a 50 msec delay, if `ReplaceFile()` fails with `ERROR_SHARING_VIOLATION` or `ERROR_UNABLE_TO_REMOVE_REPLACED`. If that still fails, I display a dialog with the name of the process that is preventing `ReplaceFile()` (see `RmGetList()` on MSDN) and offering the user a final Retry or Cancel. – Ian Goldby Apr 24 '17 at 09:53
  • @IanGoldby thank you! – max Apr 24 '17 at 09:57
  • "... that there's no race where another process can open the replacement file with GENERIC_WRITE ..." - what I find rather strange here is that the *replacemenT* file normally would be a *temporary* file created and semantically owned by FooEdit. It would have an accordingly obvious and temporary file name. What is the Use Case of protecting this file from a writer? No sane program should touch it anyway. Is this supposed to guard against Murphy or Machiavelli? – Martin Ba Feb 10 '23 at 14:01
  • "FooEdit also keeps open a handle to the file with a sharing mode of just FILE_SHARE_READ, so that other processes can open the file but can't write to it while it while FooEdit has it open for writing." - uncommon use case as well. If the data fits in memory, you normally don't keep you document-like data locked. (Except for stupid tools like Acrobat Reader ... :-) – Martin Ba Feb 10 '23 at 14:03

2 Answers2

2

Actually I think there might be a solution that doesn't involve transactions (although transactions are still available as far as I know). I haven't tried it myself, but I think on NTFS it should be possible to create a new file stream (use a long random name to ensure there are no collisions), write your data, and then rename that stream to the stream you actually wanted to write to.

FILE_RENAME_INFORMATION suggests this should be possible, since it talks about renaming data streams.

However, this would only work on NTFS. For other file systems I don't think you have a choice.

user541686
  • 205,094
  • 128
  • 528
  • 886
  • I see where you are coming from here. It's worth noting that this is a kernel API - there seems to be no way to rename an alternate data stream without invoking the kernel API directly. – Ian Goldby Jul 23 '18 at 08:42
  • @IanGoldby: No, you can definitely call it from user mode; it just happens to be described in the kernel API documentation in this link. Use [`NtSetInformationFile`](https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntsetinformationfile) from `ntdll.dll`. In fact I just confirmed that it works via [`FileTest`](http://www.zezula.net/en/fstools/filetest.html). You need to set `RootDirectory` to `NULL` and set the `FileName` to `:YourNewStreamName`, omitting the file name part. However, I would use transactions when possible since they are more robust by design. – user541686 Jul 23 '18 at 08:53
  • I'm aware you can call *NT* (I should have said, not kernel, sorry) APIs from user mode, and I've done this in the past myself. But it's usually seen as a bit of a last resort and not exactly encouraged by Microsoft. Anyway, it's a very interesting idea. I'd never heard of FileTest; thanks for the link. – Ian Goldby Jul 23 '18 at 13:45
  • 2
    @IanGoldby: I mean, this *is* a last resort. Doesn't really matter if it's encouraged by MS or not, the scaring is artificial. Those APIs are solid as a rock. Microsoft has gradually documented more of them as time has passed. I recall lots of well known programs (such as Chrome) use them, and I also don't believe anyone has ever been bitten by using them. You're only fooling yourself if you think staying away from them is going to do anyone any good. – user541686 Jul 23 '18 at 15:43
0

I'd like to know what is the simplest way to resolve this race.

There is no simple way to resolve this race. It's an inherent part of the file system which is not transactional. MS introduced a transactional file API with Vista but now strongly advise developers not to use it as it may be removed in a future release.

I have had some experience with ReplaceFile but I think it caused more trouble than it was worth. My recollection was that whilst meta data was preserved, a new file was created. A consequence of this was very annoying behaviour for files saved on the desktop. Because such files have their position preserved, creating a new file resulted in the default position being used. So you'd save a file, you'd drag it to the place on the desktop where you wanted to keep it, and then when you saved the file again, it moved back to the default position.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Did you abandon the goal of atomic saves *and* preserving file identity, or did you find another way to do it? – Ian Goldby Sep 30 '15 at 09:26
  • Yes, I just gave up on that. I decided that it wasn't an overriding goal. The risk of losing a file was so low, the consequences of losing a file not that severe usually, that the complexity and the inconvenience just made the whole damn thing look like a giant loss for me. – David Heffernan Sep 30 '15 at 09:30
  • @Mehrdad You are right. It's still there but they advise not to use it. – David Heffernan Jul 21 '18 at 12:15