go: cmd/compile: -d=checkptr should not reject unaligned pointers to non-pointer data
I’m testing some code with Go 1.14 and found that some tests are failing which passed in Go 1.13 while running under -race
.
This is due to the new checkptr checks that are enabled by -race
. Here’s a minimized repro:
// +build ignore
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
b := make([]byte, 20)
s := unsafeInts(b, 1) // with 0 or 8, no failure
fmt.Println(s)
}
func unsafeInts(b []byte, offs int) []int64 {
var s []int64
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
sh.Data = uintptr(unsafe.Pointer(&b[offs]))
sh.Cap = (len(b) - offs) / 8
sh.Len = sh.Cap
return s
}
$ go1.13.4 run -race checkptr.go
[0 0]
$ go1.14beta1 run -race checkptr.go
panic: runtime error: unsafe pointer conversion
goroutine 1 [running]:
reflect.Value.Int(...)
/home/caleb/sdk/go1.14beta1/src/reflect/value.go:974
fmt.(*pp).printValue(0xc0000a08f0, 0x5150a0, 0xc0000bc001, 0x186, 0x76, 0x1)
/home/caleb/sdk/go1.14beta1/src/fmt/print.go:749 +0x3694
fmt.(*pp).printValue(0xc0000a08f0, 0x5136e0, 0xc0000aa020, 0x97, 0x76, 0x0)
/home/caleb/sdk/go1.14beta1/src/fmt/print.go:869 +0xfd3
fmt.(*pp).printArg(0xc0000a08f0, 0x5136e0, 0xc0000aa020, 0x76)
/home/caleb/sdk/go1.14beta1/src/fmt/print.go:716 +0x25b
fmt.(*pp).doPrintln(0xc0000a08f0, 0xc00005af50, 0x1, 0x1)
/home/caleb/sdk/go1.14beta1/src/fmt/print.go:1173 +0xae
fmt.Fprintln(0x55d520, 0xc0000b8008, 0xc00005af50, 0x1, 0x1, 0x432142, 0xc000092058, 0x0)
/home/caleb/sdk/go1.14beta1/src/fmt/print.go:264 +0x66
fmt.Println(...)
/home/caleb/sdk/go1.14beta1/src/fmt/print.go:274
main.main()
/home/caleb/p/misc/checkptr/checkptr.go:14 +0x18f
exit status 2
$
This error isn’t particularly helpful (why is it unsafe?) and when I looked at the source, it still wasn’t clear to me what the problem is.
// Check that (*[n]elem)(p) is appropriately aligned.
// TODO(mdempsky): What about fieldAlign?
if uintptr(p)&(uintptr(elem.align)-1) != 0 {
throw("checkptr: unsafe pointer conversion")
}
Even if there’s some problem with unaligned pointers into the Go heap, in my application the backing data doesn’t come from make([]byte)
but rather from unix.Mmap
. And my CPU (amd64) should have no problem with reading unaligned data, right?
It’s also strange that the crash didn’t happen in my code, but inside the reflect-y fmt code that looked at my data. (Not that it matters, but in my real code the crash isn’t triggered by fmt but rather by go-cmp, also via use of reflect.Value.Int
.)
/cc @mdempsky
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 32 (28 by maintainers)
Commits related to this issue
- sha3: mark xorInUnaligned with go:nocheckptr It is unclear whether unaligned reads should be allowed, or if they are even actually a good idea here. However, while we figure that out, we should un-br... — committed to golang/crypto by bcmills 4 years ago
- sha3: remove go:nocheckptr annotation As of Go 1.14.1, -d=checkptr no longer errors on unaligned reads of non-pointer data. This reverts the code change (but not the test) from CL 222855. Fixes gol... — committed to golang/crypto by bcmills 4 years ago
Thanks for pointing that out, @zeebo. After a bit of discussion about whether we should keep the check just for ARM, we decided that we don’t really want to do an ARCH-specific checkptr check, especially since it only affects a small minority of deployments even on that architecture.
ARMv5 does a rotated load depending on the lower two bits of the address. See https://heyrick.eu/armwiki/Unaligned_data_access.
I wanted to add that the key observation for me, at least, was that unaligned access will either work or crash on all architectures we support (at least, we’re pretty sure; Cherry is confirming). That puts it in a different category from other things checkptr checks for.
We discussed this at the runtime meeting today and decided that we’d disable the alignment check if the base type of the pointer is pointer-free. So we’d still catch unaligned pointer accesses, but would allow other unaligned accesses to happen (on all architectures, even if that means they might fault).
I don’t see why this should be architecture specific. The point of
-d=checkptr
is to check for bad code. That shouldn’t be architecture specific. Bad code can run correctly on some architectures and not others, but we should always warn for it.So I think the question is: should
-d=checkptr
report conversions fromunsafe.Pointer
to an unaligned pointer? Clearly we should report conversions to an unaligned pointer to pointer type. It’s less clear for a pointer to integer type.I don’t have a good sense of the philosophy of
-d=checkptr
so I don’t know what the answer is.We only discussed not warning about unaligned pointers to integer values. We didn’t discuss whether they should be supported in any real sense.
There is a more general discussion to have about the interaction of unsafe pointers and points-to analysis, such as whether we should consider the possibility that a
*int64
and a*float64
might point to the same memory even in a package that does not import unsafe.I think we should declare unaligned pointers to be ok on at least x86. Maybe everywhere.
There are going to be too many people using unaligned pointers. I think we could break those people’s programs if we needed to, but I just don’t see a commensurate benefit in doing so.
The main benefit of the check is to catch people making alignment assumptions on x86, where their program would then fail on another architecture. That just doesn’t seem that much of a pain point to me. Even if it fails on another architecture, at least it does so loudly, without silently corrupting data (I hope: it would do so on early Alpha chips, but I don’t know any other arch that just ignores low bits of addresses when accessing memory).
I guess another benefit is just to catch people doing bad pointer arithmetic. That is a nice feature (and could potentially detect otherwise silent memory corruption bugs), but I don’t think that’s the intended purpose of this check.
Ack, it could certainly be clearer. I’m arguing my position here, but I’m not dogmatic about it. I’m open to whatever the group consensus is. (And so I’m definitely hoping more folks will weigh in with their thoughts/interpretations.)
Note that the Go spec uses “variable” to refer to any memory location used for storing a value. It’s not used exclusively in reference to declared variables such as introduced by the “var” keyword. E.g.,
new(T)
is defined as returning a pointer to a newly allocated variable; structs and arrays are defined as variables having sub-variables for each field/element.Moreover, by definition, a pointer of type *T is either nil or points to a variable of type T. There’s nothing else it can point to.
Note that the Go spec defines alignment as a property of variables, whereas
x
is an arbitrary expression (i.e., possibly non-addressable or even untyped). In that context, I interpret this wording as explaining how to handle things likeunsafe.Alignof(1)
, not that it only guarantees the alignment of declared variables.It sounds like three of our options are:
go:nocheckptr
annotation; update any flagged codeunsafe.Alignof
as @mdempsky mentioned in the preceding commentgo:nocheckptr
; update code to use this annotation where necessary(3) seems to be our de facto state in Go 1.14, but I don’t think it’s workable. [Edit: deleted some incorrect stuff here.] In my case, the flagged conversion occurs in reflect code called via fmt (demo code) or go-cmp (real code). There’s no place I can stick a
go:nocheckptr
annotation to make the problem go away. In general, the check may not be be triggered by the code that constructs the unaligned pointer, but instead by the code that reads it, which may be in any (possibly third-party) package.I think in a sense this comes down to a question of whether
unsafe.Alignof
indicates “required alignment” or only “preferred alignment.”My rationale behind
-d=checkptr
is based on interpretingunsafe.Alignof
as required alignment. That is,(*T)(unsafe.Pointer(u))
is only valid ifu % unsafe.Alignof(*new(T)) == 0
. And we defineunsafe.Alignof(*new(int))
to 8 even on amd64, hence-d=checkptr
raising an error.The rationale of code like sha3’s xor_unaligned though seems to be that
unsafe.Alignof
is merely preferred alignment. That is, that the Go runtime will guarantee variables have that alignment because it’s fastest, but (depending on the target CPU/OS) users may be able to still access variables with weaker alignment. And the question then is whether-d=checkptr
should recognize these CPU/OS-specific cases.One option would be: on x86, the compilers could define
unsafe.Alignof(*new(T)) == 1
for all numeric types T (i.e., required alignment), but then still layout variables in memory as we do today (i.e., with preferred alignment). The Go spec only requires that variables be at least aligned according tounsafe.Alignof
; it doesn’t preclude a compiler/runtime from consistently aligning them more generally.@twmb, my concern is that if we allow
checkptr
to have false-positives, folks will addgo:nocheckptr
annotations assumingamd64
semantics even on non-amd64
-specific functions — or, worse, will add annotations thinking they are working around a spurious alignment crash, and also end up suppressing a true-positive that was masked behind it.@cespare, the clarity of the error messages is #37488. Let’s leave that as a separate issue.