mount: report . and .. from windows directories (#10556)

* mount: report . and .. from windows directories

WinFsp strips the dot entries for the root itself and expects every other
directory to report them, the way a real NTFS enumeration does: its
dirctl test asserts a subdirectory's first two entries are "." and ".."
and that a hundred files enumerate as 102 entries. Dropping them
unconditionally is what fails querydir_test.

The Go test that guarded the old behaviour went with it: os.File.Readdir
filters dot entries itself, so it could never have observed either way.

* mount: give the windows dot entries their directory type

The readdir fills an attribute block only for real children, so "." and
".." arrived with a zeroed one and were reported with mode 0. Windows
refuses to enumerate a directory whose first entry is not marked as a
directory, which is the assertion querydir_test fails on with
STATUS_OBJECT_NAME_NOT_FOUND.

They now carry the type the readdir already knew. The explorer walk also
names any unexpected entry rather than only counting, so a dot entry
leaking through reads differently from a missing file.
This commit is contained in:
Chris Lu
2026-08-04 10:44:43 -07:00
committed by GitHub
parent 89ce6e175d
commit 5a5cd15054
4 changed files with 26 additions and 53 deletions
+7 -2
View File
@@ -165,8 +165,13 @@ jobs:
Write-Host "::group::explorer-style walk"
New-Item -ItemType Directory -Force -Path S:\walk | Out-Null
1..200 | ForEach-Object { Set-Content -Path "S:\walk\f$_.txt" -Value "line $_" }
$count = (Get-ChildItem S:\walk | Measure-Object).Count
if ($count -ne 200) { throw "listed $count files, expected 200" }
$names = @(Get-ChildItem S:\walk | ForEach-Object { $_.Name })
if ($names.Count -ne 200) {
# Name the strays: a dot entry surfacing here is a different problem
# from a missing or duplicated file.
$unexpected = $names | Where-Object { $_ -notmatch '^f\d+\.txt$' }
throw "listed $($names.Count) entries, expected 200; unexpected: $($unexpected -join ', ')"
}
$body = Get-Content S:\walk\f42.txt
if ($body -ne 'line 42') { throw "unexpected content: $body" }
Copy-Item S:\walk\f42.txt S:\walk\copy.txt
+3 -4
View File
@@ -73,10 +73,9 @@ setfileinfo_test
# Directory enumeration
# ---------------------
# Marker-based resumption and the buffer-overflow path, neither of which the
# hand-written suite reaches.
querydir_test
querydir_buffer_overflow_test
# querydir_namelen only: checkName caps at 255 bytes while WinFsp counts 255
# characters. The other two are expected to pass now that "." and ".." are
# reported for non-root directories, which is what WinFsp enumerates.
querydir_namelen_test
# Name length
-29
View File
@@ -270,35 +270,6 @@ func TestAwkwardNames(t *testing.T) {
}
}
// Windows enumerates a directory without "." and "..", and shows whatever the
// filesystem reports, so they must not reach it. os.ReadDir filters them, so
// this reads the handle the way Windows tooling does.
func TestNoDotEntriesInListing(t *testing.T) {
dir := testRoot(t)
for i := 0; i < 3; i++ {
if err := os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%d", i)), []byte("x"), 0644); err != nil {
t.Fatalf("write: %v", err)
}
}
f, err := os.Open(dir)
if err != nil {
t.Fatalf("open dir: %v", err)
}
defer f.Close()
names, err := f.Readdirnames(-1)
if err != nil {
t.Fatalf("readdirnames: %v", err)
}
for _, name := range names {
if name == "." || name == ".." {
t.Errorf("listing includes %q", name)
}
}
if len(names) != 3 {
t.Fatalf("listing has %d entries (%v), want 3", len(names), names)
}
}
func TestDeepDirectoryNesting(t *testing.T) {
dir := testRoot(t)
deep := dir
+16 -18
View File
@@ -593,6 +593,7 @@ type readdirSink struct {
names []string
offsets []uint64
inodes []uint64
modes []uint32
attrs []*fuse.EntryOut
limit int
@@ -600,31 +601,20 @@ type readdirSink struct {
// dot entries still moves the enumeration along.
lastOffset uint64
seen int
// discard absorbs the attributes of an entry that is being dropped; the
// raw filesystem fills the block after handing it back.
discard fuse.EntryOut
}
// The kernel expects readdir to report "." and "..", but Windows enumerates a
// directory without them and shows whatever it is given, so they are dropped
// rather than surfaced as two extra children.
func isDotEntry(name string) bool {
return name == "." || name == ".."
}
// WinFsp strips "." and ".." for the root directory itself and expects every
// other directory to report them, the way a real NTFS enumeration does.
func (s *readdirSink) AddEntry(entry fuse.DirEntry) bool {
if len(s.names) >= s.limit {
return false
}
s.seen++
s.lastOffset = entry.Off
if isDotEntry(entry.Name) {
return true
}
s.names = append(s.names, entry.Name)
s.offsets = append(s.offsets, entry.Off)
s.inodes = append(s.inodes, entry.Ino)
s.modes = append(s.modes, entry.Mode)
s.attrs = append(s.attrs, nil)
return true
}
@@ -635,13 +625,11 @@ func (s *readdirSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut {
}
s.seen++
s.lastOffset = entry.Off
if isDotEntry(entry.Name) {
return &s.discard
}
out := &fuse.EntryOut{}
s.names = append(s.names, entry.Name)
s.offsets = append(s.offsets, entry.Off)
s.inodes = append(s.inodes, entry.Ino)
s.modes = append(s.modes, entry.Mode)
s.attrs = append(s.attrs, out)
return out
}
@@ -676,9 +664,19 @@ func (w *WinFS) Readdir(path string, fill func(name string, stat *cgofuse.Stat_t
for i, name := range sink.names {
var stat cgofuse.Stat_t
var statp *cgofuse.Stat_t
if attr := sink.attrs[i]; attr != nil {
if attr := sink.attrs[i]; attr != nil && attr.Attr.Mode != 0 {
w.attrToStat(&attr.Attr, &stat)
statp = &stat
} else if sink.modes[i] != 0 {
// "." and ".." are reported without attributes: the readdir
// only fills a block for real children. Windows still needs
// their type, and enumerating a directory whose first entry is
// not marked as one fails outright.
stat.Mode = sink.modes[i]
stat.Ino = sink.inodes[i]
stat.Nlink = 1
stat.Uid, stat.Gid = w.uid, w.gid
statp = &stat
}
if filled && !fill(name, statp, int64(sink.offsets[i])) {
filled = false