Go version

go version go1.25rc1 windows/amd64

Output of go env in your module/workspace:

set AR=ar
set CC=gcc
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_ENABLED=0
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set CXX=g++
set GCCGO=gccgo
set GO111MODULE=
set GOAMD64=v1
set GOARCH=amd64
set GOAUTH=netrc
set GOBIN=
set GOCACHE=C:\Users\ContainerUser\AppData\Local\go-build
set GOCACHEPROG=
set GODEBUG=
set GOENV=C:\Users\ContainerUser\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFIPS140=off
set GOFLAGS=
set GOGCCFLAGS=-m64 -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Windows\TEMP\go-build662705847=/tmp/go-build -gno-record-gcc-switches
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMOD=NUL
set GOMODCACHE=C:\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\Program Files\Go
set GOSUMDB=sum.golang.org
set GOTELEMETRY=local
set GOTELEMETRYDIR=C:\Users\ContainerUser\AppData\Roaming\go\telemetry
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.25rc1
set GOWORK=
set PKG_CONFIG=pkg-config

What did you do?

Running a build inside a fresh Windows Nano Server container which defaults to the unprivileged ContainerUser and TMP is set to C:\Windows\TEMP: commands such as go build and go run fail to clean up from C:\Windows\Temp with go: open C:\Windows\TEMP: Access is denied.

package main

import "fmt"

func main() {
    fmt.Printf("Hello World!\n")
}

(saved as hello.go)

What did you see happen?

C:\go>go build -o hello.exe hello.go
go: open C:\Windows\TEMP: Access is denied.

What did you expect to see?

When I do the same exact process with Go 1.24, it works fine without errors.

Comment From: tianon

See also https://github.com/docker-library/golang/issues/563, especially https://github.com/docker-library/golang/issues/563#issuecomment-2968325383 -- I bisected this and got the following:

6d418096b2dfe2a2e47b7aa83b46748fb301e6cb is the first bad commit
commit 6d418096b2dfe2a2e47b7aa83b46748fb301e6cb
Author: Damien Neil <dneil@google.com>
Date:   Fri Mar 28 16:14:43 2025 -0700

    os: avoid symlink races in RemoveAll on Windows

    Make the openat-using version of RemoveAll use the appropriate
    Windows equivalent, via new portable (but internal) functions
    added for os.Root.

    We could reimplement everything in terms of os.Root,
    but this is a bit simpler and keeps the existing code structure.

    Fixes #52745

    Change-Id: I0eba0286398b351f2ee9abaa60e1675173988787
    Reviewed-on: https://go-review.googlesource.com/c/go/+/661575
    Reviewed-by: Alan Donovan <adonovan@google.com>
    Auto-Submit: Damien Neil <dneil@google.com>
    LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

 src/internal/syscall/unix/constants.go      |  2 +-
 src/internal/syscall/unix/nofollow_posix.go |  2 +-
 src/internal/syscall/windows/at_windows.go  |  4 ++--
 src/os/path_windows.go                      | 10 +++++++++
 src/os/removeall_at.go                      | 32 +++++++++--------------------
 src/os/removeall_noat.go                    |  2 +-
 src/os/removeall_unix.go                    | 20 ++++++++++++++++++
 src/os/removeall_windows.go                 | 17 +++++++++++++++
 src/os/root_unix.go                         | 12 +++++++++++
 src/os/root_windows.go                      | 10 ++++++++-
 10 files changed, 83 insertions(+), 28 deletions(-)
 create mode 100644 src/os/removeall_unix.go
 create mode 100644 src/os/removeall_windows.go

https://github.com/golang/go/commit/6d418096b2dfe2a2e47b7aa83b46748fb301e6cb

So it looks like the fix for https://github.com/golang/go/issues/52745 causes a regression in how C:\Windows\Temp is handled? 🙈

Comment From: seankhliao

cc @golang/windows

Comment From: tianon

It's also worth noting that something about running the command changes the state of the system such that running it again works fine and without error (I wasn't able to figure out what that's about).

Edit: ah, if I run with -a to force it to recompile, it also fails again.

Comment From: seankhliao

cc @neild

Comment From: gabyhelp

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: tianon

Windows Nano Server is kind of a PITA, so I spent a little time and was able to reproduce with Server Core too by setting TMP explicitly to C:\Windows\Temp and running as an unprivileged user (which would probably work on a desktop Windows system too, assuming an unprivileged user).

Comment From: alexbrainman

cc @golang/windows

Leaving for @neild because I don't even have Windows computer to debug this.

Alex

Comment From: dmitshur

Pinging as this is a release blocker for Go 1.25.

Comment From: qmuntal

Still don't know the root cause, but at least I can reproduce this locally.

This is an easier to debug reproducer

package main

import "os"

func main() {
    err := os.RemoveAll(`C:\Windows\TEMP\foobar`) // create foobar using an elevated user
    println(err)
} 

Comment From: neild

I'm having difficulty reproducing this (not aided by my main Windows machine being a gomote which is only accessible via ssh). Any tips on reproducing would be helpful, especially ones that keep in mind that I am very much clueless when it comes to all things Windows. (Except for some very specific NtCreateFile minutiae these days. :) )

What's the definition of "unprivileged user" here? Any non-administrator user, or something with less privileges than that? Is there an easy way to check if the current user is privileged or not?

For the case of os.RemoveAll(`C:\Windows\TEMP\foobar`) where foobar is created by an elevated user (I'm guessing "elevated user" would be an administrator?), wouldn't we expect removing a temp file owned by another user to fail?

Comment From: neild

Okay, I wrote that and naturally managed to finally reproduce the problem immediately afterwards.

The issue seems to be that while the unprivileged user can delete a file in c:\windows\temp, it can't open c:\windows\temp itself. In the case where we're trying to delete a file by first opening the parent directory and then deleting the file relative to that directory, we fail at the open-parent-directory step.

Comment From: tianon

Yeah this is the case of "unprivileged user creates directory or file, then cannot delete it" (like in /tmp in Linux, but imagine it doesn't have +r or maybe even +x on /tmp which thankfully isn't common in the Linux world 😅).

Comment From: neild

Except in this case, the problem is that the user can't open the parent directory of the file. The user can remove c:\windows\temp\foo, but they can't open c:\windows\temp.

I'm not sure if there are some flags we can pass to CreateFile or NtCreateFile that would allow opening a zero-permissions handle toc:\windows\temp. Basically, we want the equivalent of O_PATH: We're opening a directory strictly as a handle for future directory-relative operations.

The alternative, which I think would work but have not actually tried yet, is to rework RemoveAll to not open the parent of the directory being removed. That's an uncomfortable amount of change this late in the release cycle, but needs must.

Comment From: dmitshur

If more time is desired and the trade-offs are appropriate, also consider the path of reverting the change that caused this regression, reopening #52745, and trying again in a future cycle.

Comment From: gopherbot

Change https://go.dev/cl/684515 mentions this issue: os: use minimal file permissions when opening parent directory in RemoveAll

Comment From: qmuntal

Basically, we want the equivalent of O_PATH: We're opening a directory strictly as a handle for future directory-relative operations.

You can pass the FILE_LIST_DIRECTORY access to CreateFile, but using that still doesn't fix the issue reported. On the other hand, we really don't do any operation on the parent directory, not even traversing it, we just use its handle to open a child file.

This means that we can use the FILE_READ_ATTRIBUTES access, which is almost always allowed, even for unprivileged users. Luckily, using O_WRONLY | O_RDWR in os.OpenFile is internally mapped to FILE_READ_ATTRIBUTES (see CL 673035), so the fix is straightforward (see CL 684515).

I've verified that CL 684515 fixes the original issue, at least on my computer. I'm a bit busy these weeks, and creating a test for it is outside my ACL knowledge, so the CL doesn't have any test.