Skip to content

cmd/compile: inlined generic interface dispatch miscompiles int64 compare with -race on amd64 #81463

Open

Description

@bosdhill

Go version

go version go1.27.1 linux/amd64

Output of go env in your module/workspace:

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='/go/bin'
GOCACHE='/root/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/root/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build755864626=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPACKAGESDRIVER=''
GOPATH='/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/root/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.27.1'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Run this standalone main.go with go run -race main.go on linux/amd64:

package main

type leftInt interface {
	uint8 | int32
}

type rightInt interface {
	uint8 | int64
}

func compare[L leftInt, R rightInt](left L, right R) int {
	if int64(left) < int64(right) {
		return 1
	}
	return -1
}

func main() {
	if dispatch(uint8(0), int64(-1)) == 1 {
		panic("bad")
	}
}

func dispatch(l, r any) int {
	if left, ok := l.(int32); ok {
		right := r.(int64)
		return compare(left, right)
	}
	left := l.(uint8)
	switch right := r.(type) {
	case int64:
		return compare(left, right)
	case uint8:
		return compare(left, right)
	default:
		return 0
	}
}

What did you see happen?

With Go 1.27.1 linux/amd64 and -race, the program panics:

panic: bad

goroutine 1 [running]:
main.main()
	/workspace/main.go:20 +0x5e
exit status 2

The result 1 means the compiled code evaluated
uint8(0) < int64(-1) as true.

What did you expect to see?

The program should exit successfully. uint8(0) is greater than int64(-1),
so dispatch should not return 1.

Codex notes

Only the uint8,int64 case is executed at runtime. The int32,int64 branch
and uint8,uint8 case are still required to make cmd/compile produce the bad
inlined generic interface-dispatch shape.

The reduced value pair is intentionally small. If the right int64(-1) value
is spilled and reloaded as uint8, only the low byte survives, so the reloaded
value is 255. The compiled code then compares uint8(0) against 255,
making < incorrectly true.

Other reduction notes:

required: Go 1.27 linux/amd64, -race, inlining
required: generic helper returns int from the comparison
required: interface assertion/dispatch branch shape around the executed case
not specific: extra left type can be int32, int8, int, or uint16
stopped reproducing: removing the extra branch, uint8 case, default case, or
                   right temporary in the unused branch
stopped reproducing: returning bool from compare, or panicking inside compare

The behavior is specific to Go 1.27 linux/amd64 with -race and inlining:

Go 1.26.8 linux/amd64 + -race: pass
Go 1.27.0 linux/amd64 + -race: fail
Go 1.27.1 linux/amd64 + -race: fail
Go 1.27.1 linux/amd64 without -race: pass
Go 1.27.1 linux/amd64 + -race -gcflags=all=-l: pass

Suspected problematic change

The likely introducing change is commit
80123ef4bf5397f6f194b0a54e8c65dadb18aa1b, CL 777820:

cmd/compile: preserve pointerness during splitload

That CL changed the 386 and AMD64 splitload rules so compare-with-memory
rewrites preserve the other compare operand's type:

-(CMP(Q|L|W|B)load {sym} [off] ptr x mem) =>
   (CMP(Q|L|W|B) (MOV(Q|L|W|B)load {sym} [off] ptr mem) x)
+(CMP(Q|L|W|B)load {sym} [off] ptr x mem) =>
   (CMP(Q|L|W|B) (MOV(Q|L|W|B)load <x.Type> {sym} [off] ptr mem) x)

For pointer comparisons that makes sense: a pointer-typed load must remain
visible to stack maps if it is spilled. In this repro, however, the CMPQload
right operand is an int64 value and x.Type is the narrow unsigned left
operand type.

The unpatched Go 1.27.1 SSA for the failing uint8,int64 branch contains:

v130 = MOVQload <uint8> v92 v107 : CX
v156 = StoreReg <uint8> v130 : .autotmp_36[uint8]
CALLstatic {AuxCall{runtime.racefuncexit}}
v113 = LoadReg <uint8> v156 : CX
v125 = CMPQ <flags> v113 v110

So the machine load is 64-bit, but the SSA type is uint8. The race detector
call forces the value through a spill slot, and regalloc/codegen spills it using
the narrow SSA type.

The resulting assembly difference is just:

- MOVQ CX, 0x18(SP)
+ MOVB CL, 0xe(SP)
  CALL runtime.racefuncexit(SB)
- MOVQ 0x18(SP), CX
+ MOVZX 0xe(SP), CX

For int64(-1), the low byte is 255. The Go 1.27.1 code therefore compares
the left uint8(0) against 255, not against the original negative int64,
and returns 1.

A directed check patched only the generated Go 1.27.1 AMD64
CMPQload/CMPQloadidx splitload rewrites back from x.Type to UInt64,
rebuilt cmd/compile, and reran this repro:

unpatched Go 1.27.1 go run -race main.go: panic: bad
patched Go 1.27.1 go run -race main.go: pass

That points to AMD64 splitload type selection plus spill width, not a runtime
race detector semantic issue. The race detector call appears to expose the bad
SSA type by forcing a spill.

Activity

  1. randall77 commented on Sep 11, 2026

    @randall77
    Contributor

    I can confirm the breakage. Still broken at tip.
    @josharian

  2. JunyangShao commented on Sep 11, 2026

    @JunyangShao
    Contributor

    @golang/compiler

  3. added
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Sep 11, 2026
  4. josharian commented on Sep 11, 2026

    @josharian
    Contributor

    Thanks. Will look soon.

  5. gopherbot commented on Sep 12, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/831464 mentions this issue: cmd/compile: preserve the width of split comparison loads

  6. bdhill-arista commented on Sep 21, 2026

    @bdhill-arista

    Any updates on this?

    Can we get this fix into the next minor release?

  7. josharian commented on Sep 22, 2026

    @josharian
    Contributor

    Sorry, we got caught up in a question of the correct flavor of the fix. Will continue that conversation...

  8. bdhill-arista commented on Sep 25, 2026

    @bdhill-arista

    @josharian saw your CL is still pending

    We're wondering if this will make it into Go 1.27.2, and if it can be added to the Go 1.27.2 milestone

  9. bdhill-arista commented on Sep 30, 2026

    @bdhill-arista

    @josharian @randall77 following up on this.

    Is something you'd consider backporting to Go 1.27.2?

  10. bdhill-arista commented on Sep 30, 2026

    @bdhill-arista

    Or does this seem like not that bad of a bug (wasn't able to repro without race detector enabled)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.compiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions