Go version
1.25rc2
Output of go env
in your module/workspace:
`go env` output, not particularly relevant
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/cheetah/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/cheetah/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1694165664=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/cheetah/src/gdev/go.mod'
GOMODCACHE='/home/cheetah/.go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/cheetah/.go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/cheetah/.go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25rc2.linux-amd64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/cheetah/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/cheetah/.go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25rc2.linux-amd64/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25rc2'
GOWORK='/home/cheetah/src/gdev/go.work'
PKG_CONFIG='pkg-config'
What did you do?
I (mistakenly) tried to do a struct embedding of an os.Root
in my application, and then got confused when I got file already closed
errors.
Since os.OpenRoot
attaches a Cleanup to the *os.Root
pointer returned that calls *os.Root.Close()
once freed,, it's important that no struct copies be made, as those copies won't work properly.
What did you see happen?
file already closed
errors when my os.Root
instance was still live in active goroutine stacks
What did you expect to see?
Some combination of:
1. A documentation note that the struct is not safe to copy
2. A vet
error telling me that same thing
Comment From: gabyhelp
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: TapirLiu
Does the problem exist with toolchain 1.24?
Before 1.25, the cleaup setup code is
runtime.SetFinalizer(r.root, (*root).Close)
In 1.25rc2, it is
r.root.cleanup = runtime.AddCleanup(r, func(f *root) { f.Close() }, r.root)
Maybe, it should be
r.root.cleanup = runtime.AddCleanup(r.root, func(f *root) { f.Close() }, r.root)
?
Comment From: fastcat
Aah, I hadn't checked for changes since 1.24, as my use case needs some of the new methods on os.Root
in 1.25. That said, the code indeed looks like the problem was introduced in 1.25.
This change came from https://go.dev/cl/638555 / #70907 / fdaac84480b02e600660d0ca7c15339138807107 ... FYI @cagedmantis
Your suggested fix won't work however, due to the rules on AddCleanup:
If ptr is reachable from cleanup or arg, ptr will never be collected and the cleanup will never run. As a protection against simple cases of this, AddCleanup panics if arg is equal to ptr.
Comment From: TapirLiu
Ah, TIL.
One way is to make an extra indirect, like r.root.root
. But it is really hilarious. :D
Comment From: fastcat
Yes, that seems like it would be a bit out there for a solution :)
I don't know how many others might have code that does struct copying that would be broken by adding a "noCopy" member. Some folks have large corpuses of code they have been able to search for such things in the past, though I don't know if this would be an easy thing to search for.
I don't know how noCopy
on things like this falls into the compatibility promise, but I feel like my struct copying here was likely to be a very rare thing to do.
Comment From: cagedmantis
@neild @ianlancetaylor @bradfitz @griesemer @cagedmantis
Comment From: neild
Adding noCopy to Root seems reasonable to me. You're definitely not suppose to copy a Root, any more than you should copy a File.
(File suffers from the same cleanup problem, FWIW, and is not marked noCopy.)
Comment From: apparentlymart
From what's been discussed it seems like copying a Root
is already subtly broken anyway, unless you're lucky enough that both of your copies end up getting collected at roughly the same time, and so marking as "no copy" would help people find existing bugs and fix them.
I'm also understanding this as being a new vet check rather than a new compile-time error, so it's not really a "breaking change" in the sense that the program won't compile anymore. It might be annoying for those who consider vet checks a blocker for merging changes, but existing code that previously worked will still compile.
Comment From: mknyszek
I think we can fix this by changing the cleanup func. We also don't need another level of indirection.
At the point the cleanup func is called, (1) there can be no more mutex holders, (2) there's no point in setting root.closed
, root
is gone, and (3) same for r.refs
. root
is just gone at that point. Most of Close
is useless then, except for the syscall.Close
.
This is part of the point of using AddCleanup
over SetFinalizer
: just pass what you actually need. That's enough to break the cycle. And in this case I think all we actually need is the file descriptor.
In other words, what I'm proposing is changing
r.root.cleanup = runtime.AddCleanup(r, func(f *root) { f.Close() }, r.root)
to
r.root.cleanup = runtime.AddCleanup(r.root, func(fd sysfdType) { syscall.Close(fd) }, r.root.fd)
(With appropriate comments explaining why this is safe.)
Comment From: prattmic
(File suffers from the same cleanup problem, FWIW, and is not marked noCopy.)
This sounds like a regression in https://go.dev/cl/638555. Previously we had a finalizer on the internal *file
, but for cleanup we moved it to File
, which I believe changes the type from safe to copy to unsafe.