mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Add automatic lock inference and globals support.
Lock inference will apply annotations to all fields that seem to be protected. This is currently disabled for all code by default, but it can be enabled as annotations are applied more broadly. PiperOrigin-RevId: 407501915
This commit is contained in:
committed by
gVisor bot
parent
5185548e15
commit
80cba65bd8
@@ -48,6 +48,7 @@ global:
|
||||
- "duplicate import"
|
||||
# These will never be annotated.
|
||||
- "unexpected call to atomic function"
|
||||
- "may require checklocks annotation for"
|
||||
# Generated proto code creates declarations like 'var start int = iNdEx'
|
||||
- "should omit type .* from declaration; it will be inferred from the right-hand side"
|
||||
internal:
|
||||
@@ -63,6 +64,7 @@ global:
|
||||
- "unexpected call to atomic function.*"
|
||||
- "return with unexpected locks held.*"
|
||||
- "incompatible return states.*"
|
||||
- "may require checklocks annotation for.*"
|
||||
exclude:
|
||||
# Generated: exempt all.
|
||||
- pkg/shim/runtimeoptions/runtimeoptions_cri.go
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CheckLocks Analyzer
|
||||
|
||||
<!--* freshness: { owner: 'gvisor-eng' reviewed: '2021-10-15' } *-->
|
||||
<!--* freshness: { owner: 'gvisor-eng' reviewed: '2021-10-20' } *-->
|
||||
|
||||
Checklocks is an analyzer for lock and atomic constraints. The analyzer relies
|
||||
on explicit annotations to identify fields that should be checked for access.
|
||||
@@ -75,7 +75,26 @@ annotation refers either to something that is not a 'sync.Mutex' or
|
||||
'sync.RWMutex' or where the field does not exist at all. This will prevent the
|
||||
annotations from becoming stale over time as fields are renamed, etc.
|
||||
|
||||
# Currently not supported
|
||||
## Lock suggestions
|
||||
|
||||
Based on locks held during field access, the analyzer will suggest annotations.
|
||||
These can be ignored with the standard `+checklocksignore` annotation.
|
||||
|
||||
The annotation will be generated when the lock is held the vast majority of the
|
||||
time the field is accessed. Note that it is possible for this frequency to be
|
||||
greater than 100%, if the lock is held multiple times. For example:
|
||||
|
||||
```go
|
||||
func foo(ts1 *testStruct, ts2 *testStruct) {
|
||||
ts1.Lock()
|
||||
ts2.Lock()
|
||||
ts1.gaurdedField = 1 // 200% locks held.
|
||||
ts1.Unlock()
|
||||
ts2.Unlock()
|
||||
}
|
||||
```
|
||||
|
||||
## Currently not supported
|
||||
|
||||
1. Anonymous functions are not correctly evaluated. The analyzer does not
|
||||
currently support specifying annotations on anonymous functions as a result
|
||||
|
||||
+150
-42
@@ -168,19 +168,19 @@ func resolveStruct(typ types.Type) (*types.Struct, bool) {
|
||||
|
||||
func findField(typ types.Type, field int) (types.Object, bool) {
|
||||
structType, ok := resolveStruct(typ)
|
||||
if !ok {
|
||||
if !ok || field >= structType.NumFields() {
|
||||
return nil, false
|
||||
}
|
||||
return structType.Field(field), true
|
||||
}
|
||||
|
||||
// instructionWithReferrers is a generalization over ssa.Field, ssa.FieldAddr.
|
||||
type instructionWithReferrers interface {
|
||||
ssa.Instruction
|
||||
// almostInst is a generalization over ssa.Field, ssa.FieldAddr, ssa.Global.
|
||||
type almostInst interface {
|
||||
Pos() token.Pos
|
||||
Referrers() *[]ssa.Instruction
|
||||
}
|
||||
|
||||
// checkFieldAccess checks the validity of a field access.
|
||||
// checkGuards checks the guards held.
|
||||
//
|
||||
// This also enforces atomicity constraints for fields that must be accessed
|
||||
// atomically. The parameter isWrite indicates whether this field is used
|
||||
@@ -188,41 +188,46 @@ type instructionWithReferrers interface {
|
||||
//
|
||||
// Note that this function is not called if lff.Ignore is true, since it cannot
|
||||
// discover any local anonymous functions or closures.
|
||||
func (pc *passContext) checkFieldAccess(inst instructionWithReferrers, structObj ssa.Value, field int, ls *lockState, isWrite bool) {
|
||||
func (pc *passContext) checkGuards(inst almostInst, from ssa.Value, accessObj types.Object, ls *lockState, isWrite bool) {
|
||||
var (
|
||||
lff lockFieldFacts
|
||||
lgf lockGuardFacts
|
||||
guardsFound int
|
||||
guardsHeld int
|
||||
guardsHeld = make(map[string]struct{}) // Keyed by resolved string.
|
||||
)
|
||||
|
||||
fieldObj, _ := findField(structObj.Type(), field)
|
||||
pc.pass.ImportObjectFact(fieldObj, &lff)
|
||||
pc.pass.ImportObjectFact(fieldObj, &lgf)
|
||||
// Load the facts for the object accessed.
|
||||
pc.pass.ImportObjectFact(accessObj, &lgf)
|
||||
|
||||
for guardName, fl := range lgf.GuardedBy {
|
||||
// Check guards held.
|
||||
for guardName, fgr := range lgf.GuardedBy {
|
||||
guardsFound++
|
||||
r := fl.resolve(structObj)
|
||||
r := fgr.resolveField(pc, ls, from)
|
||||
if !r.valid() {
|
||||
// See above; this cannot be forced.
|
||||
pc.maybeFail(inst.Pos(), "field %s cannot be resolved", guardName)
|
||||
continue
|
||||
}
|
||||
s, ok := ls.isHeld(r, isWrite)
|
||||
if ok {
|
||||
guardsHeld++
|
||||
guardsHeld[s] = struct{}{}
|
||||
continue
|
||||
}
|
||||
if _, ok := pc.forced[pc.positionKey(inst.Pos())]; ok {
|
||||
// Mark this as locked, since it has been forced. All
|
||||
// forces are treated as an exclusive lock.
|
||||
ls.lockField(r, true /* exclusive */)
|
||||
guardsHeld++
|
||||
s, _ := ls.lockField(r, true /* exclusive */)
|
||||
guardsHeld[s] = struct{}{}
|
||||
continue
|
||||
}
|
||||
// Note that we may allow this if the disposition is atomic,
|
||||
// and we are allowing atomic reads only. This will fall into
|
||||
// the atomic disposition check below, which asserts that the
|
||||
// access is atomic. Further, guardsHeld < guardsFound will be
|
||||
// true for this case, so we require it to be read-only.
|
||||
// access is atomic. Further, len(guardsHeld) < guardsFound
|
||||
// will be true for this case, so we require it to be
|
||||
// read-only.
|
||||
if lgf.AtomicDisposition != atomicRequired {
|
||||
// There is no force key, no atomic access and no lock held.
|
||||
pc.maybeFail(inst.Pos(), "invalid field access, must hold %s (%s) when accessing %s (locks: %s)", guardName, s, fieldObj.Name(), ls.String())
|
||||
pc.maybeFail(inst.Pos(), "invalid field access, %s (%s) must be locked when accessing %s (locks: %s)", guardName, s, accessObj.Name(), ls.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,25 +235,75 @@ func (pc *passContext) checkFieldAccess(inst instructionWithReferrers, structObj
|
||||
switch lgf.AtomicDisposition {
|
||||
case atomicRequired:
|
||||
// Check that this is used safely as an input.
|
||||
readOnly := guardsHeld < guardsFound
|
||||
readOnly := len(guardsHeld) < guardsFound
|
||||
if refs := inst.Referrers(); refs != nil {
|
||||
for _, otherInst := range *refs {
|
||||
pc.checkAtomicCall(otherInst, fieldObj, true, readOnly)
|
||||
pc.checkAtomicCall(otherInst, accessObj, true, readOnly)
|
||||
}
|
||||
}
|
||||
// Check that this is not otherwise written non-atomically,
|
||||
// even if we do hold all the locks.
|
||||
if isWrite {
|
||||
pc.maybeFail(inst.Pos(), "non-atomic write of field %s, writes must still be atomic with locks held (locks: %s)", fieldObj.Name(), ls.String())
|
||||
pc.maybeFail(inst.Pos(), "non-atomic write of field %s, writes must still be atomic with locks held (locks: %s)", accessObj.Name(), ls.String())
|
||||
}
|
||||
case atomicDisallow:
|
||||
// Check that this is *not* used atomically.
|
||||
if refs := inst.Referrers(); refs != nil {
|
||||
for _, otherInst := range *refs {
|
||||
pc.checkAtomicCall(otherInst, fieldObj, false, false)
|
||||
pc.checkAtomicCall(otherInst, accessObj, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check inferred locks.
|
||||
if accessObj.Pkg() == pc.pass.Pkg {
|
||||
oo := pc.observationsFor(accessObj)
|
||||
oo.total++
|
||||
for s, info := range ls.lockedMutexes {
|
||||
// Is this an object for which we have facts? If there
|
||||
// is no ability to name this object, then we don't
|
||||
// bother with any inferrence. We also ignore any self
|
||||
// references (e.g. accessing a mutex while you are
|
||||
// holding that exact mutex).
|
||||
if info.object == nil || accessObj == info.object {
|
||||
continue
|
||||
}
|
||||
// Has this already been held?
|
||||
if _, ok := guardsHeld[s]; ok {
|
||||
oo.counts[info.object]++
|
||||
continue
|
||||
}
|
||||
// Is this a global? Record directly.
|
||||
if _, ok := from.(*ssa.Global); ok {
|
||||
oo.counts[info.object]++
|
||||
continue
|
||||
}
|
||||
// Is the object a sibling to the accessObj? We need to
|
||||
// check all fields and see if they match. We accept
|
||||
// only siblings and globals for this recommendation.
|
||||
structType, ok := resolveStruct(from.Type())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i := 0; i < structType.NumFields(); i++ {
|
||||
if fieldObj := structType.Field(i); fieldObj == info.object {
|
||||
// Add to the maybe list.
|
||||
oo.counts[info.object]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkFieldAccess checks the validity of a field access.
|
||||
func (pc *passContext) checkFieldAccess(inst almostInst, structObj ssa.Value, field int, ls *lockState, isWrite bool) {
|
||||
fieldObj, _ := findField(structObj.Type(), field)
|
||||
pc.checkGuards(inst, structObj, fieldObj, ls, isWrite)
|
||||
}
|
||||
|
||||
// checkGlobalAccess checks the validity of a global access.
|
||||
func (pc *passContext) checkGlobalAccess(g *ssa.Global, ls *lockState, isWrite bool) {
|
||||
pc.checkGuards(g, g, g.Object(), ls, isWrite)
|
||||
}
|
||||
|
||||
func (pc *passContext) checkCall(call callCommon, lff *lockFunctionFacts, ls *lockState) {
|
||||
@@ -320,8 +375,13 @@ func (pc *passContext) postFunctionCallUpdate(call callCommon, lff *lockFunction
|
||||
if fg.IsAlias && !aliases {
|
||||
continue
|
||||
}
|
||||
r := fg.resolveCall(call.Common().Args, call.Value())
|
||||
if s, ok := ls.unlockField(r, fg.Exclusive); !ok {
|
||||
r := fg.Resolver.resolveCall(pc, ls, call.Common().Args, call.Value())
|
||||
if !r.valid() {
|
||||
// See above: this cannot be forced.
|
||||
pc.maybeFail(call.Pos(), "field %s cannot be resolved", fieldName)
|
||||
continue
|
||||
}
|
||||
if s, ok := ls.unlockField(r, fg.Exclusive); !ok && !lff.Ignore {
|
||||
if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok && !lff.Ignore {
|
||||
pc.maybeFail(call.Pos(), "attempt to release %s (%s), but not held (locks: %s)", fieldName, s, ls.String())
|
||||
}
|
||||
@@ -337,8 +397,8 @@ func (pc *passContext) postFunctionCallUpdate(call callCommon, lff *lockFunction
|
||||
continue
|
||||
}
|
||||
// Acquire the lock per the annotation.
|
||||
r := fg.resolveCall(call.Common().Args, call.Value())
|
||||
if s, ok := ls.lockField(r, fg.Exclusive); !ok {
|
||||
r := fg.Resolver.resolveCall(pc, ls, call.Common().Args, call.Value())
|
||||
if s, ok := ls.lockField(r, fg.Exclusive); !ok && !lff.Ignore {
|
||||
if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok && !lff.Ignore {
|
||||
pc.maybeFail(call.Pos(), "attempt to acquire %s (%s), but already held (locks: %s)", fieldName, s, ls.String())
|
||||
}
|
||||
@@ -361,17 +421,15 @@ func exclusiveStr(exclusive bool) string {
|
||||
// instruction order).
|
||||
func (pc *passContext) checkFunctionCall(call callCommon, fn *types.Func, lff *lockFunctionFacts, ls *lockState) {
|
||||
// Extract the "receiver" properly.
|
||||
var rcvr ssa.Value
|
||||
var args []ssa.Value
|
||||
if call.Common().Method != nil {
|
||||
// This is an interface dispatch for sync.Locker.
|
||||
rcvr = call.Common().Value
|
||||
} else if args := call.Common().Args; len(args) > 0 && fn.Type().(*types.Signature).Recv() != nil {
|
||||
args = append([]ssa.Value{call.Common().Value}, call.Common().Args...)
|
||||
} else {
|
||||
// This matches the signature for the relevant
|
||||
// sync.Lock/sync.Unlock functions below.
|
||||
rcvr = args[0]
|
||||
args = call.Common().Args
|
||||
}
|
||||
// Note that at this point, rcvr may be nil, but it should not match any
|
||||
// of the function signatures below where rcvr may be used.
|
||||
|
||||
// Check all guards required are held. Note that this explicitly does
|
||||
// not include aliases, hence false being passed below.
|
||||
@@ -379,7 +437,7 @@ func (pc *passContext) checkFunctionCall(call callCommon, fn *types.Func, lff *l
|
||||
if fg.IsAlias {
|
||||
continue
|
||||
}
|
||||
r := fg.resolveCall(call.Common().Args, call.Value())
|
||||
r := fg.Resolver.resolveCall(pc, ls, args, call.Value())
|
||||
if s, ok := ls.isHeld(r, fg.Exclusive); !ok {
|
||||
if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok && !lff.Ignore {
|
||||
pc.maybeFail(call.Pos(), "must hold %s %s (%s) to call %s, but not held (locks: %s)", fieldName, exclusiveStr(fg.Exclusive), s, fn.Name(), ls.String())
|
||||
@@ -395,15 +453,16 @@ func (pc *passContext) checkFunctionCall(call callCommon, fn *types.Func, lff *l
|
||||
|
||||
// Check if it's a method dispatch for something in the sync package.
|
||||
// See: https://godoc.org/golang.org/x/tools/go/ssa#Function
|
||||
if fn.Pkg() != nil && fn.Pkg().Name() == "sync" {
|
||||
if fn.Pkg() != nil && fn.Pkg().Name() == "sync" && len(args) > 0 {
|
||||
rv := makeResolvedValue(args[0], nil)
|
||||
isExclusive := false
|
||||
switch fn.Name() {
|
||||
case "Lock":
|
||||
isExclusive = true
|
||||
fallthrough
|
||||
case "RLock":
|
||||
if s, ok := ls.lockField(resolvedValue{value: rcvr, valid: true}, isExclusive); !ok {
|
||||
if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok && !lff.Ignore {
|
||||
if s, ok := ls.lockField(rv, isExclusive); !ok && !lff.Ignore {
|
||||
if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok {
|
||||
// Double locking a mutex that is already locked.
|
||||
pc.maybeFail(call.Pos(), "%s already locked (locks: %s)", s, ls.String())
|
||||
}
|
||||
@@ -412,14 +471,14 @@ func (pc *passContext) checkFunctionCall(call callCommon, fn *types.Func, lff *l
|
||||
isExclusive = true
|
||||
fallthrough
|
||||
case "RUnlock":
|
||||
if s, ok := ls.unlockField(resolvedValue{value: rcvr, valid: true}, isExclusive); !ok {
|
||||
if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok && !lff.Ignore {
|
||||
if s, ok := ls.unlockField(rv, isExclusive); !ok && !lff.Ignore {
|
||||
if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok {
|
||||
// Unlocking something that is already unlocked.
|
||||
pc.maybeFail(call.Pos(), "%s already unlocked or locked differently (locks: %s)", s, ls.String())
|
||||
}
|
||||
}
|
||||
case "DowngradeLock":
|
||||
if s, ok := ls.downgradeField(resolvedValue{value: call.Common().Args[0], valid: true}); !ok {
|
||||
if s, ok := ls.downgradeField(rv); !ok {
|
||||
if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok && !lff.Ignore {
|
||||
// Downgrading something that may not be downgraded.
|
||||
pc.maybeFail(call.Pos(), "%s already unlocked or not exclusive (locks: %s)", s, ls.String())
|
||||
@@ -497,6 +556,24 @@ type callCommon interface {
|
||||
// checkInstruction checks the legality the single instruction based on the
|
||||
// current lockState.
|
||||
func (pc *passContext) checkInstruction(inst ssa.Instruction, lff *lockFunctionFacts, ls *lockState) (*ssa.Return, *lockState) {
|
||||
// Record any observed globals, and check for violations. The global
|
||||
// value is not itself an instruction, but we check all referrers to
|
||||
// see where they are consumed.
|
||||
var stackLocal [16]*ssa.Value
|
||||
ops := inst.Operands(stackLocal[:])
|
||||
for _, v := range ops {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
g, ok := (*v).(*ssa.Global)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, isWrite := inst.(*ssa.Store)
|
||||
pc.checkGlobalAccess(g, ls, isWrite)
|
||||
}
|
||||
|
||||
// Process the instruction.
|
||||
switch x := inst.(type) {
|
||||
case *ssa.Store:
|
||||
// Record that this value is holding this other value. This is
|
||||
@@ -611,7 +688,12 @@ func (pc *passContext) checkBasicBlock(fn *ssa.Function, block *ssa.BasicBlock,
|
||||
failed := false
|
||||
// Validate held locks.
|
||||
for fieldName, fg := range lff.HeldOnExit {
|
||||
r := fg.resolveStatic(fn, rv)
|
||||
r := fg.Resolver.resolveStatic(pc, ls, fn, rv)
|
||||
if !r.valid() {
|
||||
// This cannot be forced, since we have no reference.
|
||||
pc.maybeFail(rv.Pos(), "lock %s cannot be resolved", fieldName)
|
||||
continue
|
||||
}
|
||||
if s, ok := rls.isHeld(r, fg.Exclusive); !ok {
|
||||
if _, ok := pc.forced[pc.positionKey(rv.Pos())]; !ok && !lff.Ignore {
|
||||
pc.maybeFail(rv.Pos(), "lock %s (%s) not held %s (locks: %s)", fieldName, s, exclusiveStr(fg.Exclusive), rls.String())
|
||||
@@ -684,7 +766,12 @@ func (pc *passContext) checkFunction(call callCommon, fn *ssa.Function, lff *loc
|
||||
for fieldName, fg := range lff.HeldOnEntry {
|
||||
// The first is the method object itself so we skip that when looking
|
||||
// for receiver/function parameters.
|
||||
r := fg.resolveStatic(fn, call.Value())
|
||||
r := fg.Resolver.resolveStatic(pc, ls, fn, call.Value())
|
||||
if !r.valid() {
|
||||
// See above: this cannot be forced.
|
||||
pc.maybeFail(fn.Pos(), "lock %s cannot be resolved", fieldName)
|
||||
continue
|
||||
}
|
||||
if s, ok := ls.lockField(r, fg.Exclusive); !ok && !lff.Ignore {
|
||||
// This can only happen if the same value is declared
|
||||
// multiple times, and should be caught by the earlier
|
||||
@@ -710,3 +797,24 @@ func (pc *passContext) checkFunction(call callCommon, fn *ssa.Function, lff *loc
|
||||
pc.postFunctionCallUpdate(call, lff, parent, true /* aliases */)
|
||||
}
|
||||
}
|
||||
|
||||
// checkInferred checks for any inferred lock annotations.
|
||||
func (pc *passContext) checkInferred() {
|
||||
for obj, oo := range pc.observations {
|
||||
var lgf lockGuardFacts
|
||||
pc.pass.ImportObjectFact(obj, &lgf)
|
||||
for other, count := range oo.counts {
|
||||
// Is this already a guard?
|
||||
if _, ok := lgf.GuardedBy[other.Name()]; ok {
|
||||
continue
|
||||
}
|
||||
// Check to see if this field is used with a given lock
|
||||
// held above the threshold. If yes, provide a helpful
|
||||
// hint that this may something you wish to annotate.
|
||||
const threshold = 0.9
|
||||
if usage := float64(count) / float64(oo.total); usage >= threshold {
|
||||
pc.maybeFail(obj.Pos(), "may require checklocks annotation for %s, used with lock held %2.0f%% of the time", other.Name(), usage*100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,20 +30,61 @@ import (
|
||||
|
||||
// Analyzer is the main entrypoint.
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "checklocks",
|
||||
Doc: "checks lock preconditions on functions and fields",
|
||||
Run: run,
|
||||
Requires: []*analysis.Analyzer{buildssa.Analyzer},
|
||||
FactTypes: []analysis.Fact{(*atomicAlignment)(nil), (*lockFieldFacts)(nil), (*lockGuardFacts)(nil), (*lockFunctionFacts)(nil)},
|
||||
Name: "checklocks",
|
||||
Doc: "checks lock preconditions on functions and fields",
|
||||
Run: run,
|
||||
Requires: []*analysis.Analyzer{buildssa.Analyzer},
|
||||
FactTypes: []analysis.Fact{
|
||||
(*atomicAlignment)(nil),
|
||||
(*lockGuardFacts)(nil),
|
||||
(*lockFunctionFacts)(nil),
|
||||
},
|
||||
}
|
||||
|
||||
// objectObservations tracks lock correlations.
|
||||
type objectObservations struct {
|
||||
counts map[types.Object]int
|
||||
total int
|
||||
}
|
||||
|
||||
// passContext is a pass with additional expected failures.
|
||||
type passContext struct {
|
||||
pass *analysis.Pass
|
||||
failures map[positionKey]*failData
|
||||
exemptions map[positionKey]struct{}
|
||||
forced map[positionKey]struct{}
|
||||
functions map[*ssa.Function]struct{}
|
||||
pass *analysis.Pass
|
||||
failures map[positionKey]*failData
|
||||
exemptions map[positionKey]struct{}
|
||||
forced map[positionKey]struct{}
|
||||
functions map[*ssa.Function]struct{}
|
||||
observations map[types.Object]*objectObservations
|
||||
}
|
||||
|
||||
// observationsFor retrieves observations for the given object.
|
||||
func (pc *passContext) observationsFor(obj types.Object) *objectObservations {
|
||||
if pc.observations == nil {
|
||||
pc.observations = make(map[types.Object]*objectObservations)
|
||||
}
|
||||
oo, ok := pc.observations[obj]
|
||||
if !ok {
|
||||
oo = &objectObservations{
|
||||
counts: make(map[types.Object]int),
|
||||
}
|
||||
pc.observations[obj] = oo
|
||||
}
|
||||
return oo
|
||||
}
|
||||
|
||||
// forAllGlobals applies the given function to all globals.
|
||||
func (pc *passContext) forAllGlobals(fn func(ts *ast.ValueSpec)) {
|
||||
for _, f := range pc.pass.Files {
|
||||
for _, decl := range f.Decls {
|
||||
d, ok := decl.(*ast.GenDecl)
|
||||
if !ok || d.Tok != token.VAR {
|
||||
continue
|
||||
}
|
||||
for _, gs := range d.Specs {
|
||||
fn(gs.(*ast.ValueSpec))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// forAllTypes applies the given function over all types.
|
||||
@@ -88,16 +129,17 @@ func run(pass *analysis.Pass) (interface{}, error) {
|
||||
pc.extractLineFailures()
|
||||
|
||||
// Find all struct declarations and export relevant facts.
|
||||
pc.forAllTypes(func(ts *ast.TypeSpec) {
|
||||
if ss, ok := ts.Type.(*ast.StructType); ok {
|
||||
structType := pc.pass.TypesInfo.TypeOf(ts.Name).Underlying().(*types.Struct)
|
||||
pc.exportLockFieldFacts(structType, ss)
|
||||
pc.forAllGlobals(func(vs *ast.ValueSpec) {
|
||||
if ss, ok := vs.Type.(*ast.StructType); ok {
|
||||
structType := pc.pass.TypesInfo.TypeOf(vs.Type).Underlying().(*types.Struct)
|
||||
pc.structLockGuardFacts(structType, ss)
|
||||
}
|
||||
pc.globalLockGuardFacts(vs)
|
||||
})
|
||||
pc.forAllTypes(func(ts *ast.TypeSpec) {
|
||||
if ss, ok := ts.Type.(*ast.StructType); ok {
|
||||
structType := pc.pass.TypesInfo.TypeOf(ts.Name).Underlying().(*types.Struct)
|
||||
pc.exportLockGuardFacts(structType, ss)
|
||||
pc.structLockGuardFacts(structType, ss)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -112,7 +154,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
|
||||
|
||||
// Find all function declarations and export relevant facts.
|
||||
pc.forAllFunctions(func(fn *ast.FuncDecl) {
|
||||
pc.exportFunctionFacts(fn)
|
||||
pc.functionFacts(fn)
|
||||
})
|
||||
|
||||
// Scan all code looking for invalid accesses.
|
||||
@@ -144,6 +186,9 @@ func run(pass *analysis.Pass) (interface{}, error) {
|
||||
pc.checkFunction(nil, fn, &nolff, nil, false /* force */)
|
||||
}
|
||||
|
||||
// Check for inferred checklocks annotations.
|
||||
pc.checkInferred()
|
||||
|
||||
// Check for expected failures.
|
||||
pc.checkFailures()
|
||||
|
||||
|
||||
+492
-337
File diff suppressed because it is too large
Load Diff
+80
-49
@@ -24,20 +24,26 @@ import (
|
||||
"golang.org/x/tools/go/ssa"
|
||||
)
|
||||
|
||||
// lockInfo describes a held lock.
|
||||
type lockInfo struct {
|
||||
exclusive bool
|
||||
object types.Object
|
||||
}
|
||||
|
||||
// lockState tracks the locking state and aliases.
|
||||
type lockState struct {
|
||||
// lockedMutexes is used to track which mutexes in a given struct are
|
||||
// currently locked. Note that most of the heavy lifting is done by
|
||||
// valueAsString below, which maps to specific structure fields, etc.
|
||||
// valueAndObject below, which maps to specific structure fields, etc.
|
||||
//
|
||||
// The value indicates whether this is an exclusive lock.
|
||||
lockedMutexes map[string]bool
|
||||
lockedMutexes map[string]lockInfo
|
||||
|
||||
// stored stores values that have been stored in memory, bound to
|
||||
// FreeVars or passed as Parameterse.
|
||||
stored map[ssa.Value]ssa.Value
|
||||
|
||||
// used is a temporary map, used only for valueAsString. It prevents
|
||||
// used is a temporary map, used only for valueAndObject. It prevents
|
||||
// multiple use of the same memory location.
|
||||
used map[ssa.Value]struct{}
|
||||
|
||||
@@ -53,7 +59,7 @@ type lockState struct {
|
||||
func newLockState() *lockState {
|
||||
refs := int32(1) // Not shared.
|
||||
return &lockState{
|
||||
lockedMutexes: make(map[string]bool),
|
||||
lockedMutexes: make(map[string]lockInfo),
|
||||
used: make(map[ssa.Value]struct{}),
|
||||
stored: make(map[ssa.Value]ssa.Value),
|
||||
defers: make([]*ssa.Defer, 0),
|
||||
@@ -81,7 +87,7 @@ func (l *lockState) fork() *lockState {
|
||||
func (l *lockState) modify() {
|
||||
if atomic.LoadInt32(l.refs) > 1 {
|
||||
// Copy the lockedMutexes.
|
||||
lm := make(map[string]bool)
|
||||
lm := make(map[string]lockInfo)
|
||||
for k, v := range l.lockedMutexes {
|
||||
lm[k] = v
|
||||
}
|
||||
@@ -110,17 +116,19 @@ func (l *lockState) modify() {
|
||||
}
|
||||
|
||||
// isHeld indicates whether the field is held is not.
|
||||
//
|
||||
// Precondition: rv must be valid.
|
||||
func (l *lockState) isHeld(rv resolvedValue, exclusiveRequired bool) (string, bool) {
|
||||
if !rv.valid {
|
||||
return rv.valueAsString(l), false
|
||||
if !rv.valid() {
|
||||
panic("invalid resolvedValue passed to isHeld")
|
||||
}
|
||||
s := rv.valueAsString(l)
|
||||
isExclusive, ok := l.lockedMutexes[s]
|
||||
s, _ := rv.valueAndObject(l)
|
||||
info, ok := l.lockedMutexes[s]
|
||||
if !ok {
|
||||
return s, false
|
||||
}
|
||||
// Accept a weaker lock if exclusiveRequired is false.
|
||||
if exclusiveRequired && !isExclusive {
|
||||
if exclusiveRequired && !info.exclusive {
|
||||
return s, false
|
||||
}
|
||||
return s, true
|
||||
@@ -129,32 +137,39 @@ func (l *lockState) isHeld(rv resolvedValue, exclusiveRequired bool) (string, bo
|
||||
// lockField locks the given field.
|
||||
//
|
||||
// If false is returned, the field was already locked.
|
||||
//
|
||||
// Precondition: rv must be valid.
|
||||
func (l *lockState) lockField(rv resolvedValue, exclusive bool) (string, bool) {
|
||||
if !rv.valid {
|
||||
return rv.valueAsString(l), false
|
||||
if !rv.valid() {
|
||||
panic("invalid resolvedValue passed to isHeld")
|
||||
}
|
||||
s := rv.valueAsString(l)
|
||||
s, obj := rv.valueAndObject(l)
|
||||
if _, ok := l.lockedMutexes[s]; ok {
|
||||
return s, false
|
||||
}
|
||||
l.modify()
|
||||
l.lockedMutexes[s] = exclusive
|
||||
l.lockedMutexes[s] = lockInfo{
|
||||
exclusive: exclusive,
|
||||
object: obj,
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
// unlockField unlocks the given field.
|
||||
//
|
||||
// If false is returned, the field was not locked.
|
||||
//
|
||||
// Precondition: rv must be valid.
|
||||
func (l *lockState) unlockField(rv resolvedValue, exclusive bool) (string, bool) {
|
||||
if !rv.valid {
|
||||
return rv.valueAsString(l), false
|
||||
if !rv.valid() {
|
||||
panic("invalid resolvedValue passed to isHeld")
|
||||
}
|
||||
s := rv.valueAsString(l)
|
||||
wasExclusive, ok := l.lockedMutexes[s]
|
||||
s, _ := rv.valueAndObject(l)
|
||||
info, ok := l.lockedMutexes[s]
|
||||
if !ok {
|
||||
return s, false
|
||||
}
|
||||
if wasExclusive != exclusive {
|
||||
if info.exclusive != exclusive {
|
||||
return s, false
|
||||
}
|
||||
l.modify()
|
||||
@@ -165,20 +180,23 @@ func (l *lockState) unlockField(rv resolvedValue, exclusive bool) (string, bool)
|
||||
// downgradeField downgrades the given field.
|
||||
//
|
||||
// If false was returned, the field was not downgraded.
|
||||
//
|
||||
// Precondition: rv must be valid.
|
||||
func (l *lockState) downgradeField(rv resolvedValue) (string, bool) {
|
||||
if !rv.valid {
|
||||
return rv.valueAsString(l), false
|
||||
if !rv.valid() {
|
||||
panic("invalid resolvedValue passed to isHeld")
|
||||
}
|
||||
s := rv.valueAsString(l)
|
||||
wasExclusive, ok := l.lockedMutexes[s]
|
||||
s, _ := rv.valueAndObject(l)
|
||||
info, ok := l.lockedMutexes[s]
|
||||
if !ok {
|
||||
return s, false
|
||||
}
|
||||
if !wasExclusive {
|
||||
if !info.exclusive {
|
||||
return s, false
|
||||
}
|
||||
l.modify()
|
||||
l.lockedMutexes[s] = false // Downgraded.
|
||||
info.exclusive = false
|
||||
l.lockedMutexes[s] = info // Downgraded.
|
||||
return s, true
|
||||
}
|
||||
|
||||
@@ -190,13 +208,13 @@ func (l *lockState) store(addr ssa.Value, v ssa.Value) {
|
||||
|
||||
// isSubset indicates other holds all the locks held by l.
|
||||
func (l *lockState) isSubset(other *lockState) bool {
|
||||
for k, isExclusive := range l.lockedMutexes {
|
||||
otherExclusive, otherOk := other.lockedMutexes[k]
|
||||
for k, info := range l.lockedMutexes {
|
||||
otherInfo, otherOk := other.lockedMutexes[k]
|
||||
if !otherOk {
|
||||
return false
|
||||
}
|
||||
// Accept weaker locks as a subset.
|
||||
if isExclusive && !otherExclusive {
|
||||
if info.exclusive && !otherInfo.exclusive {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -218,25 +236,26 @@ type elemType interface {
|
||||
Elem() types.Type
|
||||
}
|
||||
|
||||
// valueAsString returns a string for a given value.
|
||||
// valueAndObject returns a string for a given value, along with a source level
|
||||
// object (if available and relevant).
|
||||
//
|
||||
// This decomposes the value into the simplest possible representation in terms
|
||||
// of parameters, free variables and globals. During resolution, stored values
|
||||
// may be transferred, as well as bound free variables.
|
||||
//
|
||||
// Nil may not be passed here.
|
||||
func (l *lockState) valueAsString(v ssa.Value) string {
|
||||
func (l *lockState) valueAndObject(v ssa.Value) (string, types.Object) {
|
||||
switch x := v.(type) {
|
||||
case *ssa.Parameter:
|
||||
// Was this provided as a paramter for a local anonymous
|
||||
// function invocation?
|
||||
v, ok := l.stored[x]
|
||||
if ok {
|
||||
return l.valueAsString(v)
|
||||
return l.valueAndObject(v)
|
||||
}
|
||||
return fmt.Sprintf("{param:%s}", x.Name())
|
||||
return fmt.Sprintf("{param:%s}", x.Name()), x.Object()
|
||||
case *ssa.Global:
|
||||
return fmt.Sprintf("{global:%s}", x.Name())
|
||||
return fmt.Sprintf("{global:%s}", x.Name()), x.Object()
|
||||
case *ssa.FreeVar:
|
||||
// Attempt to resolve this, in case we are being invoked in a
|
||||
// scope where all the variables are bound.
|
||||
@@ -247,16 +266,18 @@ func (l *lockState) valueAsString(v ssa.Value) string {
|
||||
// may map to the same FreeVar, which we can check.
|
||||
stored, ok := l.stored[v]
|
||||
if ok {
|
||||
return l.valueAsString(stored)
|
||||
return l.valueAndObject(stored)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("{freevar:%s}", x.Name())
|
||||
// FreeVar does not have a corresponding source-level object
|
||||
// that we can return here.
|
||||
return fmt.Sprintf("{freevar:%s}", x.Name()), nil
|
||||
case *ssa.Convert:
|
||||
// Just disregard conversion.
|
||||
return l.valueAsString(x.X)
|
||||
return l.valueAndObject(x.X)
|
||||
case *ssa.ChangeType:
|
||||
// Ditto, disregard.
|
||||
return l.valueAsString(x.X)
|
||||
return l.valueAndObject(x.X)
|
||||
case *ssa.UnOp:
|
||||
if x.Op != token.MUL {
|
||||
break
|
||||
@@ -264,7 +285,7 @@ func (l *lockState) valueAsString(v ssa.Value) string {
|
||||
// Is this loading a free variable? If yes, then this can be
|
||||
// resolved in the original isAlias function.
|
||||
if fv, ok := x.X.(*ssa.FreeVar); ok {
|
||||
return l.valueAsString(fv)
|
||||
return l.valueAndObject(fv)
|
||||
}
|
||||
// Should be try to resolve via a memory address? This needs to
|
||||
// be done since a memory location can hold its own value.
|
||||
@@ -275,12 +296,13 @@ func (l *lockState) valueAsString(v ssa.Value) string {
|
||||
if ok {
|
||||
l.used[x.X] = struct{}{}
|
||||
defer func() { delete(l.used, x.X) }()
|
||||
return l.valueAsString(v)
|
||||
return l.valueAndObject(v)
|
||||
}
|
||||
}
|
||||
// x.X.Type is pointer. We must construct this type
|
||||
// dynamically, since the ssa.Value could be synthetic.
|
||||
return fmt.Sprintf("*(%s)", l.valueAsString(x.X))
|
||||
s, obj := l.valueAndObject(x.X)
|
||||
return fmt.Sprintf("*(%s)", s), obj
|
||||
case *ssa.Field:
|
||||
structType, ok := resolveStruct(x.X.Type())
|
||||
if !ok {
|
||||
@@ -288,7 +310,8 @@ func (l *lockState) valueAsString(v ssa.Value) string {
|
||||
panic(fmt.Sprintf("structType not available for struct: %#v", x.X))
|
||||
}
|
||||
fieldObj := structType.Field(x.Field)
|
||||
return fmt.Sprintf("%s.%s", l.valueAsString(x.X), fieldObj.Name())
|
||||
s, _ := l.valueAndObject(x.X)
|
||||
return fmt.Sprintf("%s.%s", s, fieldObj.Name()), fieldObj
|
||||
case *ssa.FieldAddr:
|
||||
structType, ok := resolveStruct(x.X.Type())
|
||||
if !ok {
|
||||
@@ -296,22 +319,30 @@ func (l *lockState) valueAsString(v ssa.Value) string {
|
||||
panic(fmt.Sprintf("structType not available for struct: %#v", x.X))
|
||||
}
|
||||
fieldObj := structType.Field(x.Field)
|
||||
return fmt.Sprintf("&(%s.%s)", l.valueAsString(x.X), fieldObj.Name())
|
||||
s, _ := l.valueAndObject(x.X)
|
||||
return fmt.Sprintf("&(%s.%s)", s, fieldObj.Name()), fieldObj
|
||||
case *ssa.Index:
|
||||
return fmt.Sprintf("%s[%s]", l.valueAsString(x.X), l.valueAsString(x.Index))
|
||||
s, _ := l.valueAndObject(x.X)
|
||||
i, _ := l.valueAndObject(x.Index)
|
||||
return fmt.Sprintf("%s[%s]", s, i), nil
|
||||
case *ssa.IndexAddr:
|
||||
return fmt.Sprintf("&(%s[%s])", l.valueAsString(x.X), l.valueAsString(x.Index))
|
||||
s, _ := l.valueAndObject(x.X)
|
||||
i, _ := l.valueAndObject(x.Index)
|
||||
return fmt.Sprintf("&(%s[%s])", s, i), nil
|
||||
case *ssa.Lookup:
|
||||
return fmt.Sprintf("%s[%s]", l.valueAsString(x.X), l.valueAsString(x.Index))
|
||||
s, _ := l.valueAndObject(x.X)
|
||||
i, _ := l.valueAndObject(x.Index)
|
||||
return fmt.Sprintf("%s[%s]", s, i), nil
|
||||
case *ssa.Extract:
|
||||
return fmt.Sprintf("%s[%d]", l.valueAsString(x.Tuple), x.Index)
|
||||
s, _ := l.valueAndObject(x.Tuple)
|
||||
return fmt.Sprintf("%s[%d]", s, x.Index), nil
|
||||
}
|
||||
|
||||
// In the case of any other type (e.g. this may be an alloc, a return
|
||||
// value, etc.), just return the literal pointer value to the Value.
|
||||
// This will be unique within the ssa graph, and so if two values are
|
||||
// equal, they are from the same type.
|
||||
return fmt.Sprintf("{%T:%p}", v, v)
|
||||
return fmt.Sprintf("{%T:%p}", v, v), nil
|
||||
}
|
||||
|
||||
// String returns the full lock state.
|
||||
@@ -320,9 +351,9 @@ func (l *lockState) String() string {
|
||||
return "no locks held"
|
||||
}
|
||||
keys := make([]string, 0, len(l.lockedMutexes))
|
||||
for k, exclusive := range l.lockedMutexes {
|
||||
for k, info := range l.lockedMutexes {
|
||||
// Include the exclusive status of each lock.
|
||||
keys = append(keys, fmt.Sprintf("%s %s", k, exclusiveStr(exclusive)))
|
||||
keys = append(keys, fmt.Sprintf("%s %s", k, exclusiveStr(info.exclusive)))
|
||||
}
|
||||
return strings.Join(keys, ",")
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ go_library(
|
||||
"branches.go",
|
||||
"closures.go",
|
||||
"defer.go",
|
||||
"globals.go",
|
||||
"incompat.go",
|
||||
"inferred.go",
|
||||
"locker.go",
|
||||
"methods.go",
|
||||
"parameters.go",
|
||||
@@ -21,4 +23,8 @@ go_library(
|
||||
"rwmutex.go",
|
||||
"test.go",
|
||||
],
|
||||
# This ensures that there are no dependencies, since we want to explicitly
|
||||
# control expected failures for analysis.
|
||||
marshal = False,
|
||||
stateify = False,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
globalMu sync.Mutex
|
||||
globalRWMu sync.RWMutex
|
||||
)
|
||||
|
||||
var globalStruct struct {
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
guardedField int
|
||||
}
|
||||
|
||||
var otherStruct struct {
|
||||
// +checklocks:globalMu
|
||||
guardedField1 int
|
||||
// +checklocks:globalRWMu
|
||||
guardedField2 int
|
||||
// +checklocks:globalStruct.mu
|
||||
guardedField3 int
|
||||
}
|
||||
|
||||
func testGlobalValid() {
|
||||
globalMu.Lock()
|
||||
otherStruct.guardedField1 = 1
|
||||
globalMu.Unlock()
|
||||
|
||||
globalRWMu.Lock()
|
||||
otherStruct.guardedField2 = 1
|
||||
globalRWMu.Unlock()
|
||||
|
||||
globalRWMu.RLock()
|
||||
_ = otherStruct.guardedField2
|
||||
globalRWMu.RUnlock()
|
||||
|
||||
globalStruct.mu.Lock()
|
||||
globalStruct.guardedField = 1
|
||||
otherStruct.guardedField3 = 1
|
||||
globalStruct.mu.Unlock()
|
||||
}
|
||||
|
||||
// +checklocks:globalStruct.mu
|
||||
func testGlobalValidPreconditions0() {
|
||||
globalStruct.guardedField = 1
|
||||
}
|
||||
|
||||
// +checklocks:globalMu
|
||||
func testGlobalValidPreconditions1() {
|
||||
otherStruct.guardedField1 = 1
|
||||
}
|
||||
|
||||
// +checklocks:globalRWMu
|
||||
func testGlobalValidPreconditions2() {
|
||||
otherStruct.guardedField2 = 1
|
||||
}
|
||||
|
||||
// +checklocks:globalStruct.mu
|
||||
func testGlobalValidPreconditions3() {
|
||||
otherStruct.guardedField3 = 1
|
||||
}
|
||||
|
||||
func testGlobalInvalid() {
|
||||
globalStruct.guardedField = 1 // +checklocksfail
|
||||
otherStruct.guardedField1 = 1 // +checklocksfail
|
||||
otherStruct.guardedField2 = 1 // +checklocksfail
|
||||
otherStruct.guardedField3 = 1 // +checklocksfail
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type inferredStruct struct {
|
||||
mu sync.Mutex
|
||||
guardedField int // +checklocksfail
|
||||
unguardedField int
|
||||
}
|
||||
|
||||
func testInferredPositive(tc *inferredStruct) {
|
||||
tc.mu.Lock()
|
||||
tc.guardedField = 1
|
||||
tc.mu.Unlock()
|
||||
}
|
||||
|
||||
func testInferredNegative(tc *inferredStruct) {
|
||||
tc.unguardedField = 1
|
||||
}
|
||||
@@ -103,7 +103,7 @@ type testMethodsWithEmbedded struct {
|
||||
|
||||
// +checklocks:mu
|
||||
guardedField int
|
||||
p *testMethodsWithParameters
|
||||
p *testMethodsWithParameters // +checklocksignore: Inferred as protected by mu.
|
||||
}
|
||||
|
||||
// +checklocks:t.mu
|
||||
|
||||
@@ -51,7 +51,7 @@ type twoLocksStruct struct {
|
||||
// twoLocksDoubleGuardStruct has two locks and a single field with two guards.
|
||||
type twoLocksDoubleGuardStruct struct {
|
||||
mu sync.Mutex
|
||||
secondMu sync.Mutex
|
||||
secondMu sync.Mutex // +checklocksignore: mu is inferred as requisite.
|
||||
// +checklocks:mu
|
||||
// +checklocks:secondMu
|
||||
doubleGuardedField int
|
||||
|
||||
Reference in New Issue
Block a user