Win32 stat() didn't handle AF_UNIX socket files#20271
Conversation
|
Just a small heads up: I'm planning on doing some testing with this and some reviewing; (so if possible: wait with merging until I had a chance to test/review) |
That's no problem. One test is failing, though I don't see why yet. |
It succeeds sometimes, but mostly fails. I haven't managed to get it to fail locally, and DrMemory (like valgrind) isn't showing anything relevant that I can see. |
Or it was, I can't get it to fail in CI at all now. |
|
FWIW, this 20204-stat-socket branch is fine for me when I build it on Windows 11 using gcc-12, where _WIN32_WINNT is 0x0a00 - though some of the TODO tests fail, and the only archtype I've tested is MSWin32-x64-multi-thread. With the same archtype and same compiler on Windows 7, t/win32/stat.t doesn't run to completion. Something awry with the Of course, there's a couple of questionable practices being undertaken on Windows 7:
But this has not been a problem before with gcc-12 builds of perl on Windows 7 - so long as optimization is set to Cheers, |
Unfortunately both symbolic links and sockets can only be "statted" by opening with FILE_FLAG_OPEN_REPARSE_POINT which obviously doesn't follow symbolic links. So to find if a chain of symbolic links points to a socket, is a broken chain, or loops, we need to follow the chain ourselves.
While debugging socket stat()ing I noticed that sometimes the name returned by win32_readlink() was a full pathname rather than the name that the link was created as. Changing this to use the PrintName values changed win32_readlink() to return the create as name, which seems closer to the POSIX readlink.
|
The failing TODO tests are for bugs that had been reported against Win32 stat() before I re-worked it, unfortunately they seem to be Windows limits (or bugs, I guess). |
c735a93 to
33cac23
Compare
| CreateFileA(path, FILE_READ_ATTRIBUTES, | ||
| FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, | ||
| NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); | ||
| if (handle == INVALID_HANDLE_VALUE) { | ||
| /* AF_UNIX sockets need to be opened as a reparse point, but | ||
| that will also open symlinks rather than following them. | ||
|
|
||
| There may be other reparse points that need similar | ||
| treatment. | ||
| */ | ||
| handle = S_follow_symlinks_to(aTHX_ path, &reparse_type); |
There was a problem hiding this comment.
This part of the commit doesn't make sense to me...
Outdated comment
What I expect when path is a symlink:
- if the
FILE_FLAG_OPEN_REPARSE_POINTflag is not included then it points to the target of the symlink (i.e. to the destination, i.e. to the real file) - if the
FILE_FLAG_OPEN_REPARSE_POINTflag is included then it points to th e symlink
In this code the FILE_FLAG_OPEN_REPARSE_POINT flag is not included in the CreateFileA call so I would expect handle to refer to the target file (i.e. not the symlink) but then the comment and code reads: S_follow_symlinks_to which doesn't make sense: if it was a symlink then handle already points to the destination file.. (assuming I read the MS docs correct)~
Update: Ok, it does make a bit of sense. It's only a bit confusing.
It first calls CreateFileA without FILE_FLAG_OPEN_REPARSE_POINT.
That either returns a valid handle (to the target file) or return an invalid handle.
The invalid handle can mean three different things:
- there is no such file with that name
- there is a file with that name but it's a symlink and points to a non-existing file
- there is a file with that name but it's a unix socket
The code doesn't really check which case applies and (sort of) assumes it might be a symlink and calls S_follow_symlinks_to which calls readlink to follow the link (if it's a link I guess);
Eventually it calls CreateFileA again with the FILE_FLAG_OPEN_REPARSE_POINT flag set.
There was a problem hiding this comment.
What I have/had initially in mind for this code was something like: (pseudo-code obviously):
handle =
CreateFileA(path, FILE_READ_ATTRIBUTES,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
if (handle != INVALID_HANDLE_VALUE) {
result = GetFileInformationByHandleEx(handle, FileBasicInfo, &basicInfo, ...)
if (baiscInfo.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, ....)
....
if (reparse_type == IO_REÄRSE_TAG_SYMLINK) {
handle =
CreateFileA(path, FILE_READ_ATTRIBUTES,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
}
}
}
That is: first open with FILE_FLAG_OPEN_REPARSE_POINT and if it's a symlink then call CreateFileA again but this time without the FILE_FLAG_OPEN_REPARSE_POINT flag so that it resolves the symlink.
There was a problem hiding this comment.
An alternative to the current code could also be to change S_follow_symlinks_to to not follow the symlink chain and instead check the reparse_type.
That is:
- if
CreateFileA(...., FILE_FLAG_BACKUP_SEMANTICS, ...)fails - and if
CreateFileA(...., FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINTS, ...)succeeds - then check if
reparse_typeis 'symlink'. If it is return ENOENT
There was a problem hiding this comment.
Thinking some more.. I guess that won't do the right thing if it's a symlink to a unix socket..
(I'll do some testing tomorrow)
There was a problem hiding this comment.
I do the non-reparse-point open first because I expect it to be by far the more common case, and it does the right thing for win32_stat() for valid symlink chains.
I went direct for win32_readlink() mostly out of laziness - I didn't want to include the CreateFile/DeviceIOControl/WideCharToMultiByte() code here (though maybe a win32_readlink_handle() would help), and win32_readlink() does the job.
I don't really consider this code path to be performance critical.
And yes, a symlink chain to a socket fails the non-reparse CreateFile().
| if (reparse_type) { | ||
| /* Lie to get to the right place */ | ||
| type = FILE_TYPE_DISK; |
There was a problem hiding this comment.
In what case was this needed?
When I test with a unix socket it seems GetFileType() returns FILE_TYPE_DISK.
Did you encounter a case in which it returned something else?
There was a problem hiding this comment.
This part may be unnecessary, I ass-umed that it would fail, but didn't test.
Thanks for the review.
demerphq
left a comment
There was a problem hiding this comment.
This passes the eyeball test for me. I dont have a win32 box to play with it on however. I need to fix that.
Unfortunately both symbolic links and sockets can only be "statted" by opening with FILE_FLAG_OPEN_REPARSE_POINT which obviously doesn't follow symbolic links.
So to find if a chain of symbolic links points to a socket, is a broken chain, or loops, we need to follow the chain ourselves.
Fixes #20204