fix(worktree): recognise the Windows profile through WSL's drvfs view (#20051)

* fix(worktree): recognise the Windows profile through WSL's drvfs view

`/mnt/<letter>` under a WSL UNC alias is the distro's drvfs mount of a Windows
volume, so `\\wsl.localhost\Ubuntu\mnt\c\Users\bob` is `C:\Users\bob` wearing a
Linux spelling. The Windows-profile rule excludes every WSL UNC path by design
(the aliases normally front a Linux filesystem) and the POSIX shapes never match
a `/mnt/...` tail, so that path fell through both and read back as deletable.

The spelling is producible by the product: `resolveWslRepoWorktreeBasePath` maps a
`/mnt/c/...` worktree base against a WSL repo into exactly this UNC form, and
`getWslFilesystemBoundaryDistro` already treats it as the drvfs crossing.

A drvfs tail now takes the Windows rule on its drive form, via the existing
`toWindowsWslDrivePath`. Scoped to the UNC branch, where `parseWslUncPath` has
proven the path is a WSL alias — a plain Linux host's `/mnt/c/...` is untouched.
The lowercase-only `/mnt` match is deliberate: `/MNT` is an ordinary
case-sensitive Linux directory, never the automount.

* fix(worktree): refuse the drvfs volume root and the automount under a WSL UNC alias

`\\wsl.localhost\Ubuntu\mnt\c` is the whole C: volume and `\\wsl.localhost\Ubuntu\mnt`
holds every drvfs volume. Neither is caught by the root check in
`isDangerousWorktreeRemovalPath` (their win32 root is the distro share) nor by the
Windows-profile rule on the drive form (`C:\` is not `C:\Users`), so both read as
deletable. Measured on a Windows 11 host with WSL2: `rm -rf` inside the distro on the
`/mnt/c` spelling deletes on the Windows drive.
This commit is contained in:
Neil
2026-09-20 23:26:51 -07:00
committed by GitHub
parent 4da3a95d50
commit b2fe56def9
2 changed files with 41 additions and 2 deletions
@@ -87,6 +87,25 @@ describe('path-shape home detection', () => {
])('WSL UNC %s -> %s', (worktreePath, expected) => {
expect(isHome(worktreePath, CLIENT_REMOVAL_HOME)).toBe(expected)
})
it.each([
// `/mnt/<letter>` is the distro's drvfs view of a Windows volume, so this is
// `C:\Users\bob` wearing a Linux spelling, not a directory in the distro.
['\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\bob', true],
['\\\\wsl$\\Ubuntu\\mnt\\c\\Users', true],
// The volume itself, and the automount that holds every volume, contain the profile.
['\\\\wsl.localhost\\Ubuntu\\mnt\\c', true],
['\\\\wsl.localhost\\Ubuntu\\mnt\\c\\', true],
['\\\\wsl.localhost\\Ubuntu\\mnt', true],
['\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\bob\\ws\\wt', false],
['\\\\wsl.localhost\\Ubuntu\\mnt\\c\\src\\repo', false],
// `/MNT` is an ordinary case-sensitive Linux directory, never the automount.
['\\\\wsl.localhost\\Ubuntu\\MNT\\c\\Users\\bob', false]
])('WSL drvfs %s -> %s', (worktreePath, expected) => {
expect(withProcessPlatform('darwin', () => isHome(worktreePath, CLIENT_REMOVAL_HOME))).toBe(
expected
)
})
})
describe('whose home the guard consults', () => {
+22 -2
View File
@@ -17,7 +17,7 @@
import { homedir } from 'node:os'
import { posix, win32 } from 'node:path'
import { isWindowsAbsolutePathLike } from '../shared/cross-platform-path'
import { parseWslUncPath } from '../shared/wsl-paths'
import { parseWslUncPath, toWindowsWslDrivePath } from '../shared/wsl-paths'
export type PathOps = typeof posix
@@ -172,13 +172,33 @@ function isLikelyWindowsUserProfileDirectory(
/**
* WSL UNC aliases front a Linux filesystem, so POSIX home shapes — not
* `<root>\Users` — are what protect `\\wsl.localhost\Ubuntu\home\alice`.
*
* Except under `/mnt/<letter>`: that tail is the distro's drvfs view of a Windows
* volume, so `\\wsl.localhost\Ubuntu\mnt\c\Users\bob` is the Windows profile with
* a Linux spelling. It takes the Windows rule on its drive form, which the UNC
* exclusion above would otherwise skip.
*/
function isLikelyWslDistroHomeDirectory(resolvedWorktreePath: string, pathOps: PathOps): boolean {
if (pathOps !== win32) {
return false
}
const wsl = parseWslUncPath(resolvedWorktreePath)
return !!wsl && (wsl.linuxPath === '/' || isPosixHomeRoot(trimTrailingSlash(wsl.linuxPath)))
if (!wsl) {
return false
}
const linuxPath = trimTrailingSlash(wsl.linuxPath)
const drivePath = toWindowsWslDrivePath(linuxPath)
if (drivePath) {
const resolvedDrivePath = win32.resolve(drivePath)
// Why: `/mnt/c` is the whole volume. `C:\` is refused as a root before this guard runs; its
// drvfs spelling has to be refused here, since its win32 root is the distro share.
return (
win32.parse(resolvedDrivePath).root === resolvedDrivePath ||
isLikelyWindowsUserProfileDirectory(resolvedDrivePath, win32)
)
}
// Why `/mnt`: the automount parent holds every drvfs volume, so it contains every profile.
return wsl.linuxPath === '/' || linuxPath === '/mnt' || isPosixHomeRoot(linuxPath)
}
function isWslUncRemovalPath(resolvedWorktreePath: string): boolean {