diff options
Diffstat (limited to 'pkg/refsvfs2')
-rw-r--r-- | pkg/refsvfs2/BUILD | 37 | ||||
-rw-r--r-- | pkg/refsvfs2/README.md | 66 | ||||
-rw-r--r-- | pkg/refsvfs2/refs.go | 36 | ||||
-rw-r--r-- | pkg/refsvfs2/refs_map.go | 131 | ||||
-rw-r--r-- | pkg/refsvfs2/refs_template.go | 153 |
5 files changed, 423 insertions, 0 deletions
diff --git a/pkg/refsvfs2/BUILD b/pkg/refsvfs2/BUILD new file mode 100644 index 000000000..0377c0876 --- /dev/null +++ b/pkg/refsvfs2/BUILD @@ -0,0 +1,37 @@ +load("//tools:defs.bzl", "go_library") +load("//tools/go_generics:defs.bzl", "go_template") + +package(licenses = ["notice"]) + +go_template( + name = "refs_template", + srcs = [ + "refs_template.go", + ], + opt_consts = [ + "enableLogging", + ], + types = [ + "T", + ], + visibility = ["//:sandbox"], + deps = [ + "//pkg/log", + "//pkg/refs", + ], +) + +go_library( + name = "refsvfs2", + srcs = [ + "refs.go", + "refs_map.go", + ], + visibility = ["//:sandbox"], + deps = [ + "//pkg/context", + "//pkg/log", + "//pkg/refs", + "//pkg/sync", + ], +) diff --git a/pkg/refsvfs2/README.md b/pkg/refsvfs2/README.md new file mode 100644 index 000000000..eca53c282 --- /dev/null +++ b/pkg/refsvfs2/README.md @@ -0,0 +1,66 @@ +# Reference Counting + +Go does not offer a reliable way to couple custom resource management with +object lifetime. As a result, we need to manually implement reference counting +for many objects in gVisor to make sure that resources are acquired and released +appropriately. For example, the filesystem has many reference-counted objects +(file descriptions, dentries, inodes, etc.), and it is important that each +object persists while anything holds a reference on it and is destroyed once all +references are dropped. + +We provide a template in `refs_template.go` that can be applied to most objects +in need of reference counting. It contains a simple `Refs` struct that can be +incremented and decremented, and once the reference count reaches zero, a +destructor can be called. Note that there are some objects (e.g. `gofer.dentry`, +`overlay.dentry`) that should not immediately be destroyed upon reaching zero +references; in these cases, this template cannot be applied. + +# Reference Checking + +Unfortunately, manually keeping track of reference counts is extremely error +prone, and improper accounting can lead to production bugs that are very +difficult to root cause. + +We have several ways of discovering reference count errors in gVisor. Any +attempt to increment/decrement a `Refs` struct with a count of zero will trigger +a sentry panic, since the object should have been destroyed and become +unreachable. This allows us to identify missing increments or extra decrements, +which cause the reference count to be lower than it should be: the count will +reach zero earlier than expected, and the next increment/decrement--which should +be valid--will result in a panic. + +It is trickier to identify extra increments and missing decrements, which cause +the reference count to be higher than expected (i.e. a “reference leak”). +Reference leaks prevent resources from being released properly and can translate +to various issues that are tricky to diagnose, such as memory leaks. The +following section discusses how we implement leak checking. + +## Leak Checking + +When leak checking is enabled, reference-counted objects are added to a global +map when constructed and removed when destroyed. Near the very end of sandbox +execution, once no reference-counted objects should still be reachable, we +report everything left in the map as having leaked. Leak-checking objects +implement the `CheckedObject` interface, which allows us to print informative +warnings for each of the leaked objects. + +Leak checking is provided by `refs_template`, but objects that do not use the +template will also need to implement `CheckedObject` and be manually +registered/unregistered from the map in order to be checked. + +Note that leak checking affects performance and memory usage, so it should only +be enabled in testing environments. + +## Debugging + +Even with the checks described above, it can be difficult to track down the +exact source of a reference counting error. The error may occur far before it is +discovered (for instance, a missing `IncRef` may not be discovered until a +future `DecRef` makes the count negative). To aid in debugging, `refs_template` +provides the `enableLogging` option to log every `IncRef`, `DecRef`, and leak +check registration/unregistration, along with the object address and a call +stack. This allows us to search a log for all of the changes to a particular +object's reference count, which makes it much easier to identify the absent or +extraneous operation(s). The reference-counted objects that do not use +`refs_template` also provide logging, and others defined in the future should do +so as well. diff --git a/pkg/refsvfs2/refs.go b/pkg/refsvfs2/refs.go new file mode 100644 index 000000000..ef8beb659 --- /dev/null +++ b/pkg/refsvfs2/refs.go @@ -0,0 +1,36 @@ +// 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 refsvfs2 defines an interface for a reference-counted object. +package refsvfs2 + +import ( + "gvisor.dev/gvisor/pkg/context" +) + +// RefCounter is the interface to be implemented by objects that are reference +// counted. +type RefCounter interface { + // IncRef increments the reference counter on the object. + IncRef() + + // DecRef decrements the object's reference count. Users of refs_template.Refs + // may specify a destructor to be called once the reference count reaches zero. + DecRef(ctx context.Context) + + // TryIncRef attempts to increment the reference count, but may fail if all + // references have already been dropped, in which case it returns false. If + // true is returned, then a valid reference is now held on the object. + TryIncRef() bool +} diff --git a/pkg/refsvfs2/refs_map.go b/pkg/refsvfs2/refs_map.go new file mode 100644 index 000000000..9fbc5466f --- /dev/null +++ b/pkg/refsvfs2/refs_map.go @@ -0,0 +1,131 @@ +// 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 refsvfs2 + +import ( + "fmt" + + "gvisor.dev/gvisor/pkg/log" + refs_vfs1 "gvisor.dev/gvisor/pkg/refs" + "gvisor.dev/gvisor/pkg/sync" +) + +var ( + // liveObjects is a global map of reference-counted objects. Objects are + // inserted when leak check is enabled, and they are removed when they are + // destroyed. It is protected by liveObjectsMu. + liveObjects map[CheckedObject]struct{} + liveObjectsMu sync.Mutex +) + +// CheckedObject represents a reference-counted object with an informative +// leak detection message. +type CheckedObject interface { + // RefType is the type of the reference-counted object. + RefType() string + + // LeakMessage supplies a warning to be printed upon leak detection. + LeakMessage() string + + // LogRefs indicates whether reference-related events should be logged. + LogRefs() bool +} + +func init() { + liveObjects = make(map[CheckedObject]struct{}) +} + +// leakCheckEnabled returns whether leak checking is enabled. The following +// functions should only be called if it returns true. +func leakCheckEnabled() bool { + return refs_vfs1.GetLeakMode() != refs_vfs1.NoLeakChecking +} + +// Register adds obj to the live object map. +func Register(obj CheckedObject) { + if leakCheckEnabled() { + liveObjectsMu.Lock() + if _, ok := liveObjects[obj]; ok { + panic(fmt.Sprintf("Unexpected entry in leak checking map: reference %p already added", obj)) + } + liveObjects[obj] = struct{}{} + liveObjectsMu.Unlock() + if leakCheckEnabled() && obj.LogRefs() { + logEvent(obj, "registered") + } + } +} + +// Unregister removes obj from the live object map. +func Unregister(obj CheckedObject) { + if leakCheckEnabled() { + liveObjectsMu.Lock() + defer liveObjectsMu.Unlock() + if _, ok := liveObjects[obj]; !ok { + panic(fmt.Sprintf("Expected to find entry in leak checking map for reference %p", obj)) + } + delete(liveObjects, obj) + if leakCheckEnabled() && obj.LogRefs() { + logEvent(obj, "unregistered") + } + } +} + +// LogIncRef logs a reference increment. +func LogIncRef(obj CheckedObject, refs int64) { + if leakCheckEnabled() && obj.LogRefs() { + logEvent(obj, fmt.Sprintf("IncRef to %d", refs)) + } +} + +// LogTryIncRef logs a successful TryIncRef call. +func LogTryIncRef(obj CheckedObject, refs int64) { + if leakCheckEnabled() && obj.LogRefs() { + logEvent(obj, fmt.Sprintf("TryIncRef to %d", refs)) + } +} + +// LogDecRef logs a reference decrement. +func LogDecRef(obj CheckedObject, refs int64) { + if leakCheckEnabled() && obj.LogRefs() { + logEvent(obj, fmt.Sprintf("DecRef to %d", refs)) + } +} + +// logEvent logs a message for the given reference-counted object. +// +// obj.LogRefs() should be checked before calling logEvent, in order to avoid +// calling any text processing needed to evaluate msg. +func logEvent(obj CheckedObject, msg string) { + log.Infof("[%s %p] %s:", obj.RefType(), obj, msg) + log.Infof(refs_vfs1.FormatStack(refs_vfs1.RecordStack())) +} + +// DoLeakCheck iterates through the live object map and logs a message for each +// object. It is called once no reference-counted objects should be reachable +// anymore, at which point anything left in the map is considered a leak. +func DoLeakCheck() { + if leakCheckEnabled() { + liveObjectsMu.Lock() + defer liveObjectsMu.Unlock() + leaked := len(liveObjects) + if leaked > 0 { + log.Warningf("Leak checking detected %d leaked objects:", leaked) + for obj := range liveObjects { + log.Warningf(obj.LeakMessage()) + } + } + } +} diff --git a/pkg/refsvfs2/refs_template.go b/pkg/refsvfs2/refs_template.go new file mode 100644 index 000000000..3fbc91aa5 --- /dev/null +++ b/pkg/refsvfs2/refs_template.go @@ -0,0 +1,153 @@ +// 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 refs_template defines a template that can be used by reference +// counted objects. +package refs_template + +import ( + "fmt" + "sync/atomic" + + "gvisor.dev/gvisor/pkg/refsvfs2" +) + +// enableLogging indicates whether reference-related events should be logged (with +// stack traces). This is false by default and should only be set to true for +// debugging purposes, as it can generate an extremely large amount of output +// and drastically degrade performance. +const enableLogging = false + +// T is the type of the reference counted object. It is only used to customize +// debug output when leak checking. +type T interface{} + +// obj is used to customize logging. Note that we use a pointer to T so that +// we do not copy the entire object when passed as a format parameter. +var obj *T + +// Refs implements refs.RefCounter. It keeps a reference count using atomic +// operations and calls the destructor when the count reaches zero. +// +// +stateify savable +type Refs struct { + // refCount is composed of two fields: + // + // [32-bit speculative references]:[32-bit real references] + // + // Speculative references are used for TryIncRef, to avoid a CompareAndSwap + // loop. See IncRef, DecRef and TryIncRef for details of how these fields are + // used. + refCount int64 +} + +// InitRefs initializes r with one reference and, if enabled, activates leak +// checking. +func (r *Refs) InitRefs() { + atomic.StoreInt64(&r.refCount, 1) + refsvfs2.Register(r) +} + +// RefType implements refsvfs2.CheckedObject.RefType. +func (r *Refs) RefType() string { + return fmt.Sprintf("%T", obj)[1:] +} + +// LeakMessage implements refsvfs2.CheckedObject.LeakMessage. +func (r *Refs) LeakMessage() string { + return fmt.Sprintf("[%s %p] reference count of %d instead of 0", r.RefType(), r, r.ReadRefs()) +} + +// LogRefs implements refsvfs2.CheckedObject.LogRefs. +func (r *Refs) LogRefs() bool { + return enableLogging +} + +// ReadRefs returns the current number of references. The returned count is +// inherently racy and is unsafe to use without external synchronization. +func (r *Refs) ReadRefs() int64 { + return atomic.LoadInt64(&r.refCount) +} + +// IncRef implements refs.RefCounter.IncRef. +// +//go:nosplit +func (r *Refs) IncRef() { + v := atomic.AddInt64(&r.refCount, 1) + if enableLogging { + refsvfs2.LogIncRef(r, v) + } + if v <= 1 { + panic(fmt.Sprintf("Incrementing non-positive count %p on %s", r, r.RefType())) + } +} + +// TryIncRef implements refs.RefCounter.TryIncRef. +// +// To do this safely without a loop, a speculative reference is first acquired +// on the object. This allows multiple concurrent TryIncRef calls to distinguish +// other TryIncRef calls from genuine references held. +// +//go:nosplit +func (r *Refs) TryIncRef() bool { + const speculativeRef = 1 << 32 + if v := atomic.AddInt64(&r.refCount, speculativeRef); int32(v) == 0 { + // This object has already been freed. + atomic.AddInt64(&r.refCount, -speculativeRef) + return false + } + + // Turn into a real reference. + v := atomic.AddInt64(&r.refCount, -speculativeRef+1) + if enableLogging { + refsvfs2.LogTryIncRef(r, v) + } + return true +} + +// DecRef implements refs.RefCounter.DecRef. +// +// Note that speculative references are counted here. Since they were added +// prior to real references reaching zero, they will successfully convert to +// real references. In other words, we see speculative references only in the +// following case: +// +// A: TryIncRef [speculative increase => sees non-negative references] +// B: DecRef [real decrease] +// A: TryIncRef [transform speculative to real] +// +//go:nosplit +func (r *Refs) DecRef(destroy func()) { + v := atomic.AddInt64(&r.refCount, -1) + if enableLogging { + refsvfs2.LogDecRef(r, v) + } + switch { + case v < 0: + panic(fmt.Sprintf("Decrementing non-positive ref count %p, owned by %s", r, r.RefType())) + + case v == 0: + refsvfs2.Unregister(r) + // Call the destructor. + if destroy != nil { + destroy() + } + } +} + +func (r *Refs) afterLoad() { + if r.ReadRefs() > 0 { + refsvfs2.Register(r) + } +} |