Skip to content

Conversation

@Hakkin
Copy link
Contributor

@Hakkin Hakkin commented May 31, 2024

This is a fix for #3842.
It's a slightly modified version of the patch in #3842 (comment), I separated the code out into a maybe function and added some tests.
The root cause of this issue seems to be a regression in Go 1.22 (see #3842 (comment)), but as discussed with jkowalski in the issue, it seems better to just fix this up with a special case in Kopia.

@Hakkin
Copy link
Contributor Author

Hakkin commented May 31, 2024

Something additional to note, there is a similar check going on here in the NewEntry function:

fi, err := os.Lstat(path)
if err != nil {
// Paths such as `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy01`
// cause os.Lstat to fail with "Incorrect function" error unless they
// end with a separator. Retry the operation with the separator added.
var e syscall.Errno
//nolint:goconst
if runtime.GOOS == "windows" &&
!strings.HasSuffix(path, string(filepath.Separator)) &&
errors.As(err, &e) && e == 1 {
fi, err = os.Lstat(path + string(filepath.Separator))
}
if err != nil {
return nil, errors.Wrap(err, "unable to determine entry type")
}
}

I wonder if it would be useful to change this to use the new maybeAppendPathSeparatorForVSSOnWindows function as well.

@Whissi
Copy link

Whissi commented Jun 1, 2024

I applied this PR on top of v0.17.0 tag which I confirmed to be showing the problem reported in #3842 using go version go1.22.2 windows/amd64. With this PR applied, I am now able again to snapshot an entire volume when using a volume shadow copy.

However, it's worth noting that appending a trailing backslash when detecting the \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy prefix is not specific to shadow copies but rather ensures proper handling of paths within the Windows Object namespace. \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyXX represents a symbolic link within the Windows Object namespace (\\GLOBALROOT folder) pointing to \Device\HarddiskVolumeShadowCopyXX (which is a device and nothing within \\GLOBALROOT folder). For Windows, without a trailing backslash, it may not be clear whether we intend to operate on the symbolic link itself or on the actual resource, \Device\HarddiskVolumeShadowCopyXX. Thus, the workaround is essential for disambiguation. It might be beneficial to consider a more generalized approach by checking if a path starts with \ and ensuring it ends with a trailing backslash. This would provide robustness in handling various path scenarios, especially in our case where we always expect to deal with iterable objects (directories). And if we wouldn't access the shadow copy through Windows Object namespace at all (for example we could mount the shadow copy somewhere) we wouldn't need to take care of this at all.

@Hakkin
Copy link
Contributor Author

Hakkin commented Jun 1, 2024

Thanks for the extra info.

\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyXX represents a symbolic link within the Windows Object namespace (\GLOBALROOT folder) pointing to \Device\HarddiskVolumeShadowCopyXX (which is a device and nothing within \GLOBALROOT folder).

I wonder if there's any Windows API to detect or resolve links like this in the Object namespace? For normal symlinks / reparse points in the filesystem this can be done with os.Lstat, but as noted in the code in my second comment, Lstat seems to fail on these kinds of paths unless you "manually" resolve them with the trailing backslash. If there's a way to detect them directly, it might make sense to implement them like normal symlinks are now, and there wouldn't be any need to handle shadow volumes explicitly.

It might be beneficial to consider a more generalized approach by checking if a path starts with \ and ensuring it ends with a trailing backslash. This would provide robustness in handling various path scenarios, especially in our case where we always expect to deal with iterable objects (directories).

I considered doing something like this but I was worried about edge cases, which is why I went out of my way to make sure the workaround is only applied to the top-level root directory. Since "inside" the shadow volume device is just a normal filesystem, appending a backslash anywhere after the root might have different consequences, like resolving links. If we did something more generic, it would definitely need a lot more testing to make sure all the edge cases are handled correctly.

@Hakkin
Copy link
Contributor Author

Hakkin commented Jun 2, 2024

I ended up reading a lot about Windows path handling, this article seems to contain a lot of the relevant information.

I believe the fundamental issue here is that Windows treats opening the path \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1 as attempting to open the raw block device for that volume, while it treats \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\ as opening the root directory of the volume, and it passes it to the underlying filesystem driver.

Unfortunately, it seems non-triviable to handle this "correctly". It seems impossible to know whether a UNC path points to a device object without doing multiple Windows-specific syscalls for every path.

That being said, I think there is a way to handle this a bit more cleanly. For one, rather than the current maybe function, after thinking about it more, I think it's better to implement a method on filesystemEntry that checks the path parts. filesystemEntry already has the prefix and filename split out, so it simplifies the logic and we don't have to do any path handling in the checking function.

func (e *filesystemEntry) isWindowsVSSVolume() bool {
	return runtime.GOOS == "windows" &&
		e.prefix == `\\?\GLOBALROOT\Device\` &&
		strings.HasPrefix(e.Name(), "HarddiskVolumeShadowCopy")
}

then we can just do this in Iterate:

fullPath := fsd.fullPath()

appendPath := ""
if fsd.isWindowsVSSVolume() {
	appendPath = string(os.PathSeparator)
}

f, direrr := os.Open(fullPath + appendPath) //nolint:gosec

and that's all the logic needed.

If we ever want to make it a bit more generic in the future. the isWindowsVSSVolume function can be expanded to support other common volume paths like \\?\Volume{...} as well.

If there's no objections to this revised patch, I think I'll push another commit with it in a little bit since it's pretty much the same logic as the previous commit, just cleaner.

@codecov
Copy link

codecov bot commented Jun 3, 2024

Codecov Report

❌ Patch coverage is 70.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.38%. Comparing base (cb455c6) to head (c260c38).
⚠️ Report is 613 commits behind head on master.

Files with missing lines Patch % Lines
fs/localfs/local_fs_os.go 50.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3891      +/-   ##
==========================================
+ Coverage   75.86%   76.38%   +0.51%     
==========================================
  Files         470      530      +60     
  Lines       37301    40476    +3175     
==========================================
+ Hits        28299    30917    +2618     
- Misses       7071     7511     +440     
- Partials     1931     2048     +117     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Hakkin
Copy link
Contributor Author

Hakkin commented Jun 3, 2024

Regarding the test coverage, should I add a test that covers this case? I think I could just mostly copy https://github.com/kopia/kopia/blob/master/tests/os_snapshot_test/os_snapshot_windows_test.go but enable enable-volume-shadow-copy=always and set it to snapshot the root of the C:\ drive instead, we can add an ignore policy so it doesn't actually snapshot any of the files, just tests creating the shadow copy, since that's where it would fail before.

@xieve
Copy link

xieve commented Sep 20, 2024

Tested on Windows 10 with a boot drive SSD and a data HDD, working just fine. Thank you for your contribution, I hope to see this upstream soon :)

@wildyaboku
Copy link

I just tested this on the latest release of Windows 11 (24H2) and it's working as expected.

@bparker98
Copy link

Now that we finally have a new Kopia release is there any chance this fix could get some love?

@tekert
Copy link

tekert commented Dec 11, 2024

Upgraded from 0.16 to 0.18.2 and vss broke, applied this path, now it works, not much more to say.

@Janzert
Copy link

Janzert commented Jan 3, 2025

Are there any specific issues blocking this from getting merged?

This is the thing blocking me from upgrading off of 0.16. It'd be great to know if there is anything to help move this forward.

@Lerrissirrel
Copy link

Are there any specific issues blocking this from getting merged?

This is the thing blocking me from upgrading off of 0.16. It'd be great to know if there is anything to help move this forward.

Looks like it's (at least) awaiting a review. I built my own 18.2 with this change included, was hoping it would make 19!

@t3pco
Copy link

t3pco commented Mar 25, 2025

Also waiting for this fix, as it's a blocking usecase for me.
Would really appreciate to see finalization of the existing VSS and Windows-specific features connected to that (enabled via. UI, docs)

@xxxliqu1dxxx
Copy link

@julio-lopez or @jkowalski would it be possible get this merged? Are there any blockers preventing this from going through?

@t3pco
Copy link

t3pco commented May 28, 2025

@julio-lopez or @jkowalski would it be possible get this merged? Are there any blockers preventing this from going through?

+1, was not able to make a local working build with this, but user report it's working.
Using the very old client is not a valid option for me.

@Lerrissirrel
Copy link

I was hoping the silence on this thread meant this wasn't a problem any more in .21 but it looks like it still is. Fortunately the changes in this PR still work if you're able to build for yourself.

@t3pco
Copy link

t3pco commented Jul 24, 2025

Had the same hope that the fix will be merged at least into this release.
Managed to build it locally now, but frustrated that a working and small fix did not make it again.

I understand that Windows is not top priority, but this bug makes it nearly unuseable for windows clients. Using 0.16 is a workaround, but not for such long time

Pushing again so we maybe have it for 0.22 then :-)

@jkowalski
Copy link
Contributor

i'll get this merged, i got a hold of a Windows box and will try it out and merge after that.

@jkowalski
Copy link
Contributor

ok, i think it works, i'm going to merge, please try it for your use case when a build is produced

@jkowalski jkowalski enabled auto-merge (squash) August 3, 2025 03:47
@jkowalski jkowalski merged commit 841b1fd into kopia:master Aug 3, 2025
26 of 27 checks passed
@t3pco
Copy link

t3pco commented Aug 3, 2025

ok, i think it works, i'm going to merge, please try it for your use case when a build is produced

Thanks for your effort in this case. Tested recent master build 20250803.0.35640 installed via. KopiaUI-Setup-20250803.0.35640.exe. Confirmed working.

julio-lopez added a commit to julio-lopez/kopia that referenced this pull request Aug 21, 2025
julio-lopez added a commit that referenced this pull request Aug 21, 2025
Additional cleanups:
- make fs/localfs.isWindows a const
- move Windows-specific functionality to local_fs_windows.go
- cleanup: remove //nolint:revive annotations

Ref:
- Followup to #3891
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.