1
0
mirror of https://github.com/golang/go synced 2024-11-23 07:40:04 -07:00

cmd/compile: add -d=checkptr to validate unsafe.Pointer rules

This CL adds -d=checkptr as a compile-time option for adding
instrumentation to check that Go code is following unsafe.Pointer
safety rules dynamically. In particular, it currently checks two
things:

1. When converting unsafe.Pointer to *T, make sure the resulting
pointer is aligned appropriately for T.

2. When performing pointer arithmetic, if the result points to a Go
heap object, make sure we can find an unsafe.Pointer-typed operand
that pointed into the same object.

These checks are currently disabled for the runtime, and can also be
disabled through a new //go:nocheckptr annotation. The latter is
necessary for functions like strings.noescape, which intentionally
violate safety rules to workaround escape analysis limitations.

Fixes #22218.

Change-Id: If5a51273881d93048f74bcff10a3275c9c91da6a
Reviewed-on: https://go-review.googlesource.com/c/go/+/162237
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
This commit is contained in:
Matthew Dempsky 2019-02-12 19:40:42 -08:00
parent 3b003c3edb
commit 80a6fedea0
9 changed files with 159 additions and 1 deletions

View File

@ -181,13 +181,15 @@ var runtimeDecls = [...]struct {
{"racewriterange", funcTag, 119},
{"msanread", funcTag, 119},
{"msanwrite", funcTag, 119},
{"checkptrAlignment", funcTag, 120},
{"checkptrArithmetic", funcTag, 122},
{"x86HasPOPCNT", varTag, 15},
{"x86HasSSE41", varTag, 15},
{"arm64HasATOMICS", varTag, 15},
}
func runtimeTypes() []*types.Type {
var typs [120]*types.Type
var typs [123]*types.Type
typs[0] = types.Bytetype
typs[1] = types.NewPtr(typs[0])
typs[2] = types.Types[TANY]
@ -308,5 +310,8 @@ func runtimeTypes() []*types.Type {
typs[117] = functype(nil, []*Node{anonfield(typs[23]), anonfield(typs[23])}, []*Node{anonfield(typs[23])})
typs[118] = functype(nil, []*Node{anonfield(typs[50])}, nil)
typs[119] = functype(nil, []*Node{anonfield(typs[50]), anonfield(typs[50])}, nil)
typs[120] = functype(nil, []*Node{anonfield(typs[56]), anonfield(typs[1])}, nil)
typs[121] = types.NewSlice(typs[56])
typs[122] = functype(nil, []*Node{anonfield(typs[56]), anonfield(typs[121])}, nil)
return typs[:]
}

View File

@ -235,6 +235,9 @@ func racewriterange(addr, size uintptr)
func msanread(addr, size uintptr)
func msanwrite(addr, size uintptr)
func checkptrAlignment(unsafe.Pointer, *byte)
func checkptrArithmetic(unsafe.Pointer, []unsafe.Pointer)
// architecture variants
var x86HasPOPCNT bool
var x86HasSSE41 bool

View File

@ -135,6 +135,12 @@ func caninl(fn *Node) {
return
}
// If marked "go:nocheckptr" and -d checkptr compilation, don't inline.
if Debug_checkptr != 0 && fn.Func.Pragma&NoCheckPtr != 0 {
reason = "marked go:nocheckptr"
return
}
// If marked "go:cgo_unsafe_args", don't inline, since the
// function makes assumptions about its argument frame layout.
if fn.Func.Pragma&CgoUnsafeArgs != 0 {

View File

@ -35,6 +35,7 @@ const (
Norace // func must not have race detector annotations
Nosplit // func should not execute on separate stack
Noinline // func should not be inlined
NoCheckPtr // func should not be instrumented by checkptr
CgoUnsafeArgs // treat a pointer to one arg as a pointer to them all
UintptrEscapes // pointers converted to uintptr escape
@ -63,6 +64,8 @@ func pragmaValue(verb string) syntax.Pragma {
return Nosplit
case "go:noinline":
return Noinline
case "go:nocheckptr":
return NoCheckPtr
case "go:systemstack":
return Systemstack
case "go:nowritebarrier":

View File

@ -40,6 +40,7 @@ var (
var (
Debug_append int
Debug_checkptr int
Debug_closure int
Debug_compilelater int
debug_dclstack int
@ -65,6 +66,7 @@ var debugtab = []struct {
val interface{} // must be *int or *string
}{
{"append", "print information about append compilation", &Debug_append},
{"checkptr", "instrument unsafe pointer conversions", &Debug_checkptr},
{"closure", "print information about closure compilation", &Debug_closure},
{"compilelater", "compile functions as late as possible", &Debug_compilelater},
{"disablenil", "disable nil checks", &disable_checknil},
@ -433,6 +435,11 @@ func Main(archInit func(*Arch)) {
}
}
// Runtime can't use -d=checkptr, at least not yet.
if compiling_runtime {
Debug_checkptr = 0
}
// set via a -d flag
Ctxt.Debugpcln = Debug_pctab
if flagDWARF {

View File

@ -951,6 +951,16 @@ opswitch:
case OCONV, OCONVNOP:
n.Left = walkexpr(n.Left, init)
if n.Op == OCONVNOP && Debug_checkptr != 0 && Curfn.Func.Pragma&NoCheckPtr == 0 {
if n.Type.IsPtr() && n.Left.Type.Etype == TUNSAFEPTR { // unsafe.Pointer to *T
n = walkCheckPtrAlignment(n, init)
break
}
if n.Type.Etype == TUNSAFEPTR && n.Left.Type.Etype == TUINTPTR { // uintptr to unsafe.Pointer
n = walkCheckPtrArithmetic(n, init)
break
}
}
param, result := rtconvfn(n.Left.Type, n.Type)
if param == Txxx {
break
@ -3898,3 +3908,66 @@ func canMergeLoads() bool {
func isRuneCount(n *Node) bool {
return Debug['N'] == 0 && !instrumenting && n.Op == OLEN && n.Left.Op == OSTR2RUNES
}
func walkCheckPtrAlignment(n *Node, init *Nodes) *Node {
if n.Type.Elem().Alignment() == 1 {
return n
}
n.Left = cheapexpr(n.Left, init)
init.Append(mkcall("checkptrAlignment", nil, init, n.Left, typename(n.Type.Elem())))
return n
}
var walkCheckPtrArithmeticMarker byte
func walkCheckPtrArithmetic(n *Node, init *Nodes) *Node {
// Calling cheapexpr(n, init) below leads to a recursive call
// to walkexpr, which leads us back here again. Use n.Opt to
// prevent infinite loops.
if n.Opt() == &walkCheckPtrArithmeticMarker {
return n
}
n.SetOpt(&walkCheckPtrArithmeticMarker)
defer n.SetOpt(nil)
// TODO(mdempsky): Make stricter. We only need to exempt
// reflect.Value.Pointer and reflect.Value.UnsafeAddr.
switch n.Left.Op {
case OCALLFUNC, OCALLMETH, OCALLINTER:
return n
}
// Find original unsafe.Pointer operands involved in this
// arithmetic expression.
//
// "It is valid both to add and to subtract offsets from a
// pointer in this way. It is also valid to use &^ to round
// pointers, usually for alignment."
var originals []*Node
var walk func(n *Node)
walk = func(n *Node) {
switch n.Op {
case OADD:
walk(n.Left)
walk(n.Right)
case OSUB, OANDNOT:
walk(n.Left)
case OCONVNOP:
if n.Left.Type.Etype == TUNSAFEPTR {
n.Left = cheapexpr(n.Left, init)
originals = append(originals, n.Left)
}
}
}
walk(n.Left)
n = cheapexpr(n, init)
slice := mkdotargslice(types.NewSlice(types.Types[TUNSAFEPTR]), originals, init, nil)
slice.Esc = EscNone
slice.SetTransient(true)
init.Append(mkcall("checkptrArithmetic", nil, init, n, slice))
return n
}

View File

@ -1407,6 +1407,11 @@ func (v Value) OverflowUint(x uint64) bool {
panic(&ValueError{"reflect.Value.OverflowUint", v.kind()})
}
//go:nocheckptr
// This prevents inlining Value.Pointer when -d=checkptr is enabled,
// which ensures cmd/compile can recognize unsafe.Pointer(v.Pointer())
// and make an exception.
// Pointer returns v's value as a uintptr.
// It returns uintptr instead of unsafe.Pointer so that
// code using reflect cannot obtain unsafe.Pointers
@ -1914,6 +1919,11 @@ func (v Value) Uint() uint64 {
panic(&ValueError{"reflect.Value.Uint", v.kind()})
}
//go:nocheckptr
// This prevents inlining Value.UnsafeAddr when -d=checkptr is enabled,
// which ensures cmd/compile can recognize unsafe.Pointer(v.UnsafeAddr())
// and make an exception.
// UnsafeAddr returns a pointer to v's data.
// It is for advanced clients that also import the "unsafe" package.
// It panics if v is not addressable.

50
src/runtime/checkptr.go Normal file
View File

@ -0,0 +1,50 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package runtime
import "unsafe"
type ptrAlign struct {
ptr unsafe.Pointer
align uintptr
}
func checkptrAlignment(p unsafe.Pointer, elem *_type) {
// TODO(mdempsky): What about fieldAlign?
if uintptr(p)&(uintptr(elem.align)-1) != 0 {
panic(ptrAlign{p, uintptr(elem.align)})
}
}
type ptrArith struct {
ptr unsafe.Pointer
originals []unsafe.Pointer
}
func checkptrArithmetic(p unsafe.Pointer, originals []unsafe.Pointer) {
if 0 < uintptr(p) && uintptr(p) < minLegalPointer {
panic(ptrArith{p, originals})
}
base := checkptrBase(p)
if base == 0 {
return
}
for _, original := range originals {
if base == checkptrBase(original) {
return
}
}
panic(ptrArith{p, originals})
}
func checkptrBase(p unsafe.Pointer) uintptr {
base, _, _ := findObject(uintptr(p), 0, 0)
// TODO(mdempsky): If base == 0, then check if p points to the
// stack or a global variable.
return base
}

View File

@ -24,6 +24,7 @@ type Builder struct {
// USE CAREFULLY!
// This was copied from the runtime; see issues 23382 and 7921.
//go:nosplit
//go:nocheckptr
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)