Skip to main content

starnix_modules_ext4/
lib.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![recursion_limit = "256"]
6
7use ext4_read_only::parser::{Parser as ExtParser, XattrMap as ExtXattrMap};
8use ext4_read_only::readers::VmoReader;
9use ext4_read_only::structs::{EntryType, INode, ROOT_INODE_NUM};
10use once_cell::sync::OnceCell;
11use starnix_core::mm::ProtectionFlags;
12use starnix_core::mm::memory::MemoryObject;
13use starnix_core::task::CurrentTask;
14use starnix_core::vfs::{
15    CacheMode, DEFAULT_BYTES_PER_BLOCK, DirectoryEntryType, DirentSink, FileObject, FileOps,
16    FileSystem, FileSystemHandle, FileSystemOps, FileSystemOptions, FsNode, FsNodeHandle,
17    FsNodeInfo, FsNodeOps, FsStr, FsString, MemoryRegularFile, SeekTarget, SymlinkTarget, XattrOp,
18    XattrStorage, default_seek, fileops_impl_directory, fileops_impl_noop_sync,
19    fs_node_impl_dir_readonly, fs_node_impl_not_dir, fs_node_impl_symlink,
20    fs_node_impl_xattr_delegate,
21};
22use starnix_logging::{impossible_error, track_stub};
23use starnix_sync::{FileOpsCore, Locked, Unlocked};
24use starnix_types::vfs::default_statfs;
25use starnix_uapi::auth::FsCred;
26use starnix_uapi::errors::Errno;
27use starnix_uapi::file_mode::FileMode;
28use starnix_uapi::mount_flags::MountFlags;
29use starnix_uapi::open_flags::OpenFlags;
30use starnix_uapi::{EXT4_SUPER_MAGIC, errno, error, ino_t, off_t, statfs};
31use std::sync::Arc;
32use zx::HandleBased;
33
34mod pager;
35
36use pager::{Pager, PagerExtent};
37
38pub struct ExtFilesystem {
39    parser: ExtParser,
40    pager: Arc<Pager>,
41}
42
43impl FileSystemOps for ExtFilesystem {
44    fn name(&self) -> &'static FsStr {
45        "ext4".into()
46    }
47
48    fn statfs(
49        &self,
50        _locked: &mut Locked<FileOpsCore>,
51        _fs: &FileSystem,
52        _current_task: &CurrentTask,
53    ) -> Result<statfs, Errno> {
54        Ok(default_statfs(EXT4_SUPER_MAGIC))
55    }
56}
57
58struct ExtNode {
59    inode_num: u32,
60    inode: INode,
61    xattrs: ExtXattrMap,
62}
63
64impl ExtFilesystem {
65    pub fn new_fs(
66        locked: &mut Locked<Unlocked>,
67        current_task: &CurrentTask,
68        options: FileSystemOptions,
69    ) -> Result<FileSystemHandle, Errno> {
70        let mut open_flags = OpenFlags::RDWR;
71        let mut prot_flags = ProtectionFlags::READ | ProtectionFlags::WRITE | ProtectionFlags::EXEC;
72        if options.flags.contains(MountFlags::RDONLY) {
73            open_flags = OpenFlags::RDONLY;
74            prot_flags ^= ProtectionFlags::WRITE;
75        }
76        if options.flags.contains(MountFlags::NOEXEC) {
77            prot_flags ^= ProtectionFlags::EXEC;
78        }
79
80        let source_device = current_task.open_file(locked, options.source.as_ref(), open_flags)?;
81
82        // Note that we *require* get_memory to work here for performance reasons.  Fallback to
83        // FIDL-based read/write API is not an option.
84        let memory = source_device.get_memory(locked, current_task, None, prot_flags)?;
85        let pager_vmo = memory
86            .as_vmo()
87            .ok_or_else(|| errno!(EINVAL))?
88            .duplicate_handle(zx::Rights::SAME_RIGHTS)
89            .map_err(impossible_error)?;
90        let parser_vmo = Arc::new(
91            memory
92                .as_vmo()
93                .ok_or_else(|| errno!(EINVAL))?
94                .duplicate_handle(zx::Rights::SAME_RIGHTS)
95                .map_err(impossible_error)?,
96        );
97        let parser = ExtParser::new(Box::new(VmoReader::new(parser_vmo)));
98        let pager =
99            Arc::new(Pager::new(pager_vmo, parser.block_size().map_err(|e| errno!(EIO, e))?)?);
100        let fs = Self { parser, pager };
101        let ops = ExtDirectory { inner: Arc::new(ExtNode::new(&fs, ROOT_INODE_NUM)?) };
102        let fs = FileSystem::new(
103            locked,
104            current_task.kernel(),
105            CacheMode::Cached(current_task.kernel().fs_cache_config()),
106            fs,
107            options,
108        )?;
109        fs.create_root(ROOT_INODE_NUM as ino_t, ops);
110        Ok(fs)
111    }
112}
113
114impl ExtNode {
115    fn new(fs: &ExtFilesystem, inode_num: u32) -> Result<ExtNode, Errno> {
116        let inode = fs.parser.inode(inode_num).map_err(|e| errno!(EIO, e))?;
117        let xattrs = fs.parser.inode_xattrs(inode_num).unwrap_or_default();
118        Ok(ExtNode { inode_num, inode, xattrs })
119    }
120}
121
122impl XattrStorage for ExtNode {
123    fn list_xattrs(&self, _locked: &mut Locked<FileOpsCore>) -> Result<Vec<FsString>, Errno> {
124        Ok(self.xattrs.keys().map(|k| k.clone().into()).collect())
125    }
126
127    fn get_xattr(
128        &self,
129        _locked: &mut Locked<FileOpsCore>,
130        name: &FsStr,
131    ) -> Result<FsString, Errno> {
132        self.xattrs.get(&**name).map(|a| a.clone().into()).ok_or_else(|| errno!(ENODATA))
133    }
134
135    fn set_xattr(
136        &self,
137        _locked: &mut Locked<FileOpsCore>,
138        _name: &FsStr,
139        _value: &FsStr,
140        _op: XattrOp,
141    ) -> Result<(), Errno> {
142        error!(ENOSYS)
143    }
144    fn remove_xattr(&self, _locked: &mut Locked<FileOpsCore>, _name: &FsStr) -> Result<(), Errno> {
145        error!(ENOSYS)
146    }
147}
148
149struct ExtDirectory {
150    inner: Arc<ExtNode>,
151}
152
153impl FsNodeOps for ExtDirectory {
154    fs_node_impl_dir_readonly!();
155    fs_node_impl_xattr_delegate!(self, self.inner);
156
157    fn create_file_ops(
158        &self,
159        _locked: &mut Locked<FileOpsCore>,
160        _node: &FsNode,
161        _current_task: &CurrentTask,
162        _flags: OpenFlags,
163    ) -> Result<Box<dyn FileOps>, Errno> {
164        Ok(Box::new(ExtDirFileObject { inner: self.inner.clone() }))
165    }
166
167    fn lookup(
168        &self,
169        _locked: &mut Locked<FileOpsCore>,
170        node: &FsNode,
171        _current_task: &CurrentTask,
172        name: &FsStr,
173    ) -> Result<FsNodeHandle, Errno> {
174        let fs = node.fs();
175        let fs_ops = fs.downcast_ops::<ExtFilesystem>().unwrap();
176        let dir_entries =
177            fs_ops.parser.entries_from_inode(&self.inner.inode).map_err(|e| errno!(EIO, e))?;
178        let entry = dir_entries
179            .iter()
180            .find(|e| e.name_bytes() == name)
181            .ok_or_else(|| errno!(ENOENT, name))?;
182        let ext_node = ExtNode::new(fs_ops, entry.e2d_ino.into())?;
183        let inode_num = ext_node.inode_num as ino_t;
184        fs.get_or_create_node(inode_num, || {
185            let entry_type = EntryType::from_u8(entry.e2d_type).map_err(|e| errno!(EIO, e))?;
186            let mode = FileMode::from_bits(ext_node.inode.e2di_mode.into());
187
188            let uid = get_uid_from_node(&ext_node);
189            let gid = get_gid_from_node(&ext_node);
190            let owner = FsCred { uid, gid };
191
192            let size = get_size_from_node(&ext_node, &mode);
193            let blocks = get_blocks_from_node(&ext_node);
194            let nlink = ext_node.inode.e2di_nlink.into();
195
196            let ops: Box<dyn FsNodeOps> = match entry_type {
197                EntryType::RegularFile => Box::new(ExtFile::new(ext_node, name.to_owned())),
198                EntryType::Directory => Box::new(ExtDirectory { inner: Arc::new(ext_node) }),
199                EntryType::SymLink => Box::new(ExtSymlink { inner: ext_node }),
200                EntryType::Unknown => {
201                    track_stub!(TODO("https://fxbug.dev/322873719"), "ext4 unknown entry type");
202                    Box::new(ExtFile::new(ext_node, name.to_owned()))
203                }
204                EntryType::CharacterDevice => {
205                    track_stub!(TODO("https://fxbug.dev/322874445"), "ext4 character device");
206                    Box::new(ExtFile::new(ext_node, name.to_owned()))
207                }
208                EntryType::BlockDevice => {
209                    track_stub!(TODO("https://fxbug.dev/322874062"), "ext4 block device");
210                    Box::new(ExtFile::new(ext_node, name.to_owned()))
211                }
212                EntryType::FIFO => {
213                    track_stub!(TODO("https://fxbug.dev/322874249"), "ext4 fifo");
214                    Box::new(ExtFile::new(ext_node, name.to_owned()))
215                }
216                EntryType::Socket => {
217                    track_stub!(TODO("https://fxbug.dev/322874081"), "ext4 socket");
218                    Box::new(ExtFile::new(ext_node, name.to_owned()))
219                }
220            };
221
222            let child = FsNode::new_uncached(
223                inode_num,
224                ops,
225                &fs,
226                FsNodeInfo { mode, uid: owner.uid, gid: owner.gid, ..Default::default() },
227            );
228            child.update_info(|info| {
229                info.size = size as usize;
230                info.link_count = nlink;
231                info.blksize = DEFAULT_BYTES_PER_BLOCK;
232                info.blocks = blocks as usize;
233            });
234            Ok(child)
235        })
236    }
237}
238
239fn merge_low_high_16(low: u32, high: u32) -> u32 {
240    low | (high << 16)
241}
242
243fn merge_low_high_32(low: u64, high: u64) -> u64 {
244    low | (high << 32)
245}
246
247fn get_uid_from_node(ext_node: &ExtNode) -> u32 {
248    let uid_lower: u32 = ext_node.inode.e2di_uid.into();
249    let uid_upper: u32 = ext_node.inode.e2di_uid_high.into();
250    merge_low_high_16(uid_lower, uid_upper)
251}
252
253fn get_gid_from_node(ext_node: &ExtNode) -> u32 {
254    let gid_lower: u32 = ext_node.inode.e2di_gid.into();
255    let gid_upper: u32 = ext_node.inode.e2di_gid_high.into();
256    merge_low_high_16(gid_lower, gid_upper)
257}
258
259fn get_size_from_node(ext_node: &ExtNode, mode: &FileMode) -> u64 {
260    if mode.is_reg() {
261        let size_lower: u64 = ext_node.inode.e2di_size.into();
262        let size_upper: u64 = ext_node.inode.e2di_size_high.into();
263        merge_low_high_32(size_lower, size_upper)
264    } else {
265        ext_node.inode.e2di_size.into()
266    }
267}
268
269fn get_blocks_from_node(ext_node: &ExtNode) -> u64 {
270    let blocks_lower: u64 = ext_node.inode.e2di_nblock.into();
271    let blocks_upper: u64 = ext_node.inode.e2di_nblock_high.into();
272    merge_low_high_32(blocks_lower, blocks_upper)
273}
274
275struct ExtFile {
276    inner: ExtNode,
277    name: FsString,
278
279    // The VMO here will be a child of the main VMO that the pager holds.  We want to keep it here
280    // so that whilst ExtFile remains resident, we hold a child reference to the main VMO which
281    // will prevent the pager from dropping the VMO (and any data we might have paged-in).
282    memory: OnceCell<Arc<MemoryObject>>,
283}
284
285impl ExtFile {
286    fn new(inner: ExtNode, name: FsString) -> Self {
287        ExtFile { inner, name, memory: OnceCell::new() }
288    }
289}
290
291impl FsNodeOps for ExtFile {
292    fs_node_impl_not_dir!();
293    fs_node_impl_xattr_delegate!(self, self.inner);
294
295    fn create_file_ops(
296        &self,
297        _locked: &mut Locked<FileOpsCore>,
298        node: &FsNode,
299        _current_task: &CurrentTask,
300        _flags: OpenFlags,
301    ) -> Result<Box<dyn FileOps>, Errno> {
302        let fs = node.fs();
303        let fs_ops = fs.downcast_ops::<ExtFilesystem>().unwrap();
304        let inode_num = self.inner.inode_num;
305        let memory = self.memory.get_or_try_init(|| {
306            let (file_size, extents) = fs_ops
307                .parser
308                .read_extents(self.inner.inode_num)
309                .map_err(|e| errno!(EINVAL, format!("failed to read extents: {e}")))?;
310            // The extents should be sorted which we rely on later.
311            let mut pager_extents = Vec::with_capacity(extents.len());
312            let mut last_block = 0;
313            for e in extents {
314                let pager_extent = PagerExtent::from(e);
315                if pager_extent.logical.start < last_block {
316                    return error!(EIO, "Bad extent");
317                }
318                last_block = pager_extent.logical.end;
319                pager_extents.push(pager_extent);
320            }
321            Ok(Arc::new(MemoryObject::from(
322                fs_ops
323                    .pager
324                    .register(self.name.as_ref(), inode_num, file_size, &pager_extents)
325                    .map_err(|e| errno!(EINVAL, e))?,
326            )))
327        })?;
328
329        // TODO(https://fxbug.dev/42080696) returned memory shouldn't be writeable
330        Ok(Box::new(MemoryRegularFile::new(memory.clone())))
331    }
332}
333
334impl From<ext4_read_only::structs::Extent> for PagerExtent {
335    fn from(e: ext4_read_only::structs::Extent) -> Self {
336        let block_count: u16 = e.e_len.into();
337        let start = e.e_blk.into();
338        Self { logical: start..start + block_count as u32, physical_block: e.target_block_num() }
339    }
340}
341
342struct ExtSymlink {
343    inner: ExtNode,
344}
345
346impl FsNodeOps for ExtSymlink {
347    fs_node_impl_symlink!();
348    fs_node_impl_xattr_delegate!(self, self.inner);
349
350    fn readlink(
351        &self,
352        _locked: &mut Locked<FileOpsCore>,
353        node: &FsNode,
354        _current_task: &CurrentTask,
355    ) -> Result<SymlinkTarget, Errno> {
356        let fs = node.fs();
357        let fs_ops = fs.downcast_ops::<ExtFilesystem>().unwrap();
358        let data = fs_ops.parser.read_data(self.inner.inode_num).map_err(|e| errno!(EIO, e))?;
359        Ok(SymlinkTarget::Path(data.into()))
360    }
361}
362
363struct ExtDirFileObject {
364    inner: Arc<ExtNode>,
365}
366
367impl FileOps for ExtDirFileObject {
368    fileops_impl_directory!();
369    fileops_impl_noop_sync!();
370
371    fn seek(
372        &self,
373        _locked: &mut Locked<FileOpsCore>,
374        _file: &FileObject,
375        _current_task: &CurrentTask,
376        current_offset: off_t,
377        target: SeekTarget,
378    ) -> Result<off_t, Errno> {
379        Ok(default_seek(current_offset, target, || error!(EINVAL))?)
380    }
381
382    fn readdir(
383        &self,
384        _locked: &mut Locked<FileOpsCore>,
385        file: &FileObject,
386        _current_task: &CurrentTask,
387        sink: &mut dyn DirentSink,
388    ) -> Result<(), Errno> {
389        let fs = file.node().fs();
390        let fs_ops = fs.downcast_ops::<ExtFilesystem>().unwrap();
391        let dir_entries =
392            fs_ops.parser.entries_from_inode(&self.inner.inode).map_err(|e| errno!(EIO, e))?;
393
394        if sink.offset() as usize >= dir_entries.len() {
395            return Ok(());
396        }
397
398        for entry in dir_entries[(sink.offset() as usize)..].iter() {
399            let inode_num = entry.e2d_ino.into();
400            let entry_type = directory_entry_type(
401                EntryType::from_u8(entry.e2d_type).map_err(|e| errno!(EIO, e))?,
402            );
403            sink.add(inode_num, sink.offset() + 1, entry_type, entry.name_bytes().into())?;
404        }
405        Ok(())
406    }
407}
408
409fn directory_entry_type(entry_type: EntryType) -> DirectoryEntryType {
410    match entry_type {
411        EntryType::Unknown => DirectoryEntryType::UNKNOWN,
412        EntryType::RegularFile => DirectoryEntryType::REG,
413        EntryType::Directory => DirectoryEntryType::DIR,
414        EntryType::CharacterDevice => DirectoryEntryType::CHR,
415        EntryType::BlockDevice => DirectoryEntryType::BLK,
416        EntryType::FIFO => DirectoryEntryType::FIFO,
417        EntryType::Socket => DirectoryEntryType::SOCK,
418        EntryType::SymLink => DirectoryEntryType::LNK,
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use ext4_read_only::structs::INode;
426    use starnix_uapi::file_mode::mode;
427    use zerocopy::FromBytes;
428    use zerocopy::byteorder::little_endian::{U16 as LE16, U32 as LE32};
429
430    fn default_inode() -> INode {
431        let zero = vec![0; 160];
432        INode::read_from_bytes(&zero).expect("failed to read from bytes")
433    }
434
435    fn create_test_ext_node(inode: INode) -> ExtNode {
436        ExtNode { inode_num: 1, inode, xattrs: ExtXattrMap::default() }
437    }
438
439    #[test]
440    fn test_get_uid_from_node() {
441        let mut inode = default_inode();
442        inode.e2di_uid = LE16::new(1001);
443        inode.e2di_uid_high = LE16::new(1);
444        let node = create_test_ext_node(inode);
445        assert_eq!(get_uid_from_node(&node), (1 << 16) | 1001);
446    }
447
448    #[test]
449    fn test_get_gid_from_node() {
450        let mut inode = default_inode();
451        inode.e2di_gid = LE16::new(1002);
452        inode.e2di_gid_high = LE16::new(2);
453        let node = create_test_ext_node(inode);
454        assert_eq!(get_gid_from_node(&node), (2 << 16) | 1002);
455    }
456
457    #[test]
458    fn test_get_size_from_node() {
459        // Test with a regular file.
460        let mut inode = default_inode();
461        inode.e2di_size = LE32::new(0x12345678);
462        inode.e2di_size_high = LE32::new(0x9);
463        let node = create_test_ext_node(inode);
464        let mode = mode!(IFREG, 0o777);
465        assert_eq!(get_size_from_node(&node, &mode), (0x9 << 32) | 0x12345678);
466
467        // Test with a directory, where size_high should be ignored.
468        let mode = mode!(IFDIR, 0o777);
469        assert_eq!(get_size_from_node(&node, &mode), 0x12345678);
470    }
471
472    #[test]
473    fn test_get_blocks_from_node() {
474        let mut inode = default_inode();
475        inode.e2di_nblock = LE32::new(0xABCDE);
476        inode.e2di_nblock_high = LE16::new(0x3);
477        let node = create_test_ext_node(inode);
478        assert_eq!(get_blocks_from_node(&node), (0x3 << 32) | 0xABCDE);
479    }
480}