Skip to content

cmd/compile: nilcheck is tightened in a branch allowing the other one to execute when it should have panicked #72860

Closed
@ggambetti

Description

@ggambetti

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsFixThe path to resolution is known, but the work has not been done.compiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions