tailscale/util/deephash/pointer_race.go
Joe Tsai 31bf3874d6
util/deephash: use unsafe.Pointer instead of reflect.Value (#5459)
Use of reflect.Value.SetXXX panics if the provided argument was
obtained from an unexported struct field.
Instead, pass an unsafe.Pointer around and convert to a
reflect.Value when necessary (i.e., for maps and interfaces).
Converting from unsafe.Pointer to reflect.Value guarantees that
none of the read-only bits will be populated.

When running in race mode, we attach type information to the pointer
so that we can type check every pointer operation.
This also type-checks that direct memory hashing is within
the valid range of a struct value.

We add test cases that previously caused deephash to panic,
but now pass.

Performance:

	name              old time/op    new time/op    delta
	Hash              14.1µs ± 1%    14.1µs ± 1%    ~     (p=0.590 n=10+9)
	HashPacketFilter  2.53µs ± 2%    2.44µs ± 1%  -3.79%  (p=0.000 n=9+10)
	TailcfgNode       1.45µs ± 1%    1.43µs ± 0%  -1.36%  (p=0.000 n=9+9)
	HashArray         318ns ± 2%     318ns ± 2%    ~      (p=0.541 n=10+10)
	HashMapAcyclic    32.9µs ± 1%    31.6µs ± 1%  -4.16%  (p=0.000 n=10+9)

There is a slight performance gain due to the use of unsafe.Pointer
over reflect.Value methods. Also, passing an unsafe.Pointer (1 word)
on the stack is cheaper than passing a reflect.Value (3 words).

Performance gains are diminishing since SHA-256 hashing now dominates the runtime.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 12:30:35 -07:00

101 lines
3.3 KiB
Go

// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build race
package deephash
import (
"fmt"
"net/netip"
"reflect"
"time"
)
// pointer is a typed pointer that performs safety checks for every operation.
type pointer struct {
unsafePointer
t reflect.Type // type of pointed-at value; may be nil
n uintptr // size of valid memory after p
}
// pointerOf returns a pointer from v, which must be a reflect.Pointer.
func pointerOf(v reflect.Value) pointer {
assert(v.Kind() == reflect.Pointer, "got %v, want pointer", v.Kind())
te := v.Type().Elem()
return pointer{unsafePointerOf(v), te, te.Size()}
}
func (p pointer) pointerElem() pointer {
assert(p.t.Kind() == reflect.Pointer, "got %v, want pointer", p.t.Kind())
te := p.t.Elem()
return pointer{p.unsafePointer.pointerElem(), te, te.Size()}
}
func (p pointer) sliceLen() int {
assert(p.t.Kind() == reflect.Slice, "got %v, want slice", p.t.Kind())
return p.unsafePointer.sliceLen()
}
func (p pointer) sliceArray() pointer {
assert(p.t.Kind() == reflect.Slice, "got %v, want slice", p.t.Kind())
n := p.sliceLen()
assert(n >= 0, "got negative slice length %d", n)
ta := reflect.ArrayOf(n, p.t.Elem())
return pointer{p.unsafePointer.sliceArray(), ta, ta.Size()}
}
func (p pointer) arrayIndex(index int, size uintptr) pointer {
assert(p.t.Kind() == reflect.Array, "got %v, want array", p.t.Kind())
assert(0 <= index && index < p.t.Len(), "got array of size %d, want to access element %d", p.t.Len(), index)
assert(p.t.Elem().Size() == size, "got element size of %d, want %d", p.t.Elem().Size(), size)
te := p.t.Elem()
return pointer{p.unsafePointer.arrayIndex(index, size), te, te.Size()}
}
func (p pointer) structField(index int, offset, size uintptr) pointer {
assert(p.t.Kind() == reflect.Struct, "got %v, want struct", p.t.Kind())
assert(p.n >= offset, "got size of %d, want excessive start offset of %d", p.n, offset)
assert(p.n >= offset+size, "got size of %d, want excessive end offset of %d", p.n, offset+size)
if index < 0 {
return pointer{p.unsafePointer.structField(index, offset, size), nil, size}
}
sf := p.t.Field(index)
t := sf.Type
assert(sf.Offset == offset, "got offset of %d, want offset %d", sf.Offset, offset)
assert(t.Size() == size, "got size of %d, want size %d", t.Size(), size)
return pointer{p.unsafePointer.structField(index, offset, size), t, t.Size()}
}
func (p pointer) asString() *string {
assert(p.t.Kind() == reflect.String, "got %v, want string", p.t)
return p.unsafePointer.asString()
}
func (p pointer) asTime() *time.Time {
assert(p.t == timeTimeType, "got %v, want %v", p.t, timeTimeType)
return p.unsafePointer.asTime()
}
func (p pointer) asAddr() *netip.Addr {
assert(p.t == netipAddrType, "got %v, want %v", p.t, netipAddrType)
return p.unsafePointer.asAddr()
}
func (p pointer) asValue(typ reflect.Type) reflect.Value {
assert(p.t == typ, "got %v, want %v", p.t, typ)
return p.unsafePointer.asValue(typ)
}
func (p pointer) asMemory(size uintptr) []byte {
assert(p.n >= size, "got size of %d, want excessive size of %d", p.n, size)
return p.unsafePointer.asMemory(size)
}
func assert(b bool, f string, a ...any) {
if !b {
panic(fmt.Sprintf(f, a...))
}
}