-
Notifications
You must be signed in to change notification settings - Fork 5.3k
FileStream: Restore Position to its original value when SetLength fails #43863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| SeekCore(_fileHandle, value, SeekOrigin.Begin); | ||
| if (!Interop.Kernel32.SetEndOfFile(_fileHandle)) | ||
| { | ||
| SeekCore(_fileHandle, origPos, SeekOrigin.Begin); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you looked at the SetLengthCore implementation on Unix in FileStream.Unix.cs? Seems like we have a similar issue there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Unix, SetLength doesn't throw if it exceeds the available space.
However, FileStream.Write does throw and does not restore Position when that happens, which causes that the reported error occurs in a similar fashion.
I think we can fix that case (restore Position when Write fails) in this PR as well.
runtime/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.Unix.cs
Lines 666 to 672 in 3fca083
| while (count > 0) | |
| { | |
| int bytesWritten = CheckFileCall(Interop.Sys.Write(_fileHandle, bufPtr + offset, count)); | |
| _filePosition += bytesWritten; | |
| offset += bytesWritten; | |
| count -= bytesWritten; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Unix, SetLength doesn't throw if it exceeds the available space.
It does potentially throw if ftruncate fails with an error:
| CheckFileCall(Interop.Sys.FTruncate(_fileHandle, value)); |
It's not 100% clear to me what conditions could trigger that to fail, but the man page says it can fail with EIO if "An I/O error occurred while reading from or writing to a file system."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to find a way to make ftruncate fail since it cannot fail with ENOSPC (as windows' SetEndOfFile similarly does) but I didn't succeed.
Obviously the common errors like EINVAL cannot occur because SetLength validates up-front the FileStream instance and the arguments.
On my effort I tried to get a EPERM error by setting ulimit -f 100 and then calling ftruncate(fileno(fd), (__off64_t)60000), but that didn't work.
I am convinced that there are ways in which the ftruncate function may fail when called from .NET, I just don't find the scenario to fix and test what you are originally asking for.
Any suggestion is appreciated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
David and I looked at this offline, we found a method called posix_fallocate which seems to do the same as ftruncate, but can return ENOSPC.
I noticed we do check if we have it in mono, but I did not find any usage instances. I found neither checks nor usage instances in libraries.
@jkotas do you have any additional insight regarding changing the ftruncate method for posix_fallocate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be worried about changes in performance characteristics. posix_fallocate may be much more expensive (actually write zeros to disk) for some file systems.
I believe that the file systems do not generally guarantee that the write operation will succeed when the file is "big enough". Unix file systems tend to do this more because of different design philosophy, but it is a problem even if you are exclusively on Windows. For example, if you have a compressed file on Windows, your write operation may fail on out-of-disk space when you try to write something less compressible into the file.
what conditions could trigger that to fail, but the man page says it can fail with EIO if "An I/O error occurred while reading from or writing to a file system.
E.g. it is a network file system and you unplug the network cable?
| if (!Interop.Kernel32.SetEndOfFile(_fileHandle)) | ||
| { | ||
| SeekCore(_fileHandle, origPos, SeekOrigin.Begin); | ||
| int errorCode = Marshal.GetLastWin32Error(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SeekCore may overwrite the last error. The last error from SetEndOfFile needs to be captured before calling SeekCore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, thanks.
Another approach could be to wrap the SetEndOfFile call in a try-finally and restore Position in the finally block as suggested in #8307 issue description, thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we discussed offline, you can save the errorCode from SetEndOfFile, then call SeekCore afterwards.
carlossanlop
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the tests - I recently had to fix a bug that only happened when copying a FileStream through the network and we run out of space in the remote location. You can add a manual test like I did here: https://github.com/dotnet/runtime/tree/master/src/libraries/System.IO.FileSystem/tests/ManualTests
| if (!Interop.Kernel32.SetEndOfFile(_fileHandle)) | ||
| { | ||
| SeekCore(_fileHandle, origPos, SeekOrigin.Begin); | ||
| int errorCode = Marshal.GetLastWin32Error(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we discussed offline, you can save the errorCode from SetEndOfFile, then call SeekCore afterwards.
|
Thinking about this more: I am not sure whether this change is a good idea overall. We won't be reliably undo the whole operation when something fails. Does the best effort provide enough interesting guarantees? |
@jkotas I think the ask on #8307 is for restoring using FileStream fs = File.Create($"E:/some_file.txt");
bool success = false;
int size = 1000;
while (!success)
{
try
{
fs.SetLength(size);
int contentSize = size - fs.Position;
byte[] content = GetContent(contentSize);
fs.Write(content, 0, content.Length);
success = true;
}
catch (IOException ex)
{
// Try with a smaller file size.
size = (int)(size * 0.9);
}
}Assuming you have 900 bytes left. In the above example, the Am I understanding the problem wrong? Also, I think that as a workaround you could save
I am not sure what do you mean by best effort. |
If we were to document the behavior with this change, it would be: "If the operation fails, the file position is unchanged except when resetting the position fails too.". This is not very useful guarantee to program against. It basically says that you cannot make assumptions about what the file position is going to be if the operation fails. A strong guarantee would be: "If the operation fails, the file position is guaranteed to be unchanged.". We are not able to make this guarantee unfortunately. |
|
We may be able to fix this nicely by using SetFileInformationByHandle with It should avoid the need for the seeks that are causing the problem. It should make the implementation on Unix similar to what I have done in #44097 . |
|
Closing in favor of #44170. |
Fixes #8307
Set
Positionback to its original value whenSetEndOfFilefails, this could be caused by not having enough space on disk for the new size of the file.Note: In order to add a test that verifies this in CI frequently, I need to find a way to effectively test this without having to depend on disk space available.