devices/virtio/fs/
read_dir.rs

1// Copyright 2020 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::ffi::CStr;
6use std::ffi::OsStr;
7use std::io;
8use std::mem::size_of;
9use std::ops::Deref;
10use std::ops::DerefMut;
11use std::os::unix::ffi::OsStrExt;
12
13use base::AsRawDescriptor;
14use fuse::filesystem::DirEntry;
15use fuse::filesystem::DirectoryIterator;
16use zerocopy::FromBytes;
17use zerocopy::Immutable;
18use zerocopy::IntoBytes;
19use zerocopy::KnownLayout;
20
21use crate::virtio::fs::allowlist::ReadDirFilter;
22
23#[repr(C, packed)]
24#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)]
25struct LinuxDirent64 {
26    d_ino: libc::ino64_t,
27    d_off: libc::off64_t,
28    d_reclen: libc::c_ushort,
29    d_ty: libc::c_uchar,
30}
31
32pub struct ReadDir<P> {
33    buf: P,
34    current: usize,
35    end: usize,
36    filter: Option<ReadDirFilter>,
37}
38
39impl<P> ReadDir<P> {
40    pub fn with_filter(mut self, filter: ReadDirFilter) -> Self {
41        self.filter = Some(filter);
42        self
43    }
44}
45
46impl<P: DerefMut<Target = [u8]>> ReadDir<P> {
47    pub fn new<D: AsRawDescriptor>(dir: &D, offset: libc::off64_t, mut buf: P) -> io::Result<Self> {
48        // SAFETY:
49        // Safe because this doesn't modify any memory and we check the return value.
50        let res = unsafe { libc::lseek64(dir.as_raw_descriptor(), offset, libc::SEEK_SET) };
51        if res < 0 {
52            return Err(io::Error::last_os_error());
53        }
54
55        // SAFETY:
56        // Safe because the kernel guarantees that it will only write to `buf` and we check the
57        // return value.
58        let res = unsafe {
59            libc::syscall(
60                libc::SYS_getdents64,
61                dir.as_raw_descriptor(),
62                buf.as_mut_ptr() as *mut LinuxDirent64,
63                buf.len() as libc::c_int,
64            )
65        };
66        if res < 0 {
67            return Err(io::Error::last_os_error());
68        }
69
70        Ok(ReadDir {
71            buf,
72            current: 0,
73            end: res as usize,
74            filter: None,
75        })
76    }
77}
78
79impl<P> ReadDir<P> {
80    /// Returns the number of bytes from the internal buffer that have not yet been consumed.
81    pub fn remaining(&self) -> usize {
82        self.end.saturating_sub(self.current)
83    }
84}
85
86impl<P: Deref<Target = [u8]>> DirectoryIterator for ReadDir<P> {
87    fn next(&mut self) -> Option<DirEntry> {
88        loop {
89            let rem = &self.buf[self.current..self.end];
90            if rem.is_empty() {
91                return None;
92            }
93
94            let (dirent64, back) = LinuxDirent64::read_from_prefix(rem)
95                .expect("unable to get LinuxDirent64 from slice");
96
97            let namelen = dirent64.d_reclen as usize - size_of::<LinuxDirent64>();
98            debug_assert!(namelen <= back.len(), "back is smaller than `namelen`");
99
100            let name = strip_padding(&back[..namelen]);
101            let entry = DirEntry {
102                ino: dirent64.d_ino,
103                offset: dirent64.d_off as u64,
104                type_: dirent64.d_ty as u32,
105                name,
106            };
107
108            debug_assert!(
109                rem.len() >= dirent64.d_reclen as usize,
110                "rem is smaller than `d_reclen`"
111            );
112            self.current += dirent64.d_reclen as usize;
113
114            // Apply dynamic path allowlist filtering.
115            if let Some(filter) = &self.filter {
116                match filter {
117                    ReadDirFilter::AllowAll => {}
118                    ReadDirFilter::DenyAll => {
119                        continue;
120                    }
121                    ReadDirFilter::AllowOnly(allowed_entries) => {
122                        let name_bytes = entry.name.to_bytes();
123                        if name_bytes != b"." && name_bytes != b".." {
124                            let name_os = OsStr::from_bytes(name_bytes);
125                            if !allowed_entries.contains(name_os) {
126                                // Skip this entry and check the next one (hide from guest)
127                                continue;
128                            }
129                        }
130                    }
131                }
132            }
133
134            return Some(entry);
135        }
136    }
137}
138
139// Like `CStr::from_bytes_with_nul` but strips any bytes after the first '\0'-byte. Panics if `b`
140// doesn't contain any '\0' bytes.
141fn strip_padding(b: &[u8]) -> &CStr {
142    // It would be nice if we could use memchr here but that's locked behind an unstable gate.
143    let pos = b
144        .iter()
145        .position(|&c| c == 0)
146        .expect("`b` doesn't contain any nul bytes");
147
148    // SAFETY:
149    // Safe because we are creating this string with the first nul-byte we found so we can
150    // guarantee that it is nul-terminated and doesn't contain any interior nuls.
151    unsafe { CStr::from_bytes_with_nul_unchecked(&b[..pos + 1]) }
152}
153
154#[cfg(test)]
155mod test {
156    use super::*;
157
158    #[test]
159    fn padded_cstrings() {
160        assert_eq!(strip_padding(b".\0\0\0\0\0\0\0").to_bytes(), b".");
161        assert_eq!(strip_padding(b"..\0\0\0\0\0\0").to_bytes(), b"..");
162        assert_eq!(
163            strip_padding(b"normal cstring\0").to_bytes(),
164            b"normal cstring"
165        );
166        assert_eq!(strip_padding(b"\0\0\0\0").to_bytes(), b"");
167        assert_eq!(
168            strip_padding(b"interior\0nul bytes\0\0\0").to_bytes(),
169            b"interior"
170        );
171    }
172
173    #[test]
174    #[should_panic(expected = "`b` doesn't contain any nul bytes")]
175    fn no_nul_byte() {
176        strip_padding(b"no nul bytes in string");
177    }
178}