]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/compile: add -d=checkptr to validate unsafe.Pointer rules
authorMatthew Dempsky <mdempsky@google.com>
Wed, 13 Feb 2019 03:40:42 +0000 (19:40 -0800)
committerMatthew Dempsky <mdempsky@google.com>
Thu, 17 Oct 2019 00:40:21 +0000 (00:40 +0000)
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>
src/cmd/compile/internal/gc/builtin.go
src/cmd/compile/internal/gc/builtin/runtime.go
src/cmd/compile/internal/gc/inl.go
src/cmd/compile/internal/gc/lex.go
src/cmd/compile/internal/gc/main.go
src/cmd/compile/internal/gc/walk.go
src/reflect/value.go
src/runtime/checkptr.go [new file with mode: 0644]
src/strings/builder.go

index 5f2c0b0cbb1aab6fe93db98e013fa3c989c565e5..a770356ea0472bd35048e547706565d825fed35c 100644 (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[:]
 }
index a820bde9efc1a41142daa4c45aca8b42bdd1d321..3e9055b2accfd32a556cb523a10b7ec63c6c01c2 100644 (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
index 9b2ecc073b720c904504b32e412a5b1ad1d3e0d1..7dfff34c37b7657db3d7a747c2099c3c0e78b164 100644 (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 {
index 557f98604d2fce44c659f1093aeaa38164e37f30..27ad9b561517789afe4c2cedab8abffe28e70427 100644 (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":
index 05aac9ecb2763fc9782e0e6b6ff9da78fabd4999..e7131f10a2e6a69fb3deb5f8c4fa598ebce084f6 100644 (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 {
index 39d1ab689d276c89386c5e074cbaa98adf34f38d..d8fc0abf3f58be7688bdfb8d2607b4667bede1cc 100644 (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
+}
index ffcb204cda5f5ece03cddca3661245b0bde5a91d..ab3b9643ee657a6ad1623fca1adefaca4b00e395 100644 (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.
diff --git a/src/runtime/checkptr.go b/src/runtime/checkptr.go
new file mode 100644 (file)
index 0000000..040a19a
--- /dev/null
@@ -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
+}
index 3f33a87508764fabc08c929a6a45c68f4eb90f7a..6ff151d74b2e87e1be45ad178b00383f4e9891bc 100644 (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)