Description
Go version
go version go1.24.1 darwin/arm64
Output of go env
in your module/workspace:
AR='ar'
CC='cc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='c++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/giannigambetti/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/giannigambetti/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/0m/wl_xfppn5nn5gk2qcwj3ymwc0000gq/T/go-build178340693=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/Users/giannigambetti/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/giannigambetti/go'
GOPRIVATE=''
GOPROXY='direct'
GOROOT='/opt/homebrew/Cellar/go/1.24.1/libexec'
GOSUMDB='off'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/giannigambetti/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.24.1/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.1'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
Working with a map to a pointer (map[string]*thing
) a coworker remarked during a PR review that they were surprised it work, indeed the code would be expected to segfault.
I've reduced the code to a small reproduction sample which exhibits the behaviour: https://go.dev/play/p/6Jv5ePF2o4n.
The suspicious code in question (lines 19-22):
t, ok := m[key]
// nil pointer dereference that gets optimized to happen after the `!ok` check ... sometimes.
valid := t.field >= 0
Computing the value of valid
is expected to segfault, as t
is always nil
in the example snippet.
What did you see happen?
The code snippet executes successfully, printing out got: <nil>
instead of segfaulting.
I checked it against all versions of Go available in go.dev/play: 1.24
, 1.23
, and dev branch
. All of them exhibit this behaviour.
My guess is this might be a compiler optimization, because making changes to the code like calling fmt.Printf
or printing out the value of valid
later both cause the code to segfault as expected.
That is; I think it transforms:
v, ok := m[key]
valid := v.field >= 0
if !ok || !valid {
into
v, ok := m[key]
if !ok || !(v.field >= 0) {
Likely irrelevant, but: anecdotally 1.23
timed out while building this example several times.
What did you expect to see?
This program is expected to segfault. Computing the valid
variable unconditionally would result in a dereferenced nil
pointer.
This execution appears to violate the Go memory model and the order of evaluation, quoting:
Requirement 1: The memory operations in each goroutine must correspond to a correct sequential execution of that goroutine, given the values read from and written to memory. That execution must be consistent with the sequenced before relation, defined as the partial order requirements set out by the Go language specification for Go's control flow constructs as well as the order of evaluation for expressions.
Thanks to @nwchandler for pointing this apparent spec violation out to me.