github.com/nicocha30/gvisor-ligolo@v0.0.0-20230726075806-989fa2c0a413/pkg/lisafs/node.go (about)

     1  // Copyright 2022 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package lisafs
    16  
    17  import (
    18  	"github.com/nicocha30/gvisor-ligolo/pkg/atomicbitops"
    19  	"github.com/nicocha30/gvisor-ligolo/pkg/context"
    20  	"github.com/nicocha30/gvisor-ligolo/pkg/fspath"
    21  	"github.com/nicocha30/gvisor-ligolo/pkg/sync"
    22  )
    23  
    24  // numStaticChildren is the number of static children tracked by each node.
    25  // Sampling certain filesystem heavy workloads showed that a majority of
    26  // directories store at most 5 children in their map. This should be kept low
    27  // to minimize the memory overhead for each node. 5 is fairly low and at the
    28  // same time helps avoid map allocations for majority of nodes. Benchmarking
    29  // also showed that static arrays are faster than maps for lookups until n=8.
    30  const numStaticChildren = 5
    31  
    32  // Node is a node on the filesystem tree. A Node is shared by all the
    33  // ControlFDs opened on that position. For a given Server, there will only be
    34  // one Node for a given filesystem position.
    35  //
    36  // Reference Model:
    37  //   - Each node holds a ref on its parent for its entire lifetime.
    38  type Node struct {
    39  	// node's ref count is protected by its parent's childrenMu.
    40  	nodeRefs
    41  
    42  	// opMu synchronizes high level operations on this path.
    43  	//
    44  	// It is used to ensure the following which are important for security:
    45  	//	* This node's data is protected by opMu. So all operations that change its
    46  	//   data should hold opMu for writing. For example: write, setstat, setxattr,
    47  	//   etc. This entails that if this node represents a directory, creation and
    48  	//   deletion operations happening directly under this directory must lock
    49  	//   opMu for writing. All operations accessing data must hold opMu for
    50  	//   reading. This is to avoid the can of worms that open when creation and
    51  	//   deletion are allowed to race. This prevents any walks from occurring
    52  	//   during creation or deletion.
    53  	//	* When this node is being deleted, the deletion handler must hold opMu for
    54  	//   writing. This ensures that there are no concurrent operations going on
    55  	//   this node while it is being deleted and potentially being replaced with
    56  	//   something hazardous.
    57  	//
    58  	// A useful consequence of the above is that holding opMu for reading
    59  	// guarantees that the Server can not change Nodes on the path until this
    60  	// Node. For instance, if the grandparent needs to be renamed or deleted,
    61  	// the client must first delete this node to avoid ENOTEMPTY error. Deleting
    62  	// this node is not possible while opMu is read locked.
    63  	opMu sync.RWMutex
    64  
    65  	// deleted indicates whether the backing file has been unlinked. This can be
    66  	// used to deny operations on FDs on this Node after deletion because it is
    67  	// not safe for FD implementations to do host walks up to this position
    68  	// anymore. This node may have been replaced with something hazardous.
    69  	// deleted is protected by opMu. deleted must only be accessed/mutated using
    70  	// atomics; see markDeletedRecursive for more details.
    71  	deleted atomicbitops.Uint32
    72  
    73  	// name is the name of the file represented by this Node in parent. If this
    74  	// FD represents the root directory, then name is an empty string. name is
    75  	// protected by the backing server's rename mutex.
    76  	name string
    77  
    78  	// parent is this parent node which tracks this node as a child. parent is
    79  	// protected by the backing server's rename mutex.
    80  	parent *Node
    81  
    82  	// controlFDs is a linked list of all the ControlFDs opened on this node.
    83  	// Prefer this over a slice to avoid additional allocations. Each ControlFD
    84  	// is an implicit linked list node so there are no additional allocations
    85  	// needed to maintain the linked list.
    86  	controlFDsMu sync.Mutex
    87  	controlFDs   controlFDList
    88  
    89  	// Here is a performance hack. Past experience has shown that map allocations
    90  	// on each node for tracking children costs a lot of memory. More small
    91  	// allocations also fragment memory. To save allocations, statically track
    92  	// upto numStaticChildren children using hardcoded pointers. If more children
    93  	// are inserted then move to a map. Use dynamicChildren iff it is non-nil.
    94  
    95  	// The folowing fields are protected by childrenMu.
    96  	childrenMu     sync.Mutex
    97  	staticChildren [numStaticChildren]struct {
    98  		name string
    99  		node *Node
   100  	}
   101  	dynamicChildren map[string]*Node
   102  }
   103  
   104  // DecRef implements refs.RefCounter.DecRef. Note that the context
   105  // parameter should never be used. It exists solely to comply with the
   106  // refs.RefCounter interface.
   107  //
   108  // Precondition: server's rename mutex must be at least read locked.
   109  func (n *Node) DecRef(context.Context) {
   110  	if n.parent == nil {
   111  		n.nodeRefs.DecRef(nil)
   112  		return
   113  	}
   114  	// If this is the only ref on node then it will need to be destroyed.
   115  	n.parent.childrenMu.Lock()
   116  	deleted := false
   117  	n.nodeRefs.DecRef(func() {
   118  		n.parent.removeChildLocked(n.name)
   119  		deleted = true
   120  	})
   121  	n.parent.childrenMu.Unlock()
   122  	if deleted {
   123  		// Drop ref on parent. Keep Decref call lock free for scalability.
   124  		n.parent.DecRef(nil)
   125  	}
   126  }
   127  
   128  // InitLocked must be called before first use of fd.
   129  //
   130  // Precondition: parent.childrenMu is locked.
   131  //
   132  // Postconditions: A ref on n is transferred to the caller.
   133  func (n *Node) InitLocked(name string, parent *Node) {
   134  	n.nodeRefs.InitRefs()
   135  	n.name = name
   136  	n.parent = parent
   137  	if parent != nil {
   138  		parent.IncRef()
   139  		parent.insertChildLocked(name, n)
   140  	}
   141  }
   142  
   143  // LookupChildLocked looks up for a child with given name. Returns nil if child
   144  // does not exist.
   145  //
   146  // Preconditions: childrenMu is locked.
   147  func (n *Node) LookupChildLocked(name string) *Node {
   148  	if n.dynamicChildren != nil {
   149  		return n.dynamicChildren[name]
   150  	}
   151  
   152  	for i := 0; i < numStaticChildren; i++ {
   153  		if n.staticChildren[i].name == name {
   154  			return n.staticChildren[i].node
   155  		}
   156  	}
   157  	return nil
   158  }
   159  
   160  // WithChildrenMu executes fn with n.childrenMu locked.
   161  func (n *Node) WithChildrenMu(fn func()) {
   162  	n.childrenMu.Lock()
   163  	defer n.childrenMu.Unlock()
   164  	fn()
   165  }
   166  
   167  // FilePath returns the absolute path of the backing file. This is an expensive
   168  // operation. The returned path should be free of any intermediate symlinks
   169  // because all internal (non-leaf) nodes are directories.
   170  //
   171  // Precondition:
   172  //   - server's rename mutex must be at least read locked. Calling handlers must
   173  //     at least have read concurrency guarantee from the server.
   174  func (n *Node) FilePath() string {
   175  	// Walk upwards and prepend name to res.
   176  	var res fspath.Builder
   177  	for n.parent != nil {
   178  		res.PrependComponent(n.name)
   179  		n = n.parent
   180  	}
   181  	// n is the root node.
   182  	res.PrependByte('/')
   183  	return res.String()
   184  }
   185  
   186  func (n *Node) isDeleted() bool {
   187  	return n.deleted.Load() != 0
   188  }
   189  
   190  func (n *Node) removeFD(fd *ControlFD) {
   191  	n.controlFDsMu.Lock()
   192  	defer n.controlFDsMu.Unlock()
   193  	n.controlFDs.Remove(fd)
   194  }
   195  
   196  func (n *Node) insertFD(fd *ControlFD) {
   197  	n.controlFDsMu.Lock()
   198  	defer n.controlFDsMu.Unlock()
   199  	n.controlFDs.PushBack(fd)
   200  }
   201  
   202  func (n *Node) forEachFD(fn func(*ControlFD)) {
   203  	n.controlFDsMu.Lock()
   204  	defer n.controlFDsMu.Unlock()
   205  	for fd := n.controlFDs.Front(); fd != nil; fd = fd.Next() {
   206  		fn(fd)
   207  	}
   208  }
   209  
   210  // removeChildLocked removes child with given name from n and returns the
   211  // removed child. Returns nil if no such child existed.
   212  //
   213  // Precondition: childrenMu is locked.
   214  func (n *Node) removeChildLocked(name string) *Node {
   215  	if n.dynamicChildren != nil {
   216  		toRemove := n.dynamicChildren[name]
   217  		delete(n.dynamicChildren, name)
   218  		return toRemove
   219  	}
   220  
   221  	for i := 0; i < numStaticChildren; i++ {
   222  		if n.staticChildren[i].name == name {
   223  			toRemove := n.staticChildren[i].node
   224  			n.staticChildren[i].name = ""
   225  			n.staticChildren[i].node = nil
   226  			return toRemove
   227  		}
   228  	}
   229  	return nil
   230  }
   231  
   232  // insertChildLocked inserts child into n. It does not check for duplicates.
   233  //
   234  // Precondition: childrenMu is locked.
   235  func (n *Node) insertChildLocked(name string, child *Node) {
   236  	// Try to insert statically first if staticChildren is still being used.
   237  	if n.dynamicChildren == nil {
   238  		for i := 0; i < numStaticChildren; i++ {
   239  			if n.staticChildren[i].node == nil {
   240  				n.staticChildren[i].node = child
   241  				n.staticChildren[i].name = name
   242  				return
   243  			}
   244  		}
   245  
   246  		// Ran out of static space. Need to start inserting dynamically.
   247  		// Shift everything to the map.
   248  		n.dynamicChildren = make(map[string]*Node)
   249  		for i := 0; i < numStaticChildren; i++ {
   250  			// From above loop we know all staticChildren entries are non-nil.
   251  			n.dynamicChildren[n.staticChildren[i].name] = n.staticChildren[i].node
   252  			n.staticChildren[i].name = ""
   253  			n.staticChildren[i].node = nil
   254  		}
   255  	}
   256  
   257  	n.dynamicChildren[name] = child
   258  }
   259  
   260  func (n *Node) forEachChild(fn func(*Node)) {
   261  	n.childrenMu.Lock()
   262  	defer n.childrenMu.Unlock()
   263  
   264  	if n.dynamicChildren != nil {
   265  		for _, child := range n.dynamicChildren {
   266  			fn(child)
   267  		}
   268  		return
   269  	}
   270  
   271  	for i := 0; i < numStaticChildren; i++ {
   272  		if n.staticChildren[i].node != nil {
   273  			fn(n.staticChildren[i].node)
   274  		}
   275  	}
   276  }
   277  
   278  // Precondition: opMu must be locked for writing on the root node being marked
   279  // as deleted.
   280  func (n *Node) markDeletedRecursive() {
   281  	n.deleted.Store(1)
   282  
   283  	// No need to hold opMu for children as it introduces lock ordering issues
   284  	// because forEachChild locks childrenMu. Locking opMu after childrenMu
   285  	// violates the lock ordering. Anyway if a directory is being deleted, it
   286  	// must not have children. The client must have already deleted the entire
   287  	// subtree. If the client did not delete this subtree nodes, then the subtree
   288  	// was deleted externally and there is not much we can do. This is best
   289  	// effort work to mark the subtree as deleted.
   290  	n.forEachChild(func(child *Node) {
   291  		child.markDeletedRecursive()
   292  	})
   293  }