Skip to content
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

Use SHCreateDirectoryExW to create a directory with intermediate directories on Windows #1033

Merged
merged 1 commit into from
Nov 5, 2024

Conversation

ahoppen
Copy link
Member

@ahoppen ahoppen commented Nov 4, 2024

Instead of recursively creating all the parent directories, which is racy if the directory is created between the check if the parent directory exists and the actual directory creation, use SHCreateDirectoryExW, which also creates intermediate directories.

Alternative to #1021

@ahoppen
Copy link
Member Author

ahoppen commented Nov 4, 2024

@swift-ci Please test

@ahoppen
Copy link
Member Author

ahoppen commented Nov 4, 2024

@swift-ci Please test

@ahoppen
Copy link
Member Author

ahoppen commented Nov 4, 2024

@swift-ci Please test

@ahoppen
Copy link
Member Author

ahoppen commented Nov 4, 2024

Ah, looks like SHCreateDirectoryExW only works on fully qualified paths and not relative paths. I guess we could form a fully-qualified path for SHCreateDirectoryExW but I’m a little worried because now the implementation for withIntermediateDirectories: true starts to deviate from the one with withIntermediateDirectories: false quite significantly…

@ahoppen ahoppen force-pushed the shcreatedirectory branch 3 times, most recently from 241368a to 9ec9778 Compare November 5, 2024 02:21
@ahoppen
Copy link
Member Author

ahoppen commented Nov 5, 2024

OK, after some more thinking, maybe using SHCreateDirectoryExW is better than recursively walking up the path and creating all intermediate directories manually, if only because there are fewer atomic operations that can race with other file system operations.

The Gist for creating a directory with intermediate directories is now:

  • If the path is not absolute, make it absolute using URL(filePath: path, relativeTo: self.currentDirectoryPath) because SHCreateDirectoryExW requires an absolute path
  • Create the directory
  • If creating the directory failed but we have a directory at that path already, return success
    • Interestingly, I noticed that we don’t apply attributes to the directory in this case. Not sure if that’s intended behavior.
  • Otherwise return failure

We have two different states that we store in this implementation:

  • The current directory. But this shouldn’t cause any race conditions because if the current directory changes after we retrieve it but before we create the directory, the behavior is the same as if the current directory was changed after directory creation finished
  • The error code from directory creation and the fileExists check. But this should be fine because the only situation in which we modify behavior based on fileExists if it returns true and has isDirectory == true. At this point it doesn’t really matter what directory creation did. We have a directory and that’s the intended outcome, so we can return success without any issues.

One thing that I’m not entire sure about is whether SHCreateDirectoryExW might have subtly different behavior to CreateDirectoryW (which we use if we don’t want to create intermediate directories), which could cause issues in the future. @compnerd Do you think this is a concern? You have more experience with the Windows API. Fo example, one thing that I noticed is that the documentation of SHCreateDirectoryExW says:

SHCreateDirectoryEx also verifies that the files are visible. If they are not visible, expect one of the following:

And I’m not exactly sure what that means. I was able to create directories inside hidden folders alright, so that doesn’t seem to be it. But really I’m trying to figure out if there will be a bunch of these subtle differences or whether SHCreateDirectoryExW and CreateDirectoryW should behave mostly the same.

@ahoppen
Copy link
Member Author

ahoppen commented Nov 5, 2024

@swift-ci Please test

@ahoppen ahoppen changed the title Use SHCreateDirectory to create a directory with intermediate directories on Windows Use SHCreateDirectoryExW to create a directory with intermediate directories on Windows Nov 5, 2024
@ahoppen
Copy link
Member Author

ahoppen commented Nov 5, 2024

@swift-ci Please test

Copy link
Contributor

@jmschonfeld jmschonfeld left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for iterating on this - I personally like this approach and am in favor of it over the added fileExists check to the existing implementation assuming @compnerd doesn't see any issues with using SHCreateDirectoryExW. I agree with your assessment on the possible asynchronous state changing that I think based on the nature of it here, it doesn't matter and wouldn't result in a true TOCTOU issue. Thanks!!

guard let currentDirectoryPath else {
// The path is not absolute but we don't have a current path either.
// Pretending that the path is absolute is the best we can do.
return try body(pwszPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When exactly can this happen? Unlike Unix, the current path cannot be removed from under you as you have an open handle. The path cannot have been constructed after you entered as it would not exist. This feels like we could precondition it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure when there would be no working directory on Windows. GetCurrentDirectory says

If the function fails, the return value is zero. To get extended error information, call GetLastError.

Which leads me to believe that there are ways for it to fail.

Happy to change this to a precondition though. Crashing is probably preferable to doing unknown file system operations.

While looking into this, I also noticed that currentDirectoryPath can be nil due to a race condition, but I don’t think we need to cover that here: #1034

// A directory already exists at this path, which is not an error if we have
// `createIntermediates == true`.
break
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the else necessary? Can we not just drop the else and undent the body of the clause?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, can be flattened

…tories on Windows

Instead of recursively creating all the parent directories, which is racy if the directory is created between the check if the parent directory exists and the actual directory creation, use `SHCreateDirectoryExW`, which also creates intermediate directories.
@ahoppen
Copy link
Member Author

ahoppen commented Nov 5, 2024

@swift-ci Please test

@jmschonfeld jmschonfeld merged commit 31fef58 into swiftlang:main Nov 5, 2024
3 checks passed
cthielen pushed a commit to cthielen/swift-foundation that referenced this pull request Nov 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants