1 // Copyright 2020 Google Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License.  You may obtain a copy
5 // 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, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
12 // License for the specific language governing permissions and limitations
13 // under the License.
14 
15 use {fuse, IdGenerator};
16 use nodes::{ArcNode, Cache, Dir, File, Symlink};
17 use std::collections::HashMap;
18 use std::fs;
19 use std::path::{Path, PathBuf};
20 use std::sync::Mutex;
21 
22 /// Node factory without any caching.
23 #[derive(Default)]
24 pub struct NoCache {
25 }
26 
27 impl Cache for NoCache {
get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata, writable: bool) -> ArcNode28     fn get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata,
29         writable: bool) -> ArcNode {
30         if attr.is_dir() {
31             Dir::new_mapped(ids.next(), underlying_path, attr, writable)
32         } else if attr.file_type().is_symlink() {
33             Symlink::new_mapped(ids.next(), underlying_path, attr, writable)
34         } else {
35             File::new_mapped(ids.next(), underlying_path, attr, writable)
36         }
37     }
38 
delete(&self, _path: &Path, _file_type: fuse::FileType)39     fn delete(&self, _path: &Path, _file_type: fuse::FileType) {
40         // Nothing to do.
41     }
42 
rename(&self, _old_path: &Path, _new_path: PathBuf, _file_type: fuse::FileType)43     fn rename(&self, _old_path: &Path, _new_path: PathBuf, _file_type: fuse::FileType) {
44         // Nothing to do.
45     }
46 }
47 
48 /// Cache of sandboxfs nodes indexed by their underlying path.
49 ///
50 /// This cache is critical to offer good performance during reconfigurations: if the identity of an
51 /// underlying file changes across reconfigurations, the kernel will think it's a different file
52 /// (even if it may not be) and will therefore not be able to take advantage of any caches.  You
53 /// would think that avoiding kernel cache invalidations during the reconfiguration itself (e.g. if
54 /// file `A` was mapped and is still mapped now, don't invalidate it) would be sufficient to avoid
55 /// this problem, but it's not: `A` could be mapped, then unmapped, and then remapped again in three
56 /// different reconfigurations, and we'd still not want to lose track of it.
57 ///
58 /// Nodes should be inserted in this cache at creation time and removed from it when explicitly
59 /// deleted by the user (because there is a chance they'll be recreated, and at that point we truly
60 /// want to reload the data from disk).
61 ///
62 /// TODO(jmmv): There currently is no cache expiration, which means that memory usage can grow
63 /// unboundedly.  A preliminary attempt at expiring cache entries on a node's forget handler sounded
64 /// promising (because then cache expiration would be delegated to the kernel)... but, on Linux, the
65 /// kernel seems to be calling this very eagerly, rendering our cache useless.  I did not track down
66 /// what exactly triggered the forget notifications though.
67 ///
68 /// TODO(jmmv): This cache has proven to be problematic in some cases and should probably be
69 /// removed.  See https://jmmv.dev/2020/01/osxfuse-hardlinks-dladdr.html for details.
70 #[derive(Default)]
71 pub struct PathCache {
72     entries: Mutex<HashMap<PathBuf, ArcNode>>,
73 }
74 
75 impl Cache for PathCache {
get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata, writable: bool) -> ArcNode76     fn get_or_create(&self, ids: &IdGenerator, underlying_path: &Path, attr: &fs::Metadata,
77         writable: bool) -> ArcNode {
78         if attr.is_dir() {
79             // Directories cannot be cached because they contain entries that are created only
80             // in memory based on the mappings configuration.
81             //
82             // TODO(jmmv): Actually, they *could* be cached, but it's hard.  Investigate doing so
83             // after quantifying how much it may benefit performance.
84             return Dir::new_mapped(ids.next(), underlying_path, attr, writable);
85         }
86 
87         let mut entries = self.entries.lock().unwrap();
88 
89         if let Some(node) = entries.get(underlying_path) {
90             if node.writable() == writable {
91                 // We have a match from the cache!  Return it immediately.
92                 //
93                 // It is tempting to ensure that the type of the cached node matches the type we
94                 // want to return based on the metadata we have now in `attr`... but doing so does
95                 // not really prevent problems: the type of the underlying file can change at any
96                 // point in time.  We could check this here and the type could change immediately
97                 // afterwards behind our backs, so don't bother.
98                 return node.clone();
99             }
100 
101             // We had a match... but node writability has changed; recreate the node.
102             //
103             // You may wonder why we care about this and not the file type as described above: the
104             // reason is that the writability property is a setting of the mappings, not a property
105             // of the underlying files, and thus it's a setting that we fully control and must keep
106             // correct across reconfigurations or across different mappings of the same files.
107             info!("Missed node caching opportunity because writability has changed for {:?}",
108                 underlying_path)
109         }
110 
111         let node: ArcNode = if attr.is_dir() {
112             panic!("Directory entries cannot be cached and are handled above");
113         } else if attr.file_type().is_symlink() {
114             Symlink::new_mapped(ids.next(), underlying_path, attr, writable)
115         } else {
116             File::new_mapped(ids.next(), underlying_path, attr, writable)
117         };
118         entries.insert(underlying_path.to_path_buf(), node.clone());
119         node
120     }
121 
delete(&self, path: &Path, file_type: fuse::FileType)122     fn delete(&self, path: &Path, file_type: fuse::FileType) {
123         let mut entries = self.entries.lock().unwrap();
124         if file_type == fuse::FileType::Directory {
125             debug_assert!(!entries.contains_key(path), "Directories are not currently cached");
126         } else {
127             entries.remove(path).expect("Tried to delete unknown path from the cache");
128         }
129     }
130 
rename(&self, old_path: &Path, new_path: PathBuf, file_type: fuse::FileType)131     fn rename(&self, old_path: &Path, new_path: PathBuf, file_type: fuse::FileType) {
132         let mut entries = self.entries.lock().unwrap();
133         if file_type == fuse::FileType::Directory {
134             debug_assert!(!entries.contains_key(old_path), "Directories are not currently cached");
135         } else {
136             let node = entries.remove(old_path).expect("Tried to rename unknown path in the cache");
137             entries.insert(new_path, node);
138         }
139     }
140 }
141 
142 #[cfg(test)]
143 mod tests {
144     use super::*;
145     use tempfile::tempdir;
146     use testutils;
147 
148     #[test]
path_cache_behavior()149     fn path_cache_behavior() {
150         let root = tempdir().unwrap();
151 
152         let dir1 = root.path().join("dir1");
153         fs::create_dir(&dir1).unwrap();
154         let dir1attr = fs::symlink_metadata(&dir1).unwrap();
155 
156         let file1 = root.path().join("file1");
157         drop(fs::File::create(&file1).unwrap());
158         let file1attr = fs::symlink_metadata(&file1).unwrap();
159 
160         let file2 = root.path().join("file2");
161         drop(fs::File::create(&file2).unwrap());
162         let file2attr = fs::symlink_metadata(&file2).unwrap();
163 
164         let ids = IdGenerator::new(1);
165         let cache = PathCache::default();
166 
167         // Directories are not cached no matter what.
168         assert_eq!(1, cache.get_or_create(&ids, &dir1, &dir1attr, false).inode());
169         assert_eq!(2, cache.get_or_create(&ids, &dir1, &dir1attr, false).inode());
170         assert_eq!(3, cache.get_or_create(&ids, &dir1, &dir1attr, true).inode());
171 
172         // Different files get different nodes.
173         assert_eq!(4, cache.get_or_create(&ids, &file1, &file1attr, false).inode());
174         assert_eq!(5, cache.get_or_create(&ids, &file2, &file2attr, true).inode());
175 
176         // Files we queried before but with different writability get different nodes.
177         assert_eq!(6, cache.get_or_create(&ids, &file1, &file1attr, true).inode());
178         assert_eq!(7, cache.get_or_create(&ids, &file2, &file2attr, false).inode());
179 
180         // We get cache hits when everything matches previous queries.
181         assert_eq!(6, cache.get_or_create(&ids, &file1, &file1attr, true).inode());
182         assert_eq!(7, cache.get_or_create(&ids, &file2, &file2attr, false).inode());
183 
184         // We don't get cache hits for nodes whose writability changed.
185         assert_eq!(8, cache.get_or_create(&ids, &file1, &file1attr, false).inode());
186         assert_eq!(9, cache.get_or_create(&ids, &file2, &file2attr, true).inode());
187     }
188 
189     #[test]
path_cache_nodes_support_all_file_types()190     fn path_cache_nodes_support_all_file_types() {
191         let ids = IdGenerator::new(1);
192         let cache = PathCache::default();
193 
194         for (_fuse_type, path) in testutils::AllFileTypes::new().entries {
195             let fs_attr = fs::symlink_metadata(&path).unwrap();
196             // The following panics if it's impossible to represent the given file type, which is
197             // what we are testing.
198             cache.get_or_create(&ids, &path, &fs_attr, false);
199         }
200     }
201 }
202