• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 use anyhow::Result;
18 use log::{debug, warn};
19 use std::collections::BTreeMap;
20 use std::convert::TryFrom;
21 use std::ffi::CStr;
22 use std::fs::OpenOptions;
23 use std::io;
24 use std::mem::MaybeUninit;
25 use std::option::Option;
26 use std::os::unix::io::AsRawFd;
27 use std::path::Path;
28 use std::time::Duration;
29 
30 use fuse::filesystem::{
31     Context, DirEntry, DirectoryIterator, Entry, FileSystem, FsOptions, SetattrValid,
32     ZeroCopyReader, ZeroCopyWriter,
33 };
34 use fuse::mount::MountOption;
35 
36 use crate::common::{divide_roundup, ChunkedSizeIter, CHUNK_SIZE};
37 use crate::file::{
38     LocalFileReader, RandomWrite, ReadByChunk, RemoteFileEditor, RemoteFileReader,
39     RemoteMerkleTreeReader,
40 };
41 use crate::fsverity::{VerifiedFileEditor, VerifiedFileReader};
42 
43 const DEFAULT_METADATA_TIMEOUT: std::time::Duration = Duration::from_secs(5);
44 
45 pub type Inode = u64;
46 type Handle = u64;
47 
48 /// `FileConfig` defines the file type supported by AuthFS.
49 pub enum FileConfig {
50     /// A file type that is verified against fs-verity signature (thus read-only). The file is
51     /// backed by a local file. Debug only.
52     LocalVerifiedReadonlyFile {
53         reader: VerifiedFileReader<LocalFileReader, LocalFileReader>,
54         file_size: u64,
55     },
56     /// A file type that is a read-only passthrough from a local file. Debug only.
57     LocalUnverifiedReadonlyFile { reader: LocalFileReader, file_size: u64 },
58     /// A file type that is verified against fs-verity signature (thus read-only). The file is
59     /// served from a remote server.
60     RemoteVerifiedReadonlyFile {
61         reader: VerifiedFileReader<RemoteFileReader, RemoteMerkleTreeReader>,
62         file_size: u64,
63     },
64     /// A file type that is a read-only passthrough from a file on a remote serrver.
65     RemoteUnverifiedReadonlyFile { reader: RemoteFileReader, file_size: u64 },
66     /// A file type that is initially empty, and the content is stored on a remote server. File
67     /// integrity is guaranteed with private Merkle tree.
68     RemoteVerifiedNewFile { editor: VerifiedFileEditor<RemoteFileEditor> },
69 }
70 
71 struct AuthFs {
72     /// Store `FileConfig`s using the `Inode` number as the search index.
73     ///
74     /// For further optimization to minimize the search cost, since Inode is integer, we may
75     /// consider storing them in a Vec if we can guarantee that the numbers are small and
76     /// consecutive.
77     file_pool: BTreeMap<Inode, FileConfig>,
78 
79     /// Maximum bytes in the write transaction to the FUSE device. This limits the maximum size to
80     /// a read request (including FUSE protocol overhead).
81     max_write: u32,
82 }
83 
84 impl AuthFs {
new(file_pool: BTreeMap<Inode, FileConfig>, max_write: u32) -> AuthFs85     pub fn new(file_pool: BTreeMap<Inode, FileConfig>, max_write: u32) -> AuthFs {
86         AuthFs { file_pool, max_write }
87     }
88 
get_file_config(&self, inode: &Inode) -> io::Result<&FileConfig>89     fn get_file_config(&self, inode: &Inode) -> io::Result<&FileConfig> {
90         self.file_pool.get(&inode).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
91     }
92 }
93 
check_access_mode(flags: u32, mode: libc::c_int) -> io::Result<()>94 fn check_access_mode(flags: u32, mode: libc::c_int) -> io::Result<()> {
95     if (flags & libc::O_ACCMODE as u32) == mode as u32 {
96         Ok(())
97     } else {
98         Err(io::Error::from_raw_os_error(libc::EACCES))
99     }
100 }
101 
102 cfg_if::cfg_if! {
103     if #[cfg(all(target_arch = "aarch64", target_pointer_width = "64"))] {
104         fn blk_size() -> libc::c_int { CHUNK_SIZE as libc::c_int }
105     } else {
106         fn blk_size() -> libc::c_long { CHUNK_SIZE as libc::c_long }
107     }
108 }
109 
110 enum FileMode {
111     ReadOnly,
112     ReadWrite,
113 }
114 
create_stat(ino: libc::ino_t, file_size: u64, file_mode: FileMode) -> io::Result<libc::stat64>115 fn create_stat(ino: libc::ino_t, file_size: u64, file_mode: FileMode) -> io::Result<libc::stat64> {
116     let mut st = unsafe { MaybeUninit::<libc::stat64>::zeroed().assume_init() };
117 
118     st.st_ino = ino;
119     st.st_mode = match file_mode {
120         // Until needed, let's just grant the owner access.
121         FileMode::ReadOnly => libc::S_IFREG | libc::S_IRUSR,
122         FileMode::ReadWrite => libc::S_IFREG | libc::S_IRUSR | libc::S_IWUSR,
123     };
124     st.st_dev = 0;
125     st.st_nlink = 1;
126     st.st_uid = 0;
127     st.st_gid = 0;
128     st.st_rdev = 0;
129     st.st_size = libc::off64_t::try_from(file_size)
130         .map_err(|_| io::Error::from_raw_os_error(libc::EFBIG))?;
131     st.st_blksize = blk_size();
132     // Per man stat(2), st_blocks is "Number of 512B blocks allocated".
133     st.st_blocks = libc::c_longlong::try_from(divide_roundup(file_size, 512))
134         .map_err(|_| io::Error::from_raw_os_error(libc::EFBIG))?;
135     Ok(st)
136 }
137 
offset_to_chunk_index(offset: u64) -> u64138 fn offset_to_chunk_index(offset: u64) -> u64 {
139     offset / CHUNK_SIZE
140 }
141 
read_chunks<W: io::Write, T: ReadByChunk>( mut w: W, file: &T, file_size: u64, offset: u64, size: u32, ) -> io::Result<usize>142 fn read_chunks<W: io::Write, T: ReadByChunk>(
143     mut w: W,
144     file: &T,
145     file_size: u64,
146     offset: u64,
147     size: u32,
148 ) -> io::Result<usize> {
149     let remaining = file_size.saturating_sub(offset);
150     let size_to_read = std::cmp::min(size as usize, remaining as usize);
151     let total = ChunkedSizeIter::new(size_to_read, offset, CHUNK_SIZE as usize).try_fold(
152         0,
153         |total, (current_offset, planned_data_size)| {
154             // TODO(victorhsieh): There might be a non-trivial way to avoid this copy. For example,
155             // instead of accepting a buffer, the writer could expose the final destination buffer
156             // for the reader to write to. It might not be generally applicable though, e.g. with
157             // virtio transport, the buffer may not be continuous.
158             let mut buf = [0u8; CHUNK_SIZE as usize];
159             let read_size = file.read_chunk(offset_to_chunk_index(current_offset), &mut buf)?;
160             if read_size < planned_data_size {
161                 return Err(io::Error::from_raw_os_error(libc::ENODATA));
162             }
163 
164             let begin = (current_offset % CHUNK_SIZE) as usize;
165             let end = begin + planned_data_size;
166             let s = w.write(&buf[begin..end])?;
167             if s != planned_data_size {
168                 return Err(io::Error::from_raw_os_error(libc::EIO));
169             }
170             Ok(total + s)
171         },
172     )?;
173 
174     Ok(total)
175 }
176 
177 // No need to support enumerating directory entries.
178 struct EmptyDirectoryIterator {}
179 
180 impl DirectoryIterator for EmptyDirectoryIterator {
next(&mut self) -> Option<DirEntry>181     fn next(&mut self) -> Option<DirEntry> {
182         None
183     }
184 }
185 
186 impl FileSystem for AuthFs {
187     type Inode = Inode;
188     type Handle = Handle;
189     type DirIter = EmptyDirectoryIterator;
190 
max_buffer_size(&self) -> u32191     fn max_buffer_size(&self) -> u32 {
192         self.max_write
193     }
194 
init(&self, _capable: FsOptions) -> io::Result<FsOptions>195     fn init(&self, _capable: FsOptions) -> io::Result<FsOptions> {
196         // Enable writeback cache for better performance especially since our bandwidth to the
197         // backend service is limited.
198         Ok(FsOptions::WRITEBACK_CACHE)
199     }
200 
lookup(&self, _ctx: Context, _parent: Inode, name: &CStr) -> io::Result<Entry>201     fn lookup(&self, _ctx: Context, _parent: Inode, name: &CStr) -> io::Result<Entry> {
202         // Only accept file name that looks like an integrer. Files in the pool are simply exposed
203         // by their inode number. Also, there is currently no directory structure.
204         let num = name.to_str().map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
205         // Normally, `lookup` is required to increase a reference count for the inode (while
206         // `forget` will decrease it). It is not necessary here since the files are configured to
207         // be static.
208         let inode = num.parse::<Inode>().map_err(|_| io::Error::from_raw_os_error(libc::ENOENT))?;
209         let st = match self.get_file_config(&inode)? {
210             FileConfig::LocalVerifiedReadonlyFile { file_size, .. }
211             | FileConfig::LocalUnverifiedReadonlyFile { file_size, .. }
212             | FileConfig::RemoteUnverifiedReadonlyFile { file_size, .. }
213             | FileConfig::RemoteVerifiedReadonlyFile { file_size, .. } => {
214                 create_stat(inode, *file_size, FileMode::ReadOnly)?
215             }
216             FileConfig::RemoteVerifiedNewFile { editor } => {
217                 create_stat(inode, editor.size(), FileMode::ReadWrite)?
218             }
219         };
220         Ok(Entry {
221             inode,
222             generation: 0,
223             attr: st,
224             entry_timeout: DEFAULT_METADATA_TIMEOUT,
225             attr_timeout: DEFAULT_METADATA_TIMEOUT,
226         })
227     }
228 
getattr( &self, _ctx: Context, inode: Inode, _handle: Option<Handle>, ) -> io::Result<(libc::stat64, Duration)>229     fn getattr(
230         &self,
231         _ctx: Context,
232         inode: Inode,
233         _handle: Option<Handle>,
234     ) -> io::Result<(libc::stat64, Duration)> {
235         Ok((
236             match self.get_file_config(&inode)? {
237                 FileConfig::LocalVerifiedReadonlyFile { file_size, .. }
238                 | FileConfig::LocalUnverifiedReadonlyFile { file_size, .. }
239                 | FileConfig::RemoteUnverifiedReadonlyFile { file_size, .. }
240                 | FileConfig::RemoteVerifiedReadonlyFile { file_size, .. } => {
241                     create_stat(inode, *file_size, FileMode::ReadOnly)?
242                 }
243                 FileConfig::RemoteVerifiedNewFile { editor } => {
244                     create_stat(inode, editor.size(), FileMode::ReadWrite)?
245                 }
246             },
247             DEFAULT_METADATA_TIMEOUT,
248         ))
249     }
250 
open( &self, _ctx: Context, inode: Self::Inode, flags: u32, ) -> io::Result<(Option<Self::Handle>, fuse::sys::OpenOptions)>251     fn open(
252         &self,
253         _ctx: Context,
254         inode: Self::Inode,
255         flags: u32,
256     ) -> io::Result<(Option<Self::Handle>, fuse::sys::OpenOptions)> {
257         // Since file handle is not really used in later operations (which use Inode directly),
258         // return None as the handle.
259         match self.get_file_config(&inode)? {
260             FileConfig::LocalVerifiedReadonlyFile { .. }
261             | FileConfig::LocalUnverifiedReadonlyFile { .. }
262             | FileConfig::RemoteVerifiedReadonlyFile { .. }
263             | FileConfig::RemoteUnverifiedReadonlyFile { .. } => {
264                 check_access_mode(flags, libc::O_RDONLY)?;
265             }
266             FileConfig::RemoteVerifiedNewFile { .. } => {
267                 // No need to check access modes since all the modes are allowed to the
268                 // read-writable file.
269             }
270         }
271         // Always cache the file content. There is currently no need to support direct I/O or avoid
272         // the cache buffer. Memory mapping is only possible with cache enabled.
273         Ok((None, fuse::sys::OpenOptions::KEEP_CACHE))
274     }
275 
read<W: io::Write + ZeroCopyWriter>( &self, _ctx: Context, inode: Inode, _handle: Handle, w: W, size: u32, offset: u64, _lock_owner: Option<u64>, _flags: u32, ) -> io::Result<usize>276     fn read<W: io::Write + ZeroCopyWriter>(
277         &self,
278         _ctx: Context,
279         inode: Inode,
280         _handle: Handle,
281         w: W,
282         size: u32,
283         offset: u64,
284         _lock_owner: Option<u64>,
285         _flags: u32,
286     ) -> io::Result<usize> {
287         match self.get_file_config(&inode)? {
288             FileConfig::LocalVerifiedReadonlyFile { reader, file_size } => {
289                 read_chunks(w, reader, *file_size, offset, size)
290             }
291             FileConfig::LocalUnverifiedReadonlyFile { reader, file_size } => {
292                 read_chunks(w, reader, *file_size, offset, size)
293             }
294             FileConfig::RemoteVerifiedReadonlyFile { reader, file_size } => {
295                 read_chunks(w, reader, *file_size, offset, size)
296             }
297             FileConfig::RemoteUnverifiedReadonlyFile { reader, file_size } => {
298                 read_chunks(w, reader, *file_size, offset, size)
299             }
300             FileConfig::RemoteVerifiedNewFile { editor } => {
301                 // Note that with FsOptions::WRITEBACK_CACHE, it's possible for the kernel to
302                 // request a read even if the file is open with O_WRONLY.
303                 read_chunks(w, editor, editor.size(), offset, size)
304             }
305         }
306     }
307 
write<R: io::Read + ZeroCopyReader>( &self, _ctx: Context, inode: Self::Inode, _handle: Self::Handle, mut r: R, size: u32, offset: u64, _lock_owner: Option<u64>, _delayed_write: bool, _flags: u32, ) -> io::Result<usize>308     fn write<R: io::Read + ZeroCopyReader>(
309         &self,
310         _ctx: Context,
311         inode: Self::Inode,
312         _handle: Self::Handle,
313         mut r: R,
314         size: u32,
315         offset: u64,
316         _lock_owner: Option<u64>,
317         _delayed_write: bool,
318         _flags: u32,
319     ) -> io::Result<usize> {
320         match self.get_file_config(&inode)? {
321             FileConfig::RemoteVerifiedNewFile { editor } => {
322                 let mut buf = vec![0; size as usize];
323                 r.read_exact(&mut buf)?;
324                 editor.write_at(&buf, offset)
325             }
326             _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
327         }
328     }
329 
setattr( &self, _ctx: Context, inode: Inode, attr: libc::stat64, _handle: Option<Handle>, valid: SetattrValid, ) -> io::Result<(libc::stat64, Duration)>330     fn setattr(
331         &self,
332         _ctx: Context,
333         inode: Inode,
334         attr: libc::stat64,
335         _handle: Option<Handle>,
336         valid: SetattrValid,
337     ) -> io::Result<(libc::stat64, Duration)> {
338         match self.get_file_config(&inode)? {
339             FileConfig::RemoteVerifiedNewFile { editor } => {
340                 // Initialize the default stat.
341                 let mut new_attr = create_stat(inode, editor.size(), FileMode::ReadWrite)?;
342                 // `valid` indicates what fields in `attr` are valid. Update to return correctly.
343                 if valid.contains(SetattrValid::SIZE) {
344                     // st_size is i64, but the cast should be safe since kernel should not give a
345                     // negative size.
346                     debug_assert!(attr.st_size >= 0);
347                     new_attr.st_size = attr.st_size;
348                     editor.resize(attr.st_size as u64)?;
349                 }
350 
351                 if valid.contains(SetattrValid::MODE) {
352                     warn!("Changing st_mode is not currently supported");
353                     return Err(io::Error::from_raw_os_error(libc::ENOSYS));
354                 }
355                 if valid.contains(SetattrValid::UID) {
356                     warn!("Changing st_uid is not currently supported");
357                     return Err(io::Error::from_raw_os_error(libc::ENOSYS));
358                 }
359                 if valid.contains(SetattrValid::GID) {
360                     warn!("Changing st_gid is not currently supported");
361                     return Err(io::Error::from_raw_os_error(libc::ENOSYS));
362                 }
363                 if valid.contains(SetattrValid::CTIME) {
364                     debug!("Ignoring ctime change as authfs does not maintain timestamp currently");
365                 }
366                 if valid.intersects(SetattrValid::ATIME | SetattrValid::ATIME_NOW) {
367                     debug!("Ignoring atime change as authfs does not maintain timestamp currently");
368                 }
369                 if valid.intersects(SetattrValid::MTIME | SetattrValid::MTIME_NOW) {
370                     debug!("Ignoring mtime change as authfs does not maintain timestamp currently");
371                 }
372                 Ok((new_attr, DEFAULT_METADATA_TIMEOUT))
373             }
374             _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
375         }
376     }
377 }
378 
379 /// Mount and start the FUSE instance. This requires CAP_SYS_ADMIN.
loop_forever( file_pool: BTreeMap<Inode, FileConfig>, mountpoint: &Path, ) -> Result<(), fuse::Error>380 pub fn loop_forever(
381     file_pool: BTreeMap<Inode, FileConfig>,
382     mountpoint: &Path,
383 ) -> Result<(), fuse::Error> {
384     let max_read: u32 = 65536;
385     let max_write: u32 = 65536;
386     let dev_fuse = OpenOptions::new()
387         .read(true)
388         .write(true)
389         .open("/dev/fuse")
390         .expect("Failed to open /dev/fuse");
391 
392     fuse::mount(
393         mountpoint,
394         "authfs",
395         libc::MS_NOSUID | libc::MS_NODEV,
396         &[
397             MountOption::FD(dev_fuse.as_raw_fd()),
398             MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
399             MountOption::AllowOther,
400             MountOption::UserId(0),
401             MountOption::GroupId(0),
402             MountOption::MaxRead(max_read),
403         ],
404     )
405     .expect("Failed to mount fuse");
406 
407     fuse::worker::start_message_loop(
408         dev_fuse,
409         max_write,
410         max_read,
411         AuthFs::new(file_pool, max_write),
412     )
413 }
414