Skip to content

cmd/compile: type-based alias resolving has problems #80583

Description

@randall77

There's something not quite right about how we detect when loads are stores are not to the same address, using type information.

I don't think this is currently a bug, but it is pretty precarious and probably needs to be reorganized to be more sound.

This comes up when we see a load and try to figure out what store it comes from. Consider this program:

package main

import "unsafe"

//go:noinline
func f(p, q *uintptr) uintptr {
	*p = 0
	*(**byte)(unsafe.Pointer(q)) = &g
	return *p
}

var g byte

func main() {
	var u uintptr
	println(f(&u, &u))
}

It currently prints the address of g when run with -N and prints 0 when run without -N.
That's fine. Using unsafe here is writing a pointer to a non-pointer location, which is not allowed. The compiler optimizes by realizing the intermediate write at line 8 can't affect the value of *p because it is writing a pointer, and forwards the 0 stored at line 7 to the load at line 9.

The trouble comes when different parts of our system disagree about the type of certain fields. In particular, this comes up with the type field of interfaces. Some parts of our system treat that field as a uintptr, others treat it as an unsafe.Pointer.

The compiler uses uintptrs to load/store the type field of interfaces. We can, and want, to do that because:

  1. The object pointed to by the type field is either something static (type descriptor, interface table) or something allocated by runtime or reflect and kept alive permanently in a side table.
  2. It avoids write barriers on the store side.

Unfortunately, other parts of the system use a pointer as the type field of an interface. For example, internal/abi.EmptyInterface or sync/atomic.efaceWords.

This can lead to issues where parts of our system are reading/writing as uintptrs and parts are reading/writing as unsafe.Pointers. Our type-based alias checking, in cmd/compile/internal/ssa/rewrite.go:disjointTypes will treat the two as distinct, meaning it might redirect some load to the wrong store.

I have not figured out how to trigger such a bug at tip currently. But something about this is very fragile. The following patch, which shouldn't really do anything, causes problems:

diff --git a/src/cmd/compile/internal/ssa/_gen/dec.rules b/src/cmd/compile/internal/ssa/_gen/dec.rules
index 97bc2a5978..3ccda284c2 100644
--- a/src/cmd/compile/internal/ssa/_gen/dec.rules
+++ b/src/cmd/compile/internal/ssa/_gen/dec.rules
@@ -86,7 +86,9 @@
 
 (Load <t> ptr mem) && t.IsInterface() =>
   (IMake
-    (Load <typ.Uintptr> ptr mem)
+    (Load <typ.Uintptr>
+      (OffPtr <typ.UintptrPtr> [0] ptr)
+      mem)
     (Load <typ.BytePtr>
       (OffPtr <typ.BytePtrPtr> [config.PtrSize] ptr)
       mem))
@@ -94,7 +96,10 @@
   (Store {typ.BytePtr}
     (OffPtr <typ.BytePtrPtr> [config.PtrSize] dst)
     data
-    (Store {typ.Uintptr} dst itab mem))
+    (Store {typ.Uintptr}
+      (OffPtr <typ.UintptrPtr> [0] dst)
+      itab
+      mem))

It is just enough to get the right types to the type-based alias analysis to cause problems. In particular, it changes the pointer type of the load and store from *any to *uintptr (which is, if anything, more correct).

With that patch, tests fail. For instance

> ../bin/go test sync/atomic -test.run=Value_CompareAndSwap/6
--- FAIL: TestValue_CompareAndSwap (0.00s)
    --- FAIL: TestValue_CompareAndSwap/6 (0.00s)
        value_test.go:242: got false, want true
FAIL
FAIL	sync/atomic	0.006s
FAIL

This is the kind of thing happening in that test:

package main

import "unsafe"

type iface struct {
	typ  *byte
	data *byte
}

//go:noinline
func f(old any) bool {
	i := old
	e := (*iface)(unsafe.Pointer(&i))
	e.typ = &myType
	return i == old
}

var myType byte

func main() {
	println(f(nil))
}

This should print false, but with the patch it prints true.

I'm not sure exactly what to do here. It would be nice if we could have the whole system agree about the type of the type word of interfaces. It would take some work, but maybe we could make that happen for the stdlib. It would be somewhat risky though, as I'm sure there's code outside the stdlib that pokes into interfaces and anything we do might interact with that code badly.

We could just accept that uintptr and pointers might be aliased. This would be the most conservative route, but may end up disabling most type-based alias info.

Other ideas?

@cuonglm @andreybokhanko

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

    Status
    In Progress

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions