diff options
Diffstat (limited to 'tools/checklocks/analysis.go')
-rw-r--r-- | tools/checklocks/analysis.go | 628 |
1 files changed, 0 insertions, 628 deletions
diff --git a/tools/checklocks/analysis.go b/tools/checklocks/analysis.go deleted file mode 100644 index d3fd797d0..000000000 --- a/tools/checklocks/analysis.go +++ /dev/null @@ -1,628 +0,0 @@ -// 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 checklocks - -import ( - "go/token" - "go/types" - "strings" - - "golang.org/x/tools/go/ssa" -) - -func gcd(a, b atomicAlignment) atomicAlignment { - for b != 0 { - a, b = b, a%b - } - return a -} - -// typeAlignment returns the type alignment for the given type. -func (pc *passContext) typeAlignment(pkg *types.Package, obj types.Object) atomicAlignment { - requiredOffset := atomicAlignment(1) - if pc.pass.ImportObjectFact(obj, &requiredOffset) { - return requiredOffset - } - - switch x := obj.Type().Underlying().(type) { - case *types.Struct: - fields := make([]*types.Var, x.NumFields()) - for i := 0; i < x.NumFields(); i++ { - fields[i] = x.Field(i) - } - offsets := pc.pass.TypesSizes.Offsetsof(fields) - for i := 0; i < x.NumFields(); i++ { - // Check the offset, and then assuming that this offset - // aligns with the offset for the broader type. - fieldRequired := pc.typeAlignment(pkg, fields[i]) - if offsets[i]%int64(fieldRequired) != 0 { - // The offset of this field is not compatible. - pc.maybeFail(fields[i].Pos(), "have alignment %d, need %d", offsets[i], fieldRequired) - } - // Ensure the requiredOffset is the LCM of the offset. - requiredOffset *= fieldRequired / gcd(requiredOffset, fieldRequired) - } - case *types.Array: - // Export direct alignment requirements. - if named, ok := x.Elem().(*types.Named); ok { - requiredOffset = pc.typeAlignment(pkg, named.Obj()) - } - default: - // Use the compiler's underlying alignment. - requiredOffset = atomicAlignment(pc.pass.TypesSizes.Alignof(obj.Type().Underlying())) - } - - if pkg == obj.Pkg() { - // Cache as an object fact, to subsequent calls. Note that we - // can only export object facts for the package that we are - // currently analyzing. There may be no exported facts for - // array types or alias types, for example. - pc.pass.ExportObjectFact(obj, &requiredOffset) - } - - return requiredOffset -} - -// checkTypeAlignment checks the alignment of the given type. -// -// This calls typeAlignment, which resolves all types recursively. This method -// should be called for all types individual to ensure full coverage. -func (pc *passContext) checkTypeAlignment(pkg *types.Package, typ *types.Named) { - _ = pc.typeAlignment(pkg, typ.Obj()) -} - -// checkAtomicCall checks for an atomic access. -// -// inst is the instruction analyzed, obj is used only for maybeFail. -// -// If mustBeAtomic is true, then we assert that the instruction *is* an atomic -// fucnction call. If it is false, then we assert that it is *not* an atomic -// dispatch. -// -// If readOnly is true, then only atomic read access are allowed. Note that -// readOnly is only meaningful if mustBeAtomic is set. -func (pc *passContext) checkAtomicCall(inst ssa.Instruction, obj types.Object, mustBeAtomic, readOnly bool) { - switch x := inst.(type) { - case *ssa.Call: - if x.Common().IsInvoke() { - if mustBeAtomic { - // This is an illegal interface dispatch. - pc.maybeFail(inst.Pos(), "dynamic dispatch with atomic-only field") - } - return - } - fn, ok := x.Common().Value.(*ssa.Function) - if !ok { - if mustBeAtomic { - // This is an illegal call to a non-static function. - pc.maybeFail(inst.Pos(), "dispatch to non-static function with atomic-only field") - } - return - } - pkg := fn.Package() - if pkg == nil { - if mustBeAtomic { - // This is a call to some shared wrapper function. - pc.maybeFail(inst.Pos(), "dispatch to shared function or wrapper") - } - return - } - var lff lockFunctionFacts // Check for exemption. - if obj := fn.Object(); obj != nil && pc.pass.ImportObjectFact(obj, &lff) && lff.Ignore { - return - } - if name := pkg.Pkg.Name(); name != "atomic" && name != "atomicbitops" { - if mustBeAtomic { - // This is an illegal call to a non-atomic package function. - pc.maybeFail(inst.Pos(), "dispatch to non-atomic function with atomic-only field") - } - return - } - if !mustBeAtomic { - // We are *not* expecting an atomic dispatch. - if _, ok := pc.forced[pc.positionKey(inst.Pos())]; !ok { - pc.maybeFail(inst.Pos(), "unexpected call to atomic function") - } - } - if !strings.HasPrefix(fn.Name(), "Load") && readOnly { - // We are not allowing any reads in this context. - if _, ok := pc.forced[pc.positionKey(inst.Pos())]; !ok { - pc.maybeFail(inst.Pos(), "unexpected call to atomic write function, is a lock missing?") - } - return - } - default: - if mustBeAtomic { - // This is something else entirely. - if _, ok := pc.forced[pc.positionKey(inst.Pos())]; !ok { - pc.maybeFail(inst.Pos(), "illegal use of atomic-only field by %T instruction", inst) - } - return - } - } -} - -func resolveStruct(typ types.Type) (*types.Struct, bool) { - structType, ok := typ.Underlying().(*types.Struct) - if ok { - return structType, true - } - ptrType, ok := typ.Underlying().(*types.Pointer) - if ok { - return resolveStruct(ptrType.Elem()) - } - return nil, false -} - -func findField(typ types.Type, field int) (types.Object, bool) { - structType, ok := resolveStruct(typ) - if !ok { - return nil, false - } - return structType.Field(field), true -} - -// instructionWithReferrers is a generalization over ssa.Field, ssa.FieldAddr. -type instructionWithReferrers interface { - ssa.Instruction - Referrers() *[]ssa.Instruction -} - -// checkFieldAccess checks the validity of a field access. -// -// This also enforces atomicity constraints for fields that must be accessed -// atomically. The parameter isWrite indicates whether this field is used -// downstream for a write operation. -func (pc *passContext) checkFieldAccess(inst instructionWithReferrers, structObj ssa.Value, field int, ls *lockState, isWrite bool) { - var ( - lff lockFieldFacts - lgf lockGuardFacts - guardsFound int - guardsHeld int - ) - - fieldObj, _ := findField(structObj.Type(), field) - pc.pass.ImportObjectFact(fieldObj, &lff) - pc.pass.ImportObjectFact(fieldObj, &lgf) - - for guardName, fl := range lgf.GuardedBy { - guardsFound++ - r := fl.resolve(structObj) - if _, ok := ls.isHeld(r); ok { - guardsHeld++ - continue - } - if _, ok := pc.forced[pc.positionKey(inst.Pos())]; ok { - // Mark this as locked, since it has been forced. - ls.lockField(r) - guardsHeld++ - 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. - if lgf.AtomicDisposition != atomicRequired { - // There is no force key, no atomic access and no lock held. - pc.maybeFail(inst.Pos(), "invalid field access, %s must be locked when accessing %s (locks: %s)", guardName, fieldObj.Name(), ls.String()) - } - } - - // Check the atomic access for this field. - switch lgf.AtomicDisposition { - case atomicRequired: - // Check that this is used safely as an input. - readOnly := guardsHeld < guardsFound - if refs := inst.Referrers(); refs != nil { - for _, otherInst := range *refs { - pc.checkAtomicCall(otherInst, fieldObj, 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()) - } - 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) - } - } - } -} - -func (pc *passContext) checkCall(call callCommon, ls *lockState) { - // See: https://godoc.org/golang.org/x/tools/go/ssa#CallCommon - // - // 1. "call" mode: when Method is nil (!IsInvoke), a CallCommon represents an ordinary - // function call of the value in Value, which may be a *Builtin, a *Function or any - // other value of kind 'func'. - // - // Value may be one of: - // (a) a *Function, indicating a statically dispatched call - // to a package-level function, an anonymous function, or - // a method of a named type. - // - // (b) a *MakeClosure, indicating an immediately applied - // function literal with free variables. - // - // (c) a *Builtin, indicating a statically dispatched call - // to a built-in function. - // - // (d) any other value, indicating a dynamically dispatched - // function call. - switch fn := call.Common().Value.(type) { - case *ssa.Function: - var lff lockFunctionFacts - if fn.Object() != nil { - pc.pass.ImportObjectFact(fn.Object(), &lff) - pc.checkFunctionCall(call, fn, &lff, ls) - } else { - // Anonymous functions have no facts, and cannot be - // annotated. We don't check for violations using the - // function facts, since they cannot exist. Instead, we - // do a fresh analysis using the current lock state. - fnls := ls.fork() - for i, arg := range call.Common().Args { - fnls.store(fn.Params[i], arg) - } - pc.checkFunction(call, fn, &lff, fnls, true /* force */) - } - case *ssa.MakeClosure: - // Note that creating and then invoking closures locally is - // allowed, but analysis of passing closures is done when - // checking individual instructions. - pc.checkClosure(call, fn, ls) - default: - return - } -} - -// postFunctionCallUpdate updates all conditions. -func (pc *passContext) postFunctionCallUpdate(call callCommon, lff *lockFunctionFacts, ls *lockState) { - // Release all locks not still held. - for fieldName, fg := range lff.HeldOnEntry { - if _, ok := lff.HeldOnExit[fieldName]; ok { - continue - } - r := fg.resolveCall(call.Common().Args, call.Value()) - if s, ok := ls.unlockField(r); !ok { - if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok { - pc.maybeFail(call.Pos(), "attempt to release %s (%s), but not held (locks: %s)", fieldName, s, ls.String()) - } - } - } - - // Update all held locks if acquired. - for fieldName, fg := range lff.HeldOnExit { - if _, ok := lff.HeldOnEntry[fieldName]; ok { - continue - } - // Acquire the lock per the annotation. - r := fg.resolveCall(call.Common().Args, call.Value()) - if s, ok := ls.lockField(r); !ok { - if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok { - pc.maybeFail(call.Pos(), "attempt to acquire %s (%s), but already held (locks: %s)", fieldName, s, ls.String()) - } - } - } -} - -// checkFunctionCall checks preconditions for function calls, and tracks the -// lock state by recording relevant calls to sync functions. Note that calls to -// atomic functions are tracked by checkFieldAccess by looking directly at the -// referrers (because ordering doesn't matter there, so we need not scan in -// instruction order). -func (pc *passContext) checkFunctionCall(call callCommon, fn *ssa.Function, lff *lockFunctionFacts, ls *lockState) { - // Check all guards required are held. - for fieldName, fg := range lff.HeldOnEntry { - r := fg.resolveCall(call.Common().Args, call.Value()) - if s, ok := ls.isHeld(r); !ok { - if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok { - pc.maybeFail(call.Pos(), "must hold %s (%s) to call %s, but not held (locks: %s)", fieldName, s, fn.Name(), ls.String()) - } else { - // Force the lock to be acquired. - ls.lockField(r) - } - } - } - - // Update all lock state accordingly. - pc.postFunctionCallUpdate(call, lff, ls) - - // 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.Package() != nil && fn.Package().Pkg.Name() == "sync" && fn.Signature.Recv() != nil { - switch fn.Name() { - case "Lock", "RLock": - if s, ok := ls.lockField(resolvedValue{value: call.Common().Args[0], valid: true}); !ok { - 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()) - } - } - case "Unlock", "RUnlock": - if s, ok := ls.unlockField(resolvedValue{value: call.Common().Args[0], valid: true}); !ok { - if _, ok := pc.forced[pc.positionKey(call.Pos())]; !ok { - // Unlocking something that is already unlocked. - pc.maybeFail(call.Pos(), "%s already unlocked (locks: %s)", s, ls.String()) - } - } - } - } -} - -// checkClosure forks the lock state, and creates a binding for the FreeVars of -// the closure. This allows the analysis to resolve the closure. -func (pc *passContext) checkClosure(call callCommon, fn *ssa.MakeClosure, ls *lockState) { - clls := ls.fork() - clfn := fn.Fn.(*ssa.Function) - for i, fv := range clfn.FreeVars { - clls.store(fv, fn.Bindings[i]) - } - - // Note that this is *not* a call to check function call, which checks - // against the function preconditions. Instead, this does a fresh - // analysis of the function from source code with a different state. - var nolff lockFunctionFacts - pc.checkFunction(call, clfn, &nolff, clls, true /* force */) -} - -// freshAlloc indicates that v has been allocated within the local scope. There -// is no lock checking done on objects that are freshly allocated. -func freshAlloc(v ssa.Value) bool { - switch x := v.(type) { - case *ssa.Alloc: - return true - case *ssa.FieldAddr: - return freshAlloc(x.X) - case *ssa.Field: - return freshAlloc(x.X) - case *ssa.IndexAddr: - return freshAlloc(x.X) - case *ssa.Index: - return freshAlloc(x.X) - case *ssa.Convert: - return freshAlloc(x.X) - case *ssa.ChangeType: - return freshAlloc(x.X) - default: - return false - } -} - -// isWrite indicates that this value is used as the addr field in a store. -// -// Note that this may still be used for a write. The return here is optimistic -// but sufficient for basic analysis. -func isWrite(v ssa.Value) bool { - refs := v.Referrers() - if refs == nil { - return false - } - for _, ref := range *refs { - if s, ok := ref.(*ssa.Store); ok && s.Addr == v { - return true - } - } - return false -} - -// callCommon is an ssa.Value that also implements Common. -type callCommon interface { - Pos() token.Pos - Common() *ssa.CallCommon - Value() *ssa.Call -} - -// checkInstruction checks the legality the single instruction based on the -// current lockState. -func (pc *passContext) checkInstruction(inst ssa.Instruction, ls *lockState) (*ssa.Return, *lockState) { - switch x := inst.(type) { - case *ssa.Store: - // Record that this value is holding this other value. This is - // because at the beginning of each ssa execution, there is a - // series of assignments of parameter values to alloc objects. - // This allows us to trace these back to the original - // parameters as aliases above. - // - // Note that this may overwrite an existing value in the lock - // state, but this is intentional. - ls.store(x.Addr, x.Val) - case *ssa.Field: - if !freshAlloc(x.X) { - pc.checkFieldAccess(x, x.X, x.Field, ls, false) - } - case *ssa.FieldAddr: - if !freshAlloc(x.X) { - pc.checkFieldAccess(x, x.X, x.Field, ls, isWrite(x)) - } - case *ssa.Call: - pc.checkCall(x, ls) - case *ssa.Defer: - ls.pushDefer(x) - case *ssa.RunDefers: - for d := ls.popDefer(); d != nil; d = ls.popDefer() { - pc.checkCall(d, ls) - } - case *ssa.MakeClosure: - refs := x.Referrers() - if refs == nil { - // This is strange, it's not used? Ignore this case, - // since it will probably be optimized away. - return nil, nil - } - hasNonCall := false - for _, ref := range *refs { - switch ref.(type) { - case *ssa.Call, *ssa.Defer: - // Analysis will be done on the call itself - // subsequently, including the lock state at - // the time of the call. - default: - // We need to analyze separately. Per below, - // this means that we'll analyze at closure - // construction time no zero assumptions about - // when it will be called. - hasNonCall = true - } - } - if !hasNonCall { - return nil, nil - } - // Analyze the closure without bindings. This means that we - // assume no lock facts or have any existing lock state. Only - // trivial closures are acceptable in this case. - clfn := x.Fn.(*ssa.Function) - var nolff lockFunctionFacts - pc.checkFunction(nil, clfn, &nolff, nil, false /* force */) - case *ssa.Return: - return x, ls // Valid return state. - } - return nil, nil -} - -// checkBasicBlock traverses the control flow graph starting at a set of given -// block and checks each instruction for allowed operations. -func (pc *passContext) checkBasicBlock(fn *ssa.Function, block *ssa.BasicBlock, lff *lockFunctionFacts, parent *lockState, seen map[*ssa.BasicBlock]*lockState) *lockState { - if oldLS, ok := seen[block]; ok && oldLS.isCompatible(parent) { - return nil - } - - // If the lock state is not compatible, then we need to do the - // recursive analysis to ensure that it is still sane. For example, the - // following is guaranteed to generate incompatible locking states: - // - // if foo { - // mu.Lock() - // } - // other stuff ... - // if foo { - // mu.Unlock() - // } - - var ( - rv *ssa.Return - rls *lockState - ) - - // Analyze this block. - seen[block] = parent - ls := parent.fork() - for _, inst := range block.Instrs { - rv, rls = pc.checkInstruction(inst, ls) - if rls != nil { - failed := false - // Validate held locks. - for fieldName, fg := range lff.HeldOnExit { - r := fg.resolveStatic(fn, rv) - if s, ok := rls.isHeld(r); !ok { - if _, ok := pc.forced[pc.positionKey(rv.Pos())]; !ok { - pc.maybeFail(rv.Pos(), "lock %s (%s) not held (locks: %s)", fieldName, s, rls.String()) - failed = true - } else { - // Force the lock to be acquired. - rls.lockField(r) - } - } - } - // Check for other locks, but only if the above didn't trip. - if !failed && rls.count() != len(lff.HeldOnExit) { - pc.maybeFail(rv.Pos(), "return with unexpected locks held (locks: %s)", rls.String()) - } - } - } - - // Analyze all successors. - for _, succ := range block.Succs { - // Collect possible return values, and make sure that the lock - // state aligns with any return value that we may have found - // above. Note that checkBasicBlock will recursively analyze - // the lock state to ensure that Releases and Acquires are - // respected. - if pls := pc.checkBasicBlock(fn, succ, lff, ls, seen); pls != nil { - if rls != nil && !rls.isCompatible(pls) { - if _, ok := pc.forced[pc.positionKey(fn.Pos())]; !ok { - pc.maybeFail(fn.Pos(), "incompatible return states (first: %s, second: %v)", rls.String(), pls.String()) - } - } - rls = pls - } - } - return rls -} - -// checkFunction checks a function invocation, typically starting with nil lockState. -func (pc *passContext) checkFunction(call callCommon, fn *ssa.Function, lff *lockFunctionFacts, parent *lockState, force bool) { - defer func() { - // Mark this function as checked. This is used by the top-level - // loop to ensure that all anonymous functions are scanned, if - // they are not explicitly invoked here. Note that this can - // happen if the anonymous functions are e.g. passed only as - // parameters or used to initialize some structure. - pc.functions[fn] = struct{}{} - }() - if _, ok := pc.functions[fn]; !force && ok { - // This function has already been analyzed at least once. - // That's all we permit for each function, although this may - // cause some anonymous functions to be analyzed in only one - // context. - return - } - - // If no return value is provided, then synthesize one. This is used - // below only to check against the locks preconditions, which may - // include return values. - if call == nil { - call = &ssa.Call{Call: ssa.CallCommon{Value: fn}} - } - - // Initialize ls with any preconditions that require locks to be held - // for the method to be invoked. Note that in the overwhleming majority - // of cases, parent will be nil. However, in the case of closures and - // anonymous functions, we may start with a non-nil lock state. - ls := parent.fork() - 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()) - if s, ok := ls.lockField(r); !ok { - // This can only happen if the same value is declared - // multiple times, and should be caught by the earlier - // fact scanning. Keep it here as a sanity check. - pc.maybeFail(fn.Pos(), "lock %s (%s) acquired multiple times (locks: %s)", fieldName, s, ls.String()) - } - } - - // Scan the blocks. - seen := make(map[*ssa.BasicBlock]*lockState) - if len(fn.Blocks) > 0 { - pc.checkBasicBlock(fn, fn.Blocks[0], lff, ls, seen) - } - - // Scan the recover block. - if fn.Recover != nil { - pc.checkBasicBlock(fn, fn.Recover, lff, ls, seen) - } - - // Update all lock state accordingly. This will be called only if we - // are doing inline analysis for e.g. an anonymous function. - if call != nil && parent != nil { - pc.postFunctionCallUpdate(call, lff, parent) - } -} |