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:
- 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.
- 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
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:
It currently prints the address of
gwhen run with-Nand prints0when 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
*pbecause it is writing a pointer, and forwards the0stored 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 anunsafe.Pointer.The compiler uses
uintptrs to load/store the type field of interfaces. We can, and want, to do that because:runtimeorreflectand kept alive permanently in a side table.Unfortunately, other parts of the system use a pointer as the type field of an interface. For example,
internal/abi.EmptyInterfaceorsync/atomic.efaceWords.This can lead to issues where parts of our system are reading/writing as
uintptrs and parts are reading/writing asunsafe.Pointers. Our type-based alias checking, incmd/compile/internal/ssa/rewrite.go:disjointTypeswill 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:
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
*anyto*uintptr(which is, if anything, more correct).With that patch, tests fail. For instance
This is the kind of thing happening in that test:
This should print
false, but with the patch it printstrue.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