From 1808960ae25bfe2ccfc9440a18ab8c6482272844 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 6 Oct 2026 01:10:37 +0000 Subject: [PATCH] gomodfs: declare the WinFsp volume case-sensitive and case-preserving gomodfs's paths are case-sensitive, like the GOMODCACHE layout it emulates (which escapes capital letters as "!x"), but the WinFsp mount claimed to be case-insensitive and not case-preserving. So for opens relative to a working directory on the mount, Windows passed gomodfs the working directory's path upcased, e.g. "TSGO-WINDOWS-AMD64//SRC/INTERNAL/ABI/textflag.h". gomodfs didn't recognize those paths and answered with an empty directory for any of them, so relative opens of files that don't exist succeeded and relative opens of files that do returned a directory. This broke Go builds using a Tailscale Go toolchain served from the mount: cmd/asm runs in each package's source directory and first tries to open #include files relative to it, so it "found" textflag.h there and then failed reading it with "The parameter is incorrect". Mount with winfsp.CaseSensitive(true) and FspFSAttributeCasePreservedNames so Windows passes names through verbatim. Add a CI test, run in the WinFsp job, that reads a file in the mounted module cache by a relative path; it fails before this change and passes after. Updates tailscale/corp#24037 Signed-off-by: Brad Fitzpatrick --- .github/workflows/test.yml | 2 +- testing/ci/ci_test.go | 35 ++++++++++++++++++++++++++++++++++- winfsp.go | 8 +++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9e6b40..32e5e58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -186,4 +186,4 @@ jobs: - name: "verify used" run: | - go test -v ./testing/ci '-verify-used' + go test -v ./testing/ci '-verify-used' '-relative-open' diff --git a/testing/ci/ci_test.go b/testing/ci/ci_test.go index d86adfd..cf1b321 100644 --- a/testing/ci/ci_test.go +++ b/testing/ci/ci_test.go @@ -4,15 +4,48 @@ package ci import ( + "bytes" "encoding/json" "flag" "net/http" + "os" + "path/filepath" "testing" "github.com/tailscale/gomodfs" ) -var runVerifyUsed = flag.Bool("verify-used", false, "if set, runs TestVerifyUsed") +var ( + runVerifyUsed = flag.Bool("verify-used", false, "if set, runs TestVerifyUsed") + runRelativeOpen = flag.Bool("relative-open", false, "if set, runs TestRelativeOpen against the mounted $GOMODCACHE") +) + +// TestRelativeOpen opens files in the mounted module cache by paths relative +// to a working directory on the mount, as cmd/asm does with #include files. +// On Windows, WinFsp used to pass such opens to gomodfs with the working +// directory's path upcased, because gomodfs's volume claimed to be +// case-insensitive and not case-preserving. +func TestRelativeOpen(t *testing.T) { + if !*runRelativeOpen { + t.Skip("only runs in CI with --relative-open set") + } + modCache := os.Getenv("GOMODCACHE") + if modCache == "" { + t.Fatal("GOMODCACHE not set") + } + t.Chdir(filepath.Join(modCache, "golang.org", "x", "sys@v0.16.0")) + + b, err := os.ReadFile("go.mod") + if err != nil { + t.Fatalf("reading go.mod relative to the module dir: %v", err) + } + if !bytes.Contains(b, []byte("module golang.org/x/sys")) { + t.Errorf("go.mod contents = %q; want the golang.org/x/sys module", b) + } + if _, err := os.Stat("no-such-file"); !os.IsNotExist(err) { + t.Errorf("Stat of a missing relative path: err = %v; want not exist", err) + } +} func TestVerifyUsed(t *testing.T) { if !*runVerifyUsed { diff --git a/winfsp.go b/winfsp.go index fc35918..e2efe75 100644 --- a/winfsp.go +++ b/winfsp.go @@ -36,7 +36,13 @@ func (mfs *FS) MountWinFSP(mntPoint string) (MountRunner, error) { }, mntPoint, // e.g. "M:" winfsp.FileSystemName("gomodfs"), - winfsp.Attributes(winfsp.FspFSAttributeReadOnlyVolume), + // gomodfs paths are case-sensitive, like the GOMODCACHE layout it + // emulates (which escapes capitals as "!x"). Say so: on a volume + // that claims to be case-insensitive and not case-preserving, + // Windows passes relative opens to us with the working directory's + // path upcased, which we don't recognize. + winfsp.CaseSensitive(true), + winfsp.Attributes(winfsp.FspFSAttributeReadOnlyVolume|winfsp.FspFSAttributeCasePreservedNames), ) if err != nil { return nil, fmt.Errorf("winfsp.Mount: %w", err)